-
Notifications
You must be signed in to change notification settings - Fork 20
/
session_test.go
302 lines (253 loc) · 9.8 KB
/
session_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
// +build windows
package etw_test
import (
"context"
"errors"
"fmt"
"testing"
"time"
msetw "github.com/Microsoft/go-winio/pkg/etw"
"github.com/stretchr/testify/suite"
"golang.org/x/sys/windows"
"github.com/bi-zone/etw"
)
func TestSession(t *testing.T) {
suite.Run(t, new(sessionSuite))
}
type sessionSuite struct {
suite.Suite
ctx context.Context
cancel context.CancelFunc
provider *msetw.Provider
guid windows.GUID
}
func (s *sessionSuite) SetupTest() {
provider, err := msetw.NewProvider("TestProvider", nil)
s.Require().NoError(err, "Failed to initialize test provider.")
s.provider = provider
s.guid = windows.GUID(provider.ID)
s.ctx, s.cancel = context.WithCancel(context.Background())
}
func (s *sessionSuite) TearDownTest() {
s.cancel()
s.Require().NoError(s.provider.Close(), "Failed to close test provider.")
}
// TestSmoke ensures that etw.Session is working as expected: it could start, process incoming
// events and stop properly.
func (s *sessionSuite) TestSmoke() {
const deadline = 10 * time.Second
// Spam some events to emulate a normal ETW provider behaviour.
go s.generateEvents(s.ctx, []msetw.Level{msetw.LevelInfo})
// Ensure we can subscribe to our in-house ETW provider.
session, err := etw.NewSession(s.guid)
s.Require().NoError(err, "Failed to create session")
// The only thing we are going to do is signal that we've got something.
gotEvent := make(chan struct{})
cb := func(_ *etw.Event) {
s.trySignal(gotEvent)
}
// Start the processing routine. We expect the routine will stop on `session.Close()`.
done := make(chan struct{})
go func() {
s.Require().NoError(session.Process(cb), "Error processing events")
close(done)
}()
// Ensure that we are able to receive events from the provider. An ability
// to get the proper content is tested in TestParsing.
s.waitForSignal(gotEvent, deadline, "Failed to receive event from provider")
// Now stop the session and ensure that processing goroutine will also stop.
s.Require().NoError(session.Close(), "Failed to close session properly")
s.waitForSignal(done, deadline, "Failed to stop event processing")
}
// TestUpdating ensures that etw.Session is able to update its properties in runtime.
func (s *sessionSuite) TestUpdating() {
const deadline = 10 * time.Second
// Create a provider that will spam both INFO and CRITICAL events.
go s.generateEvents(s.ctx, []msetw.Level{msetw.LevelInfo, msetw.LevelCritical})
// Then subscribe for CRITICAL only.
session, err := etw.NewSession(s.guid, etw.WithLevel(etw.TRACE_LEVEL_CRITICAL))
s.Require().NoError(err, "Failed to create session")
// Callback will signal about seen event level through corresponding channels.
var (
gotCriticalEvent = make(chan struct{}, 1)
gotInformationEvent = make(chan struct{}, 1)
)
cb := func(e *etw.Event) {
switch etw.TraceLevel(e.Header.Level) {
case etw.TRACE_LEVEL_INFORMATION:
s.trySignal(gotInformationEvent)
case etw.TRACE_LEVEL_CRITICAL:
s.trySignal(gotCriticalEvent)
}
}
done := make(chan struct{})
go func() {
s.Require().NoError(session.Process(cb), "Error processing events")
close(done)
}()
// Ensure that we are getting INFO events but NO CRITICAL ones.
s.waitForSignal(gotCriticalEvent, deadline, "Failed to get event with CRITICAL level")
select {
case <-time.After(deadline): // pass
case <-gotInformationEvent:
s.Fail("Received event with unexpected level")
}
// Now bump the subscription option with new event level.
// (We could actually update any updatable option, level is just the most obvious.)
err = session.UpdateOptions(etw.WithLevel(etw.TRACE_LEVEL_INFORMATION))
s.Require().NoError(err, "Failed to update session options")
// If the options update was successfully applied we should catch event with INFO level too.
s.waitForSignal(gotInformationEvent, deadline,
"Failed to receive event with INFO level after updating session options")
// Stop the session and ensure that processing goroutine will also stop.
s.Require().NoError(session.Close(), "Failed to close session properly")
s.waitForSignal(done, deadline, "Failed to stop event processing")
}
// TestParsing ensures that etw.Session is able to parse events with all common field types.
func (s *sessionSuite) TestParsing() {
const deadline = 20 * time.Second
go s.generateEvents(
s.ctx,
[]msetw.Level{msetw.LevelInfo},
msetw.StringField("string", "string value"),
msetw.StringArray("stringArray", []string{"1", "2", "3"}),
msetw.Float64Field("float64", 45.7),
msetw.Struct("struct",
msetw.StringField("string", "string value"),
msetw.Float64Field("float64", 46.7),
msetw.Struct("subStructure",
msetw.StringField("string", "string value"),
),
),
msetw.StringArray("anotherArray", []string{"3", "4"}),
)
expectedMap := map[string]interface{}{
"string": "string value",
"stringArray.Count": "3", // OS artifacts
"stringArray": []interface{}{"1", "2", "3"},
"float64": "45.700000",
"struct": map[string]interface{}{
"string": "string value",
"float64": "46.700000",
"subStructure": map[string]interface{}{
"string": "string value",
},
},
"anotherArray.Count": "2", // OS artifacts
"anotherArray": []interface{}{"3", "4"},
}
session, err := etw.NewSession(s.guid, etw.WithLevel(etw.TRACE_LEVEL_VERBOSE))
s.Require().NoError(err, "Failed to create a session")
var (
properties map[string]interface{}
gotProps = make(chan struct{}, 1)
)
cb := func(e *etw.Event) {
properties, err = e.EventProperties()
s.Require().NoError(err, "Got error parsing event properties")
s.trySignal(gotProps)
}
done := make(chan struct{})
go func() {
s.Require().NoError(session.Process(cb), "Error processing events")
close(done)
}()
s.waitForSignal(gotProps, deadline, "Failed to get event")
s.Equal(expectedMap, properties, "Received unexpected properties")
s.Require().NoError(session.Close(), "Failed to close session properly")
s.waitForSignal(done, deadline, "Failed to stop event processing")
}
// TestKillSession ensures that we are able to force kill the lost session using only
// its name.
func (s *sessionSuite) TestKillSession() {
sessionName := fmt.Sprintf("go-etw-suicide-%d", time.Now().UnixNano())
// Ensure we can create a session with a given name.
_, err := etw.NewSession(s.guid, etw.WithName(sessionName))
s.Require().NoError(err, "Failed to create session with name %s", sessionName)
// Ensure we've got ExistsError creating a session with the same name.
_, err = etw.NewSession(s.guid, etw.WithName(sessionName))
s.Require().Error(err)
var exists etw.ExistsError
s.Require().True(errors.As(err, &exists), "Got unexpected error starting session with a same name")
s.Equal(exists.SessionName, sessionName, "Got unexpected name in etw.ExistsError")
// Try to force-kill the session by name.
s.Require().NoError(etw.KillSession(sessionName), "Failed to force stop session")
// Ensure that fresh session could normally started and stopped.
session, err := etw.NewSession(s.guid, etw.WithName(sessionName))
s.Require().NoError(err, "Failed to create session after a successful kill")
s.Require().NoError(session.Close(), "Failed to close session properly")
}
// TestEventOutsideCallback ensures *etw.Event can't be used outside EventCallback.
func (s *sessionSuite) TestEventOutsideCallback() {
const deadline = 10 * time.Second
go s.generateEvents(s.ctx, []msetw.Level{msetw.LevelInfo})
session, err := etw.NewSession(s.guid)
s.Require().NoError(err, "Failed to create session")
// Grab event pointer from the callback. We expect that outdated pointer
// will protect user from calling Windows API on freed memory.
var evt *etw.Event
gotEvent := make(chan struct{})
cb := func(e *etw.Event) {
// Signal on second event only to guarantee that callback with stored event will finish.
if evt != nil {
s.trySignal(gotEvent)
} else {
evt = e
}
}
done := make(chan struct{})
go func() {
s.Require().NoError(session.Process(cb), "Error processing events")
close(done)
}()
// Wait for event arrived and try to access event data.
s.waitForSignal(gotEvent, deadline, "Failed to receive event from provider")
s.Assert().Zero(evt.ExtendedInfo(), "Got non-nil ExtendedInfo for freed event")
_, err = evt.EventProperties()
s.Assert().Error(err, "Don't get an error using freed event")
s.Assert().Contains(err.Error(), "EventCallback", "Got unexpected error: %s", err)
s.Require().NoError(session.Close(), "Failed to close session properly")
s.waitForSignal(done, deadline, "Failed to stop event processing")
}
// trySignal tries to send a signal to @done if it's ready to receive.
// @done expected to be a buffered channel.
func (s sessionSuite) trySignal(done chan<- struct{}) {
select {
case done <- struct{}{}:
default:
}
}
// waitForSignal waits for anything on @done no longer than @deadline.
// Fails test run if deadline exceeds.
func (s sessionSuite) waitForSignal(done <-chan struct{}, deadline time.Duration, failMsg string) {
select {
case <-done:
// pass.
case <-time.After(deadline):
s.Fail(failMsg, "deadline %s exceeded", deadline)
}
}
// We have no easy way to ensure that etw session is started and ready to process events,
// so it seems easier to just flood an events and catch some of them than try to catch
// the actual session readiness and sent the only one.
func (s sessionSuite) generateEvents(ctx context.Context, levels []msetw.Level, fields ...msetw.FieldOpt) {
// If nothing provided, receiver doesn't care about the event content -- send anything.
if fields == nil {
fields = msetw.WithFields(msetw.StringField("TestField", "Foo"))
}
s.Require().NotEmpty(levels, "Incorrect generateEvents usage")
for {
select {
case <-ctx.Done():
return
default:
for _, l := range levels {
_ = s.provider.WriteEvent(
"TestEvent",
msetw.WithEventOpts(msetw.WithLevel(l)),
fields,
)
}
}
}
}