org.apache.maven.surefire.booter.PpidChecker Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of surefire-booter Show documentation
Show all versions of surefire-booter Show documentation
API and Facilities used by forked tests running in JVM sub-process.
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.surefire.booter;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Queue;
import java.util.Scanner;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.maven.surefire.api.booter.DumpErrorSingleton;
import org.apache.maven.surefire.api.util.SureFireFileManager;
import static java.lang.Integer.parseInt;
import static java.lang.Long.parseLong;
import static java.lang.String.join;
import static java.nio.file.Files.delete;
import static java.nio.file.Files.readAllBytes;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.regex.Pattern.compile;
import static org.apache.maven.surefire.api.util.internal.StringUtils.NL;
import static org.apache.maven.surefire.booter.ProcessInfo.ERR_PROCESS_INFO;
import static org.apache.maven.surefire.booter.ProcessInfo.INVALID_PROCESS_INFO;
import static org.apache.maven.surefire.booter.ProcessInfo.unixProcessInfo;
import static org.apache.maven.surefire.booter.ProcessInfo.windowsProcessInfo;
import static org.apache.maven.surefire.shared.lang3.StringUtils.isNotBlank;
import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_HP_UX;
import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_LINUX;
import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_UNIX;
import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_WINDOWS;
/**
* Recognizes PID of Plugin process and determines lifetime.
*
* @author Tibor Digana (tibor17)
* @since 2.20.1
*/
final class PpidChecker {
private static final long MINUTES_TO_MILLIS = 60L * 1000L;
// 25 chars https://superuser.com/questions/937380/get-creation-time-of-file-in-milliseconds/937401#937401
private static final int WMIC_CREATION_DATE_VALUE_LENGTH = 25;
private static final int WMIC_CREATION_DATE_TIMESTAMP_LENGTH = 18;
private static final SimpleDateFormat WMIC_CREATION_DATE_FORMAT =
IS_OS_WINDOWS ? createWindowsCreationDateFormat() : null;
private static final String WMIC_CREATION_DATE = "CreationDate";
private static final String WINDOWS_SYSTEM_ROOT_ENV = "SystemRoot";
private static final String RELATIVE_PATH_TO_WMIC = "System32\\Wbem";
private static final String SYSTEM_PATH_TO_WMIC =
"%" + WINDOWS_SYSTEM_ROOT_ENV + "%\\" + RELATIVE_PATH_TO_WMIC + "\\";
private static final String PS_ETIME_HEADER = "ELAPSED";
private static final String PS_PID_HEADER = "PID";
private final Queue destroyableCommands = new ConcurrentLinkedQueue<>();
/**
* The etime is in the form of [[dd-]hh:]mm:ss on Unix like systems.
* See the workaround https://issues.apache.org/jira/browse/SUREFIRE-1451.
*/
static final Pattern UNIX_CMD_OUT_PATTERN = compile("^(((\\d+)-)?(\\d{1,2}):)?(\\d{1,2}):(\\d{1,2})\\s+(\\d+)$");
static final Pattern BUSYBOX_CMD_OUT_PATTERN = compile("^(\\d+)[hH](\\d{1,2})\\s+(\\d+)$");
private final String ppid;
private volatile ProcessInfo parentProcessInfo;
private volatile boolean stopped;
PpidChecker(@Nonnull String ppid) {
this.ppid = ppid;
}
boolean canUse() {
if (isStopped()) {
return false;
}
final ProcessInfo ppi = parentProcessInfo;
return ppi == null ? IS_OS_WINDOWS || IS_OS_UNIX && canExecuteUnixPs() : ppi.canUse();
}
/**
* This method can be called only after {@link #canUse()} has returned {@code true}.
*
* @return {@code true} if parent process is alive; {@code false} otherwise
* @throws IllegalStateException if {@link #canUse()} returns {@code false}, error to read process
* or this object has been {@link #destroyActiveCommands() destroyed}
* @throws NullPointerException if extracted e-time is null
*/
boolean isProcessAlive() {
if (!canUse()) {
throw new IllegalStateException("irrelevant to call isProcessAlive()");
}
final ProcessInfo previousInfo = parentProcessInfo;
if (IS_OS_WINDOWS) {
parentProcessInfo = windows();
checkProcessInfo();
// let's compare creation time, should be same unless killed or PID is reused by OS into another process
return !parentProcessInfo.isInvalid()
&& (previousInfo == null || parentProcessInfo.isTimeEqualTo(previousInfo));
} else if (IS_OS_UNIX) {
parentProcessInfo = unix();
checkProcessInfo();
// let's compare elapsed time, should be greater or equal if parent process is the same and still alive
return !parentProcessInfo.isInvalid()
&& (previousInfo == null || !parentProcessInfo.isTimeBefore(previousInfo));
}
parentProcessInfo = ERR_PROCESS_INFO;
throw new IllegalStateException("unknown platform or you did not call canUse() before isProcessAlive()");
}
private void checkProcessInfo() {
if (isStopped()) {
throw new IllegalStateException("error [STOPPED] to read process " + ppid);
}
if (!parentProcessInfo.canUse()) {
throw new IllegalStateException(
"Cannot use PPID " + ppid + " process information. " + "Going to use NOOP events.");
}
}
// https://www.freebsd.org/cgi/man.cgi?ps(1)
// etimes elapsed running time, in decimal integer seconds
// http://manpages.ubuntu.com/manpages/xenial/man1/ps.1.html
// etimes elapsed time since the process was started, in seconds.
// http://hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/test/java/lang/ProcessBuilder/Basic.java#L167
ProcessInfo unix() {
String charset = System.getProperty("native.encoding", System.getProperty("file.encoding", "UTF-8"));
ProcessInfoConsumer reader = new ProcessInfoConsumer(charset) {
@Override
@Nonnull
ProcessInfo consumeLine(String line, ProcessInfo previousOutputLine) {
if (previousOutputLine.isInvalid()) {
if (hasHeader) {
Matcher matcher = UNIX_CMD_OUT_PATTERN.matcher(line);
if (matcher.matches() && ppid.equals(fromPID(matcher))) {
long pidUptime = fromDays(matcher)
+ fromHours(matcher)
+ fromMinutes(matcher)
+ fromSeconds(matcher);
return unixProcessInfo(ppid, pidUptime);
}
matcher = BUSYBOX_CMD_OUT_PATTERN.matcher(line);
if (matcher.matches() && ppid.equals(fromBusyboxPID(matcher))) {
long pidUptime = fromBusyboxHours(matcher) + fromBusyboxMinutes(matcher);
return unixProcessInfo(ppid, pidUptime);
}
} else {
hasHeader = line.contains(PS_ETIME_HEADER) && line.contains(PS_PID_HEADER);
}
}
return previousOutputLine;
}
};
String cmd = unixPathToPS() + " -o etime,pid " + (IS_OS_LINUX ? "" : "-p ") + ppid;
return reader.execute("/bin/sh", "-c", cmd);
}
ProcessInfo windows() {
ProcessInfoConsumer reader = new ProcessInfoConsumer("US-ASCII") {
@Override
@Nonnull
ProcessInfo consumeLine(String line, ProcessInfo previousProcessInfo) throws Exception {
if (previousProcessInfo.isInvalid() && !line.isEmpty()) {
if (hasHeader) {
// now the line is CreationDate, e.g. 20180406142327.741074+120
if (line.length() != WMIC_CREATION_DATE_VALUE_LENGTH) {
throw new IllegalStateException("WMIC CreationDate should have 25 characters " + line);
}
String startTimestamp = line.substring(0, WMIC_CREATION_DATE_TIMESTAMP_LENGTH);
int indexOfTimeZone = WMIC_CREATION_DATE_VALUE_LENGTH - 4;
long startTimestampMillisUTC =
WMIC_CREATION_DATE_FORMAT.parse(startTimestamp).getTime()
- parseInt(line.substring(indexOfTimeZone)) * MINUTES_TO_MILLIS;
return windowsProcessInfo(ppid, startTimestampMillisUTC);
} else {
hasHeader = WMIC_CREATION_DATE.equals(line);
}
}
return previousProcessInfo;
}
};
String wmicPath = hasWmicStandardSystemPath() ? SYSTEM_PATH_TO_WMIC : "";
return reader.execute(
"CMD",
"/A",
"/X",
"/C",
wmicPath + "wmic process where (ProcessId=" + ppid + ") get " + WMIC_CREATION_DATE);
}
void destroyActiveCommands() {
stopped = true;
for (Process p = destroyableCommands.poll(); p != null; p = destroyableCommands.poll()) {
p.destroy();
}
}
boolean isStopped() {
return stopped;
}
private static String unixPathToPS() {
return canExecuteLocalUnixPs() ? "/usr/bin/ps" : "/bin/ps";
}
static boolean canExecuteUnixPs() {
return canExecuteLocalUnixPs() || canExecuteStandardUnixPs();
}
private static boolean canExecuteLocalUnixPs() {
try {
return new File("/usr/bin/ps").canExecute();
} catch (SecurityException e) {
return false;
}
}
private static boolean canExecuteStandardUnixPs() {
try {
return new File("/bin/ps").canExecute();
} catch (SecurityException e) {
return false;
}
}
private static boolean hasWmicStandardSystemPath() {
String systemRoot = System.getenv(WINDOWS_SYSTEM_ROOT_ENV);
return isNotBlank(systemRoot) && new File(systemRoot, RELATIVE_PATH_TO_WMIC + "\\wmic.exe").isFile();
}
static long fromDays(Matcher matcher) {
String s = matcher.group(3);
return s == null ? 0L : DAYS.toSeconds(parseLong(s));
}
static long fromHours(Matcher matcher) {
String s = matcher.group(4);
return s == null ? 0L : HOURS.toSeconds(parseLong(s));
}
static long fromMinutes(Matcher matcher) {
String s = matcher.group(5);
return s == null ? 0L : MINUTES.toSeconds(parseLong(s));
}
static long fromSeconds(Matcher matcher) {
String s = matcher.group(6);
return s == null ? 0L : parseLong(s);
}
static String fromPID(Matcher matcher) {
return matcher.group(7);
}
static long fromBusyboxHours(Matcher matcher) {
String s = matcher.group(1);
return s == null ? 0L : HOURS.toSeconds(parseLong(s));
}
static long fromBusyboxMinutes(Matcher matcher) {
String s = matcher.group(2);
return s == null ? 0L : MINUTES.toSeconds(parseLong(s));
}
static String fromBusyboxPID(Matcher matcher) {
return matcher.group(3);
}
private static void checkValid(Scanner scanner) throws IOException {
IOException exception = scanner.ioException();
if (exception != null) {
throw exception;
}
}
/**
* The beginning part of Windows WMIC format yyyymmddHHMMSS.xxx
* https://technet.microsoft.com/en-us/library/ee198928.aspx
* We use UTC time zone which avoids DST changes, see SUREFIRE-1512.
*
* @return Windows WMIC format yyyymmddHHMMSS.xxx
*/
private static SimpleDateFormat createWindowsCreationDateFormat() {
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss'.'SSS");
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
return formatter;
}
public void stop() {
stopped = true;
}
/**
* Reads standard output from {@link Process}.
*
* The artifact maven-shared-utils has non-daemon Threads which is an issue in Surefire to satisfy System.exit.
* This implementation is taylor made without using any Thread.
* It's easy to destroy Process from other Thread.
*/
abstract class ProcessInfoConsumer {
private final String charset;
boolean hasHeader;
ProcessInfoConsumer(String charset) {
this.charset = charset;
}
abstract @Nonnull ProcessInfo consumeLine(String line, ProcessInfo previousProcessInfo) throws Exception;
ProcessInfo execute(String... command) {
ProcessBuilder processBuilder = new ProcessBuilder(command);
Process process = null;
ProcessInfo processInfo = INVALID_PROCESS_INFO;
StringBuilder out = new StringBuilder(64);
out.append(join(" ", command)).append(NL);
Path stdErr = null;
try {
stdErr = SureFireFileManager.createTempFile("surefire", null).toPath();
processBuilder.redirectError(stdErr.toFile());
if (IS_OS_HP_UX) // force to run shell commands in UNIX Standard mode on HP-UX
{
processBuilder.environment().put("UNIX95", "1");
}
process = processBuilder.start();
destroyableCommands.add(process);
Scanner scanner = new Scanner(process.getInputStream(), charset);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
out.append(line).append(NL);
processInfo = consumeLine(line.trim(), processInfo);
}
checkValid(scanner);
int exitCode = process.waitFor();
boolean isError = Thread.interrupted() || isStopped();
if (exitCode != 0 || isError) {
out.append("<> <<")
.append(exitCode)
.append(">>")
.append(NL)
.append("<> <<")
.append(isStopped())
.append(">>");
DumpErrorSingleton.getSingleton().dumpText(out.toString());
}
return isError ? ERR_PROCESS_INFO : (exitCode == 0 ? processInfo : INVALID_PROCESS_INFO);
} catch (Exception e) {
if (!(e instanceof InterruptedException
|| e instanceof InterruptedIOException
|| e.getCause() instanceof InterruptedException)) {
DumpErrorSingleton.getSingleton().dumpText(out.toString());
DumpErrorSingleton.getSingleton().dumpException(e);
}
//noinspection ResultOfMethodCallIgnored
Thread.interrupted();
return ERR_PROCESS_INFO;
} finally {
if (process != null) {
destroyableCommands.remove(process);
closeQuietly(process.getInputStream());
closeQuietly(process.getErrorStream());
closeQuietly(process.getOutputStream());
}
if (stdErr != null) {
try {
String error = new String(readAllBytes(stdErr)).trim();
if (!error.isEmpty()) {
DumpErrorSingleton.getSingleton().dumpText(error);
}
delete(stdErr);
} catch (IOException e) {
// cannot do anything about it, the dump file writes would fail as well
}
}
}
}
private void closeQuietly(AutoCloseable autoCloseable) {
if (autoCloseable != null) {
try {
autoCloseable.close();
} catch (Exception e) {
// ignore
}
}
}
}
@Override
public String toString() {
String args = "ppid=" + ppid + ", stopped=" + stopped;
ProcessInfo processInfo = parentProcessInfo;
if (processInfo != null) {
args += ", invalid=" + processInfo.isInvalid() + ", error=" + processInfo.isError();
}
if (IS_OS_UNIX) {
args += ", canExecuteLocalUnixPs=" + canExecuteLocalUnixPs() + ", canExecuteStandardUnixPs="
+ canExecuteStandardUnixPs();
}
return "PpidChecker{" + args + '}';
}
}