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 }