/* * Universal Media Server, for streaming any media to DLNA * compatible renderers based on the http://www.ps3mediaserver.org. * Copyright (C) 2012 UMS developers. * * This program is a free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.pms.util; import com.sun.jna.Platform; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.lang.management.ManagementFactory; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import net.pms.PMS; import net.pms.io.StreamGobbler; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // see https://code.google.com/p/ps3mediaserver/issues/detail?id=680 // for background/issues/discussion related to this class public class ProcessUtil { private static final Logger LOGGER = LoggerFactory.getLogger(ProcessUtil.class); // how long to wait in milliseconds until a kill -TERM on Unix has been deemed to fail private static final int TERM_TIMEOUT = 10000; // how long to wait in milliseconds until a kill -ALRM on Unix has been deemed to fail private static final int ALRM_TIMEOUT = 2000; // work around a Java bug // see: http://www.cnblogs.com/abnercai/archive/2012/12/27/2836008.html public static int waitFor(Process p) { int exit = -1; try { exit = p.waitFor(); } catch (InterruptedException e) { Thread.interrupted(); } return exit; } // get the process ID on Unix (returns null otherwise) public static Integer getProcessID(Process p) { Integer pid = null; if (p != null && p.getClass().getName().equals("java.lang.UNIXProcess")) { try { Field f = p.getClass().getDeclaredField("pid"); f.setAccessible(true); pid = f.getInt(p); } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { LOGGER.debug("Can't determine the Unix process ID: " + e.getMessage()); } } return pid; } // kill -9 a Unix process public static void kill(Integer pid) { kill(pid, 9); } /* * FIXME: this is a hack - destroy() *should* work * * call chain (innermost last): * * WaitBufferedInputStream.close * BufferedOutputFile.detachInputStream * ProcessWrapperImpl.stopProcess * ProcessUtil.destroy * ProcessUtil.kill * * my best guess is that the process's stdout/stderr streams * aren't being/haven't been fully/promptly consumed. * From the abovelinked article: * * The Java 6 API clearly states that failure to promptly * “read the output stream of the subprocess may cause the subprocess * to block, and even deadlock. * * This is corroborated by the fact that destroy() works fine if the * process is allowed to run to completion: * * https://code.google.com/p/ps3mediaserver/issues/detail?id=680#c11 */ // send a Unix process the specified signal public static boolean kill(Integer pid, int signal) { boolean killed = false; LOGGER.warn("Sending kill -" + signal + " to the Unix process: " + pid); try { ProcessBuilder processBuilder = new ProcessBuilder("kill", "-" + signal, Integer.toString(pid)); processBuilder.redirectErrorStream(true); Process process = processBuilder.start(); // consume the error and output process streams StreamGobbler.consume(process.getInputStream(), true); int exit = waitFor(process); if (exit == 0) { killed = true; LOGGER.debug("Successfully sent kill -" + signal + " to the Unix process: " + pid); } } catch (IOException e) { LOGGER.error("Error calling: kill -" + signal + " " + pid, e); } return killed; } // destroy a process safely (kill -TERM on Unix) public static void destroy(final Process p) { if (p != null) { final Integer pid = getProcessID(p); if (pid != null) { // Unix only LOGGER.trace("Killing the Unix process: " + pid); Runnable r = new Runnable() { @Override public void run() { try { Thread.sleep(TERM_TIMEOUT); } catch (InterruptedException e) { } try { p.exitValue(); } catch (IllegalThreadStateException itse) { // still running: nuke it // kill -14 (ALRM) works (for MEncoder) and is less dangerous than kill -9 // so try that first if (!kill(pid, 14)) { try { // This is a last resort, so let's not be too eager Thread.sleep(ALRM_TIMEOUT); } catch (InterruptedException ie) { } kill(pid, 9); } } } }; Thread failsafe = new Thread(r, "Process Destroyer"); failsafe.start(); } p.destroy(); } } public static String getShortFileNameIfWideChars(String name) { return PMS.get().getRegistry().getShortPathNameW(name); } // Run cmd and return combined stdout/stderr public static String run(int[] expectedExitCodes, String... cmd) { try { ProcessBuilder pb = new ProcessBuilder(cmd); pb.redirectErrorStream(true); Process p = pb.start(); StringBuilder output; try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) { String line; output = new StringBuilder(); while ((line = br.readLine()) != null) { output.append(line).append("\n"); } } p.waitFor(); boolean expected = false; if (expectedExitCodes != null) { for (int expectedCode : expectedExitCodes) { if (expectedCode == p.exitValue()) { expected = true; break; } } } if (!expected) { LOGGER.debug("Warning: command {} returned {}", Arrays.toString(cmd), p.exitValue()); } return output.toString(); } catch (Exception e) { LOGGER.error("Error running command " + Arrays.toString(cmd), e); } return ""; } public static String run(String... cmd) { int[] zeroExpected = { 0 }; return run(zeroExpected, cmd); } // Whitewash any arguments not suitable to display in dbg messages // and make one single printable string public static String dbgWashCmds(String[] cmd) { for (int i=0; i < cmd.length; i++) { // Wrap arguments with spaces in double quotes to make them runnable if copy-pasted if (cmd[i].contains(" ")) { cmd[i] = "\"" + cmd[i] + "\""; } // Hide sensitive information from the log if (cmd[i].contains("headers")) { cmd[i+1]= cmd[i+1].replaceAll("Authorization: [^\n]+\n", "Authorization: ****\n"); i++; continue; } } return StringUtils.join(cmd, " "); } // Rebooting // Reboot UMS same as now public static void reboot() { reboot((ArrayList<String>)null, null, null); } // Reboot UMS same as now, adding these options public static void reboot(String... UMSOptions) { reboot(null, null, null, UMSOptions); } // Shutdown UMS and either reboot or run the given command (e.g. a script to restart UMS) public static void reboot(ArrayList<String> cmd, Map<String,String> env, String startdir, String... UMSOptions) { final ArrayList<String> reboot = getUMSCommand(); if (UMSOptions.length > 0) { reboot.addAll(Arrays.asList(UMSOptions)); } if (cmd == null) { // We're doing a straight reboot cmd = reboot; } else { // We're running a script that will eventually restart UMS if (env == null) { env = new HashMap<>(); } // Tell the script how to restart UMS env.put("RESTART_CMD", StringUtils.join(reboot, " ")); env.put("RESTART_DIR", System.getProperty("user.dir")); } if (startdir == null) { startdir = System.getProperty("user.dir"); } System.out.println("starting: " + StringUtils.join(cmd, " ")); final ProcessBuilder pb = new ProcessBuilder(cmd); if (env != null) { pb.environment().putAll(env); } pb.directory(new File(startdir)); System.out.println("in directory: " + pb.directory()); try { pb.start(); } catch (Exception e) { e.printStackTrace(); return; } System.exit(0); } // Reconstruct the command that started this jvm, including all options. // See http://stackoverflow.com/questions/4159802/how-can-i-restart-a-java-application // http://stackoverflow.com/questions/1518213/read-java-jvm-startup-parameters-eg-xmx public static ArrayList<String> getUMSCommand() { ArrayList<String> reboot = new ArrayList<>(); reboot.add(StringUtil.quoteArg( System.getProperty("java.home") + File.separator + "bin" + File.separator + ((Platform.isWindows() && System.console() == null) ? "javaw" : "java"))); for (String jvmArg : ManagementFactory.getRuntimeMXBean().getInputArguments()) { reboot.add(StringUtil.quoteArg(jvmArg)); } reboot.add("-cp"); reboot.add(ManagementFactory.getRuntimeMXBean().getClassPath()); // Could also use generic main discovery instead: // see http://stackoverflow.com/questions/41894/0-program-name-in-java-discover-main-class reboot.add(PMS.class.getName()); return reboot; } }