View Javadoc
1   package org.metricshub.wmi.windows.remote;
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.io.IOException;
24  import java.nio.charset.Charset;
25  import java.nio.charset.StandardCharsets;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.nio.file.StandardCopyOption;
30  import java.nio.file.attribute.FileTime;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Objects;
36  import java.util.concurrent.TimeoutException;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  import org.metricshub.wmi.Utils;
40  import org.metricshub.wmi.exceptions.WindowsRemoteException;
41  import org.metricshub.wmi.exceptions.WqlQuerySyntaxException;
42  
43  public class WindowsRemoteProcessUtils {
44  
45  	private WindowsRemoteProcessUtils() {}
46  
47  	private static final String DEFAULT_CODESET = "1252";
48  	private static final Charset DEFAULT_CHARSET = Charset.forName("windows-1252");
49  
50  	/**
51  	 * Windows CodeSet to java.nio.charset Charset Code map.
52  	 *
53  	 * @see <a href="https://en.wikipedia.org/wiki/Windows_code_page">Windows code page</a>
54  	 * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html">
55  	 * Supported Encodings</a>
56  	 *
57  	 */
58  	private static final Map<String, Charset> CODESET_MAP;
59  
60  	static {
61  		final Map<String, Charset> map = new HashMap<>();
62  		addToCharsetMap(map, "1250", "windows-1250");
63  		addToCharsetMap(map, "1251", "windows-1251");
64  		map.put("1252", DEFAULT_CHARSET);
65  		addToCharsetMap(map, "1253", "windows-1253");
66  		addToCharsetMap(map, "1254", "windows-1254");
67  		addToCharsetMap(map, "1255", "windows-1255");
68  		addToCharsetMap(map, "1256", "windows-1256");
69  		addToCharsetMap(map, "1257", "windows-1257");
70  		addToCharsetMap(map, "1258", "windows-1258");
71  		addToCharsetMap(map, "874", "x-windows-874");
72  		addToCharsetMap(map, "932", "Shift_JIS");
73  		addToCharsetMap(map, "936", "GBK");
74  		if (!addToCharsetMap(map, "949", "EUC-KR")) {
75  			addToCharsetMap(map, "949", "x-windows-949");
76  		}
77  		if (!addToCharsetMap(map, "950", "Big5")) {
78  			addToCharsetMap(map, "950", "x-windows-950");
79  		}
80  		addToCharsetMap(map, "951", "Big5-HKSCS");
81  		map.put("28591", StandardCharsets.ISO_8859_1);
82  		map.put("20127", StandardCharsets.US_ASCII);
83  		map.put("65001", StandardCharsets.UTF_8);
84  		map.put("1200", StandardCharsets.UTF_16LE);
85  		map.put("1201", StandardCharsets.UTF_16BE);
86  
87  		CODESET_MAP = Collections.unmodifiableMap(map);
88  	}
89  
90  	/**
91  	 * Adds a charset to the provided map if it is supported by the current JVM
92  	 *
93  	 * @param map         the map to which the charset will be added.
94  	 * @param key         the key with which the specified charset is to be
95  	 *                    associated
96  	 * @param charsetName the name of the charset to be added.
97  	 * @return true if the charset was supported and added to the map, false otherwise
98  	 */
99  	private static boolean addToCharsetMap(Map<String, Charset> map, String key, String charsetName) {
100 		if (Charset.isSupported(charsetName)) {
101 			map.put(key, Charset.forName(charsetName));
102 			return true;
103 		}
104 		return false;
105 	}
106 
107 	/**
108 	 * Get the CharSet from the Win32_OperatingSystem CodeSet. (if not found by default Latin-1 windows-1252)
109 	 *
110 	 * @param windowsRemoteExecutor WindowsRemoteExecutor instance
111 	 * @param timeout Timeout in milliseconds.
112 	 * @return the encoding charset from Win32_OperatingSystem
113 	 * @throws TimeoutException To notify userName of timeout
114 	 * @throws WqlQuerySyntaxException On WQL syntax errors
115 	 * @throws WindowsRemoteException For any problem encountered on remote
116 	 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-operatingsystem">
117 	 * Win32_OperatingSystem class</a>
118 	 *
119 	 */
120 	public static Charset getWindowsEncodingCharset(
121 		final WindowsRemoteExecutor windowsRemoteExecutor,
122 		final long timeout
123 	) throws TimeoutException, WqlQuerySyntaxException, WindowsRemoteException {
124 		if (windowsRemoteExecutor == null || timeout < 1) {
125 			return DEFAULT_CHARSET;
126 		}
127 
128 		final List<Map<String, Object>> result = windowsRemoteExecutor.executeWql(
129 			"SELECT CodeSet FROM Win32_OperatingSystem",
130 			timeout
131 		);
132 
133 		final String codeSet = result
134 			.stream()
135 			.map(row -> (String) row.get("CodeSet"))
136 			.filter(Objects::nonNull)
137 			.findFirst()
138 			.orElse(DEFAULT_CODESET);
139 
140 		return CODESET_MAP.getOrDefault(codeSet, DEFAULT_CHARSET);
141 	}
142 
143 	/**
144 	 * Builds a new output file name, with 99.9999999% chances of being unique
145 	 * on the remote system
146 	 * @return file name
147 	 */
148 	public static String buildNewOutputFileName() {
149 		return String.format(
150 			"SEN_%s_%d_%d",
151 			Utils.getComputerName(),
152 			Utils.getCurrentTimeMillis(),
153 			(long) (Math.random() * 1000000)
154 		);
155 	}
156 
157 	/**
158 	 * Copy the local files to the share and update the command with their path as seen in the remote system.
159 	 *
160 	 * @param command The command (mandatory)
161 	 * @param localFiles The local files to copy list
162 	 * @param uncSharePath The UNC path of the share
163 	 * @param remotePath The remote path
164 	 * @return The updated command.
165 	 * @throws IOException If an I/O error occurs.
166 	 */
167 	public static String copyLocalFilesToShare(
168 		final String command,
169 		final List<String> localFiles,
170 		final String uncSharePath,
171 		final String remotePath
172 	) throws IOException {
173 		Utils.checkNonNull(command, "command");
174 
175 		if (localFiles == null || localFiles.isEmpty()) {
176 			return command;
177 		}
178 
179 		Utils.checkNonNull(uncSharePath, "uncSharePath");
180 		Utils.checkNonNull(remotePath, "remotePath");
181 
182 		try {
183 			return localFiles
184 				.stream()
185 				.reduce(
186 					command,
187 					(cmd, localFile) -> {
188 						try {
189 							final Path localFilePath = Paths.get(localFile);
190 							final Path remoteFilePath = copyToShare(localFilePath, uncSharePath, remotePath);
191 
192 							return caseInsensitiveReplace(cmd, localFile, remoteFilePath.toString());
193 						} catch (final IOException e) {
194 							throw new RuntimeException(e);
195 						}
196 					}
197 				);
198 		} catch (final Exception e) {
199 			if (e.getCause() instanceof IOException) {
200 				throw (IOException) e.getCause();
201 			}
202 			throw e;
203 		}
204 	}
205 
206 	/**
207 	 * Copy a file to the share.
208 	 * <p>
209 	 * If the same file is already present on the share, the copy is not performed.
210 	 * The "last-modified" time is used to determine whether the file needs to be
211 	 * copied or not.
212 	 * <p>
213 	 * @param localFilePath The path to the file to copy
214 	 * @param uncSharePath The UNC path of the share
215 	 * @param remotePath The remote path
216 	 * @return the path to the copied file, as seen in the remote system
217 	 * @throws IOException If an I/O error occurs.
218 	 */
219 	static Path copyToShare(final Path localFilePath, final String uncSharePath, final String remotePath)
220 		throws IOException {
221 		final Path targetUncPath = Paths.get(uncSharePath, localFilePath.getFileName().toString());
222 		final Path targetRemotePath = Paths.get(remotePath, localFilePath.getFileName().toString());
223 
224 		if (Files.exists(targetUncPath)) {
225 			final FileTime sourceFileTime = Files.getLastModifiedTime(localFilePath);
226 			final FileTime targetFileTime = Files.getLastModifiedTime(targetUncPath);
227 			if (sourceFileTime.compareTo(targetFileTime) <= 0) {
228 				// File is already present on the target, simply skip the copy operation
229 				return targetRemotePath;
230 			}
231 		}
232 
233 		// Copy
234 		Files.copy(localFilePath, targetUncPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
235 
236 		// Return the path to the copied file, as seen in the remote system
237 		return targetRemotePath;
238 	}
239 
240 	/**
241 	 * Perform a case-insensitive replace of all occurrences of <em>target</em> string with
242 	 * specified <em>replacement</em>
243 	 * <p>
244 	 * Similar to <code>String.replace(target, replacement)</code>
245 	 * <p>
246 	 * @param string The string to parse
247 	 * @param target The string to replace
248 	 * @param replacement The replacement string
249 	 * <p>
250 	 * @return updated string
251 	 */
252 	static String caseInsensitiveReplace(final String string, final String target, final String replacement) {
253 		return string == null || target == null
254 			? string
255 			: Pattern
256 				.compile(target, Pattern.LITERAL | Pattern.CASE_INSENSITIVE)
257 				.matcher(string)
258 				.replaceAll(Matcher.quoteReplacement(replacement == null ? Utils.EMPTY : replacement));
259 	}
260 }