Commits

Michael Heemskerk  committed f050f52

FE-1073 View which SCM commands fisheye is running via the Administration

Introduced ProcessMonitor as a way to process 'beforeStart' and 'afterFinished' callbacks from an ExternalProcess. This can be used to log information about the process, and is also used to show recent activities in the FishEye administration view

  • Participants
  • Parent commits 1adef99

Comments (0)

Files changed (6)

File processutils/src/main/java/com/atlassian/utils/process/BaseProcessMonitor.java

+package com.atlassian.utils.process;
+
+/**
+ * Base implementation of {@link ProcessMonitor}. Useful for subclassing if you only want to implement a single
+ * callback method.
+ */
+public class BaseProcessMonitor implements ProcessMonitor {
+
+    public void onAfterFinished(ExternalProcess process) {
+        // does nothing
+    }
+
+    public void onBeforeStart(ExternalProcess process) {
+        // does nothing
+    }
+
+}

File processutils/src/main/java/com/atlassian/utils/process/ExternalProcess.java

 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.log4j.Logger;
+
 /**
  * This class manages the execution of an external process, using separate threads to process
  * the process' IO requirements.
     private ProcessHandler handler;
     private Process process;
 
+    private List<ProcessMonitor> monitors = new ArrayList<ProcessMonitor>();
+    
     private ProcessException processException;
 
     private LatchedRunnable outputPump;
 
     private long lastWatchdogReset;
     private long timeout = 60000L;
+    private Long startTime;
     private boolean cancelled;
-
+    
     public void resetWatchdog() {
         lastWatchdogReset = System.currentTimeMillis();
     }
     }
 
     /**
+     * @return the time process execution started. null if the process has not yet started.
+     */
+    public Long getStartTime() {
+        return this.startTime;
+    }
+    
+    public void addMonitor(ProcessMonitor monitor) {
+        this.monitors.add(monitor);
+    }
+    
+    public void removeMonitor(ProcessMonitor monitor) {
+        this.monitors.remove(monitor);
+    }
+    
+    /**
      * Start the external process and setup the IO pump threads needed to
      * manage the process IO. If you call this method you must eventually call the
      * finish() method. Using this method you may execute additional code between process
      */
     public void start() {
         try {
+            this.startTime = System.currentTimeMillis();
             this.process = Runtime.getRuntime().exec(cmdArray, environment, workingDir);
             setupIOPumps();
         } catch (IOException e) {
             handler.complete(-1, processException);
         }
     }
+    
+    /**
+     * Notifies all ProcessMonitors of the 'beforeStart' event.
+     */
+    private void notifyBeforeStart() {
+        for (ProcessMonitor monitor: monitors) {
+            try {
+                monitor.onBeforeStart(this);
+            } catch (Exception e) {
+                // catch and log error, but continue
+                Logger.getLogger(ExternalProcess.class).error("Error while processing 'beforeStarted' event:", e);
+            }
+        }
+    }
 
     /**
+     * Notifies all ProcessMonitors of the 'afterFinished' event.
+     */
+    private void notifyAfterFinished() {
+        for (ProcessMonitor monitor: monitors) {
+            try {
+                monitor.onAfterFinished(this);
+            } catch (Exception e) {
+                Logger.getLogger(ExternalProcess.class).error("Error while processing 'afterFinished' event:", e);
+            }
+        }
+    }
+    
+    /**
      * Execute the external command. When this method returns, the process handler
      * provided at construction time should be consulted to collect exit code, exceptions,
      * process output, etc.
      */
     public void execute() {
-        start();
-        finish();
+        notifyBeforeStart();
+        try {
+            start();
+            finish();
+        } finally {
+            notifyAfterFinished();
+        }
     }
 
     /**

File processutils/src/main/java/com/atlassian/utils/process/ExternalProcessBuilder.java

+package com.atlassian.utils.process;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.Priority;
+
+
+/**
+ * Utility class to simply the building of an ExternalProcess instance
+ *
+ */
+public class ExternalProcessBuilder {
+    private ProcessHandler handler;
+    private InputHandler input;
+    private OutputHandler output;
+    private OutputHandler error;
+    private List<ProcessMonitor> monitors;
+    private List<String> command;
+    private File workingDir;
+    private long timeout;
+        
+    public ExternalProcessBuilder handlers(InputHandler input, OutputHandler output, OutputHandler error) {
+        this.input = input;
+        this.output = output;
+        this.error = error;
+        return this;
+    }
+    
+    public ExternalProcessBuilder handler(ProcessHandler handler) {
+        this.handler = handler;
+        return this;
+    }
+    
+    public ExternalProcessBuilder handlers(OutputHandler output, OutputHandler error) {
+        return this.handlers(null, output, error);
+    }
+    
+    public ExternalProcessBuilder handlers(OutputHandler output) {
+        return this.handlers(null, output, null);
+    }
+
+    public ExternalProcessBuilder handlers(InputHandler input, OutputHandler output) {
+        return this.handlers(input, output, null);
+    }
+
+    public ExternalProcessBuilder command(List<String> command, File workingDir, long timeout) {
+        this.command = command;
+        this.workingDir = workingDir;
+        this.timeout = timeout;
+        return this;
+    }
+
+    public ExternalProcessBuilder command(List<String> command, File workingDir) {
+        this.command = command;
+        this.workingDir = workingDir;
+        return this;
+    }
+
+    public ExternalProcessBuilder command(List<String> command) {
+        this.command = command;
+        return this;
+    }
+    
+    public ExternalProcessBuilder timeout(long timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+    
+    public ExternalProcessBuilder log(Logger logger, Priority priority) {
+        addMonitor(new LoggingProcessMonitor(logger, priority));
+        return this;
+    }
+
+    public ExternalProcessBuilder log(Logger logger, Priority priority, StringObfuscator obfuscator) {
+        addMonitor(new LoggingProcessMonitor(logger, priority, obfuscator));
+        return this;
+    }
+    
+    public ExternalProcessBuilder addMonitor(ProcessMonitor... monitors) {
+        if (monitors.length > 0) {
+            if (this.monitors == null) {
+                this.monitors = new ArrayList<ProcessMonitor>();
+            }
+            for (ProcessMonitor mon : monitors) {
+                this.monitors.add(mon);
+            }
+        }
+        return this;
+    }
+    
+    public ExternalProcess build() {
+        ProcessHandler h = this.handler;
+        if (this.handler == null) {
+            // no processHandler defined, create a pluggableprocesshandler
+            PluggableProcessHandler plugHandler = new PluggableProcessHandler();
+            plugHandler.setInputHandler(this.input);
+            plugHandler.setOutputHandler(this.output);
+            if (this.error != null) {
+                plugHandler.setErrorHandler(this.error);
+            } else {
+                plugHandler.setErrorHandler(new StringOutputHandler());
+            }
+            
+            h = plugHandler;
+        }
+        
+        ExternalProcess process = new ExternalProcess(command, handler);
+        if (timeout > 0L) {
+            process.setTimeout(timeout);
+        }
+        process.setWorkingDir(workingDir);
+        if (monitors != null) {
+            for (ProcessMonitor monitor: monitors) {
+                if (monitor != null) {
+                    process.addMonitor(monitor);
+                }
+            }
+        }
+        
+        return process;
+    }
+}

File processutils/src/main/java/com/atlassian/utils/process/LoggingProcessMonitor.java

+package com.atlassian.utils.process;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.Priority;
+
+/**
+ * ProcessMonitor that logs the results to the provided {@link Logger} using the provided {@link Priority}. When
+ * the external process does not succeed, the error messages are logged using the info priority.
+ */
+public class LoggingProcessMonitor implements ProcessMonitor {
+    private Logger logger;
+    private Priority priority;
+    private StringObfuscator obfuscator;
+    
+    public LoggingProcessMonitor(Logger logger, Priority priority) {
+        this.logger = logger;
+        this.priority = priority;
+    }
+
+    public LoggingProcessMonitor(Logger logger, Priority priority, StringObfuscator obfuscator) {
+        this(logger, priority);
+        this.obfuscator = obfuscator;
+    }
+    
+    /**
+     * Override this method to obfuscate parts of the command line, e.g. passwords etc. This implementation
+     * returns the unaltered command line. 
+     * @param process the external process.
+     * @return the command line. 
+     */
+    protected String getCommandLine(ExternalProcess process) {
+        if (obfuscator != null) {
+            return obfuscator.obfuscate(process.getCommandLine()); 
+        } else {
+            return process.getCommandLine();
+        }
+    }
+
+    /**
+     * Logs message to Log4j logger
+     */
+    public void onBeforeStart(ExternalProcess process) {
+        if (this.logger != null && this.logger.isEnabledFor(this.priority)) {
+            logger.log(this.priority, "Starting process: " + getCommandLine(process));
+        }
+    }
+
+    /**
+     * Logs message to Log4j logger
+     */
+    public void onAfterFinished(ExternalProcess process) {
+        if (this.logger != null && this.logger.isEnabledFor(this.priority)) {
+            StringBuilder message = new StringBuilder();
+            String commandLine = getCommandLine(process);
+            message.append("Finished process: ").append(commandLine);
+            Long startTime = process.getStartTime();
+            if (startTime != null) {
+            	message.append(" took ").append(startTime.longValue() - System.currentTimeMillis()).append("ms");
+            }
+            logger.log(priority, message.toString());
+            
+            // log errors if present
+            ProcessHandler handler = process.getHandler();
+            if (handler != null && !handler.succeeded()) {
+                logger.info(getErrorMessage(process));
+            }
+        }
+    }
+    
+    public String getErrorMessage(ExternalProcess process) {
+        ProcessHandler handler = process.getHandler();
+        String commandLine = getCommandLine(process);
+        StringBuilder message = new StringBuilder();
+
+        if (handler.getException() != null) {
+            message.append("Exception executing command \"")
+                .append(commandLine).append("\" ")
+                .append(handler.getException().getMessage()).append("\n")
+                .append(handler.getException()).append("\n");
+        }
+
+        String reason = null;
+        if (handler instanceof PluggableProcessHandler) {
+            OutputHandler errorHandler = ((PluggableProcessHandler) handler).getErrorHandler();
+            if (errorHandler instanceof StringOutputHandler) {
+                StringOutputHandler errorStringHandler = (StringOutputHandler) errorHandler;
+                if (errorStringHandler.getOutput() != null) {
+                    reason = errorStringHandler.getOutput();
+                }
+            }
+        }
+        if (reason != null && reason.trim().length() > 0) {
+            message.append("Error executing command \"").append(commandLine).append("\": ").append(reason);
+        }
+        return message.toString();
+    }
+}

File processutils/src/main/java/com/atlassian/utils/process/ProcessMonitor.java

+package com.atlassian.utils.process;
+
+/**
+ * Interface for monitoring an external process.
+ * 
+ *  @see ExternalProcess 
+ */
+public interface ProcessMonitor {
+    /**
+     * Call-back method, called just before the external process is started.
+     * @param process the {@link ExternalProcess} that is about to be started.
+     */
+    public void onBeforeStart(ExternalProcess process);
+    
+    /**
+     * Call-back method, called right after the external process has finished. The process might have
+     * been finished normally, in an error, or canceled.
+     * @param process the {@link ExternalProcess} that has just finished.
+     */
+    public void onAfterFinished(ExternalProcess process);
+}

File processutils/src/main/java/com/atlassian/utils/process/StringObfuscator.java

+package com.atlassian.utils.process;
+
+/**
+ * Interface for obfuscating (parts of) a string. Useful for removing sensitive data from a string (passwords) before logging.
+ */
+public interface StringObfuscator {
+    String obfuscate(String string);
+}