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<Integer, String> METHOD_RETURNVALUE_MAP;
58  
59  	static {
60  		final Map<Integer, String> map = new HashMap<>();
61  		map.put(2, "Access denied");
62  		map.put(3, "Insufficient privilege");
63  		map.put(8, "Unknown failure");
64  		map.put(9, "Path not found");
65  		map.put(21, "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 int 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 Integer processId = (Integer) createResult.get("ProcessId");
111 			if (processId == null || processId.intValue() < 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 (Integer) 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 int 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(final WmiWbemServices wmiWbemServices, final int pid, final long timeout)
169 		throws WmiComException, TimeoutException {
170 		// First, get the children
171 		try {
172 			final long start = Utils.getCurrentTimeMillis();
173 
174 			final List<Integer> pidToKillList = new ArrayList<>();
175 			pidToKillList.add(pid);
176 			pidToKillList.addAll(
177 				wmiWbemServices
178 					.executeWql(String.format("SELECT Handle FROM Win32_Process WHERE ParentProcessId = '%d'", pid), timeout)
179 					.stream()
180 					.map(row -> (String) row.get("Handle"))
181 					.filter(Objects::nonNull)
182 					.map(Integer::parseInt)
183 					.collect(Collectors.toList())
184 			);
185 
186 			// Kill
187 			for (final int pidToKill : pidToKillList) {
188 				if (TimeoutHelper.getRemainingTime(timeout, start, "No time left to kill the process") < 0) {
189 					throw new TimeoutException("Timeout while killing remaining processes");
190 				}
191 				try {
192 					killProcess(wmiWbemServices, pidToKill);
193 				} catch (final ProcessNotFoundException e) {
194 					/* Do nothing, just ignore */
195 				}
196 			}
197 		} catch (final WqlQuerySyntaxException e) {
198 			throw new WmiComException(e); // Impossible
199 		}
200 	}
201 
202 	/**
203 	 * Kill the process with id.
204 	 *
205 	 * @param wmiWbemServices WBEM Services handling
206 	 * @param pid The process Id.
207 	 * @throws WmiComException For any problem encountered with JNA.
208 	 */
209 	private static void killProcess(final WmiWbemServices wmiWbemServices, final int pid)
210 		throws WmiComException, ProcessNotFoundException {
211 		// Call the Terminate method of the Win32_Process class
212 		// Reason is set to 1, just so that process exit code is non-zero, to indicate a failure
213 		final Map<String, Object> inputs = Collections.singletonMap("Reason", 1);
214 		final Map<String, Object> terminateResult;
215 		try {
216 			terminateResult =
217 				wmiWbemServices.executeMethod(
218 					String.format("Win32_Process.Handle='%d'", pid),
219 					WIN32_PROCESS,
220 					TERMINATE,
221 					inputs
222 				);
223 		} catch (final WmiComException e) {
224 			// Special case if we got an exception because the specified process could not be found
225 			if (e.getMessage().contains("WBEM_E_NOT_FOUND")) {
226 				throw new ProcessNotFoundException(pid);
227 			}
228 			throw e;
229 		}
230 
231 		// Check ReturnValue
232 		final Integer returnCode = (Integer) terminateResult.get("ReturnValue");
233 		if (returnCode == null || returnCode.intValue() != 0) {
234 			throw new WmiComException("Could not terminate the process (%d): %s", pid, getReturnErrorMessage(returnCode));
235 		}
236 	}
237 
238 	private static String getReturnErrorMessage(final int returnCode) {
239 		return METHOD_RETURNVALUE_MAP.getOrDefault(returnCode, String.format("Unknown return code (%d)", returnCode));
240 	}
241 }