//go:build windows // +build windows package wmiext import ( "errors" "fmt" "reflect" "strconv" "strings" "syscall" "time" "unsafe" "github.com/go-ole/go-ole" "github.com/sirupsen/logrus" ) const ( WmiPathKey = "__PATH" ) var ( WindowsEpoch = time.Date(1601, 1, 1, 0, 0, 0, 0, time.UTC) ) type Instance struct { object *ole.IUnknown vTable *IWbemClassObjectVtbl service *Service } type IWbemClassObjectVtbl struct { QueryInterface uintptr AddRef uintptr Release uintptr GetQualifierSet uintptr Get uintptr Put uintptr Delete uintptr GetNames uintptr BeginEnumeration uintptr Next uintptr EndEnumeration uintptr GetPropertyQualifierSet uintptr Clone uintptr GetObjectText uintptr SpawnDerivedClass uintptr SpawnInstance uintptr CompareTo uintptr GetPropertyOrigin uintptr InheritsFrom uintptr GetMethod uintptr PutMethod uintptr DeleteMethod uintptr BeginMethodEnumeration uintptr NextMethod uintptr EndMethodEnumeration uintptr GetMethodQualifierSet uintptr GetMethodOrigin uintptr } type CIMTYPE_ENUMERATION uint32 const ( CIM_ILLEGAL CIMTYPE_ENUMERATION = 0xFFF CIM_EMPTY CIMTYPE_ENUMERATION = 0 CIM_SINT8 CIMTYPE_ENUMERATION = 16 CIM_UINT8 CIMTYPE_ENUMERATION = 17 CIM_SINT16 CIMTYPE_ENUMERATION = 2 CIM_UINT16 CIMTYPE_ENUMERATION = 18 CIM_SINT32 CIMTYPE_ENUMERATION = 3 CIM_UINT32 CIMTYPE_ENUMERATION = 19 CIM_SINT64 CIMTYPE_ENUMERATION = 20 CIM_UINT64 CIMTYPE_ENUMERATION = 21 CIM_REAL32 CIMTYPE_ENUMERATION = 4 CIM_REAL64 CIMTYPE_ENUMERATION = 5 CIM_BOOLEAN CIMTYPE_ENUMERATION = 11 CIM_STRING CIMTYPE_ENUMERATION = 8 CIM_DATETIME CIMTYPE_ENUMERATION = 101 CIM_REFERENCE CIMTYPE_ENUMERATION = 102 CIM_CHAR16 CIMTYPE_ENUMERATION = 103 CIM_OBJECT CIMTYPE_ENUMERATION = 13 CIM_FLAG_ARRAY CIMTYPE_ENUMERATION = 0x2000 ) type WBEM_FLAVOR_TYPE uint32 const ( WBEM_FLAVOR_DONT_PROPAGATE WBEM_FLAVOR_TYPE = 0 WBEM_FLAVOR_FLAG_PROPAGATE_TO_INSTANCE WBEM_FLAVOR_TYPE = 0x1 WBEM_FLAVOR_FLAG_PROPAGATE_TO_DERIVED_CLASS WBEM_FLAVOR_TYPE = 0x2 WBEM_FLAVOR_MASK_PROPAGATION WBEM_FLAVOR_TYPE = 0xf WBEM_FLAVOR_OVERRIDABLE WBEM_FLAVOR_TYPE = 0 WBEM_FLAVOR_NOT_OVERRIDABLE WBEM_FLAVOR_TYPE = 0x10 WBEM_FLAVOR_MASK_PERMISSIONS WBEM_FLAVOR_TYPE = 0x10 WBEM_FLAVOR_ORIGIN_LOCAL WBEM_FLAVOR_TYPE = 0 WBEM_FLAVOR_ORIGIN_PROPAGATED WBEM_FLAVOR_TYPE = 0x20 WBEM_FLAVOR_ORIGIN_SYSTEM WBEM_FLAVOR_TYPE = 0x40 WBEM_FLAVOR_MASK_ORIGIN WBEM_FLAVOR_TYPE = 0x60 WBEM_FLAVOR_NOT_AMENDED WBEM_FLAVOR_TYPE = 0 WBEM_FLAVOR_AMENDED WBEM_FLAVOR_TYPE = 0x80 WBEM_FLAVOR_MASK_AMENDED WBEM_FLAVOR_TYPE = 0x80 ) func newInstance(object *ole.IUnknown, service *Service) *Instance { instance := &Instance{ object: object, vTable: (*IWbemClassObjectVtbl)(unsafe.Pointer(object.RawVTable)), service: service, } return instance } // Close cleans up all memory associated with this instance. func (i *Instance) Close() { if i != nil && i.object != nil { i.object.Release() } } // GetClassName Gets the WMI class name for this WMI object instance func (i *Instance) GetClassName() (className string, err error) { return i.GetAsString(`__CLASS`) } // Path gets the WMI object path of this instance func (i *Instance) Path() (string, error) { ref, _, _, err := i.GetAsAny(WmiPathKey) return ref.(string), err } // IsReferenceProperty returns whether the property is of type CIM_REFERENCE, a string which points to // an object path of another instance. func (i *Instance) IsReferenceProperty(name string) (bool, error) { _, cimType, _, err := i.GetAsAny(name) return cimType == CIM_REFERENCE, err } // SpawnInstance create a new WMI object instance that is zero-initialized. The returned instance // will not respect expected default values, which must be populated by other means. func (i *Instance) SpawnInstance() (instance *Instance, err error) { var res uintptr var newUnknown *ole.IUnknown res, _, _ = syscall.SyscallN( i.vTable.SpawnInstance, // IWbemClassObject::SpawnInstance( uintptr(unsafe.Pointer(i.object)), // IWbemClassObject ptr uintptr(0), // [in] long lFlags, uintptr(unsafe.Pointer(&newUnknown))) // [out] IWbemClassObject **ppNewInstance) if res != 0 { return nil, NewWmiError(res) } return newInstance(newUnknown, i.service), nil } // CloneInstance create a new cloned copy of this WMI instance. func (i *Instance) CloneInstance() (*Instance, error) { classObj := i.object vTable := (*IWbemClassObjectVtbl)(unsafe.Pointer(classObj.RawVTable)) var cloned *ole.IUnknown ret, _, _ := syscall.SyscallN( vTable.Clone, // IWbemClassObject::Clone( uintptr(unsafe.Pointer(classObj)), // IWbemClassObject ptr uintptr(unsafe.Pointer(&cloned))) // [out] IWbemClassObject **ppCopy) if ret != 0 { return nil, NewWmiError(ret) } return newInstance(cloned, i.service), nil } // PutAll sets all fields of this instance to the passed src parameter's fields, converting accordingly. // The src parameter must be a pointer to a struct, otherwise an error will be returned. func (i *Instance) PutAll(src interface{}) error { val := reflect.ValueOf(src) if val.Kind() == reflect.Pointer { val = val.Elem() } if val.Kind() != reflect.Struct { return errors.New("not a struct or pointer to struct") } props, err := i.GetAllProperties() if err != nil { return err } return i.instancePutAllTraverse(val, props) } func (i *Instance) instancePutAllTraverse(val reflect.Value, propMap map[string]interface{}) error { for j := 0; j < val.NumField(); j++ { fieldVal := val.Field(j) fieldType := val.Type().Field(j) if fieldType.Type.Kind() == reflect.Struct && fieldType.Anonymous { if err := i.instancePutAllTraverse(fieldVal, propMap); err != nil { return err } continue } if strings.HasPrefix(fieldType.Name, "S__") { continue } if !fieldType.IsExported() { continue } if _, exists := propMap[fieldType.Name]; !exists { continue } if fieldVal.Kind() == reflect.String && fieldVal.Len() == 0 { continue } if err := i.Put(fieldType.Name, fieldVal.Interface()); err != nil { return err } } return nil } // Put sets the specified property to the passed Golang value, converting appropriately. func (i *Instance) Put(name string, value interface{}) (err error) { var variant ole.VARIANT switch cast := value.(type) { case ole.VARIANT: variant = cast case *ole.VARIANT: variant = *cast default: variant, err = NewAutomationVariant(value) if err != nil { return err } } var wszName *uint16 if wszName, err = syscall.UTF16PtrFromString(name); err != nil { return } classObj := i.object vTable := (*IWbemClassObjectVtbl)(unsafe.Pointer(classObj.RawVTable)) res, _, _ := syscall.SyscallN( vTable.Put, // IWbemClassObject::Put( uintptr(unsafe.Pointer(classObj)), // IWbemClassObject ptr uintptr(unsafe.Pointer(wszName)), // [in] LPCWSTR wszName, uintptr(0), // [in] long lFlags, uintptr(unsafe.Pointer(&variant)), // [in] VARIANT *pVal, uintptr(0)) // [in] CIMTYPE Type) if res != 0 { return NewWmiError(res) } _ = variant.Clear() return } // GetCimText returns the CIM XML representation of this instance. Some WMI methods use a string // parameter to represent a full complex object, and this method is used to generate // the expected format. func (i *Instance) GetCimText() string { type wmiWbemTxtSrcVtable struct { QueryInterface uintptr AddRef uintptr Release uintptr GetTxt uintptr } const CIM_XML_FORMAT = 1 classObj := i.object vTable := (*wmiWbemTxtSrcVtable)(unsafe.Pointer(wmiWbemTxtLocator.RawVTable)) var retString *uint16 res, _, _ := syscall.SyscallN( vTable.GetTxt, // IWbemObjectTextSrc::GetText() uintptr(unsafe.Pointer(wmiWbemLocator)), // IWbemObjectTextSrc ptr uintptr(0), // [in] long lFlags uintptr(unsafe.Pointer(classObj)), // [in] IWbemClassObject *pObj uintptr(CIM_XML_FORMAT), // [in] ULONG uObjTextFormat, uintptr(0), // [in] IWbemContext *pCtx, uintptr(unsafe.Pointer(&retString))) // [out] BSTR *strText) if res != 0 { return "" } itemStr := ole.BstrToString(retString) return itemStr } // GetAll gets all fields that map to a target struct and populates all struct fields according to // the expected type information. The target parameter should be a pointer to a struct, and // will return an error otherwise. func (i *Instance) GetAll(target interface{}) error { elem := reflect.ValueOf(target) if elem.Kind() != reflect.Ptr || elem.IsNil() { return errors.New("invalid destination type for mapping a WMI instance to an object") } // deref pointer elem = elem.Elem() var err error if err = i.BeginEnumeration(); err != nil { return err } properties := make(map[string]*ole.VARIANT) for { var name string var value *ole.VARIANT var done bool if done, name, value, _, _, err = i.NextAsVariant(); err != nil { return err } if done { break } if value != nil { properties[name] = value } } defer func() { for _, v := range properties { _ = v.Clear() } }() _ = i.EndEnumeration() return i.instanceGetAllPopulate(elem, elem.Type(), properties) } // GetAsAny gets a property and converts it to a Golang type that matches the internal // variant automation type passed back from WMI. For usage with predictable static // type mapping, use GetAsString(), GetAsUint(), or GetAll() instead of this method. func (i *Instance) GetAsAny(name string) (interface{}, CIMTYPE_ENUMERATION, WBEM_FLAVOR_TYPE, error) { variant, cimType, flavor, err := i.GetAsVariant(name) if err != nil { return nil, cimType, flavor, err } defer func() { if err := variant.Clear(); err != nil { logrus.Error(err) } }() // Since there is no type information only perform the stock conversion result := convertToGenericValue(variant) return result, cimType, flavor, err } // GetAsString gets a property value as a string value, converting if necessary func (i *Instance) GetAsString(name string) (value string, err error) { variant, _, _, err := i.GetAsVariant(name) if err != nil || variant == nil { return "", err } defer func() { if err := variant.Clear(); err != nil { logrus.Error(err) } }() // TODO: replace with something better return fmt.Sprintf("%v", convertToGenericValue(variant)), nil } // GetAsUint gets a property value as a uint value, if conversion is possible. Otherwise, // returns an error. func (i *Instance) GetAsUint(name string) (uint, error) { val, _, _, err := i.GetAsAny(name) if err != nil { return 0, err } switch ret := val.(type) { case int: return uint(ret), nil case int8: return uint(ret), nil case int16: return uint(ret), nil case int32: return uint(ret), nil case int64: return uint(ret), nil case uint: return ret, nil case uint8: return uint(ret), nil case uint16: return uint(ret), nil case uint32: return uint(ret), nil case uint64: return uint(ret), nil case string: parse, err := strconv.ParseUint(ret, 10, 64) return uint(parse), err default: return 0, fmt.Errorf("type conversion from %T on param %s not supported", val, name) } } // GetAsVariant obtains a specified property value, if it exists. func (i *Instance) GetAsVariant(name string) (*ole.VARIANT, CIMTYPE_ENUMERATION, WBEM_FLAVOR_TYPE, error) { var variant ole.VARIANT var err error var wszName *uint16 var cimType CIMTYPE_ENUMERATION var flavor WBEM_FLAVOR_TYPE if wszName, err = syscall.UTF16PtrFromString(name); err != nil { return nil, 0, 0, err } classObj := i.object vTable := (*IWbemClassObjectVtbl)(unsafe.Pointer(classObj.RawVTable)) res, _, _ := syscall.SyscallN( vTable.Get, // IWbemClassObject::Get( uintptr(unsafe.Pointer(classObj)), // IWbemClassObject ptr uintptr(unsafe.Pointer(wszName)), // [in] LPCWSTR wszName, uintptr(0), // [in] long lFlags, uintptr(unsafe.Pointer(&variant)), // [out] VARIANT *pVal, uintptr(unsafe.Pointer(&cimType)), // [out, optional] CIMTYPE *pType, uintptr(unsafe.Pointer(&flavor))) // [out, optional] long *plFlavor) if res != 0 { return nil, 0, 0, NewWmiError(res) } return &variant, cimType, flavor, nil } // Next retrieves the next property as a Golang type when iterating the properties using an enumerator // created by BeginEnumeration(). The returned value's type represents the internal automation type // used by WMI. It is usually preferred to use GetAsXXX(), GetAll(), or GetAll Properties() over this // method. func (i *Instance) Next() (done bool, name string, value interface{}, cimType CIMTYPE_ENUMERATION, flavor WBEM_FLAVOR_TYPE, err error) { var variant *ole.VARIANT done, name, variant, cimType, flavor, err = i.NextAsVariant() if err == nil && !done { defer func() { if err := variant.Clear(); err != nil { logrus.Error(err) } }() value = convertToGenericValue(variant) } return } // NextAsVariant retrieves the next property as a VARIANT type when iterating the properties using an enumerator // created by BeginEnumeration(). The returned value's type represents the internal automation type // used by WMI. It is usually preferred to use GetAsXXX(), GetAll(), or GetAllProperties() over this // method. Callers are responsible for clearing the VARIANT, otherwise associated memory will leak. func (i *Instance) NextAsVariant() (bool, string, *ole.VARIANT, CIMTYPE_ENUMERATION, WBEM_FLAVOR_TYPE, error) { var res uintptr var strName *uint16 var variant ole.VARIANT var cimType CIMTYPE_ENUMERATION var flavor WBEM_FLAVOR_TYPE res, _, _ = syscall.SyscallN( i.vTable.Next, // IWbemClassObject::Next( uintptr(unsafe.Pointer(i.object)), // IWbemClassObject ptr uintptr(0), // [in] long lFlags, uintptr(unsafe.Pointer(&strName)), // [out] BSTR *strName, uintptr(unsafe.Pointer(&variant)), // [out] VARIANT *pVal, uintptr(unsafe.Pointer(&cimType)), // [out, optional] CIMTYPE *pType, uintptr(unsafe.Pointer(&flavor))) // [out, optional] long *plFlavor if int(res) < 0 { return false, "", nil, cimType, flavor, NewWmiError(res) } if res == WBEM_S_NO_MORE_DATA { return true, "", nil, cimType, flavor, nil } defer ole.SysFreeString((*int16)(unsafe.Pointer(strName))) //nolint:errcheck name := ole.BstrToString(strName) return false, name, &variant, cimType, flavor, nil } // GetAllProperties gets all properties on this instance. The returned map is keyed by the field name and the value // is a Golang type which matches the WMI internal implementation. For static type conversions, // it's recommended to use either GetAll(), which uses struct fields for type information, or // the GetAsXXX() methods. func (i *Instance) GetAllProperties() (map[string]interface{}, error) { var err error properties := make(map[string]interface{}) if err = i.BeginEnumeration(); err != nil { return nil, err } defer func() { if err := i.EndEnumeration(); err != nil { logrus.Error(err) } }() for { var name string var value interface{} var done bool if done, name, value, _, _, err = i.Next(); err != nil || done { return properties, err } properties[name] = value } } // GetMethodParameters returns a WMI class object which represents the [in] method parameters for a method invocation. // This is an advanced method, used for dynamic introspection or manual method invocation. In most // cases it is recommended to use BeginInvoke() instead, which constructs the parameter payload // automatically. func (i *Instance) GetMethodParameters(method string) (*Instance, error) { var err error var res uintptr var inSignature *ole.IUnknown var wszName *uint16 if wszName, err = syscall.UTF16PtrFromString(method); err != nil { return nil, err } res, _, _ = syscall.SyscallN( i.vTable.GetMethod, // IWbemClassObject::GetMethod( uintptr(unsafe.Pointer(i.object)), // IWbemClassObject ptr uintptr(unsafe.Pointer(wszName)), // [in] LPCWSTR wszName uintptr(0), // [in] long lFlags, uintptr(unsafe.Pointer(&inSignature)), // [out] IWbemClassObject **ppInSignature, uintptr(0)) // [out] IWbemClassObject **ppOutSignature) if res != 0 { return nil, NewWmiError(res) } return newInstance(inSignature, i.service), nil } func (i *Instance) instanceGetAllPopulate(elem reflect.Value, elemType reflect.Type, properties map[string]*ole.VARIANT) error { var err error for j := 0; j < elemType.NumField(); j++ { fieldType := elemType.Field(j) fieldVal := elem.Field(j) if !fieldType.IsExported() { continue } if fieldType.Type.Kind() == reflect.Struct && fieldType.Anonymous { if err := i.instanceGetAllPopulate(fieldVal, fieldType.Type, properties); err != nil { return err } continue } fieldName := fieldType.Name if strings.HasPrefix(fieldName, "S__") { fieldName = fieldName[1:] } if variant, ok := properties[fieldName]; ok { var val interface{} if val, err = convertToGoType(variant, fieldVal, fieldType.Type); err != nil { return err } if val != nil { fieldVal.Set(reflect.ValueOf(val)) } } } return nil } // BeginEnumeration begins iterating the property list on this instance. This is an advanced method. // In most cases, the GetAsXXX() methods, GetAll(), and GetAllProperties() methods should be // preferred. func (i *Instance) BeginEnumeration() error { classObj := i.object vTable := (*IWbemClassObjectVtbl)(unsafe.Pointer(classObj.RawVTable)) result, _, _ := syscall.SyscallN( vTable.BeginEnumeration, // IWbemClassObject::BeginEnumeration( uintptr(unsafe.Pointer(classObj)), // IWbemClassObject ptr, uintptr(0)) // [in] long lEnumFlags) // 0 = defaults if result != 0 { return NewWmiError(result) } return nil } // EndEnumeration completes iterating a property list on this instance. This is an advanced method. // In most cases, the GetAsXXX() methods, GetAll(), and GetAllProperties() methods // should be preferred. func (i *Instance) EndEnumeration() error { res, _, _ := syscall.SyscallN( i.vTable.EndEnumeration, // IWbemClassObject::EndEnumeration( uintptr(unsafe.Pointer(i.object))) // IWbemClassObject ptr) if res != 0 { return NewWmiError(res) } return nil } // BeginInvoke invokes a method on this Instance. Returns a MethodExecutor builder object // that is used to construct the input parameters (via calls to In()), perform the // invocation (using calls to Execute()), retrieve output parameters (via calls to // Out()), and finally the method return value (using a call to End()) func (i *Instance) BeginInvoke(method string) *MethodExecutor { objPath, err := i.Path() if err != nil { return &MethodExecutor{err: err} } var class, inParam *Instance if class, err = i.service.GetClassInstance(i); err == nil { inParam, err = class.GetMethodParameters(method) class.Close() } return &MethodExecutor{method: method, path: objPath, service: i.service, inParam: inParam, err: err} }