From 500a1e6ff4ec1e0aa82cecb3d18f89ca33ca49e6 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp <marc@gsites.de> Date: Tue, 27 Jul 2021 19:39:34 +0200 Subject: [PATCH] fix(common-web): Sub-processes hang if output buffers fill up ExternalProcessExecutor spawned sub-processes with stderr and stdout buffers, but did not read from those buffers. Sub-processes producing output sometimes would fill up these buffers and then hang until the timeout occurs and the process is killed. This fixes #12839 (PDF conversion hangs for 10 seconds per page) by properly discarding command output. The patch also deprecates an outdated and dangerous API and offers a safer alternative. Splitting command arguments based on whitespace may result in unwanted behavior if an argument (e.g. a filename) contains whitespace. --- .../imp/ExternalProcessExecutor.java | 100 ++++++++++-------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java index c92943eb4d..d583f1eaad 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java @@ -19,66 +19,76 @@ package org.bigbluebutton.presentation.imp; -import java.util.Timer; -import java.util.TimerTask; +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * A wrapper class the executes an external command. - * - * See http://kylecartmell.com/?p=9 + * A wrapper class the executes an external command. * * @author Richard Alam - * + * @author Marcel Hellkamp */ public class ExternalProcessExecutor { private static Logger log = LoggerFactory.getLogger(ExternalProcessExecutor.class); - - public boolean exec(String COMMAND, long timeoutMillis) { - Timer timer = new Timer(false); - Process p = null; - try { - InterruptTimerTask interrupter = new InterruptTimerTask(Thread.currentThread()); - timer.schedule(interrupter, timeoutMillis); - p = Runtime.getRuntime().exec(COMMAND); - int result = p.waitFor(); - if (result == 0) { - return true; - } else { - return false; - } + // Replace with ProcessBuilder.Redirect.DISCARD in java 9+ + private static File DISCARD = new File( + System.getProperty("os.name").startsWith("Windows") ? "NUL" : "/dev/null"); - } catch(Exception e) { - log.info("TIMEDOUT excuting : {}", COMMAND); - if (p != null) { - p.destroy(); - } - } finally { - timer.cancel(); // If the process returns within the timeout period, we have to stop the interrupter - // so that it does not unexpectedly interrupt some other code later. - - Thread.interrupted(); // We need to clear the interrupt flag on the current thread just in case - // interrupter executed after waitFor had already returned but before timer.cancel - // took effect. - // - // Oh, and there's also Sun bug 6420270 to worry about here. - } - return false; + /** + * Run COMMAND for at most timeoutMillis while ignoring any output. + * + * @deprecated The COMMAND string is split on whitespace to create an argument + * list. This won't work for arguments that contain whitespace. Use + * {@link #exec(List, Duration)} instead. + * + * @param COMMAND A single command or whitespace separated list of + * arguments. + * @param timeoutMillis Timeout in milliseconds. + * @return true if the command terminated in time with an exit value of 0. + */ + @Deprecated + public boolean exec(String COMMAND, long timeoutMillis) { + return exec(Arrays.asList(COMMAND.split("[ \\t\\n\\r\\f]+")), Duration.ofMillis(timeoutMillis)); } - - class InterruptTimerTask extends TimerTask { - private Thread thread; + /** + * Run a command for a limited amount of time while ignoring any output. + * + * @param cmd List containing the program and its arguments. + * @param timeout Maximum execution time. + * @return true if the command terminated in time with an exit value of 0. + */ + public boolean exec(List<String> cmd, Duration timeout) { - public InterruptTimerTask(Thread t) { - this.thread = t; - } + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectError(DISCARD); + pb.redirectOutput(DISCARD); - public void run() { - thread.interrupt(); - } + Process proc; + try { + proc = pb.start(); + } catch (IOException e) { + log.error("Failed to execute: {}", String.join(" ", cmd), e); + return false; + } + try { + if (!proc.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + log.warn("TIMEDOUT excuting: {}", String.join(" ", cmd)); + proc.destroy(); + } + return !proc.isAlive() && proc.exitValue() == 0; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + proc.destroy(); + return false; + } } } -- GitLab