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