View Javadoc
1   package org.metricshub.wmi.remotecommand;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * WMI Java Client
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2023 - 2025 MetricsHub
8    * ჻჻჻჻჻჻
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   *
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
21   */
22  
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Objects;
29  import java.util.concurrent.TimeoutException;
30  import java.util.stream.Collectors;
31  import org.metricshub.wmi.TimeoutHelper;
32  import org.metricshub.wmi.Utils;
33  import org.metricshub.wmi.WmiHelper;
34  import org.metricshub.wmi.exceptions.ProcessNotFoundException;
35  import org.metricshub.wmi.exceptions.WmiComException;
36  import org.metricshub.wmi.exceptions.WqlQuerySyntaxException;
37  import org.metricshub.wmi.wbem.WmiWbemServices;
38  
39  /**
40   * Class for the Win32 related methods.
41   *
42   */
43  public class RemoteProcess {
44  
45  	private RemoteProcess() {}
46  
47  	private static final String TERMINATE = "Terminate";
48  	private static final String CREATE = "Create";
49  
50  	private static final String CIMV2_NAMESPACE = "ROOT\\CIMV2";
51  
52  	private static final String WIN32_PROCESS = "Win32_Process";
53  
54  	/**
55  	 * Map of the possible (and known) ReturnValue of the Win32_Process methods
56  	 */
57  	private static final Map<Long, String> METHOD_RETURNVALUE_MAP;
58  
59  	static {
60  		final Map<Long, String> map = new HashMap<>();
61  		map.put(2L, "Access denied");
62  		map.put(3L, "Insufficient privilege");
63  		map.put(8L, "Unknown failure");
64  		map.put(9L, "Path not found");
65  		map.put(21L, "Invalid parameter");
66  		METHOD_RETURNVALUE_MAP = Collections.unmodifiableMap(map);
67  	}
68  
69  	/**
70  	 * Execute the command on the remote
71  	 * @param command The command to execute
72  	 * @param hostname Hostname of IP address where to execute the command
73  	 * @param username Username (may be null)
74  	 * @param password Password (may be null)
75  	 * @param workingDirectory Path of the directory for the spawned process on the remote system (can be null)
76  	 * @param timeout Timeout in milliseconds
77  	 * @return the command status code
78  	 * @throws WmiComException  For any problem encountered with JNA
79  	 * @throws TimeoutException To notify userName of timeout.
80  	 */
81  	public static long executeCommand(
82  		final String command,
83  		final String hostname,
84  		final String username,
85  		final char[] password,
86  		final String workingDirectory,
87  		final long timeout
88  	) throws WmiComException, TimeoutException {
89  		Utils.checkNonNull(command, "command");
90  		Utils.checkArgumentNotZeroOrNegative(timeout, "timeout");
91  
92  		final long start = Utils.getCurrentTimeMillis();
93  
94  		final String networkResource = WmiHelper.createNetworkResource(hostname, CIMV2_NAMESPACE);
95  		try (WmiWbemServices wmiWbemServices = WmiWbemServices.getInstance(networkResource, username, password)) {
96  			// Execute Win32_Process::Create
97  			final Map<String, Object> createInputs = new HashMap<>();
98  			createInputs.put("CommandLine", command);
99  			if (!Utils.isBlank(workingDirectory)) {
100 				createInputs.put("CurrentDirectory", workingDirectory.trim());
101 			}
102 			final Map<String, Object> createResult = wmiWbemServices.executeMethod(
103 				WIN32_PROCESS,
104 				WIN32_PROCESS,
105 				CREATE,
106 				createInputs
107 			);
108 
109 			// Extract ProcessId from the result
110 			final Long processId = (Long) createResult.get("ProcessId");
111 			if (processId == null || processId.longValue() < 1) {
112 				throw new WmiComException("Could not spawn the process: No ProcessId was returned by Win32_Process::Create");
113 			}
114 
115 			// Wait for the process to complete
116 			try {
117 				while (
118 					existProcess(
119 						wmiWbemServices,
120 						processId,
121 						TimeoutHelper.getRemainingTime(timeout, start, "No time left to check if the process exists")
122 					)
123 				) {
124 					TimeoutHelper.stagedSleep(timeout, start, String.format("Command %s execution has timed out", command));
125 				}
126 			} catch (final TimeoutException e) {
127 				// TIME'S UP!
128 				// Kill the process and its children (and give us a 10-second extra time to do this)
129 				killProcessWithChildren(wmiWbemServices, processId, 10000);
130 				throw e;
131 			}
132 
133 			return (Long) createResult.get("ReturnValue");
134 		}
135 	}
136 
137 	/**
138 	 * Check if a process exist in Win32_Process.
139 	 *
140 	 * @param wbemServices WBEM Services handling
141 	 * @param pid The process Id.
142 	 * @param timeout Timeout in milliseconds.
143 	 * @return true if the process exist false otherwise.
144 	 *
145 	 * @throws WmiComException  For any problem encountered with JNA
146 	 * @throws TimeoutException To notify userName of timeout.
147 	 */
148 	static boolean existProcess(final WmiWbemServices wbemServices, final long pid, final long timeout)
149 		throws WmiComException, TimeoutException {
150 		try {
151 			return !wbemServices
152 				.executeWql(String.format("SELECT Handle FROM Win32_Process WHERE Handle = '%d'", pid), timeout)
153 				.isEmpty();
154 		} catch (final WqlQuerySyntaxException e) {
155 			throw new WmiComException(e);
156 		}
157 	}
158 
159 	/**
160 	 * Kill the specified process and all its children.
161 	 *
162 	 * @param wmiWbemServices WBEM Services handling
163 	 * @param pid The process Id.
164 	 * @param timeout Timeout in milliseconds.
165 	 * @throws WmiComException For any problem encountered with JNA.
166 	 * @throws TimeoutException To notify userName of timeout.
167 	 */
168 	private static void killProcessWithChildren(
169 		final WmiWbemServices wmiWbemServices,
170 		final long pid,
171 		final long timeout
172 	) throws WmiComException, TimeoutException {
173 		// First, get the children
174 		try {
175 			final long start = Utils.getCurrentTimeMillis();
176 
177 			final List<Long> pidToKillList = new ArrayList<>();
178 			pidToKillList.add(pid);
179 			pidToKillList.addAll(
180 				wmiWbemServices
181 					.executeWql(String.format("SELECT Handle FROM Win32_Process WHERE ParentProcessId = '%d'", pid), timeout)
182 					.stream()
183 					.map(row -> (String) row.get("Handle"))
184 					.filter(Objects::nonNull)
185 					.map(Long::parseLong)
186 					.collect(Collectors.toList())
187 			);
188 
189 			// Kill
190 			for (final long pidToKill : pidToKillList) {
191 				if (TimeoutHelper.getRemainingTime(timeout, start, "No time left to kill the process") < 0) {
192 					throw new TimeoutException("Timeout while killing remaining processes");
193 				}
194 				try {
195 					killProcess(wmiWbemServices, pidToKill);
196 				} catch (final ProcessNotFoundException e) {
197 					/* Do nothing, just ignore */
198 				}
199 			}
200 		} catch (final WqlQuerySyntaxException e) {
201 			throw new WmiComException(e); // Impossible
202 		}
203 	}
204 
205 	/**
206 	 * Kill the process with id.
207 	 *
208 	 * @param wmiWbemServices WBEM Services handling
209 	 * @param pid The process Id.
210 	 * @throws WmiComException For any problem encountered with JNA.
211 	 */
212 	private static void killProcess(final WmiWbemServices wmiWbemServices, final long pid)
213 		throws WmiComException, ProcessNotFoundException {
214 		// Call the Terminate method of the Win32_Process class
215 		// Reason is set to 1, just so that process exit code is non-zero, to indicate a failure
216 		final Map<String, Object> inputs = Collections.singletonMap("Reason", 1);
217 		final Map<String, Object> terminateResult;
218 		try {
219 			terminateResult =
220 				wmiWbemServices.executeMethod(
221 					String.format("Win32_Process.Handle='%d'", pid),
222 					WIN32_PROCESS,
223 					TERMINATE,
224 					inputs
225 				);
226 		} catch (final WmiComException e) {
227 			// Special case if we got an exception because the specified process could not be found
228 			if (e.getMessage().contains("WBEM_E_NOT_FOUND")) {
229 				throw new ProcessNotFoundException(pid);
230 			}
231 			throw e;
232 		}
233 
234 		// Check ReturnValue
235 		final Long returnCode = (Long) terminateResult.get("ReturnValue");
236 		if (returnCode == null || returnCode.longValue() != 0) {
237 			throw new WmiComException("Could not terminate the process (%d): %s", pid, getReturnErrorMessage(returnCode));
238 		}
239 	}
240 
241 	private static String getReturnErrorMessage(final long returnCode) {
242 		return METHOD_RETURNVALUE_MAP.getOrDefault(returnCode, String.format("Unknown return code (%d)", returnCode));
243 	}
244 }