From 939e09f29a3e60e2691bc5362adb1dc8607a4f5a Mon Sep 17 00:00:00 2001 From: ribeaudc <ribeaudc> Date: Tue, 20 May 2008 21:18:16 +0000 Subject: [PATCH] add: - 'ProcessWatchdog' and 'DestroyableFileSystemUtils'. change: - Refactor 'ProcessExecutionHelper' and extract 'ProcessWatchdog'. SVN: 6173 --- .../process/ProcessExecutionHelper.java | 230 +++++------------- .../cisd/common/process/ProcessWatchdog.java | 151 ++++++++++++ .../io/DestroyableFileSystemUtils.java | 147 +++++++++++ 3 files changed, 360 insertions(+), 168 deletions(-) create mode 100644 common/source/java/ch/systemsx/cisd/common/process/ProcessWatchdog.java create mode 100644 common/source/java/org/apache/commons/io/DestroyableFileSystemUtils.java diff --git a/common/source/java/ch/systemsx/cisd/common/process/ProcessExecutionHelper.java b/common/source/java/ch/systemsx/cisd/common/process/ProcessExecutionHelper.java index bf49ac7a768..3b4811a32c0 100644 --- a/common/source/java/ch/systemsx/cisd/common/process/ProcessExecutionHelper.java +++ b/common/source/java/ch/systemsx/cisd/common/process/ProcessExecutionHelper.java @@ -21,8 +21,6 @@ import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; -import java.util.Timer; -import java.util.TimerTask; import org.apache.commons.io.IOUtils; import org.apache.log4j.Level; @@ -36,8 +34,9 @@ import ch.systemsx.cisd.common.utilities.OSUtilities; * @author Tomasz Pylak * @author Bernd Rinn */ -public class ProcessExecutionHelper +public final class ProcessExecutionHelper { + /** * The value indicating that there is no exit value available for a process execution. */ @@ -73,7 +72,8 @@ public class ProcessExecutionHelper * @return <code>true</code>, if the process did complete successfully, <code>false</code> * otherwise. */ - public static boolean runAndLog(List<String> commandLine, Logger operationLog, Logger machineLog) + public static boolean runAndLog(final List<String> commandLine, final Logger operationLog, + final Logger machineLog) { return new ProcessExecutionHelper(operationLog, machineLog).runAndLog(commandLine, 0L); } @@ -86,9 +86,10 @@ public class ProcessExecutionHelper * @param machineLog The {@link Logger} to use for all message on the lower (machine) level. * @return The process result. */ - public static ProcessResult run(List<String> commandLine, Logger operationLog, Logger machineLog) + public static ProcessResult run(final List<String> commandLine, final Logger operationLog, + final Logger machineLog) { - return new ProcessExecutionHelper(operationLog, machineLog).run(commandLine, 0L); + return new ProcessExecutionHelper(operationLog, machineLog).runWithoutWatchdog(commandLine); } /** @@ -103,8 +104,8 @@ public class ProcessExecutionHelper * @return <code>true</code>, if the process did complete successfully, <code>false</code> * otherwise. */ - public static boolean runAndLog(List<String> cmd, long millisToWaitForCompletion, - Logger operationLog, Logger machineLog) + public static boolean runAndLog(final List<String> cmd, final long millisToWaitForCompletion, + final Logger operationLog, final Logger machineLog) { return new ProcessExecutionHelper(operationLog, machineLog).runAndLog(cmd, millisToWaitForCompletion); @@ -121,10 +122,10 @@ public class ProcessExecutionHelper * @param machineLog The {@link Logger} to use for all message on the lower (machine) level. * @return The process result. */ - public static ProcessResult run(List<String> cmd, long millisToWaitForCompletion, - Logger operationLog, Logger machineLog) + public static ProcessResult run(final List<String> cmd, final long millisToWaitForCompletion, + final Logger operationLog, final Logger machineLog) { - return new ProcessExecutionHelper(operationLog, machineLog).run(cmd, + return new ProcessExecutionHelper(operationLog, machineLog).runWithWatchdog(cmd, millisToWaitForCompletion); } @@ -147,7 +148,8 @@ public class ProcessExecutionHelper * Returns the stdout (and stderr if {@link ProcessBuilder#redirectErrorStream(boolean)} has * been called with <code>true</code>). */ - public static List<String> readProcessOutputLines(Process processOrNull, Logger machineLog) + public static List<String> readProcessOutputLines(final Process processOrNull, + final Logger machineLog) { final List<String> processOutput = new ArrayList<String>(); if (processOrNull == null) @@ -163,7 +165,7 @@ public class ProcessExecutionHelper { processOutput.add(ln); } - } catch (IOException e) + } catch (final IOException e) { machineLog.warn(String.format("IOException when reading stdout, msg='%s'.", e .getMessage())); @@ -178,99 +180,60 @@ public class ProcessExecutionHelper // Implementation // - private ProcessExecutionHelper(Logger operationLog, Logger machineLog) + private ProcessExecutionHelper(final Logger operationLog, final Logger machineLog) { this.operationLog = operationLog; this.machineLog = machineLog; } - private static class ProcessStatus + private final ProcessResult runWithWatchdog(final List<String> commandLine, + final long millisoWaitForCompletion) { - private boolean canInterrupt; - - private boolean isInterruptedAfterTimeout; - - private Process processOrNull; - - public ProcessStatus(boolean canInterrupt) - { - this.canInterrupt = canInterrupt; - this.isInterruptedAfterTimeout = false; - } - - // this prevents interruption from watch-dog when we are outside of catch - // InterruptedException - synchronized public void interruptionUnnecesary() - { - canInterrupt = false; - } - - synchronized public boolean canInterrupt() - { - return canInterrupt; - } - - synchronized public void setInterruptedAfterTimeout() - { - interruptionUnnecesary(); - isInterruptedAfterTimeout = true; - } - - synchronized public boolean isInterruptedAfterTimeout() - { - return isInterruptedAfterTimeout; - } - - synchronized public Process tryGetProcess() + assert millisoWaitForCompletion > 0L : "Unspecified time out."; + final ProcessWatchdog processWatchdog = new ProcessWatchdog(millisoWaitForCompletion); + try { - return processOrNull; - } - - synchronized public void setProcess(Process process) + final Process process = launchProcess(commandLine); + processWatchdog.start(process); + try + { + process.waitFor(); + processWatchdog.stop(); + } catch (final InterruptedException e) + { + process.destroy(); + operationLog.warn(String.format("Execution of %s interrupted after timeout.", + commandLine)); + } + return createResult(commandLine, process, processWatchdog.isProcessKilled()); + } catch (final IOException ex) { - this.processOrNull = process; + return createNotStartedResult(commandLine, ex); } - } - private ProcessResult run(List<String> commandLine, long millisoWaitForCompletion) + private final ProcessResult runWithoutWatchdog(final List<String> commandLine) { - ProcessStatus processStatus = new ProcessStatus(true); - Timer watchDogOrNull = null; - boolean isInterrupted = false; try { - watchDogOrNull = - tryCreateWatchDog(processStatus, millisoWaitForCompletion, commandLine.get(0)); - final Process process; + final Process process = launchProcess(commandLine); try { - process = launchProcess(commandLine); - processStatus.setProcess(process); - } catch (IOException ex) + process.waitFor(); + } catch (final InterruptedException e) { - processStatus.interruptionUnnecesary(); - return createNotStartedResult(commandLine, ex); + process.destroy(); + operationLog.warn(String.format("Execution of %s interrupted after timeout.", + commandLine)); } - process.waitFor(); - processStatus.interruptionUnnecesary(); // the process terminated and does not block - // anymore - } catch (InterruptedException ex) + return createResult(commandLine, process, false); + } catch (final IOException ex) { - processStatus.interruptionUnnecesary(); - logInterruption(commandLine.get(0), processStatus, ex); - isInterrupted = true; - } finally - { - if (watchDogOrNull != null) - { - watchDogOrNull.cancel(); - } + return createNotStartedResult(commandLine, ex); } - return createResult(commandLine, processStatus.tryGetProcess(), isInterrupted); } - private Process launchProcess(List<String> commandLine) throws IOException + private final Process launchProcess(final List<String> commandLine) throws IOException { final ProcessBuilder processBuilder = new ProcessBuilder(commandLine); processBuilder.redirectErrorStream(true); @@ -285,14 +248,15 @@ public class ProcessExecutionHelper return process; } - private ProcessResult createNotStartedResult(List<String> commandLine, IOException ex) + private final ProcessResult createNotStartedResult(final List<String> commandLine, + final IOException ex) { machineLog.error(String.format("Cannot execute executable %s", commandLine), ex); return ProcessResult.createNotStarted(commandLine, operationLog, machineLog); } - private ProcessResult createResult(List<String> commandLine, final Process processOrNull, - boolean isInterrupted) + private final ProcessResult createResult(final List<String> commandLine, + final Process processOrNull, final boolean isInterrupted) { if (processOrNull == null) { @@ -310,93 +274,23 @@ public class ProcessExecutionHelper } } - private void logInterruption(final String commandLine, ProcessStatus terminationStatus, - InterruptedException ex) + private final boolean runAndLog(final List<String> cmd, final long millisToWaitForCompletion) { - if (terminationStatus.isInterruptedAfterTimeout() == false) // have NOT been stopped by the - // watchDog - { - machineLog.error(String.format("Execution of %s interrupted", commandLine), ex); - } else - { - operationLog.warn(String.format("Execution of %s interrupted after timeout", - commandLine)); - } - } - - /* - * isTerminated is passed by reference. Access to it should be synchronized on process variable - */ - private Timer tryCreateWatchDog(final ProcessStatus processStatus, - final long millisToWaitForCompletion, final String commandForLog) - { - final Timer watchDogOrNull; + final ProcessResult result; if (millisToWaitForCompletion > 0L) { - final Thread processThread = Thread.currentThread(); - watchDogOrNull = new Timer(String.format("Watch Dog [%s]", commandForLog)); - watchDogOrNull.schedule(new TimerTask() - { - @Override - public void run() - { - operationLog.warn(String.format( - "Destroy process since it didn't finish in %d milli seconds", - millisToWaitForCompletion)); - Process process = processStatus.tryGetProcess(); - if (process != null) - { - process.destroy(); - sleep(millisToWaitForCompletion / 2); // allow the process to - // terminate normally - } - synchronized (processStatus) - { - // Interrupt waiting for the process termination if we still wait. - if (processStatus.canInterrupt()) - { - processStatus.setInterruptedAfterTimeout(); - operationLog.info(String.format( - "Interrupting waiting for the process %s by the watchDog", - commandForLog)); - // stop waiting for the process. We want to prevent situations when - // the child process, - // which is an external program, gets stuck during the start or - // cannot be destroyed. It - // would cause the whole system to hang and we do not want that. - processThread.interrupt(); - } - } - } - - private void sleep(final long millisToWait) - { - try - { - Thread.sleep(millisToWait); - } catch (InterruptedException ex) - { - ex.printStackTrace(); - } - } - }, millisToWaitForCompletion); + result = runWithWatchdog(cmd, millisToWaitForCompletion); } else { - watchDogOrNull = null; + result = runWithoutWatchdog(cmd); } - return watchDogOrNull; - } - - private boolean runAndLog(List<String> cmd, long millisToWaitForCompletion) - { - final ProcessResult result = run(cmd, millisToWaitForCompletion); result.log(); result.destroyProcess(); return result.isOK(); } - public static void logProcessExecution(String commandName, int exitValue, - List<String> processOutput, Logger operationLog, Logger machineLog) + public final static void logProcessExecution(final String commandName, final int exitValue, + final List<String> processOutput, final Logger operationLog, final Logger machineLog) { if (exitValue != EXIT_VALUE_OK) { @@ -409,8 +303,8 @@ public class ProcessExecutionHelper } } - private static void logProcessExitValue(final Level logLevel, Logger operationLog, - String commandName, int exitValue) + private final static void logProcessExitValue(final Level logLevel, final Logger operationLog, + final String commandName, final int exitValue) { assert logLevel != null; assert operationLog != null; @@ -426,8 +320,8 @@ public class ProcessExecutionHelper } } - private static void logProcessOutput(final Level logLevel, Logger machineLog, - String commandName, List<String> processOutputLines) + private final static void logProcessOutput(final Level logLevel, final Logger machineLog, + final String commandName, final List<String> processOutputLines) { assert logLevel != null; assert machineLog != null; @@ -439,7 +333,7 @@ public class ProcessExecutionHelper return; } machineLog.log(logLevel, String.format("[%s] output:", commandName)); - for (String ln : processOutputLines) + for (final String ln : processOutputLines) { if (ln.trim().length() > 0) { diff --git a/common/source/java/ch/systemsx/cisd/common/process/ProcessWatchdog.java b/common/source/java/ch/systemsx/cisd/common/process/ProcessWatchdog.java new file mode 100644 index 00000000000..b0657de329f --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/process/ProcessWatchdog.java @@ -0,0 +1,151 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.common.process; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Destroys a process running for too long. + * + * @author Christian Ribeaud + */ +public final class ProcessWatchdog implements Runnable +{ + private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); + + private final TimeUnit timeUnit; + + private final long timeOut; + + private Process process; + + /** say whether or not the watchdog is currently monitoring a process */ + private boolean watch; + + /** say whether or not the process was killed due to running overtime */ + private boolean processKilled; + + private ScheduledFuture<?> scheduledFuture; + + public ProcessWatchdog(final long timeOutInMillis) + { + this(TimeUnit.MILLISECONDS, timeOutInMillis); + } + + public ProcessWatchdog(final TimeUnit timeUnit, final long timeOut) + { + assert timeOut > 0L : "Unspecified time out"; + this.timeUnit = timeUnit; + this.timeOut = timeOut; + } + + /** + * Watches the given process and terminates it, if it runs for too long. All information from + * the previous run are reset. + * + * @param p the process to monitor. It cannot be <tt>null</tt>. * + * @throws IllegalStateException if a process is still being monitored. + */ + public synchronized void start(final Process p) + { + assert p != null : "Unspecified process"; + if (this.process != null) + { + throw new IllegalStateException("Already running."); + } + this.process = p; + this.processKilled = false; + this.watch = true; + this.process = p; + scheduledFuture = executorService.schedule(this, timeOut, timeUnit); + } + + /** + * Stops the watcher. + */ + public final synchronized void stop() + { + if (scheduledFuture != null) + { + scheduledFuture.cancel(true); + scheduledFuture = null; + } + watch = false; + process = null; + } + + /** + * reset the monitor flag and the process. + */ + protected void cleanUp() + { + watch = false; + process = null; + } + + /** + * Indicates whether or not the watchdog is still monitoring the process. + * + * @return <tt>true</tt> if the process is still running, otherwise <tt>false</tt>. + */ + public final boolean isWatching() + { + return watch; + } + + /** + * Indicates whether the last process run was killed on timeout or not. + * + * @return <tt>true</tt> if the process was killed otherwise <tt>false</tt>. + */ + public final boolean isProcessKilled() + { + return processKilled; + } + + // + // Runnable + // + + public final void run() + { + try + { + try + { + // We must check if the process was not stopped + // before being here + process.exitValue(); + } catch (final IllegalThreadStateException itse) + { + // the process is not terminated, if this is really + // a timeout and not a manual stop then kill it. + if (watch) + { + processKilled = true; + process.destroy(); + } + } + } finally + { + cleanUp(); + } + } +} diff --git a/common/source/java/org/apache/commons/io/DestroyableFileSystemUtils.java b/common/source/java/org/apache/commons/io/DestroyableFileSystemUtils.java new file mode 100644 index 00000000000..a1d992e4063 --- /dev/null +++ b/common/source/java/org/apache/commons/io/DestroyableFileSystemUtils.java @@ -0,0 +1,147 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * Licensed 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.commons.io; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import ch.systemsx.cisd.common.Constants; +import ch.systemsx.cisd.common.process.ProcessWatchdog; + +/** + * A <code>FileSystemUtils</code> extension which puts a watch dog to the process created. + * + * @author Christian Ribeaud + */ +public final class DestroyableFileSystemUtils extends FileSystemUtils +{ + + private final static DestroyableFileSystemUtils INSTANCE = new DestroyableFileSystemUtils(); + + private final static int OS; + + static + { + int os = -1; + try + { + final Field osField = FileSystemUtils.class.getDeclaredField("OS"); + osField.setAccessible(true); + os = osField.getInt(FileSystemUtils.class); + } catch (final Exception ex) + { + } + OS = os; + } + + final ProcessWatchdog watchdog = new ProcessWatchdog(Constants.MILLIS_TO_WAIT_BEFORE_TIMEOUT); + + private DestroyableFileSystemUtils() + { + // Can not be instantiated. + } + + /** + * Waits for a given process. + * + * @param process the process one wants to wait for. + */ + protected final void waitFor(final Process process, final String[] cmdAttribs) + throws IOException + { + try + { + process.waitFor(); + watchdog.stop(); + } catch (final InterruptedException e) + { + process.destroy(); + throw new IOException("Command line threw an InterruptedException '" + e.getMessage() + + "' for command " + Arrays.asList(cmdAttribs)); + } + } + + /** + * Returns the free space on a drive or volume in kilobytes by invoking the command line. + */ + public static long freeSpaceKb(final String path) throws IOException + { + return INSTANCE.freeSpaceOS(path, OS, true); + } + + // + // FileSystemUtils + // + + @Override + final List<String> performCommand(final String[] cmdAttribs, final int max) throws IOException + { + final List<String> lines = new ArrayList<String>(20); + Process proc = null; + InputStream in = null; + OutputStream out = null; + InputStream err = null; + BufferedReader inr = null; + try + { + proc = openProcess(cmdAttribs); + watchdog.start(proc); + in = proc.getInputStream(); + out = proc.getOutputStream(); + err = proc.getErrorStream(); + inr = new BufferedReader(new InputStreamReader(in)); + String line = inr.readLine(); + while (line != null && lines.size() < max) + { + line = line.toLowerCase().trim(); + lines.add(line); + line = inr.readLine(); + } + waitFor(proc, cmdAttribs); + if (proc.exitValue() != 0) + { + // os command problem, throw exception + throw new IOException("Command line returned OS error code '" + proc.exitValue() + + "' for command " + Arrays.asList(cmdAttribs)); + } + if (lines.size() == 0) + { + // unknown problem, throw exception + throw new IOException("Command line did not return any info " + "for command " + + Arrays.asList(cmdAttribs)); + } + return lines; + } finally + { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(err); + IOUtils.closeQuietly(inr); + if (proc != null) + { + proc.destroy(); + } + } + } +} -- GitLab