View Javadoc
1   /*
2    * Copyright 2020-2022 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.driver.windows.wmi;
6   
7   import static oshi.util.Memoizer.memoize;
8   
9   import java.util.HashMap;
10  import java.util.Map;
11  import java.util.concurrent.locks.ReentrantLock;
12  import java.util.function.Supplier;
13  
14  import com.sun.jna.platform.win32.COM.WbemcliUtil.WmiResult;
15  
16  import oshi.annotation.concurrent.GuardedBy;
17  import oshi.annotation.concurrent.ThreadSafe;
18  import oshi.driver.windows.wmi.Win32Process.CommandLineProperty;
19  import oshi.util.platform.windows.WmiUtil;
20  import oshi.util.tuples.Pair;
21  
22  /**
23   * Utility to query WMI class {@code Win32_Process} using cache
24   */
25  @ThreadSafe
26  public final class Win32ProcessCached {
27  
28      private static final Supplier<Win32ProcessCached> INSTANCE = memoize(Win32ProcessCached::createInstance);
29  
30      // Use a map to cache command line queries
31      @GuardedBy("commandLineCacheLock")
32      private final Map<Integer, Pair<Long, String>> commandLineCache = new HashMap<>();
33      private final ReentrantLock commandLineCacheLock = new ReentrantLock();
34  
35      private Win32ProcessCached() {
36      }
37  
38      /**
39       * Get the singleton instance of this class, instantiating the map which caches command lines.
40       *
41       * @return the singleton instance
42       */
43      public static Win32ProcessCached getInstance() {
44          return INSTANCE.get();
45      }
46  
47      private static Win32ProcessCached createInstance() {
48          return new Win32ProcessCached();
49      }
50  
51      /**
52       * Gets the process command line, while also querying and caching command lines for all running processes if the
53       * specified process is not in the cache.
54       * <p>
55       * When iterating over a process list, the WMI overhead of querying each single command line can quickly exceed the
56       * time it takes to query all command lines. This method permits access to cached queries from a previous call,
57       * significantly improving aggregate performance.
58       *
59       * @param processId The process ID for which to return the command line.
60       * @param startTime The start time of the process, in milliseconds since the 1970 epoch. If this start time is after
61       *                  the time this process was previously queried, the prior entry will be deemed invalid and the
62       *                  cache refreshed.
63       * @return The command line of the specified process. If the command line is cached from a previous call and the
64       *         start time is prior to the time it was cached, this method will quickly return the cached value.
65       *         Otherwise, will refresh the cache with all running processes prior to returning, which may incur some
66       *         latency.
67       *         <p>
68       *         May return a command line from the cache even after a process has terminated. Otherwise will return the
69       *         empty string.
70       */
71      public String getCommandLine(int processId, long startTime) {
72          // We could use synchronized method but this is more clear
73          commandLineCacheLock.lock();
74          try {
75              // See if this process is in the cache already
76              Pair<Long, String> pair = commandLineCache.get(processId);
77              // Valid process must have been started before map insertion
78              if (pair != null && startTime < pair.getA()) {
79                  // Entry is valid, return it!
80                  return pair.getB();
81              } else {
82                  // Invalid entry, rebuild cache
83                  // Processes started after this time will be invalid
84                  long now = System.currentTimeMillis();
85                  // Gets all processes. Takes ~200ms
86                  WmiResult<CommandLineProperty> commandLineAllProcs = Win32Process.queryCommandLines(null);
87                  // Stale processes use resources. Periodically clear before building
88                  // Use a threshold of map size > 2x # of processes
89                  if (commandLineCache.size() > commandLineAllProcs.getResultCount() * 2) {
90                      commandLineCache.clear();
91                  }
92                  // Iterate results and put in map. Save value for current PID along the way
93                  String result = "";
94                  for (int i = 0; i < commandLineAllProcs.getResultCount(); i++) {
95                      int pid = WmiUtil.getUint32(commandLineAllProcs, CommandLineProperty.PROCESSID, i);
96                      String cl = WmiUtil.getString(commandLineAllProcs, CommandLineProperty.COMMANDLINE, i);
97                      commandLineCache.put(pid, new Pair<>(now, cl));
98                      if (pid == processId) {
99                          result = cl;
100                     }
101                 }
102                 return result;
103             }
104         } finally {
105             commandLineCacheLock.unlock();
106         }
107     }
108 }