Запрос WMI от Go

Я хотел бы выполнять запросы WMI от Go. Есть способы вызова функций DLL из Go. Насколько я понимаю, где-то должна быть какая-то DLL, которая при правильном вызове вернет некоторые данные, которые я могу проанализировать и использовать. Я предпочел бы избегать вызова в C или C++, тем более, что я предполагаю, что это обертки над самим Windows API.

Я рассмотрел вывод dumpbin.exe /exports c:\windows\system32\wmi.dll, и следующая запись выглядит многообещающе:

WmiQueryAllDataA (forwarded to wmiclnt.WmiQueryAllDataA)

Однако я не уверен, что делать отсюда. Какие аргументы принимает эта функция? Что это возвращает? Поиск WmiQueryAllDataA не помогает. И это имя появляется только в комментарии к c:\program files (x86)\windows kits\8.1\include\shared\wmistr.h, но без сигнатуры функции.

Есть ли лучшие методы? Есть еще одна DLL? Я что-то пропустил? Должен ли я просто использовать оболочку C?

Выполнение запроса WMI в Linqpad с помощью .NET Reflector показывает использование WmiNetUtilsHelper:ExecQueryWmi (и версии _f), но ни один из них не имеет видимой реализации.

Обновление: используйте пакет github.com/StackExchange/wmi, который использует решение в принятом ответе.

Ответ 1

Добро пожаловать в чудесный мир COM, объектно-ориентированное программирование на C, когда С++ был "молодым выскочкой".

В github mattn сгенерирована небольшая обертка в Go, который я использовал для быстрой смены примера. "Этот репозиторий создан для экспериментов и должен считаться нестабильным". вселяет уверенность.

Я оставляю много ошибок. Поверьте мне, когда я скажу, вы захотите добавить его обратно.

package main

import (
        "github.com/mattn/go-ole"
        "github.com/mattn/go-ole/oleutil"
)

func main() {
    // init COM, oh yeah
    ole.CoInitialize(0)
    defer ole.CoUninitialize()

    unknown, _ := oleutil.CreateObject("WbemScripting.SWbemLocator")
    defer unknown.Release()

    wmi, _ := unknown.QueryInterface(ole.IID_IDispatch)
    defer wmi.Release()

    // service is a SWbemServices
    serviceRaw, _ := oleutil.CallMethod(wmi, "ConnectServer")
    service := serviceRaw.ToIDispatch()
    defer service.Release()

    // result is a SWBemObjectSet
    resultRaw, _ := oleutil.CallMethod(service, "ExecQuery", "SELECT * FROM Win32_Process")
    result := resultRaw.ToIDispatch()
    defer result.Release()

    countVar, _ := oleutil.GetProperty(result, "Count")
    count := int(countVar.Val)

    for i :=0; i < count; i++ {
        // item is a SWbemObject, but really a Win32_Process
        itemRaw, _ := oleutil.CallMethod(result, "ItemIndex", i)
        item := itemRaw.ToIDispatch()
        defer item.Release()

        asString, _ := oleutil.GetProperty(item, "Name")

        println(asString.ToString())
    }
}

Реальное мясо - это вызов ExecQuery, я случайно захватил Win32_Process из доступных классов, потому что это легко понять и распечатать.

На моей машине это печатает:

System Idle Process
System
smss.exe
csrss.exe
wininit.exe
services.exe
lsass.exe
svchost.exe
svchost.exe
atiesrxx.exe
svchost.exe
svchost.exe
svchost.exe
svchost.exe
svchost.exe
spoolsv.exe
svchost.exe
AppleOSSMgr.exe
AppleTimeSrv.exe
... and so on
go.exe
main.exe

Я не запускаю его повышенным или с отключенным UAC, но некоторым провайдерам WMI потребуется привилегированный пользователь.

Я тоже не на 100%, что это не пропустит немного, вы захотите вникать в это. COM-объекты подсчитываются по ссылке, поэтому defer должен быть очень хорош там (при условии, что этот метод не сумасшедший долго работает), но у go-ole может быть какая-то магия внутри, которую я не заметил.

Ответ 2

Я комментирую год спустя, но есть решение здесь на github (и опубликовано ниже для потомков).

// +build windows

/*
Package wmi provides a WQL interface for WMI on Windows.

Example code to print names of running processes:

    type Win32_Process struct {
        Name string
    }

    func main() {
        var dst []Win32_Process
        q := wmi.CreateQuery(&dst, "")
        err := wmi.Query(q, &dst)
        if err != nil {
            log.Fatal(err)
        }
        for i, v := range dst {
            println(i, v.Name)
        }
    }

*/
package wmi

import (
    "bytes"
    "errors"
    "fmt"
    "log"
    "os"
    "reflect"
    "runtime"
    "strconv"
    "strings"
    "sync"
    "time"

    "github.com/mattn/go-ole"
    "github.com/mattn/go-ole/oleutil"
)

var l = log.New(os.Stdout, "", log.LstdFlags)

var (
    ErrInvalidEntityType = errors.New("wmi: invalid entity type")
    lock                 sync.Mutex
)

// QueryNamespace invokes Query with the given namespace on the local machine.
func QueryNamespace(query string, dst interface{}, namespace string) error {
    return Query(query, dst, nil, namespace)
}

// Query runs the WQL query and appends the values to dst.
//
// dst must have type *[]S or *[]*S, for some struct type S. Fields selected in
// the query must have the same name in dst. Supported types are all signed and
// unsigned integers, time.Time, string, bool, or a pointer to one of those.
// Array types are not supported.
//
// By default, the local machine and default namespace are used. These can be
// changed using connectServerArgs. See
// http://msdn.microsoft.com/en-us/library/aa393720.aspx for details.
func Query(query string, dst interface{}, connectServerArgs ...interface{}) error {
    dv := reflect.ValueOf(dst)
    if dv.Kind() != reflect.Ptr || dv.IsNil() {
        return ErrInvalidEntityType
    }
    dv = dv.Elem()
    mat, elemType := checkMultiArg(dv)
    if mat == multiArgTypeInvalid {
        return ErrInvalidEntityType
    }

    lock.Lock()
    defer lock.Unlock()
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
    if err != nil {
        oleerr := err.(*ole.OleError)
        // S_FALSE           = 0x00000001 // CoInitializeEx was already called on this thread
        if oleerr.Code() != ole.S_OK && oleerr.Code() != 0x00000001 {
            return err
        }
    } else {
        // Only invoke CoUninitialize if the thread was not initizlied before.
        // This will allow other go packages based on go-ole play along
        // with this library.
        defer ole.CoUninitialize()
    }

    unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
    if err != nil {
        return err
    }
    defer unknown.Release()

    wmi, err := unknown.QueryInterface(ole.IID_IDispatch)
    if err != nil {
        return err
    }
    defer wmi.Release()

    // service is a SWbemServices
    serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", connectServerArgs...)
    if err != nil {
        return err
    }
    service := serviceRaw.ToIDispatch()
    defer serviceRaw.Clear()

    // result is a SWBemObjectSet
    resultRaw, err := oleutil.CallMethod(service, "ExecQuery", query)
    if err != nil {
        return err
    }
    result := resultRaw.ToIDispatch()
    defer resultRaw.Clear()

    count, err := oleInt64(result, "Count")
    if err != nil {
        return err
    }

    // Initialize a slice with Count capacity
    dv.Set(reflect.MakeSlice(dv.Type(), 0, int(count)))

    var errFieldMismatch error
    for i := int64(0); i < count; i++ {
        err := func() error {
            // item is a SWbemObject, but really a Win32_Process
            itemRaw, err := oleutil.CallMethod(result, "ItemIndex", i)
            if err != nil {
                return err
            }
            item := itemRaw.ToIDispatch()
            defer itemRaw.Clear()

            ev := reflect.New(elemType)
            if err = loadEntity(ev.Interface(), item); err != nil {
                if _, ok := err.(*ErrFieldMismatch); ok {
                    // We continue loading entities even in the face of field mismatch errors.
                    // If we encounter any other error, that other error is returned. Otherwise,
                    // an ErrFieldMismatch is returned.
                    errFieldMismatch = err
                } else {
                    return err
                }
            }
            if mat != multiArgTypeStructPtr {
                ev = ev.Elem()
            }
            dv.Set(reflect.Append(dv, ev))
            return nil
        }()
        if err != nil {
            return err
        }
    }
    return errFieldMismatch
}

// ErrFieldMismatch is returned when a field is to be loaded into a different
// type than the one it was stored from, or when a field is missing or
// unexported in the destination struct.
// StructType is the type of the struct pointed to by the destination argument.
type ErrFieldMismatch struct {
    StructType reflect.Type
    FieldName  string
    Reason     string
}

func (e *ErrFieldMismatch) Error() string {
    return fmt.Sprintf("wmi: cannot load field %q into a %q: %s",
        e.FieldName, e.StructType, e.Reason)
}

var timeType = reflect.TypeOf(time.Time{})

// loadEntity loads a SWbemObject into a struct pointer.
func loadEntity(dst interface{}, src *ole.IDispatch) (errFieldMismatch error) {
    v := reflect.ValueOf(dst).Elem()
    for i := 0; i < v.NumField(); i++ {
        f := v.Field(i)
        isPtr := f.Kind() == reflect.Ptr
        if isPtr {
            ptr := reflect.New(f.Type().Elem())
            f.Set(ptr)
            f = f.Elem()
        }
        n := v.Type().Field(i).Name
        if !f.CanSet() {
            return &ErrFieldMismatch{
                StructType: f.Type(),
                FieldName:  n,
                Reason:     "CanSet() is false",
            }
        }
        prop, err := oleutil.GetProperty(src, n)
        if err != nil {
            errFieldMismatch = &ErrFieldMismatch{
                StructType: f.Type(),
                FieldName:  n,
                Reason:     "no such struct field",
            }
            continue
        }
        defer prop.Clear()

        switch val := prop.Value().(type) {
        case int, int64:
            var v int64
            switch val := val.(type) {
            case int:
                v = int64(val)
            case int64:
                v = val
            default:
                panic("unexpected type")
            }
            switch f.Kind() {
            case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                f.SetInt(v)
            case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
                f.SetUint(uint64(v))
            default:
                return &ErrFieldMismatch{
                    StructType: f.Type(),
                    FieldName:  n,
                    Reason:     "not an integer class",
                }
            }
        case string:
            iv, err := strconv.ParseInt(val, 10, 64)
            switch f.Kind() {
            case reflect.String:
                f.SetString(val)
            case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                if err != nil {
                    return err
                }
                f.SetInt(iv)
            case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
                if err != nil {
                    return err
                }
                f.SetUint(uint64(iv))
            case reflect.Struct:
                switch f.Type() {
                case timeType:
                    if len(val) == 25 {
                        mins, err := strconv.Atoi(val[22:])
                        if err != nil {
                            return err
                        }
                        val = val[:22] + fmt.Sprintf("%02d%02d", mins/60, mins%60)
                    }
                    t, err := time.Parse("20060102150405.000000-0700", val)
                    if err != nil {
                        return err
                    }
                    f.Set(reflect.ValueOf(t))
                }
            }
        case bool:
            switch f.Kind() {
            case reflect.Bool:
                f.SetBool(val)
            default:
                return &ErrFieldMismatch{
                    StructType: f.Type(),
                    FieldName:  n,
                    Reason:     "not a bool",
                }
            }
        default:
            typeof := reflect.TypeOf(val)
            if isPtr && typeof == nil {
                break
            }
            return &ErrFieldMismatch{
                StructType: f.Type(),
                FieldName:  n,
                Reason:     fmt.Sprintf("unsupported type (%T)", val),
            }
        }
    }
    return errFieldMismatch
}

type multiArgType int

const (
    multiArgTypeInvalid multiArgType = iota
    multiArgTypeStruct
    multiArgTypeStructPtr
)

// checkMultiArg checks that v has type []S, []*S for some struct type S.
//
// It returns what category the slice elements are, and the reflect.Type
// that represents S.
func checkMultiArg(v reflect.Value) (m multiArgType, elemType reflect.Type) {
    if v.Kind() != reflect.Slice {
        return multiArgTypeInvalid, nil
    }
    elemType = v.Type().Elem()
    switch elemType.Kind() {
    case reflect.Struct:
        return multiArgTypeStruct, elemType
    case reflect.Ptr:
        elemType = elemType.Elem()
        if elemType.Kind() == reflect.Struct {
            return multiArgTypeStructPtr, elemType
        }
    }
    return multiArgTypeInvalid, nil
}

func oleInt64(item *ole.IDispatch, prop string) (int64, error) {
    v, err := oleutil.GetProperty(item, prop)
    if err != nil {
        return 0, err
    }
    defer v.Clear()

    i := int64(v.Val)
    return i, nil
}

// CreateQuery returns a WQL query string that queries all columns of src. where
// is an optional string that is appended to the query, to be used with WHERE
// clauses. In such a case, the "WHERE" string should appear at the beginning.
func CreateQuery(src interface{}, where string) string {
    var b bytes.Buffer
    b.WriteString("SELECT ")
    s := reflect.Indirect(reflect.ValueOf(src))
    t := s.Type()
    if s.Kind() == reflect.Slice {
        t = t.Elem()
    }
    if t.Kind() != reflect.Struct {
        return ""
    }
    var fields []string
    for i := 0; i < t.NumField(); i++ {
        fields = append(fields, t.Field(i).Name)
    }
    b.WriteString(strings.Join(fields, ", "))
    b.WriteString(" FROM ")
    b.WriteString(t.Name())
    b.WriteString(" " + where)
    return b.String()
}

Ответ 3

Чтобы получить доступ к объекту winmgmts или пространству имен (которое является тем же самым), вы можете использовать код ниже. По сути, вам нужно указать пространство имен в качестве параметра, что не задокументировано должным образом в go-ole.

В приведенном ниже коде вы также можете увидеть, как получить доступ к классу в этом пространстве имен и выполнить метод.

package main

import (
    "log"

    "github.com/go-ole/go-ole"
    "github.com/go-ole/go-ole/oleutil"
)

func main() {
    ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
    defer ole.CoUninitialize()

    unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
    if err != nil {
        log.Panic(err)
    }
    defer unknown.Release()

    wmi, err := unknown.QueryInterface(ole.IID_IDispatch)
    if err != nil {
        log.Panic(err)
    }
    defer wmi.Release()

    // Connect to namespace
    // root/PanasonicPC = winmgmts:\\.\root\PanasonicPC
    serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", nil, "root/PanasonicPC")
    if err != nil {
        log.Panic(err)
    }
    service := serviceRaw.ToIDispatch()
    defer serviceRaw.Clear()

    // Get class
    setBiosRaw, err := oleutil.CallMethod(service, "Get", "SetBIOS4Conf")
    if err != nil {
        log.Panic(err)
    }
    setBios := setBiosRaw.ToIDispatch()
    defer setBiosRaw.Clear()

    // Run method
    resultRaw, err := oleutil.CallMethod(setBios, "AccessAuthorization", "letmein")
    resultVal := resultRaw.Value().(int32)

    log.Println("Return Code:", resultVal)
}