/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.utils.executor; import java.io.IOException; import java.lang.reflect.Field; import java.util.Arrays; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.ExecuteException; import org.apache.commons.exec.PumpStreamHandler; import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.Platform; import de.rcenvironment.core.utils.common.StringUtils; /** * Utility class to interact with processes in a system dependent way. * * @author Tobias Rodehutskors */ final class ProcessUtils { public static final int LINUX_EXIT_CODE_SUCCESS = 0; public static final int LINUX_EXIT_CODE_SIGTERM = 143; public static final int WINDOWS_EXIT_CODE_SUCCESS = 0; public static final int WINDOWS_EXIT_CODE_SIGTERM = 1; /** * Will be returned if the process is already terminated. */ public static final int WINDOWS_EXIT_CODE_WAIT_NO_CHILDREN = 128; /** * Will be returned if taskkill returns "there is no running instance of the task.". */ public static final int WINDOWS_EXIT_CODE_EA_LIST_INCONSISTENT = 255; private static final int TEN_SECONDS_IN_MILLIS = 10000; /** Top-level command token template for Windows invocation. */ private static final String[] WINDOWS_SHELL_TOKENS = { "cmd.exe", "/c", "[command]" }; /** * Top-level command token template for Linux invocation. * * Calls setsid to set a unique session id. This session id is used to identify all descendants of this process which may be spawned * during the process execution. */ private static final String[] LINUX_SHELL_TOKENS = { "setsid", "/bin/sh", "-c", "[command]" }; private static final String WINDOWS_KILL_COMMAND_TEMPLATE = "Taskkill /T /F /PID %d"; private static final String LINUX_TERM_COMMAND_TEMPLATE = "for pid in $(ps -s %d -o pid=); do kill ${pid}; done"; private static final String LINUX_KILL_COMMAND_TEMPLATE = "for pid in $(ps -s %d -o pid=); do kill -9 ${pid}; done"; /** * Lists the PIDs of all processes with the SID %d. */ private static final String LINUX_CHECK_COMMAND_TEMPLATE = "ps -s %d -o pid="; private static final int LINUX_TERM_ATTEMPTS = 3; private static final int LINUX_KILL_ATTEMPTS = 3; private static final String UNKOWN_PLATFORM_ERROR = "Unkown platform. Currently only Windows and Linux are supported."; private ProcessUtils() {} /** * This interface is needed for JNA to get access to the native windows api. */ interface Kernel32 extends Library { Kernel32 INSTANCE = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class); /** * Retrieves the process identifier of the specified process. */ // CHECKSTYLE:DISABLE (MethodName) - method name needs to be identical to the Kernel32 method name int GetProcessId(Long handle); // CHECKSTYLE:ENABLE (MethodName) } static CommandLine constructCommandLine(String command) { String[] commandTokens; if (Platform.isWindows()) { commandTokens = Arrays.copyOf(ProcessUtils.WINDOWS_SHELL_TOKENS, ProcessUtils.WINDOWS_SHELL_TOKENS.length); commandTokens[ProcessUtils.WINDOWS_SHELL_TOKENS.length - 1] = command; } else if (Platform.isLinux()) { commandTokens = Arrays.copyOf(ProcessUtils.LINUX_SHELL_TOKENS, ProcessUtils.LINUX_SHELL_TOKENS.length); commandTokens[ProcessUtils.LINUX_SHELL_TOKENS.length - 1] = command; } else { throw new IllegalStateException(UNKOWN_PLATFORM_ERROR); } CommandLine cmd = new CommandLine(commandTokens[0]); for (int i = 1; i < commandTokens.length; i++) { cmd.addArgument(commandTokens[i], false); } return cmd; } /** * Extracts the system dependent process id from {@link Process}. This method is implemented using Reflection API and JNA and is * therefore naturally platform dependent. It is possible to extract the PID from already finished processes for both Windows as well as * Linux. */ static int getPid(Process pProcess) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { int pid; if (Platform.isWindows()) { // extract the handle from the process object using the reflection API Field f = pProcess.getClass().getDeclaredField("handle"); f.setAccessible(true); long processHandle = f.getLong(pProcess); // get the process id from the process handle using JNA pid = Kernel32.INSTANCE.GetProcessId(processHandle); } else if (Platform.isLinux()) { // extract the process id from the process object using the reflection API Field f = pProcess.getClass().getDeclaredField("pid"); f.setAccessible(true); pid = f.getInt(pProcess); } else { throw new IllegalStateException(UNKOWN_PLATFORM_ERROR); } return pid; } /** * Kills the process tree starting with the given process id. * * Precondition: For Linux this method assumes that the given process id is equal to a session id and kills all processes with this * session id. Therefore, the process needs to be started with the setsid prefix beforehand. * * TODO JAVA9 Presumably Java 9 is going to introduce a new method {@link java.lang.Process#toHandle()} which will return a * ProcessHandle object. This handles will allow to query the descendants of a process and kill them. * * * @return true, if the process and its descendants have been killed or if the process already finished * @exception ExecuteException Thrown if the process id/session id was not found or the command failed for another reason. * @throws InterruptedException */ static boolean killProcessTree(int pid) throws ExecuteException, IOException, InterruptedException { DefaultExecutor executor = new DefaultExecutor(); if (Platform.isWindows()) { int[] validExitValues = { WINDOWS_EXIT_CODE_SUCCESS, WINDOWS_EXIT_CODE_WAIT_NO_CHILDREN, WINDOWS_EXIT_CODE_EA_LIST_INCONSISTENT }; executor.setExitValues(validExitValues); executor.setStreamHandler(new PumpStreamHandler(null)); CommandLine cl = ProcessUtils.constructCommandLine(StringUtils.format(WINDOWS_KILL_COMMAND_TEMPLATE, pid)); // check if the returned exit code signals a success int exitCode = executor.execute(cl); for (int validExitValue : validExitValues) { if (exitCode == validExitValue) { return true; } } return false; } else if (Platform.isLinux()) { // try to terminate the processes first boolean terminated = termOrKill(executor, LINUX_TERM_ATTEMPTS, LINUX_TERM_COMMAND_TEMPLATE, pid); if (terminated) { return true; } // wait for some seconds Thread.sleep(TEN_SECONDS_IN_MILLIS); // kill the remaining processes return termOrKill(executor, LINUX_KILL_ATTEMPTS, LINUX_KILL_COMMAND_TEMPLATE, pid); } else { throw new IllegalStateException(UNKOWN_PLATFORM_ERROR); } } // Linux only function private static boolean termOrKill(DefaultExecutor executor, int attempts, String terminationCommand, int pid) throws ExecuteException, IOException { // as child processes can be created asynchronously, we check if we succeeded in terminating them and repeat the termination several // times if necessary for (int i = 0; i < attempts; i++) { // terminate all commands with the given SID CommandLine terminate = ProcessUtils.constructCommandLine(StringUtils.format(terminationCommand, pid)); // returns 0 on success // returns 1: // since the kill command is not atomic, it can happen that "$(ps -s %d -o pid=)" returns some process ids, whose corresponding // processes finish, before the loop can issue the kill command for this process. In this case the script will return 1. executor.setExitValues(new int[] { 0, 1 }); executor.execute(terminate); // check if there is any process left CommandLine check = ProcessUtils.constructCommandLine(StringUtils.format(LINUX_CHECK_COMMAND_TEMPLATE, pid)); // 0: found some processes for the given SID // 1: found no processes executor.setExitValues(new int[] { 0, 1 }); int checkExitCode = executor.execute(check); // exit if all processes died if (checkExitCode == 1) { return true; } } return false; } }