diff --git a/pkg/etw/eventopt.go b/pkg/etw/eventopt.go index 0a8386d..a0a8176 100644 --- a/pkg/etw/eventopt.go +++ b/pkg/etw/eventopt.go @@ -1,13 +1,13 @@ package etw import ( - "golang.org/x/sys/windows" + "github.com/Microsoft/go-winio/pkg/guid" ) type eventOptions struct { descriptor *eventDescriptor - activityID *windows.GUID - relatedActivityID *windows.GUID + activityID *guid.GUID + relatedActivityID *guid.GUID tags uint32 } @@ -59,14 +59,14 @@ func WithTags(newTags uint32) EventOpt { } // WithActivityID specifies the activity ID of the event to be written. -func WithActivityID(activityID *windows.GUID) EventOpt { +func WithActivityID(activityID *guid.GUID) EventOpt { return func(options *eventOptions) { options.activityID = activityID } } // WithRelatedActivityID specifies the parent activity ID of the event to be written. -func WithRelatedActivityID(activityID *windows.GUID) EventOpt { +func WithRelatedActivityID(activityID *guid.GUID) EventOpt { return func(options *eventOptions) { options.relatedActivityID = activityID } diff --git a/pkg/etw/provider.go b/pkg/etw/provider.go index 88eb75f..7a3c13d 100644 --- a/pkg/etw/provider.go +++ b/pkg/etw/provider.go @@ -4,12 +4,11 @@ import ( "bytes" "crypto/sha1" "encoding/binary" - "encoding/hex" - "fmt" "strings" "unicode/utf16" "unsafe" + "github.com/Microsoft/go-winio/pkg/guid" "golang.org/x/sys/windows" ) @@ -17,7 +16,7 @@ import ( // name and ID (GUID), which should always have a 1:1 mapping to each other // (e.g. don't use multiple provider names with the same ID, or vice versa). type Provider struct { - ID *windows.GUID + ID *guid.GUID handle providerHandle metadata []byte callback EnableCallback @@ -30,19 +29,7 @@ type Provider struct { // String returns the `provider`.ID as a string func (provider *Provider) String() string { - data1 := make([]byte, 4) - binary.BigEndian.PutUint32(data1, provider.ID.Data1) - data2 := make([]byte, 2) - binary.BigEndian.PutUint16(data2, provider.ID.Data2) - data3 := make([]byte, 2) - binary.BigEndian.PutUint16(data3, provider.ID.Data3) - return fmt.Sprintf( - "%s-%s-%s-%s-%s", - hex.EncodeToString(data1), - hex.EncodeToString(data2), - hex.EncodeToString(data3), - hex.EncodeToString(provider.ID.Data4[:2]), - hex.EncodeToString(provider.ID.Data4[2:])) + return provider.ID.String() } type providerHandle windows.Handle @@ -72,9 +59,9 @@ const ( // EnableCallback is the form of the callback function that receives provider // enable/disable notifications from ETW. -type EnableCallback func(*windows.GUID, ProviderState, Level, uint64, uint64, uintptr) +type EnableCallback func(*guid.GUID, ProviderState, Level, uint64, uint64, uintptr) -func providerCallback(sourceID *windows.GUID, state ProviderState, level Level, matchAnyKeyword uint64, matchAllKeyword uint64, filterData uintptr, i uintptr) { +func providerCallback(sourceID *guid.GUID, state ProviderState, level Level, matchAnyKeyword uint64, matchAllKeyword uint64, filterData uintptr, i uintptr) { provider := providers.getProvider(uint(i)) switch state { @@ -96,7 +83,7 @@ func providerCallback(sourceID *windows.GUID, state ProviderState, level Level, // for provider notifications. Because Go has trouble with callback arguments of // different size, it has only pointer-sized arguments, which are then cast to // the appropriate types when calling providerCallback. -func providerCallbackAdapter(sourceID *windows.GUID, state uintptr, level uintptr, matchAnyKeyword uintptr, matchAllKeyword uintptr, filterData uintptr, i uintptr) uintptr { +func providerCallbackAdapter(sourceID *guid.GUID, state uintptr, level uintptr, matchAnyKeyword uintptr, matchAllKeyword uintptr, filterData uintptr, i uintptr) uintptr { providerCallback(sourceID, ProviderState(state), Level(level), uint64(matchAnyKeyword), uint64(matchAllKeyword), filterData, i) return 0 } @@ -108,7 +95,7 @@ func providerCallbackAdapter(sourceID *windows.GUID, state uintptr, level uintpt // The algorithm is roughly: // Hash = Sha1(namespace + arg.ToUpper().ToUtf16be()) // Guid = Hash[0..15], with Hash[7] tweaked according to RFC 4122 -func providerIDFromName(name string) *windows.GUID { +func providerIDFromName(name string) *guid.GUID { buffer := sha1.New() namespace := []byte{0x48, 0x2C, 0x2D, 0xB2, 0xC3, 0x90, 0x47, 0xC8, 0x87, 0xF8, 0x1A, 0x15, 0xBF, 0xC1, 0x30, 0xFB} @@ -119,7 +106,7 @@ func providerIDFromName(name string) *windows.GUID { sum := buffer.Sum(nil) sum[7] = (sum[7] & 0xf) | 0x50 - return &windows.GUID{ + return &guid.GUID{ Data1: binary.LittleEndian.Uint32(sum[0:4]), Data2: binary.LittleEndian.Uint16(sum[4:6]), Data3: binary.LittleEndian.Uint16(sum[6:8]), @@ -137,7 +124,7 @@ func NewProvider(name string, callback EnableCallback) (provider *Provider, err // provider ID to be manually specified. This is most useful when there is an // existing provider ID that must be used to conform to existing diagnostic // infrastructure. -func NewProviderWithID(name string, id *windows.GUID, callback EnableCallback) (provider *Provider, err error) { +func NewProviderWithID(name string, id *guid.GUID, callback EnableCallback) (provider *Provider, err error) { providerCallbackOnce.Do(func() { globalProviderCallback = windows.NewCallback(providerCallbackAdapter) }) @@ -151,7 +138,7 @@ func NewProviderWithID(name string, id *windows.GUID, callback EnableCallback) ( provider.ID = id provider.callback = callback - if err := eventRegister(provider.ID, globalProviderCallback, uintptr(provider.index), &provider.handle); err != nil { + if err := eventRegister((*windows.GUID)(provider.ID), globalProviderCallback, uintptr(provider.index), &provider.handle); err != nil { return nil, err } @@ -259,8 +246,8 @@ func (provider *Provider) WriteEvent(name string, eventOpts []EventOpt, fieldOpt // the ETW infrastructure. func (provider *Provider) writeEventRaw( descriptor *eventDescriptor, - activityID *windows.GUID, - relatedActivityID *windows.GUID, + activityID *guid.GUID, + relatedActivityID *guid.GUID, metadataBlobs [][]byte, dataBlobs [][]byte) error { @@ -275,5 +262,5 @@ func (provider *Provider) writeEventRaw( dataDescriptors = append(dataDescriptors, newEventDataDescriptor(eventDataDescriptorTypeUserData, blob)) } - return eventWriteTransfer(provider.handle, descriptor, activityID, relatedActivityID, dataDescriptorCount, &dataDescriptors[0]) + return eventWriteTransfer(provider.handle, descriptor, (*windows.GUID)(activityID), (*windows.GUID)(relatedActivityID), dataDescriptorCount, &dataDescriptors[0]) } diff --git a/pkg/etw/sample/sample.go b/pkg/etw/sample/sample.go index f9ece39..d86d43e 100644 --- a/pkg/etw/sample/sample.go +++ b/pkg/etw/sample/sample.go @@ -7,12 +7,11 @@ import ( "os" "github.com/Microsoft/go-winio/pkg/etw" + "github.com/Microsoft/go-winio/pkg/guid" "github.com/sirupsen/logrus" - - "golang.org/x/sys/windows" ) -func callback(sourceID *windows.GUID, state etw.ProviderState, level etw.Level, matchAnyKeyword uint64, matchAllKeyword uint64, filterData uintptr) { +func callback(sourceID *guid.GUID, state etw.ProviderState, level etw.Level, matchAnyKeyword uint64, matchAllKeyword uint64, filterData uintptr) { fmt.Printf("Callback: isEnabled=%d, level=%d, matchAnyKeyword=%d\n", state, level, matchAnyKeyword) } diff --git a/pkg/etwlogrus/hook_test.go b/pkg/etwlogrus/hook_test.go index 26193d2..ed2f519 100644 --- a/pkg/etwlogrus/hook_test.go +++ b/pkg/etwlogrus/hook_test.go @@ -1,8 +1,9 @@ package etwlogrus import ( - "github.com/Microsoft/go-winio/pkg/etw" "testing" + + "github.com/Microsoft/go-winio/pkg/etw" ) func fireEvent(t *testing.T, p *etw.Provider, name string, value interface{}) { diff --git a/pkg/guid/guid.go b/pkg/guid/guid.go new file mode 100644 index 0000000..cc50972 --- /dev/null +++ b/pkg/guid/guid.go @@ -0,0 +1,110 @@ +package guid + +import ( + "crypto/rand" + "encoding/binary" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" + "golang.org/x/sys/windows" +) + +var _ = (json.Marshaler)(&GUID{}) +var _ = (json.Unmarshaler)(&GUID{}) + +// GUID represents a GUID/UUID. It has the same structure as +// golang.org/x/sys/windows.GUID so that it can be used with functions expecting +// that type. It is defined as its own type so that stringification and +// marshaling can be supported. The representation matches that used by native +// Windows code. +type GUID windows.GUID + +// NewV4 returns a new version 4 (pseudorandom) GUID, as defined by RFC 4122. +func NewV4() (*GUID, error) { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + return nil, err + } + + var g GUID + g.Data1 = binary.LittleEndian.Uint32(b[0:4]) + g.Data2 = binary.LittleEndian.Uint16(b[4:6]) + g.Data3 = binary.LittleEndian.Uint16(b[6:8]) + copy(g.Data4[:], b[8:16]) + + g.Data3 = (g.Data3 & 0x0fff) | 0x4000 // Version 4 (randomly generated) + g.Data4[0] = (g.Data4[0] & 0x3f) | 0x80 // RFC4122 variant + return &g, nil +} + +func (g *GUID) String() string { + return fmt.Sprintf( + "%08x-%04x-%04x-%04x-%012x", + g.Data1, + g.Data2, + g.Data3, + g.Data4[:2], + g.Data4[2:]) +} + +// FromString parses a string containing a GUID and returns the GUID. The only +// format currently supported is the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` +// format. +func FromString(s string) (*GUID, error) { + if len(s) != 36 { + return nil, errors.New("invalid GUID format (length)") + } + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return nil, errors.New("invalid GUID format (dashes)") + } + + var g GUID + + data1, err := strconv.ParseUint(s[0:8], 16, 32) + if err != nil { + return nil, errors.Wrap(err, "invalid GUID format (Data1)") + } + g.Data1 = uint32(data1) + + data2, err := strconv.ParseUint(s[9:13], 16, 16) + if err != nil { + return nil, errors.Wrap(err, "invalid GUID format (Data2)") + } + g.Data2 = uint16(data2) + + data3, err := strconv.ParseUint(s[14:18], 16, 16) + if err != nil { + return nil, errors.Wrap(err, "invalid GUID format (Data3)") + } + g.Data3 = uint16(data3) + + for i, x := range []int{19, 21, 24, 26, 28, 30, 32, 34} { + v, err := strconv.ParseUint(s[x:x+2], 16, 8) + if err != nil { + return nil, errors.Wrap(err, "invalid GUID format (Data4)") + } + g.Data4[i] = uint8(v) + } + + return &g, nil +} + +// MarshalJSON marshals the GUID to JSON representation and returns it as a +// slice of bytes. +func (g *GUID) MarshalJSON() ([]byte, error) { + return json.Marshal(g.String()) +} + +// UnmarshalJSON unmarshals a GUID from JSON representation and sets itself to +// the unmarshaled GUID. +func (g *GUID) UnmarshalJSON(data []byte) error { + g2, err := FromString(strings.Trim(string(data), "\"")) + if err != nil { + return err + } + *g = *g2 + return nil +} diff --git a/pkg/guid/guid_test.go b/pkg/guid/guid_test.go new file mode 100644 index 0000000..9123cda --- /dev/null +++ b/pkg/guid/guid_test.go @@ -0,0 +1,107 @@ +package guid + +import ( + "encoding/json" + "fmt" + "testing" +) + +func Test_New(t *testing.T) { + g, err := NewV4() + if err != nil { + t.Fatal(err) + } + g2, err := NewV4() + if err != nil { + t.Fatal(err) + } + if *g == *g2 { + t.Fatalf("GUIDs are equal: %s, %s", g, g2) + } +} + +func Test_FromString(t *testing.T) { + orig := "8e35239e-2084-490e-a3db-ab18ee0744cb" + g, err := FromString(orig) + if err != nil { + t.Fatal(err) + } + s := g.String() + if orig != s { + t.Fatalf("GUIDs not equal: %s, %s", orig, s) + } +} + +func Test_MarshalJSON(t *testing.T) { + g, err := NewV4() + if err != nil { + t.Fatal(err) + } + j, err := json.Marshal(g) + if err != nil { + t.Fatal(err) + } + gj := fmt.Sprintf("\"%s\"", g.String()) + if string(j) != gj { + t.Fatalf("JSON not equal: %s, %s", j, gj) + } +} + +func Test_MarshalJSON_Nested(t *testing.T) { + type test struct { + G *GUID + } + g, err := NewV4() + if err != nil { + t.Fatal(err) + } + t1 := test{g} + j, err := json.Marshal(t1) + if err != nil { + t.Fatal(err) + } + gj := fmt.Sprintf("{\"G\":\"%s\"}", g.String()) + if string(j) != gj { + t.Fatalf("JSON not equal: %s, %s", j, gj) + } +} + +func Test_UnmarshalJSON(t *testing.T) { + g, err := NewV4() + if err != nil { + t.Fatal(err) + } + j, err := json.Marshal(g) + if err != nil { + t.Fatal(err) + } + var g2 GUID + if err := json.Unmarshal(j, &g2); err != nil { + t.Fatal(err) + } + if *g != g2 { + t.Fatalf("GUIDs not equal: %s, %s", g, &g2) + } +} + +func Test_UnmarshalJSON_Nested(t *testing.T) { + type test struct { + G *GUID + } + g, err := NewV4() + if err != nil { + t.Fatal(err) + } + t1 := test{g} + j, err := json.Marshal(t1) + if err != nil { + t.Fatal(err) + } + var t2 test + if err := json.Unmarshal(j, &t2); err != nil { + t.Fatal(err) + } + if *t1.G != *t2.G { + t.Fatalf("GUIDs not equal: %v, %v", t1.G, t2.G) + } +} diff --git a/tools/etw-provider-gen/main.go b/tools/etw-provider-gen/main.go index 96b76aa..1da4657 100644 --- a/tools/etw-provider-gen/main.go +++ b/tools/etw-provider-gen/main.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/Microsoft/go-winio/internal/etw" + "github.com/Microsoft/go-winio/pkg/etw" ) func main() {