Skip to content

Commit 678f37e

Browse files
committed
Add WMI events tracking example
1 parent 4a9d139 commit 678f37e

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ func main() {
4040
}
4141
}
4242
```
43+
44+
A more sophisticated examples are located at in [`examples`](./examples) folder.
4345

4446
## Benchmarks
4547
Using `DefaultClient`, `SWbemServices` or `SWbemServicesConnection` differ in a number

examples/events/main.go

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package main
2+
3+
// In the example we are going to track some events happen on WMI subscriptions.
4+
// This is a good way to show tricky cases in WMI results decoding.
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"log"
10+
"os"
11+
"os/signal"
12+
"syscall"
13+
"unsafe"
14+
15+
"github.com/bi-zone/wmi"
16+
"github.com/go-ole/go-ole"
17+
)
18+
19+
// Notifications source in "root\subscription".
20+
// Important thing here is that we can get an event of 3 different types.
21+
var query = `
22+
SELECT * FROM __InstanceOperationEvent
23+
WITHIN 5
24+
WHERE
25+
TargetInstance ISA '__EventFilter'
26+
OR TargetInstance ISA '__FilterToConsumerBinding'
27+
`
28+
29+
// wmiEvent has a straightforward implementation. The only non-usual thing here
30+
// is access to the system property `Path_.Class`.
31+
type wmiEvent struct {
32+
TimeStamp uint64 `wmi:"TIME_CREATED"`
33+
System struct {
34+
Class string
35+
} `wmi:"Path_"`
36+
Instance instance `wmi:"TargetInstance"`
37+
}
38+
39+
// `TargetInstance` property in `__InstanceOperationEvent` can contain one of
40+
// 2 different classes (cos os our query). To handle this in statically typed
41+
// language we could either use an interface or add all of them and fill the
42+
// only one.
43+
//
44+
// Lets create a field for every result type + `.Class` field to select the
45+
// right one.
46+
type instance struct {
47+
Class string
48+
CreatorSID string
49+
50+
EventFilter *eventFilter `json:",omitempty"`
51+
EventBinding *eventFilterBinding `json:",omitempty"`
52+
}
53+
54+
// UnmarshalOLE extracts system properties of the resulting object and then
55+
// unmarshalls the result into the proper `instance` field.
56+
func (i *instance) UnmarshalOLE(d wmi.Decoder, src *ole.IDispatch) error {
57+
// Here is a temp object for the fields common for both classes.
58+
var commonProps struct {
59+
System struct {
60+
Class string
61+
} `wmi:"Path_"`
62+
CreatorSID []byte
63+
}
64+
if err := d.Unmarshal(src, &commonProps); err != nil {
65+
return err
66+
}
67+
68+
sid, err := unmarshalSID(commonProps.CreatorSID)
69+
if err != nil {
70+
return err
71+
}
72+
i.Class = commonProps.System.Class
73+
i.CreatorSID = sid
74+
75+
// And here we unmarshal the right class based on the `class` string from
76+
// the object system property.
77+
switch i.Class {
78+
case "__EventFilter":
79+
i.EventFilter = &eventFilter{}
80+
return d.Unmarshal(src, i.EventFilter)
81+
case "__FilterToConsumerBinding":
82+
i.EventBinding = &eventFilterBinding{}
83+
return d.Unmarshal(src, i.EventBinding)
84+
}
85+
return fmt.Errorf("unknown target class %q", i.Class)
86+
}
87+
88+
// Golang-core mad skillz.
89+
// If you know a better way to unmarshal []byte SID - please open a PR.
90+
func unmarshalSID(sid []byte) (string, error) {
91+
p := unsafe.Pointer(&sid[0])
92+
s := (*syscall.SID)(p)
93+
return s.String()
94+
}
95+
96+
// eventFilter is a simple struct with common fields.
97+
type eventFilter struct {
98+
Name string
99+
EventNamespace string
100+
Query string
101+
QueryLanguage string
102+
}
103+
104+
// eventFilterBinding has 2 reference fields, which is a bit more tricky.
105+
type eventFilterBinding struct {
106+
Consumer eventConsumer `wmi:",ref"`
107+
Filter eventFilter `wmi:",ref"`
108+
}
109+
110+
// eventConsumer is never returned as is - it is always some descendant, so
111+
// the best thing we could do - extract a Type name.
112+
type eventConsumer struct {
113+
Type string `wmi:"-"`
114+
}
115+
116+
func (e *eventConsumer) UnmarshalOLE(d wmi.Decoder, src *ole.IDispatch) error {
117+
var systemProps struct {
118+
Path struct {
119+
Class string
120+
} `wmi:"Path_"`
121+
}
122+
if err := d.Unmarshal(src, &systemProps); err != nil {
123+
return err
124+
}
125+
e.Type = systemProps.Path.Class
126+
return nil
127+
}
128+
129+
// To produce an event you could use a powershell script, e.g.
130+
// #Creating a new event filter
131+
// $ServiceFilter = ([wmiclass]"\\.\root\subscription:__EventFilter").CreateInstance()
132+
//
133+
// # Set the properties of the instance
134+
// $ServiceFilter.QueryLanguage = 'WQL'
135+
// $ServiceFilter.Query = "select * from __instanceModificationEvent within 5 where targetInstance isa 'win32_Service'"
136+
// $ServiceFilter.Name = "ServiceFilter"
137+
// $ServiceFilter.EventNamespace = 'root\cimv2'
138+
//
139+
// # Sets the instance in the namespace
140+
// $FilterResult = $ServiceFilter.Put()
141+
// $ServiceFilterObj = $FilterResult.Path
142+
143+
func main() {
144+
events := make(chan wmiEvent)
145+
q, err := wmi.NewNotificationQuery(events, query)
146+
if err != nil {
147+
log.Fatalf("Failed to create NotificationQuery; %s", err)
148+
}
149+
150+
// Set namespace.
151+
q.SetConnectServerArgs(nil, `root\subscription`)
152+
153+
// Set exit hook
154+
sigs := make(chan os.Signal, 1)
155+
done := make(chan error, 1)
156+
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
157+
158+
go func() {
159+
done <- q.StartNotifications()
160+
}()
161+
162+
log.Println("Listening for events")
163+
for {
164+
select {
165+
case ev := <-events:
166+
data, err := json.MarshalIndent(ev, "", " ")
167+
if err != nil {
168+
log.Printf("[ERR] Failed to marshal event; %s", err)
169+
} else {
170+
log.Println(string(data))
171+
}
172+
case sig := <-sigs:
173+
log.Printf("Got system signal %s; stopping", sig)
174+
q.Stop()
175+
return
176+
case err := <-done: // Query will never stop here w/o error.
177+
log.Printf("[ERR] Got StartNotifications error; %s", err)
178+
return
179+
}
180+
}
181+
}

0 commit comments

Comments
 (0)