github.com/boomhut/fiber/v2@v2.0.0-20230603160335-b65c856e57d3/internal/wmi/swbemservices.go (about)

     1  //go:build windows
     2  // +build windows
     3  
     4  package wmi
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"runtime"
    10  	"sync"
    11  
    12  	"github.com/boomhut/fiber/v2/internal/go-ole"
    13  	"github.com/boomhut/fiber/v2/internal/go-ole/oleutil"
    14  )
    15  
    16  // SWbemServices is used to access wmi. See https://msdn.microsoft.com/en-us/library/aa393719(v=vs.85).aspx
    17  type SWbemServices struct {
    18  	//TODO: track namespace. Not sure if we can re connect to a different namespace using the same instance
    19  	cWMIClient            *Client //This could also be an embedded struct, but then we would need to branch on Client vs SWbemServices in the Query method
    20  	sWbemLocatorIUnknown  *ole.IUnknown
    21  	sWbemLocatorIDispatch *ole.IDispatch
    22  	queries               chan *queryRequest
    23  	closeError            chan error
    24  	lQueryorClose         sync.Mutex
    25  }
    26  
    27  type queryRequest struct {
    28  	query    string
    29  	dst      interface{}
    30  	args     []interface{}
    31  	finished chan error
    32  }
    33  
    34  // InitializeSWbemServices will return a new SWbemServices object that can be used to query WMI
    35  func InitializeSWbemServices(c *Client, connectServerArgs ...interface{}) (*SWbemServices, error) {
    36  	//fmt.Println("InitializeSWbemServices: Starting")
    37  	//TODO: implement connectServerArgs as optional argument for init with connectServer call
    38  	s := new(SWbemServices)
    39  	s.cWMIClient = c
    40  	s.queries = make(chan *queryRequest)
    41  	initError := make(chan error)
    42  	go s.process(initError)
    43  
    44  	err, ok := <-initError
    45  	if ok {
    46  		return nil, err //Send error to caller
    47  	}
    48  	//fmt.Println("InitializeSWbemServices: Finished")
    49  	return s, nil
    50  }
    51  
    52  // Close will clear and release all of the SWbemServices resources
    53  func (s *SWbemServices) Close() error {
    54  	s.lQueryorClose.Lock()
    55  	if s == nil || s.sWbemLocatorIDispatch == nil {
    56  		s.lQueryorClose.Unlock()
    57  		return fmt.Errorf("SWbemServices is not Initialized")
    58  	}
    59  	if s.queries == nil {
    60  		s.lQueryorClose.Unlock()
    61  		return fmt.Errorf("SWbemServices has been closed")
    62  	}
    63  	//fmt.Println("Close: sending close request")
    64  	var result error
    65  	ce := make(chan error)
    66  	s.closeError = ce //Race condition if multiple callers to close. May need to lock here
    67  	close(s.queries)  //Tell background to shut things down
    68  	s.lQueryorClose.Unlock()
    69  	err, ok := <-ce
    70  	if ok {
    71  		result = err
    72  	}
    73  	//fmt.Println("Close: finished")
    74  	return result
    75  }
    76  
    77  func (s *SWbemServices) process(initError chan error) {
    78  	//fmt.Println("process: starting background thread initialization")
    79  	//All OLE/WMI calls must happen on the same initialized thead, so lock this goroutine
    80  	runtime.LockOSThread()
    81  	defer runtime.UnlockOSThread()
    82  
    83  	err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
    84  	if err != nil {
    85  		oleCode := err.(*ole.OleError).Code()
    86  		if oleCode != ole.S_OK && oleCode != S_FALSE {
    87  			initError <- fmt.Errorf("ole.CoInitializeEx error: %v", err)
    88  			return
    89  		}
    90  	}
    91  	defer ole.CoUninitialize()
    92  
    93  	unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
    94  	if err != nil {
    95  		initError <- fmt.Errorf("CreateObject SWbemLocator error: %v", err)
    96  		return
    97  	} else if unknown == nil {
    98  		initError <- ErrNilCreateObject
    99  		return
   100  	}
   101  	defer unknown.Release()
   102  	s.sWbemLocatorIUnknown = unknown
   103  
   104  	dispatch, err := s.sWbemLocatorIUnknown.QueryInterface(ole.IID_IDispatch)
   105  	if err != nil {
   106  		initError <- fmt.Errorf("SWbemLocator QueryInterface error: %v", err)
   107  		return
   108  	}
   109  	defer dispatch.Release()
   110  	s.sWbemLocatorIDispatch = dispatch
   111  
   112  	// we can't do the ConnectServer call outside the loop unless we find a way to track and re-init the connectServerArgs
   113  	//fmt.Println("process: initialized. closing initError")
   114  	close(initError)
   115  	//fmt.Println("process: waiting for queries")
   116  	for q := range s.queries {
   117  		//fmt.Printf("process: new query: len(query)=%d\n", len(q.query))
   118  		errQuery := s.queryBackground(q)
   119  		//fmt.Println("process: s.queryBackground finished")
   120  		if errQuery != nil {
   121  			q.finished <- errQuery
   122  		}
   123  		close(q.finished)
   124  	}
   125  	//fmt.Println("process: queries channel closed")
   126  	s.queries = nil //set channel to nil so we know it is closed
   127  	//TODO: I think the Release/Clear calls can panic if things are in a bad state.
   128  	//TODO: May need to recover from panics and send error to method caller instead.
   129  	close(s.closeError)
   130  }
   131  
   132  // Query runs the WQL query using a SWbemServices instance and appends the values to dst.
   133  //
   134  // dst must have type *[]S or *[]*S, for some struct type S. Fields selected in
   135  // the query must have the same name in dst. Supported types are all signed and
   136  // unsigned integers, time.Time, string, bool, or a pointer to one of those.
   137  // Array types are not supported.
   138  //
   139  // By default, the local machine and default namespace are used. These can be
   140  // changed using connectServerArgs. See
   141  // http://msdn.microsoft.com/en-us/library/aa393720.aspx for details.
   142  func (s *SWbemServices) Query(query string, dst interface{}, connectServerArgs ...interface{}) error {
   143  	s.lQueryorClose.Lock()
   144  	if s == nil || s.sWbemLocatorIDispatch == nil {
   145  		s.lQueryorClose.Unlock()
   146  		return fmt.Errorf("SWbemServices is not Initialized")
   147  	}
   148  	if s.queries == nil {
   149  		s.lQueryorClose.Unlock()
   150  		return fmt.Errorf("SWbemServices has been closed")
   151  	}
   152  
   153  	//fmt.Println("Query: Sending query request")
   154  	qr := queryRequest{
   155  		query:    query,
   156  		dst:      dst,
   157  		args:     connectServerArgs,
   158  		finished: make(chan error),
   159  	}
   160  	s.queries <- &qr
   161  	s.lQueryorClose.Unlock()
   162  	err, ok := <-qr.finished
   163  	if ok {
   164  		//fmt.Println("Query: Finished with error")
   165  		return err //Send error to caller
   166  	}
   167  	//fmt.Println("Query: Finished")
   168  	return nil
   169  }
   170  
   171  func (s *SWbemServices) queryBackground(q *queryRequest) error {
   172  	if s == nil || s.sWbemLocatorIDispatch == nil {
   173  		return fmt.Errorf("SWbemServices is not Initialized")
   174  	}
   175  	wmi := s.sWbemLocatorIDispatch //Should just rename in the code, but this will help as we break things apart
   176  	//fmt.Println("queryBackground: Starting")
   177  
   178  	dv := reflect.ValueOf(q.dst)
   179  	if dv.Kind() != reflect.Ptr || dv.IsNil() {
   180  		return ErrInvalidEntityType
   181  	}
   182  	dv = dv.Elem()
   183  	mat, elemType := checkMultiArg(dv)
   184  	if mat == multiArgTypeInvalid {
   185  		return ErrInvalidEntityType
   186  	}
   187  
   188  	// service is a SWbemServices
   189  	serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", q.args...)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	service := serviceRaw.ToIDispatch()
   194  	defer serviceRaw.Clear()
   195  
   196  	// result is a SWBemObjectSet
   197  	resultRaw, err := oleutil.CallMethod(service, "ExecQuery", q.query)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	result := resultRaw.ToIDispatch()
   202  	defer resultRaw.Clear()
   203  
   204  	count, err := oleInt64(result, "Count")
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	enumProperty, err := result.GetProperty("_NewEnum")
   210  	if err != nil {
   211  		return err
   212  	}
   213  	defer enumProperty.Clear()
   214  
   215  	enum, err := enumProperty.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant)
   216  	if err != nil {
   217  		return err
   218  	}
   219  	if enum == nil {
   220  		return fmt.Errorf("can't get IEnumVARIANT, enum is nil")
   221  	}
   222  	defer enum.Release()
   223  
   224  	// Initialize a slice with Count capacity
   225  	dv.Set(reflect.MakeSlice(dv.Type(), 0, int(count)))
   226  
   227  	var errFieldMismatch error
   228  	for itemRaw, length, err := enum.Next(1); length > 0; itemRaw, length, err = enum.Next(1) {
   229  		if err != nil {
   230  			return err
   231  		}
   232  
   233  		err := func() error {
   234  			// item is a SWbemObject, but really a Win32_Process
   235  			item := itemRaw.ToIDispatch()
   236  			defer item.Release()
   237  
   238  			ev := reflect.New(elemType)
   239  			if err = s.cWMIClient.loadEntity(ev.Interface(), item); err != nil {
   240  				if _, ok := err.(*ErrFieldMismatch); ok {
   241  					// We continue loading entities even in the face of field mismatch errors.
   242  					// If we encounter any other error, that other error is returned. Otherwise,
   243  					// an ErrFieldMismatch is returned.
   244  					errFieldMismatch = err
   245  				} else {
   246  					return err
   247  				}
   248  			}
   249  			if mat != multiArgTypeStructPtr {
   250  				ev = ev.Elem()
   251  			}
   252  			dv.Set(reflect.Append(dv, ev))
   253  			return nil
   254  		}()
   255  		if err != nil {
   256  			return err
   257  		}
   258  	}
   259  	//fmt.Println("queryBackground: Finished")
   260  	return errFieldMismatch
   261  }