Improve Windows RSS memory retrieval with multiple fallback strategies (#1414)

This commit enhances the Windows RSS memory retrieval functionality in OsUtils
to provide better compatibility and reliability across different Windows versions.

Key improvements:
- Added multiple fallback strategies for Windows RSS retrieval:
  1. PowerShell approach (modern, Windows 7+ with PowerShell 2.0+)
  2. WMIC approach (fallback for older systems)
  3. Tasklist approach (final fallback, most compatible from XP to Win11)

- Enhanced error handling with debug-level logging for memory queries
  to avoid log spam while maintaining visibility for debugging

- Added robust CSV parsing for tasklist output with proper handling
  of quoted fields and comma-separated numbers (e.g., '1,234 K')

- Improved regex pattern matching for memory values that handles
  various formats returned by Windows commands

- Refactored code structure by separating Unix and Windows logic
  into dedicated methods for better maintainability

These changes ensure reliable RSS memory retrieval across all supported
Windows versions while maintaining backward compatibility and improving
error handling.
This commit is contained in:
Guillaume Nodet
2025-09-08 15:49:31 +02:00
committed by GitHub
parent 919ecf3a83
commit 09e9f54391

View File

@@ -27,6 +27,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -38,6 +40,9 @@ public class OsUtils {
private static final long KB = 1024; private static final long KB = 1024;
private static final String UNITS = "Bkmgt"; private static final String UNITS = "Bkmgt";
// Pattern for parsing tasklist CSV output memory column (handles "1,234 K" format)
private static final Pattern MEMORY_PATTERN = Pattern.compile("([\\d,]+)\\s*K?");
private OsUtils() {} private OsUtils() {}
public static String bytesToHumanReadable(long bytes) { public static String bytesToHumanReadable(long bytes) {
@@ -63,46 +68,154 @@ public class OsUtils {
public static long findProcessRssInKb(long pid) { public static long findProcessRssInKb(long pid) {
final Os os = Os.current(); final Os os = Os.current();
if (os.isUnixLike()) { if (os.isUnixLike()) {
String[] cmd = {"ps", "-o", "rss=", "-p", String.valueOf(pid)}; return findProcessRssUnix(pid);
final List<String> output = new ArrayList<>(1);
exec(cmd, output);
if (output.size() == 1) {
try {
return Long.parseLong(output.get(0).trim());
} catch (NumberFormatException e) {
LOGGER.warn(
"Could not parse the output of {} as a long:\n{}",
String.join(" ", cmd),
String.join("\n", output));
}
} else {
LOGGER.warn("Unexpected output of {}:\n{}", String.join(" ", cmd), String.join("\n", output));
}
return -1;
} else if (os == Os.WINDOWS) { } else if (os == Os.WINDOWS) {
String[] cmd = {"wmic", "process", "where", "processid=" + pid, "get", "WorkingSetSize"}; return findProcessRssWindows(pid);
final List<String> output = new ArrayList<>(1);
exec(cmd, output);
final List<String> nonEmptyLines =
output.stream().filter(l -> !l.isEmpty()).collect(Collectors.toList());
if (nonEmptyLines.size() >= 2) {
try {
return Long.parseLong(nonEmptyLines.get(1).trim()) / KB;
} catch (NumberFormatException e) {
LOGGER.warn(
"Could not parse the second line of {} output as a long:\n{}",
String.join(" ", cmd),
String.join("\n", nonEmptyLines));
}
} else {
LOGGER.warn("Unexpected output of {}:\n{}", String.join(" ", cmd), String.join("\n", output));
}
return -1;
} else { } else {
return -1; return -1;
} }
} }
private static long findProcessRssUnix(long pid) {
String[] cmd = {"ps", "-o", "rss=", "-p", String.valueOf(pid)};
final List<String> output = new ArrayList<>(1);
exec(cmd, output);
if (output.size() == 1) {
try {
return Long.parseLong(output.get(0).trim());
} catch (NumberFormatException e) {
LOGGER.warn(
"Could not parse the output of {} as a long:\n{}",
String.join(" ", cmd),
String.join("\n", output));
}
} else {
LOGGER.warn("Unexpected output of {}:\n{}", String.join(" ", cmd), String.join("\n", output));
}
return -1;
}
private static long findProcessRssWindows(long pid) {
// Try modern PowerShell approach first (Windows 7+ with PowerShell 2.0+)
long result = tryPowerShellMemory(pid);
if (result > 0) {
return result;
}
// Fallback to wmic for older systems or if PowerShell fails
result = tryWmicMemory(pid);
if (result > 0) {
return result;
}
// Final fallback to tasklist (most compatible, works from Windows XP to Windows 11)
return tryTasklistMemory(pid);
}
private static long tryPowerShellMemory(long pid) {
// Use PowerShell with error handling to get WorkingSet64
String[] cmd = {
"powershell",
"-Command",
"try { " + "(Get-Process -Id "
+ pid + " -ErrorAction Stop).WorkingSet64 " + "} catch { "
+ "Write-Output 'ERROR' "
+ "}"
};
final List<String> output = new ArrayList<>(1);
exec(cmd, output);
if (!output.isEmpty()) {
String result = output.get(0).trim();
if (!result.isEmpty() && !result.equals("ERROR") && !result.contains("Get-Process")) {
try {
return Long.parseLong(result) / KB;
} catch (NumberFormatException e) {
LOGGER.debug("Could not parse PowerShell output as a long: {}", result);
}
}
}
return -1;
}
private static long tryWmicMemory(long pid) {
String[] cmd = {"wmic", "process", "where", "processid=" + pid, "get", "WorkingSetSize"};
final List<String> output = new ArrayList<>(1);
exec(cmd, output);
final List<String> nonEmptyLines =
output.stream().filter(l -> !l.isEmpty()).collect(Collectors.toList());
if (nonEmptyLines.size() >= 2) {
try {
return Long.parseLong(nonEmptyLines.get(1).trim()) / KB;
} catch (NumberFormatException e) {
LOGGER.debug(
"Could not parse wmic output as a long: {}",
nonEmptyLines.get(1).trim());
}
}
return -1;
}
private static long tryTasklistMemory(long pid) {
// Use tasklist with CSV format for easier parsing
String[] cmd = {"tasklist", "/fi", "PID eq " + pid, "/fo", "csv"};
final List<String> output = new ArrayList<>();
exec(cmd, output);
if (output.size() >= 2) { // Header + data row
try {
// Parse CSV line - memory is typically in the 5th column (index 4)
String dataLine = output.get(1);
String[] fields = parseCsvLine(dataLine);
if (fields.length >= 5) {
String memoryField = fields[4].trim();
// Remove quotes if present and parse memory value
memoryField = memoryField.replaceAll("\"", "");
Matcher matcher = MEMORY_PATTERN.matcher(memoryField);
if (matcher.find()) {
String memoryStr = matcher.group(1).replaceAll(",", "");
return Long.parseLong(memoryStr);
}
}
} catch (Exception e) {
LOGGER.debug("Could not parse tasklist output: {}", e.getMessage());
}
} else if (output.size() == 1 && output.get(0).contains("No tasks")) {
// Process not found
LOGGER.debug("Process {} not found", pid);
} else {
LOGGER.debug("Unexpected tasklist output for PID {}: {}", pid, String.join("\n", output));
}
return -1;
}
/**
* Simple CSV line parser that handles quoted fields
*/
private static String[] parseCsvLine(String line) {
List<String> fields = new ArrayList<>();
boolean inQuotes = false;
StringBuilder currentField = new StringBuilder();
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '"') {
inQuotes = !inQuotes;
} else if (c == ',' && !inQuotes) {
fields.add(currentField.toString());
currentField = new StringBuilder();
} else {
currentField.append(c);
}
}
fields.add(currentField.toString());
return fields.toArray(new String[0]);
}
/** /**
* Executes the given {@code javaExecutable} with {@code -XshowSettings:properties -version} parameters and extracts * Executes the given {@code javaExecutable} with {@code -XshowSettings:properties -version} parameters and extracts
* the value of {@code java.home} from the output. * the value of {@code java.home} from the output.
@@ -127,15 +240,34 @@ public class OsUtils {
try (CommandProcess ps = new CommandProcess(builder.start(), output::add)) { try (CommandProcess ps = new CommandProcess(builder.start(), output::add)) {
final int exitCode = ps.waitFor(1000); final int exitCode = ps.waitFor(1000);
if (exitCode != 0) { if (exitCode != 0) {
LOGGER.warn("{} exited with {}:\n{}", String.join(" ", cmd), exitCode, String.join("\n", output)); // Only log as debug for memory queries to avoid spam in logs
if (isMemoryQuery(cmd)) {
LOGGER.debug(
"{} exited with {}: {}",
String.join(" ", cmd),
exitCode,
output.isEmpty() ? "no output" : output.get(0));
} else {
LOGGER.warn("{} exited with {}:\n{}", String.join(" ", cmd), exitCode, String.join("\n", output));
}
} }
} catch (IOException e) { } catch (IOException e) {
LOGGER.warn("Could not execute {}", String.join(" ", cmd)); if (isMemoryQuery(cmd)) {
LOGGER.debug("Could not execute {}: {}", String.join(" ", cmd), e.getMessage());
} else {
LOGGER.warn("Could not execute {}", String.join(" ", cmd));
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
} }
private static boolean isMemoryQuery(String[] cmd) {
if (cmd.length == 0) return false;
String firstCmd = cmd[0].toLowerCase();
return firstCmd.contains("powershell") || firstCmd.contains("wmic") || firstCmd.contains("tasklist");
}
/** /**
* A simple wrapper over {@link Process} that manages its destroying and offers Java 8-like * A simple wrapper over {@link Process} that manages its destroying and offers Java 8-like
* {@link #waitFor(long)} with timeout. * {@link #waitFor(long)} with timeout.