├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── swbemservices.go ├── swbemservices_test.go ├── wmi.go └── wmi_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Stack Exchange 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wmi 2 | === 3 | 4 | Package wmi provides a WQL interface to Windows WMI. 5 | 6 | Note: It interfaces with WMI on the local machine, therefore it only runs on Windows. 7 | 8 | --- 9 | 10 | NOTE: This project is no longer being actively maintained. 11 | 12 | We recommend you refer to this fork: https://github.com/yusufpapurcu/wmi 13 | 14 | --- 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/StackExchange/wmi 2 | 3 | go 1.13 4 | 5 | require github.com/go-ole/go-ole v1.2.5 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= 2 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 3 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1cHUZgO1Ebq5r2hIjfo= 4 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 5 | -------------------------------------------------------------------------------- /swbemservices.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package wmi 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | "runtime" 9 | "sync" 10 | 11 | "github.com/go-ole/go-ole" 12 | "github.com/go-ole/go-ole/oleutil" 13 | ) 14 | 15 | // SWbemServices is used to access wmi. See https://msdn.microsoft.com/en-us/library/aa393719(v=vs.85).aspx 16 | type SWbemServices struct { 17 | //TODO: track namespace. Not sure if we can re connect to a different namespace using the same instance 18 | cWMIClient *Client //This could also be an embedded struct, but then we would need to branch on Client vs SWbemServices in the Query method 19 | sWbemLocatorIUnknown *ole.IUnknown 20 | sWbemLocatorIDispatch *ole.IDispatch 21 | queries chan *queryRequest 22 | closeError chan error 23 | lQueryorClose sync.Mutex 24 | } 25 | 26 | type queryRequest struct { 27 | query string 28 | dst interface{} 29 | args []interface{} 30 | finished chan error 31 | } 32 | 33 | // InitializeSWbemServices will return a new SWbemServices object that can be used to query WMI 34 | func InitializeSWbemServices(c *Client, connectServerArgs ...interface{}) (*SWbemServices, error) { 35 | //fmt.Println("InitializeSWbemServices: Starting") 36 | //TODO: implement connectServerArgs as optional argument for init with connectServer call 37 | s := new(SWbemServices) 38 | s.cWMIClient = c 39 | s.queries = make(chan *queryRequest) 40 | initError := make(chan error) 41 | go s.process(initError) 42 | 43 | err, ok := <-initError 44 | if ok { 45 | return nil, err //Send error to caller 46 | } 47 | //fmt.Println("InitializeSWbemServices: Finished") 48 | return s, nil 49 | } 50 | 51 | // Close will clear and release all of the SWbemServices resources 52 | func (s *SWbemServices) Close() error { 53 | s.lQueryorClose.Lock() 54 | if s == nil || s.sWbemLocatorIDispatch == nil { 55 | s.lQueryorClose.Unlock() 56 | return fmt.Errorf("SWbemServices is not Initialized") 57 | } 58 | if s.queries == nil { 59 | s.lQueryorClose.Unlock() 60 | return fmt.Errorf("SWbemServices has been closed") 61 | } 62 | //fmt.Println("Close: sending close request") 63 | var result error 64 | ce := make(chan error) 65 | s.closeError = ce //Race condition if multiple callers to close. May need to lock here 66 | close(s.queries) //Tell background to shut things down 67 | s.lQueryorClose.Unlock() 68 | err, ok := <-ce 69 | if ok { 70 | result = err 71 | } 72 | //fmt.Println("Close: finished") 73 | return result 74 | } 75 | 76 | func (s *SWbemServices) process(initError chan error) { 77 | //fmt.Println("process: starting background thread initialization") 78 | //All OLE/WMI calls must happen on the same initialized thead, so lock this goroutine 79 | runtime.LockOSThread() 80 | defer runtime.UnlockOSThread() 81 | 82 | err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) 83 | if err != nil { 84 | oleCode := err.(*ole.OleError).Code() 85 | if oleCode != ole.S_OK && oleCode != S_FALSE { 86 | initError <- fmt.Errorf("ole.CoInitializeEx error: %v", err) 87 | return 88 | } 89 | } 90 | defer ole.CoUninitialize() 91 | 92 | unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator") 93 | if err != nil { 94 | initError <- fmt.Errorf("CreateObject SWbemLocator error: %v", err) 95 | return 96 | } else if unknown == nil { 97 | initError <- ErrNilCreateObject 98 | return 99 | } 100 | defer unknown.Release() 101 | s.sWbemLocatorIUnknown = unknown 102 | 103 | dispatch, err := s.sWbemLocatorIUnknown.QueryInterface(ole.IID_IDispatch) 104 | if err != nil { 105 | initError <- fmt.Errorf("SWbemLocator QueryInterface error: %v", err) 106 | return 107 | } 108 | defer dispatch.Release() 109 | s.sWbemLocatorIDispatch = dispatch 110 | 111 | // we can't do the ConnectServer call outside the loop unless we find a way to track and re-init the connectServerArgs 112 | //fmt.Println("process: initialized. closing initError") 113 | close(initError) 114 | //fmt.Println("process: waiting for queries") 115 | for q := range s.queries { 116 | //fmt.Printf("process: new query: len(query)=%d\n", len(q.query)) 117 | errQuery := s.queryBackground(q) 118 | //fmt.Println("process: s.queryBackground finished") 119 | if errQuery != nil { 120 | q.finished <- errQuery 121 | } 122 | close(q.finished) 123 | } 124 | //fmt.Println("process: queries channel closed") 125 | s.queries = nil //set channel to nil so we know it is closed 126 | //TODO: I think the Release/Clear calls can panic if things are in a bad state. 127 | //TODO: May need to recover from panics and send error to method caller instead. 128 | close(s.closeError) 129 | } 130 | 131 | // Query runs the WQL query using a SWbemServices instance and appends the values to dst. 132 | // 133 | // dst must have type *[]S or *[]*S, for some struct type S. Fields selected in 134 | // the query must have the same name in dst. Supported types are all signed and 135 | // unsigned integers, time.Time, string, bool, or a pointer to one of those. 136 | // Array types are not supported. 137 | // 138 | // By default, the local machine and default namespace are used. These can be 139 | // changed using connectServerArgs. See 140 | // http://msdn.microsoft.com/en-us/library/aa393720.aspx for details. 141 | func (s *SWbemServices) Query(query string, dst interface{}, connectServerArgs ...interface{}) error { 142 | s.lQueryorClose.Lock() 143 | if s == nil || s.sWbemLocatorIDispatch == nil { 144 | s.lQueryorClose.Unlock() 145 | return fmt.Errorf("SWbemServices is not Initialized") 146 | } 147 | if s.queries == nil { 148 | s.lQueryorClose.Unlock() 149 | return fmt.Errorf("SWbemServices has been closed") 150 | } 151 | 152 | //fmt.Println("Query: Sending query request") 153 | qr := queryRequest{ 154 | query: query, 155 | dst: dst, 156 | args: connectServerArgs, 157 | finished: make(chan error), 158 | } 159 | s.queries <- &qr 160 | s.lQueryorClose.Unlock() 161 | err, ok := <-qr.finished 162 | if ok { 163 | //fmt.Println("Query: Finished with error") 164 | return err //Send error to caller 165 | } 166 | //fmt.Println("Query: Finished") 167 | return nil 168 | } 169 | 170 | func (s *SWbemServices) queryBackground(q *queryRequest) error { 171 | if s == nil || s.sWbemLocatorIDispatch == nil { 172 | return fmt.Errorf("SWbemServices is not Initialized") 173 | } 174 | wmi := s.sWbemLocatorIDispatch //Should just rename in the code, but this will help as we break things apart 175 | //fmt.Println("queryBackground: Starting") 176 | 177 | dv := reflect.ValueOf(q.dst) 178 | if dv.Kind() != reflect.Ptr || dv.IsNil() { 179 | return ErrInvalidEntityType 180 | } 181 | dv = dv.Elem() 182 | mat, elemType := checkMultiArg(dv) 183 | if mat == multiArgTypeInvalid { 184 | return ErrInvalidEntityType 185 | } 186 | 187 | // service is a SWbemServices 188 | serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", q.args...) 189 | if err != nil { 190 | return err 191 | } 192 | service := serviceRaw.ToIDispatch() 193 | defer serviceRaw.Clear() 194 | 195 | // result is a SWBemObjectSet 196 | resultRaw, err := oleutil.CallMethod(service, "ExecQuery", q.query) 197 | if err != nil { 198 | return err 199 | } 200 | result := resultRaw.ToIDispatch() 201 | defer resultRaw.Clear() 202 | 203 | count, err := oleInt64(result, "Count") 204 | if err != nil { 205 | return err 206 | } 207 | 208 | enumProperty, err := result.GetProperty("_NewEnum") 209 | if err != nil { 210 | return err 211 | } 212 | defer enumProperty.Clear() 213 | 214 | enum, err := enumProperty.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant) 215 | if err != nil { 216 | return err 217 | } 218 | if enum == nil { 219 | return fmt.Errorf("can't get IEnumVARIANT, enum is nil") 220 | } 221 | defer enum.Release() 222 | 223 | // Initialize a slice with Count capacity 224 | dv.Set(reflect.MakeSlice(dv.Type(), 0, int(count))) 225 | 226 | var errFieldMismatch error 227 | for itemRaw, length, err := enum.Next(1); length > 0; itemRaw, length, err = enum.Next(1) { 228 | if err != nil { 229 | return err 230 | } 231 | 232 | err := func() error { 233 | // item is a SWbemObject, but really a Win32_Process 234 | item := itemRaw.ToIDispatch() 235 | defer item.Release() 236 | 237 | ev := reflect.New(elemType) 238 | if err = s.cWMIClient.loadEntity(ev.Interface(), item); err != nil { 239 | if _, ok := err.(*ErrFieldMismatch); ok { 240 | // We continue loading entities even in the face of field mismatch errors. 241 | // If we encounter any other error, that other error is returned. Otherwise, 242 | // an ErrFieldMismatch is returned. 243 | errFieldMismatch = err 244 | } else { 245 | return err 246 | } 247 | } 248 | if mat != multiArgTypeStructPtr { 249 | ev = ev.Elem() 250 | } 251 | dv.Set(reflect.Append(dv, ev)) 252 | return nil 253 | }() 254 | if err != nil { 255 | return err 256 | } 257 | } 258 | //fmt.Println("queryBackground: Finished") 259 | return errFieldMismatch 260 | } 261 | -------------------------------------------------------------------------------- /swbemservices_test.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package wmi 4 | 5 | import ( 6 | "fmt" 7 | "runtime" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestWbemQuery(t *testing.T) { 13 | s, err := InitializeSWbemServices(DefaultClient) 14 | if err != nil { 15 | t.Fatalf("InitializeSWbemServices: %s", err) 16 | } 17 | 18 | var dst []Win32_Process 19 | q := CreateQuery(&dst, "WHERE name='lsass.exe'") 20 | errQuery := s.Query(q, &dst) 21 | if errQuery != nil { 22 | t.Fatalf("Query1: %s", errQuery) 23 | } 24 | count := len(dst) 25 | if count < 1 { 26 | t.Fatal("Query1: no results found for lsass.exe") 27 | } 28 | //fmt.Printf("dst[0].ProcessID=%d\n", dst[0].ProcessId) 29 | 30 | q2 := CreateQuery(&dst, "WHERE name='svchost.exe'") 31 | errQuery = s.Query(q2, &dst) 32 | if errQuery != nil { 33 | t.Fatalf("Query2: %s", errQuery) 34 | } 35 | count = len(dst) 36 | if count < 1 { 37 | t.Fatal("Query2: no results found for svchost.exe") 38 | } 39 | //for index, item := range dst { 40 | // fmt.Printf("dst[%d].ProcessID=%d\n", index, item.ProcessId) 41 | //} 42 | errClose := s.Close() 43 | if errClose != nil { 44 | t.Fatalf("Close: %s", errClose) 45 | } 46 | } 47 | 48 | func TestWbemQueryNamespace(t *testing.T) { 49 | s, err := InitializeSWbemServices(DefaultClient) 50 | if err != nil { 51 | t.Fatalf("InitializeSWbemServices: %s", err) 52 | } 53 | var dst []MSFT_NetAdapter 54 | q := CreateQuery(&dst, "") 55 | errQuery := s.Query(q, &dst, nil, "root\\StandardCimv2") 56 | if errQuery != nil { 57 | t.Fatalf("Query: %s", errQuery) 58 | } 59 | count := len(dst) 60 | if count < 1 { 61 | t.Fatal("Query: no results found for MSFT_NetAdapter in root\\StandardCimv2") 62 | } 63 | errClose := s.Close() 64 | if errClose != nil { 65 | t.Fatalf("Close: %s", errClose) 66 | } 67 | } 68 | 69 | // Run using: go test -run TestWbemMemory -timeout 60m 70 | func TestWbemMemory(t *testing.T) { 71 | s, err := InitializeSWbemServices(DefaultClient) 72 | if err != nil { 73 | t.Fatalf("InitializeSWbemServices: %s", err) 74 | } 75 | start := time.Now() 76 | limit := 500000 77 | fmt.Printf("Benchmark Iterations: %d (Memory should stabilize around 7MB after ~3000)\n", limit) 78 | var privateMB, allocMB, allocTotalMB float64 79 | for i := 0; i < limit; i++ { 80 | privateMB, allocMB, allocTotalMB = WbemGetMemoryUsageMB(s) 81 | if i%100 == 0 { 82 | privateMB, allocMB, allocTotalMB = WbemGetMemoryUsageMB(s) 83 | fmt.Printf("Time: %4ds Count: %5d Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", time.Now().Sub(start)/time.Second, i, privateMB, allocMB, allocTotalMB) 84 | } 85 | } 86 | errClose := s.Close() 87 | if errClose != nil { 88 | t.Fatalf("Close: %s", err) 89 | } 90 | fmt.Printf("Final Time: %4ds Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", time.Now().Sub(start)/time.Second, privateMB, allocMB, allocTotalMB) 91 | } 92 | 93 | func WbemGetMemoryUsageMB(s *SWbemServices) (float64, float64, float64) { 94 | runtime.ReadMemStats(&mMemoryUsageMB) 95 | errGetMemoryUsageMB = s.Query(qGetMemoryUsageMB, &dstGetMemoryUsageMB) 96 | if errGetMemoryUsageMB != nil { 97 | fmt.Println("ERROR GetMemoryUsage", errGetMemoryUsageMB) 98 | return 0, 0, 0 99 | } 100 | return float64(dstGetMemoryUsageMB[0].WorkingSetPrivate) / MB, float64(mMemoryUsageMB.Alloc) / MB, float64(mMemoryUsageMB.TotalAlloc) / MB 101 | } 102 | 103 | //Run all benchmarks (should run for at least 60s to get a stable number): 104 | //go test -run=NONE -bench=Version -benchtime=120s 105 | 106 | //Individual benchmarks: 107 | //go test -run=NONE -bench=NewVersion -benchtime=120s 108 | func BenchmarkNewVersion(b *testing.B) { 109 | s, err := InitializeSWbemServices(DefaultClient) 110 | if err != nil { 111 | b.Fatalf("InitializeSWbemServices: %s", err) 112 | } 113 | var dst []Win32_OperatingSystem 114 | q := CreateQuery(&dst, "") 115 | for n := 0; n < b.N; n++ { 116 | errQuery := s.Query(q, &dst) 117 | if errQuery != nil { 118 | b.Fatalf("Query%d: %s", n, errQuery) 119 | } 120 | count := len(dst) 121 | if count < 1 { 122 | b.Fatalf("Query%d: no results found for Win32_OperatingSystem", n) 123 | } 124 | } 125 | errClose := s.Close() 126 | if errClose != nil { 127 | b.Fatalf("Close: %s", errClose) 128 | } 129 | } 130 | 131 | //go test -run=NONE -bench=OldVersion -benchtime=120s 132 | func BenchmarkOldVersion(b *testing.B) { 133 | var dst []Win32_OperatingSystem 134 | q := CreateQuery(&dst, "") 135 | for n := 0; n < b.N; n++ { 136 | errQuery := Query(q, &dst) 137 | if errQuery != nil { 138 | b.Fatalf("Query%d: %s", n, errQuery) 139 | } 140 | count := len(dst) 141 | if count < 1 { 142 | b.Fatalf("Query%d: no results found for Win32_OperatingSystem", n) 143 | } 144 | } 145 | } 146 | 147 | type MSFT_NetAdapter struct { 148 | Name string 149 | InterfaceIndex int 150 | DriverDescription string 151 | } 152 | -------------------------------------------------------------------------------- /wmi.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | /* 4 | Package wmi provides a WQL interface for WMI on Windows. 5 | 6 | Example code to print names of running processes: 7 | 8 | type Win32_Process struct { 9 | Name string 10 | } 11 | 12 | func main() { 13 | var dst []Win32_Process 14 | q := wmi.CreateQuery(&dst, "") 15 | err := wmi.Query(q, &dst) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | for i, v := range dst { 20 | println(i, v.Name) 21 | } 22 | } 23 | 24 | */ 25 | package wmi 26 | 27 | import ( 28 | "bytes" 29 | "errors" 30 | "fmt" 31 | "log" 32 | "os" 33 | "reflect" 34 | "runtime" 35 | "strconv" 36 | "strings" 37 | "sync" 38 | "time" 39 | 40 | "github.com/go-ole/go-ole" 41 | "github.com/go-ole/go-ole/oleutil" 42 | ) 43 | 44 | var l = log.New(os.Stdout, "", log.LstdFlags) 45 | 46 | var ( 47 | ErrInvalidEntityType = errors.New("wmi: invalid entity type") 48 | // ErrNilCreateObject is the error returned if CreateObject returns nil even 49 | // if the error was nil. 50 | ErrNilCreateObject = errors.New("wmi: create object returned nil") 51 | lock sync.Mutex 52 | ) 53 | 54 | // S_FALSE is returned by CoInitializeEx if it was already called on this thread. 55 | const S_FALSE = 0x00000001 56 | 57 | // QueryNamespace invokes Query with the given namespace on the local machine. 58 | func QueryNamespace(query string, dst interface{}, namespace string) error { 59 | return Query(query, dst, nil, namespace) 60 | } 61 | 62 | // Query runs the WQL query and appends the values to dst. 63 | // 64 | // dst must have type *[]S or *[]*S, for some struct type S. Fields selected in 65 | // the query must have the same name in dst. Supported types are all signed and 66 | // unsigned integers, time.Time, string, bool, or a pointer to one of those. 67 | // Array types are not supported. 68 | // 69 | // By default, the local machine and default namespace are used. These can be 70 | // changed using connectServerArgs. See 71 | // https://docs.microsoft.com/en-us/windows/desktop/WmiSdk/swbemlocator-connectserver 72 | // for details. 73 | // 74 | // Query is a wrapper around DefaultClient.Query. 75 | func Query(query string, dst interface{}, connectServerArgs ...interface{}) error { 76 | if DefaultClient.SWbemServicesClient == nil { 77 | return DefaultClient.Query(query, dst, connectServerArgs...) 78 | } 79 | return DefaultClient.SWbemServicesClient.Query(query, dst, connectServerArgs...) 80 | } 81 | 82 | // CallMethod calls a method named methodName on an instance of the class named 83 | // className, with the given params. 84 | // 85 | // CallMethod is a wrapper around DefaultClient.CallMethod. 86 | func CallMethod(connectServerArgs []interface{}, className, methodName string, params []interface{}) (int32, error) { 87 | return DefaultClient.CallMethod(connectServerArgs, className, methodName, params) 88 | } 89 | 90 | // A Client is an WMI query client. 91 | // 92 | // Its zero value (DefaultClient) is a usable client. 93 | type Client struct { 94 | // NonePtrZero specifies if nil values for fields which aren't pointers 95 | // should be returned as the field types zero value. 96 | // 97 | // Setting this to true allows stucts without pointer fields to be used 98 | // without the risk failure should a nil value returned from WMI. 99 | NonePtrZero bool 100 | 101 | // PtrNil specifies if nil values for pointer fields should be returned 102 | // as nil. 103 | // 104 | // Setting this to true will set pointer fields to nil where WMI 105 | // returned nil, otherwise the types zero value will be returned. 106 | PtrNil bool 107 | 108 | // AllowMissingFields specifies that struct fields not present in the 109 | // query result should not result in an error. 110 | // 111 | // Setting this to true allows custom queries to be used with full 112 | // struct definitions instead of having to define multiple structs. 113 | AllowMissingFields bool 114 | 115 | // SWbemServiceClient is an optional SWbemServices object that can be 116 | // initialized and then reused across multiple queries. If it is null 117 | // then the method will initialize a new temporary client each time. 118 | SWbemServicesClient *SWbemServices 119 | } 120 | 121 | // DefaultClient is the default Client and is used by Query, QueryNamespace, and CallMethod. 122 | var DefaultClient = &Client{} 123 | 124 | // coinitService coinitializes WMI service. If no error is returned, a cleanup function 125 | // is returned which must be executed (usually deferred) to clean up allocated resources. 126 | func (c *Client) coinitService(connectServerArgs ...interface{}) (*ole.IDispatch, func(), error) { 127 | var unknown *ole.IUnknown 128 | var wmi *ole.IDispatch 129 | var serviceRaw *ole.VARIANT 130 | 131 | // be sure teardown happens in the reverse 132 | // order from that which they were created 133 | deferFn := func() { 134 | if serviceRaw != nil { 135 | serviceRaw.Clear() 136 | } 137 | if wmi != nil { 138 | wmi.Release() 139 | } 140 | if unknown != nil { 141 | unknown.Release() 142 | } 143 | ole.CoUninitialize() 144 | } 145 | 146 | // if we error'ed here, clean up immediately 147 | var err error 148 | defer func() { 149 | if err != nil { 150 | deferFn() 151 | } 152 | }() 153 | 154 | err = ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) 155 | if err != nil { 156 | oleCode := err.(*ole.OleError).Code() 157 | if oleCode != ole.S_OK && oleCode != S_FALSE { 158 | return nil, nil, err 159 | } 160 | } 161 | 162 | unknown, err = oleutil.CreateObject("WbemScripting.SWbemLocator") 163 | if err != nil { 164 | return nil, nil, err 165 | } else if unknown == nil { 166 | return nil, nil, ErrNilCreateObject 167 | } 168 | 169 | wmi, err = unknown.QueryInterface(ole.IID_IDispatch) 170 | if err != nil { 171 | return nil, nil, err 172 | } 173 | 174 | // service is a SWbemServices 175 | serviceRaw, err = oleutil.CallMethod(wmi, "ConnectServer", connectServerArgs...) 176 | if err != nil { 177 | return nil, nil, err 178 | } 179 | 180 | return serviceRaw.ToIDispatch(), deferFn, nil 181 | } 182 | 183 | // CallMethod calls a WMI method named methodName on an instance 184 | // of the class named className. It passes in the arguments given 185 | // in params. Use connectServerArgs to customize the machine and 186 | // namespace; by default, the local machine and default namespace 187 | // are used. See 188 | // https://docs.microsoft.com/en-us/windows/desktop/WmiSdk/swbemlocator-connectserver 189 | // for details. 190 | func (c *Client) CallMethod(connectServerArgs []interface{}, className, methodName string, params []interface{}) (int32, error) { 191 | service, cleanup, err := c.coinitService(connectServerArgs...) 192 | if err != nil { 193 | return 0, fmt.Errorf("coinit: %v", err) 194 | } 195 | defer cleanup() 196 | 197 | // Get class 198 | classRaw, err := oleutil.CallMethod(service, "Get", className) 199 | if err != nil { 200 | return 0, fmt.Errorf("CallMethod Get class %s: %v", className, err) 201 | } 202 | class := classRaw.ToIDispatch() 203 | defer classRaw.Clear() 204 | 205 | // Run method 206 | resultRaw, err := oleutil.CallMethod(class, methodName, params...) 207 | if err != nil { 208 | return 0, fmt.Errorf("CallMethod %s.%s: %v", className, methodName, err) 209 | } 210 | resultInt, ok := resultRaw.Value().(int32) 211 | if !ok { 212 | return 0, fmt.Errorf("return value was not an int32: %v (%T)", resultRaw, resultRaw) 213 | } 214 | 215 | return resultInt, nil 216 | } 217 | 218 | // Query runs the WQL query and appends the values to dst. 219 | // 220 | // dst must have type *[]S or *[]*S, for some struct type S. Fields selected in 221 | // the query must have the same name in dst. Supported types are all signed and 222 | // unsigned integers, time.Time, string, bool, or a pointer to one of those. 223 | // Array types are not supported. 224 | // 225 | // By default, the local machine and default namespace are used. These can be 226 | // changed using connectServerArgs. See 227 | // https://docs.microsoft.com/en-us/windows/desktop/WmiSdk/swbemlocator-connectserver 228 | // for details. 229 | func (c *Client) Query(query string, dst interface{}, connectServerArgs ...interface{}) error { 230 | dv := reflect.ValueOf(dst) 231 | if dv.Kind() != reflect.Ptr || dv.IsNil() { 232 | return ErrInvalidEntityType 233 | } 234 | dv = dv.Elem() 235 | mat, elemType := checkMultiArg(dv) 236 | if mat == multiArgTypeInvalid { 237 | return ErrInvalidEntityType 238 | } 239 | 240 | lock.Lock() 241 | defer lock.Unlock() 242 | runtime.LockOSThread() 243 | defer runtime.UnlockOSThread() 244 | 245 | service, cleanup, err := c.coinitService(connectServerArgs...) 246 | if err != nil { 247 | return err 248 | } 249 | defer cleanup() 250 | 251 | // result is a SWBemObjectSet 252 | resultRaw, err := oleutil.CallMethod(service, "ExecQuery", query) 253 | if err != nil { 254 | return err 255 | } 256 | result := resultRaw.ToIDispatch() 257 | defer resultRaw.Clear() 258 | 259 | count, err := oleInt64(result, "Count") 260 | if err != nil { 261 | return err 262 | } 263 | 264 | enumProperty, err := result.GetProperty("_NewEnum") 265 | if err != nil { 266 | return err 267 | } 268 | defer enumProperty.Clear() 269 | 270 | enum, err := enumProperty.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant) 271 | if err != nil { 272 | return err 273 | } 274 | if enum == nil { 275 | return fmt.Errorf("can't get IEnumVARIANT, enum is nil") 276 | } 277 | defer enum.Release() 278 | 279 | // Initialize a slice with Count capacity 280 | dv.Set(reflect.MakeSlice(dv.Type(), 0, int(count))) 281 | 282 | var errFieldMismatch error 283 | for itemRaw, length, err := enum.Next(1); length > 0; itemRaw, length, err = enum.Next(1) { 284 | if err != nil { 285 | return err 286 | } 287 | 288 | err := func() error { 289 | // item is a SWbemObject, but really a Win32_Process 290 | item := itemRaw.ToIDispatch() 291 | defer item.Release() 292 | 293 | ev := reflect.New(elemType) 294 | if err = c.loadEntity(ev.Interface(), item); err != nil { 295 | if _, ok := err.(*ErrFieldMismatch); ok { 296 | // We continue loading entities even in the face of field mismatch errors. 297 | // If we encounter any other error, that other error is returned. Otherwise, 298 | // an ErrFieldMismatch is returned. 299 | errFieldMismatch = err 300 | } else { 301 | return err 302 | } 303 | } 304 | if mat != multiArgTypeStructPtr { 305 | ev = ev.Elem() 306 | } 307 | dv.Set(reflect.Append(dv, ev)) 308 | return nil 309 | }() 310 | if err != nil { 311 | return err 312 | } 313 | } 314 | return errFieldMismatch 315 | } 316 | 317 | // ErrFieldMismatch is returned when a field is to be loaded into a different 318 | // type than the one it was stored from, or when a field is missing or 319 | // unexported in the destination struct. 320 | // StructType is the type of the struct pointed to by the destination argument. 321 | type ErrFieldMismatch struct { 322 | StructType reflect.Type 323 | FieldName string 324 | Reason string 325 | } 326 | 327 | func (e *ErrFieldMismatch) Error() string { 328 | return fmt.Sprintf("wmi: cannot load field %q into a %q: %s", 329 | e.FieldName, e.StructType, e.Reason) 330 | } 331 | 332 | var timeType = reflect.TypeOf(time.Time{}) 333 | 334 | // loadEntity loads a SWbemObject into a struct pointer. 335 | func (c *Client) loadEntity(dst interface{}, src *ole.IDispatch) (errFieldMismatch error) { 336 | v := reflect.ValueOf(dst).Elem() 337 | for i := 0; i < v.NumField(); i++ { 338 | f := v.Field(i) 339 | of := f 340 | isPtr := f.Kind() == reflect.Ptr 341 | if isPtr { 342 | ptr := reflect.New(f.Type().Elem()) 343 | f.Set(ptr) 344 | f = f.Elem() 345 | } 346 | n := v.Type().Field(i).Name 347 | if n[0] < 'A' || n[0] > 'Z' { 348 | continue 349 | } 350 | if !f.CanSet() { 351 | return &ErrFieldMismatch{ 352 | StructType: of.Type(), 353 | FieldName: n, 354 | Reason: "CanSet() is false", 355 | } 356 | } 357 | prop, err := oleutil.GetProperty(src, n) 358 | if err != nil { 359 | if !c.AllowMissingFields { 360 | errFieldMismatch = &ErrFieldMismatch{ 361 | StructType: of.Type(), 362 | FieldName: n, 363 | Reason: "no such struct field", 364 | } 365 | } 366 | continue 367 | } 368 | defer prop.Clear() 369 | 370 | if prop.VT == 0x1 { //VT_NULL 371 | continue 372 | } 373 | 374 | switch val := prop.Value().(type) { 375 | case int8, int16, int32, int64, int: 376 | v := reflect.ValueOf(val).Int() 377 | switch f.Kind() { 378 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 379 | f.SetInt(v) 380 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 381 | f.SetUint(uint64(v)) 382 | default: 383 | return &ErrFieldMismatch{ 384 | StructType: of.Type(), 385 | FieldName: n, 386 | Reason: "not an integer class", 387 | } 388 | } 389 | case uint8, uint16, uint32, uint64: 390 | v := reflect.ValueOf(val).Uint() 391 | switch f.Kind() { 392 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 393 | f.SetInt(int64(v)) 394 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 395 | f.SetUint(v) 396 | default: 397 | return &ErrFieldMismatch{ 398 | StructType: of.Type(), 399 | FieldName: n, 400 | Reason: "not an integer class", 401 | } 402 | } 403 | case string: 404 | switch f.Kind() { 405 | case reflect.String: 406 | f.SetString(val) 407 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 408 | iv, err := strconv.ParseInt(val, 10, 64) 409 | if err != nil { 410 | return err 411 | } 412 | f.SetInt(iv) 413 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 414 | uv, err := strconv.ParseUint(val, 10, 64) 415 | if err != nil { 416 | return err 417 | } 418 | f.SetUint(uv) 419 | case reflect.Struct: 420 | switch f.Type() { 421 | case timeType: 422 | if len(val) == 25 { 423 | mins, err := strconv.Atoi(val[22:]) 424 | if err != nil { 425 | return err 426 | } 427 | val = val[:22] + fmt.Sprintf("%02d%02d", mins/60, mins%60) 428 | } 429 | t, err := time.Parse("20060102150405.000000-0700", val) 430 | if err != nil { 431 | return err 432 | } 433 | f.Set(reflect.ValueOf(t)) 434 | } 435 | } 436 | case bool: 437 | switch f.Kind() { 438 | case reflect.Bool: 439 | f.SetBool(val) 440 | default: 441 | return &ErrFieldMismatch{ 442 | StructType: of.Type(), 443 | FieldName: n, 444 | Reason: "not a bool", 445 | } 446 | } 447 | case float32: 448 | switch f.Kind() { 449 | case reflect.Float32: 450 | f.SetFloat(float64(val)) 451 | default: 452 | return &ErrFieldMismatch{ 453 | StructType: of.Type(), 454 | FieldName: n, 455 | Reason: "not a Float32", 456 | } 457 | } 458 | default: 459 | if f.Kind() == reflect.Slice { 460 | switch f.Type().Elem().Kind() { 461 | case reflect.String: 462 | safeArray := prop.ToArray() 463 | if safeArray != nil { 464 | arr := safeArray.ToValueArray() 465 | fArr := reflect.MakeSlice(f.Type(), len(arr), len(arr)) 466 | for i, v := range arr { 467 | s := fArr.Index(i) 468 | s.SetString(v.(string)) 469 | } 470 | f.Set(fArr) 471 | } 472 | case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: 473 | safeArray := prop.ToArray() 474 | if safeArray != nil { 475 | arr := safeArray.ToValueArray() 476 | fArr := reflect.MakeSlice(f.Type(), len(arr), len(arr)) 477 | for i, v := range arr { 478 | s := fArr.Index(i) 479 | s.SetUint(reflect.ValueOf(v).Uint()) 480 | } 481 | f.Set(fArr) 482 | } 483 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: 484 | safeArray := prop.ToArray() 485 | if safeArray != nil { 486 | arr := safeArray.ToValueArray() 487 | fArr := reflect.MakeSlice(f.Type(), len(arr), len(arr)) 488 | for i, v := range arr { 489 | s := fArr.Index(i) 490 | s.SetInt(reflect.ValueOf(v).Int()) 491 | } 492 | f.Set(fArr) 493 | } 494 | default: 495 | return &ErrFieldMismatch{ 496 | StructType: of.Type(), 497 | FieldName: n, 498 | Reason: fmt.Sprintf("unsupported slice type (%T)", val), 499 | } 500 | } 501 | } else { 502 | typeof := reflect.TypeOf(val) 503 | if typeof == nil && (isPtr || c.NonePtrZero) { 504 | if (isPtr && c.PtrNil) || (!isPtr && c.NonePtrZero) { 505 | of.Set(reflect.Zero(of.Type())) 506 | } 507 | break 508 | } 509 | return &ErrFieldMismatch{ 510 | StructType: of.Type(), 511 | FieldName: n, 512 | Reason: fmt.Sprintf("unsupported type (%T)", val), 513 | } 514 | } 515 | } 516 | } 517 | return errFieldMismatch 518 | } 519 | 520 | type multiArgType int 521 | 522 | const ( 523 | multiArgTypeInvalid multiArgType = iota 524 | multiArgTypeStruct 525 | multiArgTypeStructPtr 526 | ) 527 | 528 | // checkMultiArg checks that v has type []S, []*S for some struct type S. 529 | // 530 | // It returns what category the slice's elements are, and the reflect.Type 531 | // that represents S. 532 | func checkMultiArg(v reflect.Value) (m multiArgType, elemType reflect.Type) { 533 | if v.Kind() != reflect.Slice { 534 | return multiArgTypeInvalid, nil 535 | } 536 | elemType = v.Type().Elem() 537 | switch elemType.Kind() { 538 | case reflect.Struct: 539 | return multiArgTypeStruct, elemType 540 | case reflect.Ptr: 541 | elemType = elemType.Elem() 542 | if elemType.Kind() == reflect.Struct { 543 | return multiArgTypeStructPtr, elemType 544 | } 545 | } 546 | return multiArgTypeInvalid, nil 547 | } 548 | 549 | func oleInt64(item *ole.IDispatch, prop string) (int64, error) { 550 | v, err := oleutil.GetProperty(item, prop) 551 | if err != nil { 552 | return 0, err 553 | } 554 | defer v.Clear() 555 | 556 | i := int64(v.Val) 557 | return i, nil 558 | } 559 | 560 | // CreateQuery returns a WQL query string that queries all columns of src. where 561 | // is an optional string that is appended to the query, to be used with WHERE 562 | // clauses. In such a case, the "WHERE" string should appear at the beginning. 563 | // The wmi class is obtained by the name of the type. You can pass a optional 564 | // class throught the variadic class parameter which is useful for anonymous 565 | // structs. 566 | func CreateQuery(src interface{}, where string, class ...string) string { 567 | var b bytes.Buffer 568 | b.WriteString("SELECT ") 569 | s := reflect.Indirect(reflect.ValueOf(src)) 570 | t := s.Type() 571 | if s.Kind() == reflect.Slice { 572 | t = t.Elem() 573 | } 574 | if t.Kind() != reflect.Struct { 575 | return "" 576 | } 577 | var fields []string 578 | for i := 0; i < t.NumField(); i++ { 579 | fields = append(fields, t.Field(i).Name) 580 | } 581 | b.WriteString(strings.Join(fields, ", ")) 582 | b.WriteString(" FROM ") 583 | if len(class) > 0 { 584 | b.WriteString(class[0]) 585 | } else { 586 | b.WriteString(t.Name()) 587 | } 588 | b.WriteString(" " + where) 589 | return b.String() 590 | } 591 | -------------------------------------------------------------------------------- /wmi_test.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package wmi 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "reflect" 10 | "runtime" 11 | "runtime/debug" 12 | "sync" 13 | "testing" 14 | "time" 15 | 16 | ole "github.com/go-ole/go-ole" 17 | "github.com/go-ole/go-ole/oleutil" 18 | ) 19 | 20 | func TestQuery(t *testing.T) { 21 | var dst []Win32_Process 22 | q := CreateQuery(&dst, "") 23 | err := Query(q, &dst) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | 29 | func TestFieldMismatch(t *testing.T) { 30 | type s struct { 31 | Name string 32 | HandleCount uint32 33 | Blah uint32 34 | } 35 | var dst []s 36 | err := Query("SELECT Name, HandleCount FROM Win32_Process", &dst) 37 | if err == nil || err.Error() != `wmi: cannot load field "Blah" into a "uint32": no such struct field` { 38 | t.Error("Expected err field mismatch") 39 | } 40 | } 41 | 42 | func TestStrings(t *testing.T) { 43 | printed := false 44 | f := func() { 45 | var dst []Win32_Process 46 | zeros := 0 47 | q := CreateQuery(&dst, "") 48 | for i := 0; i < 5; i++ { 49 | err := Query(q, &dst) 50 | if err != nil { 51 | t.Fatal(err, q) 52 | } 53 | for _, d := range dst { 54 | v := reflect.ValueOf(d) 55 | for j := 0; j < v.NumField(); j++ { 56 | f := v.Field(j) 57 | if f.Kind() != reflect.String { 58 | continue 59 | } 60 | s := f.Interface().(string) 61 | if len(s) > 0 && s[0] == '\u0000' { 62 | zeros++ 63 | if !printed { 64 | printed = true 65 | j, _ := json.MarshalIndent(&d, "", " ") 66 | t.Log("Example with \\u0000:\n", string(j)) 67 | } 68 | } 69 | } 70 | } 71 | fmt.Println("iter", i, "zeros:", zeros) 72 | } 73 | if zeros > 0 { 74 | t.Error("> 0 zeros") 75 | } 76 | } 77 | 78 | fmt.Println("Disabling GC") 79 | debug.SetGCPercent(-1) 80 | f() 81 | fmt.Println("Enabling GC") 82 | debug.SetGCPercent(100) 83 | f() 84 | } 85 | 86 | func TestNamespace(t *testing.T) { 87 | var dst []Win32_Process 88 | q := CreateQuery(&dst, "") 89 | err := QueryNamespace(q, &dst, `root\CIMV2`) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | dst = nil 94 | err = QueryNamespace(q, &dst, `broken\nothing`) 95 | if err == nil { 96 | t.Fatal("expected error") 97 | } 98 | } 99 | 100 | func TestCreateQuery(t *testing.T) { 101 | type TestStruct struct { 102 | Name string 103 | Count int 104 | } 105 | var dst []TestStruct 106 | output := "SELECT Name, Count FROM TestStruct WHERE Count > 2" 107 | tests := []interface{}{ 108 | &dst, 109 | dst, 110 | TestStruct{}, 111 | &TestStruct{}, 112 | } 113 | for i, test := range tests { 114 | if o := CreateQuery(test, "WHERE Count > 2"); o != output { 115 | t.Error("bad output on", i, o) 116 | } 117 | } 118 | if CreateQuery(3, "") != "" { 119 | t.Error("expected empty string") 120 | } 121 | } 122 | 123 | // Run using: go test -run TestMemoryWMISimple -timeout 60m 124 | func _TestMemoryWMISimple(t *testing.T) { 125 | start := time.Now() 126 | limit := 500000 127 | fmt.Printf("Benchmark Iterations: %d (Memory should stabilize around 7MB after ~3000)\n", limit) 128 | var privateMB, allocMB, allocTotalMB float64 129 | //var dst []Win32_PerfRawData_PerfDisk_LogicalDisk 130 | //q := CreateQuery(&dst, "") 131 | for i := 0; i < limit; i++ { 132 | privateMB, allocMB, allocTotalMB = GetMemoryUsageMB() 133 | if i%1000 == 0 { 134 | //privateMB, allocMB, allocTotalMB = GetMemoryUsageMB() 135 | fmt.Printf("Time: %4ds Count: %5d Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", time.Now().Sub(start)/time.Second, i, privateMB, allocMB, allocTotalMB) 136 | } 137 | //Query(q, &dst) 138 | } 139 | //privateMB, allocMB, allocTotalMB = GetMemoryUsageMB() 140 | fmt.Printf("Final Time: %4ds Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", time.Now().Sub(start)/time.Second, privateMB, allocMB, allocTotalMB) 141 | } 142 | 143 | func _TestMemoryWMIConcurrent(t *testing.T) { 144 | if testing.Short() { 145 | return 146 | } 147 | start := time.Now() 148 | limit := 50000 149 | fmt.Println("Total Iterations:", limit) 150 | fmt.Println("No panics mean it succeeded. Other errors are OK. Memory should stabilize after ~1500 iterations.") 151 | runtime.GOMAXPROCS(2) 152 | wg := sync.WaitGroup{} 153 | wg.Add(2) 154 | go func() { 155 | for i := 0; i < limit; i++ { 156 | if i%500 == 0 { 157 | privateMB, allocMB, allocTotalMB := GetMemoryUsageMB() 158 | fmt.Printf("Time: %4ds Count: %4d Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", time.Now().Sub(start)/time.Second, i, privateMB, allocMB, allocTotalMB) 159 | } 160 | var dst []Win32_PerfRawData_PerfDisk_LogicalDisk 161 | q := CreateQuery(&dst, "") 162 | err := Query(q, &dst) 163 | if err != nil { 164 | fmt.Println("ERROR disk", err) 165 | } 166 | } 167 | wg.Done() 168 | }() 169 | go func() { 170 | for i := 0; i > -limit; i-- { 171 | //if i%500 == 0 { 172 | // fmt.Println(i) 173 | //} 174 | var dst []Win32_OperatingSystem 175 | q := CreateQuery(&dst, "") 176 | err := Query(q, &dst) 177 | if err != nil { 178 | fmt.Println("ERROR OS", err) 179 | } 180 | } 181 | wg.Done() 182 | }() 183 | wg.Wait() 184 | //privateMB, allocMB, allocTotalMB := GetMemoryUsageMB() 185 | //fmt.Printf("Final Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", privateMB, allocMB, allocTotalMB) 186 | } 187 | 188 | var lockthread sync.Mutex 189 | var refcount1 int32 190 | var refcount2 int32 191 | var refcount3 int32 192 | 193 | // Test function showing memory leak in unknown.QueryInterface call on Server2016/Windows10 194 | func getRSS(url string, xmlhttp *ole.IDispatch, MinimalTest bool) (int, error) { 195 | 196 | // call using url,nil to see memory leak 197 | if xmlhttp == nil { 198 | //Initialize inside loop if not passed in from outer section 199 | lockthread.Lock() 200 | defer lockthread.Unlock() 201 | runtime.LockOSThread() 202 | defer runtime.UnlockOSThread() 203 | 204 | err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) 205 | if err != nil { 206 | oleCode := err.(*ole.OleError).Code() 207 | if oleCode != ole.S_OK && oleCode != S_FALSE { 208 | return 0, err 209 | } 210 | } 211 | defer ole.CoUninitialize() 212 | 213 | //fmt.Println("CreateObject Microsoft.XMLHTTP") 214 | unknown, err := oleutil.CreateObject("Microsoft.XMLHTTP") 215 | if err != nil { 216 | return 0, err 217 | } 218 | defer func() { refcount1 += xmlhttp.Release() }() 219 | 220 | //Memory leak occurs here 221 | xmlhttp, err = unknown.QueryInterface(ole.IID_IDispatch) 222 | if err != nil { 223 | return 0, err 224 | } 225 | defer func() { refcount2 += xmlhttp.Release() }() 226 | //Nothing below this really matters. Can be removed if you want a tighter loop 227 | } 228 | 229 | //fmt.Printf("Download %s\n", url) 230 | openRaw, err := oleutil.CallMethod(xmlhttp, "open", "GET", url, false) 231 | if err != nil { 232 | return 0, err 233 | } 234 | defer openRaw.Clear() 235 | 236 | if MinimalTest { 237 | return 1, nil 238 | } 239 | 240 | //Initiate http request 241 | sendRaw, err := oleutil.CallMethod(xmlhttp, "send", nil) 242 | if err != nil { 243 | return 0, err 244 | } 245 | defer sendRaw.Clear() 246 | state := -1 // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState 247 | for state != 4 { 248 | time.Sleep(5 * time.Millisecond) 249 | stateRaw := oleutil.MustGetProperty(xmlhttp, "readyState") 250 | state = int(stateRaw.Val) 251 | stateRaw.Clear() 252 | } 253 | 254 | responseXMLRaw := oleutil.MustGetProperty(xmlhttp, "responseXml") 255 | responseXML := responseXMLRaw.ToIDispatch() 256 | defer responseXMLRaw.Clear() 257 | itemsRaw := oleutil.MustCallMethod(responseXML, "selectNodes", "/rdf:RDF/item") 258 | items := itemsRaw.ToIDispatch() 259 | defer itemsRaw.Clear() 260 | lengthRaw := oleutil.MustGetProperty(items, "length") 261 | defer lengthRaw.Clear() 262 | length := int(lengthRaw.Val) 263 | 264 | /* This just bloats the TotalAlloc and slows the test down. Doesn't effect Private Working Set 265 | for n := 0; n < length; n++ { 266 | itemRaw := oleutil.MustGetProperty(items, "item", n) 267 | item := itemRaw.ToIDispatch() 268 | title := oleutil.MustCallMethod(item, "selectSingleNode", "title").ToIDispatch() 269 | 270 | //fmt.Println(oleutil.MustGetProperty(title, "text").ToString()) 271 | textRaw := oleutil.MustGetProperty(title, "text") 272 | textRaw.ToString() 273 | 274 | link := oleutil.MustCallMethod(item, "selectSingleNode", "link").ToIDispatch() 275 | //fmt.Println(" " + oleutil.MustGetProperty(link, "text").ToString()) 276 | textRaw2 := oleutil.MustGetProperty(link, "text") 277 | textRaw2.ToString() 278 | 279 | textRaw2.Clear() 280 | link.Release() 281 | textRaw.Clear() 282 | title.Release() 283 | itemRaw.Clear() 284 | } 285 | */ 286 | return length, nil 287 | } 288 | 289 | // Testing go-ole/oleutil 290 | // Run using: go test -run TestMemoryOLE -timeout 60m 291 | // Code from https://github.com/go-ole/go-ole/blob/master/example/msxml/rssreader.go 292 | func _TestMemoryOLE(t *testing.T) { 293 | defer func() { 294 | if r := recover(); r != nil { 295 | t.Error(r) 296 | } 297 | }() 298 | 299 | start := time.Now() 300 | limit := 50000000 301 | url := "http://localhost/slashdot.xml" //http://rss.slashdot.org/Slashdot/slashdot" 302 | fmt.Printf("Benchmark Iterations: %d (Memory should stabilize around 8MB to 12MB after ~2k full or 250k minimal)\n", limit) 303 | 304 | //On Server 2016 or Windows 10 changing leakMemory=true will cause it to leak ~1.5MB per 10000 calls to unknown.QueryInterface 305 | leakMemory := true 306 | 307 | //////////////////////////////////////// 308 | //Start outer section 309 | var unknown *ole.IUnknown 310 | var xmlhttp *ole.IDispatch 311 | if !leakMemory { 312 | runtime.LockOSThread() 313 | defer runtime.UnlockOSThread() 314 | 315 | err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) 316 | if err != nil { 317 | oleCode := err.(*ole.OleError).Code() 318 | if oleCode != ole.S_OK && oleCode != S_FALSE { 319 | t.Fatal(err) 320 | } 321 | } 322 | defer ole.CoUninitialize() 323 | 324 | //fmt.Println("CreateObject Microsoft.XMLHTTP") 325 | unknown, err = oleutil.CreateObject("Microsoft.XMLHTTP") 326 | if err != nil { 327 | t.Fatal(err) 328 | } 329 | defer unknown.Release() 330 | 331 | //Memory leak starts here 332 | xmlhttp, err = unknown.QueryInterface(ole.IID_IDispatch) 333 | if err != nil { 334 | t.Fatal(err) 335 | } 336 | defer xmlhttp.Release() 337 | } 338 | //End outer section 339 | //////////////////////////////////////// 340 | 341 | totalItems := uint64(0) 342 | for i := 0; i < limit; i++ { 343 | if i%2000 == 0 { 344 | privateMB, allocMB, allocTotalMB := GetMemoryUsageMB() 345 | fmt.Printf("Time: %4ds Count: %7d Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB %7d/%7d\n", time.Now().Sub(start)/time.Second, i, privateMB, allocMB, allocTotalMB, refcount1, refcount2) 346 | } 347 | //This should use less than 10MB for 1 million iterations if xmlhttp was initialized above 348 | //On Server 2016 or Windows 10 changing leakMemory=true above will cause it to leak ~1.5MB per 10000 calls to unknown.QueryInterface 349 | count, err := getRSS(url, xmlhttp, true) //last argument is for Minimal test. Doesn't effect leak just overall allocations/time 350 | if err != nil { 351 | t.Fatal(err) 352 | } 353 | totalItems += uint64(count) 354 | } 355 | privateMB, allocMB, allocTotalMB := GetMemoryUsageMB() 356 | fmt.Printf("Final totalItems: %d Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", totalItems, privateMB, allocMB, allocTotalMB) 357 | } 358 | 359 | const MB = 1024 * 1024 360 | 361 | var ( 362 | mMemoryUsageMB runtime.MemStats 363 | errGetMemoryUsageMB error 364 | dstGetMemoryUsageMB []Win32_PerfRawData_PerfProc_Process 365 | filterProcessID = fmt.Sprintf("WHERE IDProcess = %d", os.Getpid()) 366 | qGetMemoryUsageMB = CreateQuery(&dstGetMemoryUsageMB, filterProcessID) 367 | ) 368 | 369 | func GetMemoryUsageMB() (float64, float64, float64) { 370 | runtime.ReadMemStats(&mMemoryUsageMB) 371 | //errGetMemoryUsageMB = nil //Query(qGetMemoryUsageMB, &dstGetMemoryUsageMB) float64(dstGetMemoryUsageMB[0].WorkingSetPrivate) 372 | errGetMemoryUsageMB = Query(qGetMemoryUsageMB, &dstGetMemoryUsageMB) 373 | if errGetMemoryUsageMB != nil { 374 | fmt.Println("ERROR GetMemoryUsage", errGetMemoryUsageMB) 375 | return 0, 0, 0 376 | } 377 | return float64(dstGetMemoryUsageMB[0].WorkingSetPrivate) / MB, float64(mMemoryUsageMB.Alloc) / MB, float64(mMemoryUsageMB.TotalAlloc) / MB 378 | } 379 | 380 | type Win32_PerfRawData_PerfProc_Process struct { 381 | IDProcess uint32 382 | WorkingSetPrivate uint64 383 | } 384 | 385 | type Win32_Process struct { 386 | CSCreationClassName string 387 | CSName string 388 | Caption *string 389 | CommandLine *string 390 | CreationClassName string 391 | CreationDate *time.Time 392 | Description *string 393 | ExecutablePath *string 394 | ExecutionState *uint16 395 | Handle string 396 | HandleCount uint32 397 | InstallDate *time.Time 398 | KernelModeTime uint64 399 | MaximumWorkingSetSize *uint32 400 | MinimumWorkingSetSize *uint32 401 | Name string 402 | OSCreationClassName string 403 | OSName string 404 | OtherOperationCount uint64 405 | OtherTransferCount uint64 406 | PageFaults uint32 407 | PageFileUsage uint32 408 | ParentProcessId uint32 409 | PeakPageFileUsage uint32 410 | PeakVirtualSize uint64 411 | PeakWorkingSetSize uint32 412 | Priority uint32 413 | PrivatePageCount uint64 414 | ProcessId uint32 415 | QuotaNonPagedPoolUsage uint32 416 | QuotaPagedPoolUsage uint32 417 | QuotaPeakNonPagedPoolUsage uint32 418 | QuotaPeakPagedPoolUsage uint32 419 | ReadOperationCount uint64 420 | ReadTransferCount uint64 421 | SessionId uint32 422 | Status *string 423 | TerminationDate *time.Time 424 | ThreadCount uint32 425 | UserModeTime uint64 426 | VirtualSize uint64 427 | WindowsVersion string 428 | WorkingSetSize uint64 429 | WriteOperationCount uint64 430 | WriteTransferCount uint64 431 | } 432 | 433 | type Win32_PerfRawData_PerfDisk_LogicalDisk struct { 434 | AvgDiskBytesPerRead uint64 435 | AvgDiskBytesPerRead_Base uint32 436 | AvgDiskBytesPerTransfer uint64 437 | AvgDiskBytesPerTransfer_Base uint32 438 | AvgDiskBytesPerWrite uint64 439 | AvgDiskBytesPerWrite_Base uint32 440 | AvgDiskQueueLength uint64 441 | AvgDiskReadQueueLength uint64 442 | AvgDiskSecPerRead uint32 443 | AvgDiskSecPerRead_Base uint32 444 | AvgDiskSecPerTransfer uint32 445 | AvgDiskSecPerTransfer_Base uint32 446 | AvgDiskSecPerWrite uint32 447 | AvgDiskSecPerWrite_Base uint32 448 | AvgDiskWriteQueueLength uint64 449 | Caption *string 450 | CurrentDiskQueueLength uint32 451 | Description *string 452 | DiskBytesPerSec uint64 453 | DiskReadBytesPerSec uint64 454 | DiskReadsPerSec uint32 455 | DiskTransfersPerSec uint32 456 | DiskWriteBytesPerSec uint64 457 | DiskWritesPerSec uint32 458 | FreeMegabytes uint32 459 | Frequency_Object uint64 460 | Frequency_PerfTime uint64 461 | Frequency_Sys100NS uint64 462 | Name string 463 | PercentDiskReadTime uint64 464 | PercentDiskReadTime_Base uint64 465 | PercentDiskTime uint64 466 | PercentDiskTime_Base uint64 467 | PercentDiskWriteTime uint64 468 | PercentDiskWriteTime_Base uint64 469 | PercentFreeSpace uint32 470 | PercentFreeSpace_Base uint32 471 | PercentIdleTime uint64 472 | PercentIdleTime_Base uint64 473 | SplitIOPerSec uint32 474 | Timestamp_Object uint64 475 | Timestamp_PerfTime uint64 476 | Timestamp_Sys100NS uint64 477 | } 478 | 479 | type Win32_OperatingSystem struct { 480 | BootDevice string 481 | BuildNumber string 482 | BuildType string 483 | Caption *string 484 | CodeSet string 485 | CountryCode string 486 | CreationClassName string 487 | CSCreationClassName string 488 | CSDVersion *string 489 | CSName string 490 | CurrentTimeZone int16 491 | DataExecutionPrevention_Available bool 492 | DataExecutionPrevention_32BitApplications bool 493 | DataExecutionPrevention_Drivers bool 494 | DataExecutionPrevention_SupportPolicy *uint8 495 | Debug bool 496 | Description *string 497 | Distributed bool 498 | EncryptionLevel uint32 499 | ForegroundApplicationBoost *uint8 500 | FreePhysicalMemory uint64 501 | FreeSpaceInPagingFiles uint64 502 | FreeVirtualMemory uint64 503 | InstallDate time.Time 504 | LargeSystemCache *uint32 505 | LastBootUpTime time.Time 506 | LocalDateTime time.Time 507 | Locale string 508 | Manufacturer string 509 | MaxNumberOfProcesses uint32 510 | MaxProcessMemorySize uint64 511 | MUILanguages *[]string 512 | Name string 513 | NumberOfLicensedUsers *uint32 514 | NumberOfProcesses uint32 515 | NumberOfUsers uint32 516 | OperatingSystemSKU uint32 517 | Organization string 518 | OSArchitecture string 519 | OSLanguage uint32 520 | OSProductSuite uint32 521 | OSType uint16 522 | OtherTypeDescription *string 523 | PAEEnabled *bool 524 | PlusProductID *string 525 | PlusVersionNumber *string 526 | PortableOperatingSystem bool 527 | Primary bool 528 | ProductType uint32 529 | RegisteredUser string 530 | SerialNumber string 531 | ServicePackMajorVersion uint16 532 | ServicePackMinorVersion uint16 533 | SizeStoredInPagingFiles uint64 534 | Status string 535 | SuiteMask uint32 536 | SystemDevice string 537 | SystemDirectory string 538 | SystemDrive string 539 | TotalSwapSpaceSize *uint64 540 | TotalVirtualMemorySize uint64 541 | TotalVisibleMemorySize uint64 542 | Version string 543 | WindowsDirectory string 544 | } 545 | --------------------------------------------------------------------------------