View Javadoc
1   /*
2    * Copyright 2019-2022 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.util.platform.windows;
6   
7   import java.lang.reflect.InvocationTargetException;
8   import java.util.HashSet;
9   import java.util.Set;
10  import java.util.concurrent.TimeoutException;
11  
12  import org.slf4j.Logger;
13  import org.slf4j.LoggerFactory;
14  
15  import com.sun.jna.platform.win32.Ole32;
16  import com.sun.jna.platform.win32.WinError;
17  import com.sun.jna.platform.win32.WinNT;
18  import com.sun.jna.platform.win32.COM.COMException;
19  import com.sun.jna.platform.win32.COM.COMUtils;
20  import com.sun.jna.platform.win32.COM.Wbemcli;
21  import com.sun.jna.platform.win32.COM.WbemcliUtil;
22  
23  import oshi.annotation.concurrent.ThreadSafe;
24  import oshi.util.GlobalConfig;
25  
26  /**
27   * Utility to handle WMI Queries. Designed to be extended with user-customized behavior.
28   */
29  @ThreadSafe
30  public class WmiQueryHandler {
31  
32      private static final Logger LOG = LoggerFactory.getLogger(WmiQueryHandler.class);
33  
34      private static int globalTimeout = GlobalConfig.get(GlobalConfig.OSHI_UTIL_WMI_TIMEOUT, -1);
35  
36      static {
37          if (globalTimeout == 0 || globalTimeout < -1) {
38              throw new GlobalConfig.PropertyException(GlobalConfig.OSHI_UTIL_WMI_TIMEOUT);
39          }
40      }
41  
42      // Timeout for WMI queries
43      private int wmiTimeout = globalTimeout;
44  
45      // Cache failed wmi classes
46      private final Set<String> failedWmiClassNames = new HashSet<>();
47  
48      // Preferred threading model
49      private int comThreading = Ole32.COINIT_MULTITHREADED;
50  
51      // Track initialization of Security
52      private boolean securityInitialized = false;
53  
54      private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
55      private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
56  
57      // Factory to create this or a subclass
58      private static Class<? extends WmiQueryHandler> customClass = null;
59  
60      protected WmiQueryHandler() {
61          // Allow subclassing but not instantiation
62      }
63  
64      /**
65       * Factory method to create an instance of this class. To override this class, use {@link #setInstanceClass(Class)}
66       * to define a subclass which extends {@link oshi.util.platform.windows.WmiQueryHandler}.
67       *
68       * @return An instance of this class or a class defined by {@link #setInstanceClass(Class)}
69       */
70      public static synchronized WmiQueryHandler createInstance() {
71          if (customClass == null) {
72              return new WmiQueryHandler();
73          }
74          try {
75              return customClass.getConstructor(EMPTY_CLASS_ARRAY).newInstance(EMPTY_OBJECT_ARRAY);
76          } catch (NoSuchMethodException | SecurityException e) {
77              LOG.error("Failed to find or access a no-arg constructor for {}", customClass);
78          } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
79                  | InvocationTargetException e) {
80              LOG.error("Failed to create a new instance of {}", customClass);
81          }
82          return null;
83      }
84  
85      /**
86       * Define a subclass to be instantiated by {@link #createInstance()}. The class must extend
87       * {@link oshi.util.platform.windows.WmiQueryHandler}.
88       *
89       * @param instanceClass The class to instantiate with {@link #createInstance()}.
90       */
91      public static synchronized void setInstanceClass(Class<? extends WmiQueryHandler> instanceClass) {
92          customClass = instanceClass;
93      }
94  
95      /**
96       * Query WMI for values. Makes no assumptions on whether the user has previously initialized COM.
97       *
98       * @param <T>   WMI queries use an Enum to identify the fields to query, and use the enum values as keys to retrieve
99       *              the results.
100      * @param query A WmiQuery object encapsulating the namespace, class, and properties
101      * @return a WmiResult object containing the query results, wrapping an EnumMap
102      */
103     public <T extends Enum<T>> WbemcliUtil.WmiResult<T> queryWMI(WbemcliUtil.WmiQuery<T> query) {
104         return queryWMI(query, true);
105     }
106 
107     /**
108      * Query WMI for values.
109      *
110      * @param <T>     WMI queries use an Enum to identify the fields to query, and use the enum values as keys to
111      *                retrieve the results.
112      * @param query   A WmiQuery object encapsulating the namespace, class, and properties
113      * @param initCom Whether to initialize COM. If {@code true}, initializes COM before the query and uninitializes
114      *                after. If {@code false}, assumes the user has initialized COM separately. This can improve WMI
115      *                query performance.
116      * @return a WmiResult object containing the query results, wrapping an EnumMap
117      */
118     public <T extends Enum<T>> WbemcliUtil.WmiResult<T> queryWMI(WbemcliUtil.WmiQuery<T> query, boolean initCom) {
119         WbemcliUtil.WmiResult<T> result = WbemcliUtil.INSTANCE.new WmiResult<>(query.getPropertyEnum());
120         if (failedWmiClassNames.contains(query.getWmiClassName())) {
121             return result;
122         }
123         boolean comInit = false;
124         try {
125             if (initCom) {
126                 comInit = initCOM();
127             }
128             result = query.execute(wmiTimeout);
129         } catch (COMException e) {
130             // Ignore any exceptions with OpenHardwareMonitor
131             if (!WmiUtil.OHM_NAMESPACE.equals(query.getNameSpace())) {
132                 final int hresult = e.getHresult() == null ? -1 : e.getHresult().intValue();
133                 switch (hresult) {
134                 case Wbemcli.WBEM_E_INVALID_NAMESPACE:
135                     LOG.warn("COM exception: Invalid Namespace {}", query.getNameSpace());
136                     break;
137                 case Wbemcli.WBEM_E_INVALID_CLASS:
138                     LOG.warn("COM exception: Invalid Class {}", query.getWmiClassName());
139                     break;
140                 case Wbemcli.WBEM_E_INVALID_QUERY:
141                     LOG.warn("COM exception: Invalid Query: {}", WmiUtil.queryToString(query));
142                     break;
143                 default:
144                     handleComException(query, e);
145                     break;
146                 }
147                 failedWmiClassNames.add(query.getWmiClassName());
148             }
149         } catch (TimeoutException e) {
150             LOG.warn("WMI query timed out after {} ms: {}", wmiTimeout, WmiUtil.queryToString(query));
151         } finally {
152             if (comInit) {
153                 unInitCOM();
154             }
155         }
156         return result;
157     }
158 
159     /**
160      * COM Exception handler. Logs a warning message.
161      *
162      * @param query a {@link com.sun.jna.platform.win32.COM.WbemcliUtil.WmiQuery} object.
163      * @param ex    a {@link com.sun.jna.platform.win32.COM.COMException} object.
164      */
165     protected void handleComException(WbemcliUtil.WmiQuery<?> query, COMException ex) {
166         LOG.warn(
167                 "COM exception querying {}, which might not be on your system. Will not attempt to query it again. Error was {}: {}",
168                 query.getWmiClassName(), ex.getHresult() == null ? null : ex.getHresult().intValue(), ex.getMessage());
169     }
170 
171     /**
172      * Initializes COM library and sets security to impersonate the local user
173      *
174      * @return True if COM was initialized and needs to be uninitialized, false otherwise
175      */
176     public boolean initCOM() {
177         boolean comInit = false;
178         // Step 1: --------------------------------------------------
179         // Initialize COM. ------------------------------------------
180         comInit = initCOM(getComThreading());
181         if (!comInit) {
182             comInit = initCOM(switchComThreading());
183         }
184         // Step 2: --------------------------------------------------
185         // Set general COM security levels --------------------------
186         if (comInit && !isSecurityInitialized()) {
187             WinNT.HRESULT hres = Ole32.INSTANCE.CoInitializeSecurity(null, -1, null, null,
188                     Ole32.RPC_C_AUTHN_LEVEL_DEFAULT, Ole32.RPC_C_IMP_LEVEL_IMPERSONATE, null, Ole32.EOAC_NONE, null);
189             // If security already initialized we get RPC_E_TOO_LATE
190             // This can be safely ignored
191             if (COMUtils.FAILED(hres) && hres.intValue() != WinError.RPC_E_TOO_LATE) {
192                 Ole32.INSTANCE.CoUninitialize();
193                 throw new COMException("Failed to initialize security.", hres);
194             }
195             securityInitialized = true;
196         }
197         return comInit;
198     }
199 
200     /**
201      * Initializes COM with a specific threading model
202      *
203      * @param coInitThreading The threading model
204      * @return True if COM was initialized and needs to be uninitialized, false otherwise
205      */
206     protected boolean initCOM(int coInitThreading) {
207         WinNT.HRESULT hres = Ole32.INSTANCE.CoInitializeEx(null, coInitThreading);
208         switch (hres.intValue()) {
209         // Successful local initialization (S_OK) or was already initialized
210         // (S_FALSE) but still needs uninit
211         case COMUtils.S_OK:
212         case COMUtils.S_FALSE:
213             return true;
214         // COM was already initialized with a different threading model
215         case WinError.RPC_E_CHANGED_MODE:
216             return false;
217         // Any other results is impossible
218         default:
219             throw new COMException("Failed to initialize COM library.", hres);
220         }
221     }
222 
223     /**
224      * UnInitializes COM library. This should be called once for every successful call to initCOM.
225      */
226     public void unInitCOM() {
227         Ole32.INSTANCE.CoUninitialize();
228     }
229 
230     /**
231      * Returns the current threading model for COM initialization, as OSHI is required to match if an external program
232      * has COM initialized already.
233      *
234      * @return The current threading model
235      */
236     public int getComThreading() {
237         return comThreading;
238     }
239 
240     /**
241      * Switches the current threading model for COM initialization, as OSHI is required to match if an external program
242      * has COM initialized already.
243      *
244      * @return The new threading model after switching
245      */
246     public int switchComThreading() {
247         if (comThreading == Ole32.COINIT_APARTMENTTHREADED) {
248             comThreading = Ole32.COINIT_MULTITHREADED;
249         } else {
250             comThreading = Ole32.COINIT_APARTMENTTHREADED;
251         }
252         return comThreading;
253     }
254 
255     /**
256      * Security only needs to be initialized once. This boolean identifies whether that has happened.
257      *
258      * @return Returns the securityInitialized.
259      */
260     public boolean isSecurityInitialized() {
261         return securityInitialized;
262     }
263 
264     /**
265      * Gets the current WMI timeout. WMI queries will fail if they take longer than this number of milliseconds. A value
266      * of -1 is infinite (no timeout).
267      *
268      * @return Returns the current value of wmiTimeout.
269      */
270     public int getWmiTimeout() {
271         return wmiTimeout;
272     }
273 
274     /**
275      * Sets the WMI timeout. WMI queries will fail if they take longer than this number of milliseconds.
276      *
277      * @param wmiTimeout The wmiTimeout to set, in milliseconds. To disable timeouts, set timeout as -1 (infinite).
278      */
279     public void setWmiTimeout(int wmiTimeout) {
280         this.wmiTimeout = wmiTimeout;
281     }
282 }