/******************************************************************************* * This file is part of OpenNMS(R). * * Copyright (C) 2006-2011 The OpenNMS Group, Inc. * OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc. * * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc. * * OpenNMS(R) is 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, either version 3 of the License, * or (at your option) any later version. * * OpenNMS(R) 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 OpenNMS(R). If not, see: * http://www.gnu.org/licenses/ * * For more information contact: * OpenNMS(R) Licensing <license@opennms.org> * http://www.opennms.org/ * http://www.opennms.com/ *******************************************************************************/ package org.opennms.netmgt.actiond; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import org.opennms.core.fiber.PausableFiber; import org.opennms.core.queue.FifoQueue; import org.opennms.core.queue.FifoQueueException; import org.opennms.core.utils.ThreadCategory; /** * This class is used as a thread for launching and executing actions as they * are discovered by the action daemon. The actions are read from an execution * queue and the processes are created by the fiber. Each created process is * added to garbage collection list that is periodically polled and culled based * upon the status of the process or how long the process is run. If the process * has run long than allocated it is terminated during collection. * * @author <a href="mailto:mike@opennms.org">Mike Davidson </a> * @author <a href="mailto:weave@oculan.com">Brian Weaver </a> * @author <a href="http://www.opennms.org/>OpenNMS </a> * */ final class Executor implements Runnable, PausableFiber { /** * The input queue of runnable commands. */ private FifoQueue<String> m_execQ; /** * The list of outstanding commands. */ private List<DatedProc> m_processes; /** * The maximum time that a command can execute. */ private long m_maxWait; /** * The maximum number of outstanding processes. */ private int m_maxProcCount; /** * The process garbage collection thread. */ private Thread m_reaper; /** * The garbage collection instance. */ private Runnable m_reaperRun; /** * The worker thread that executes the <code>run</code> method. */ private Thread m_worker; /** * The name of this Fiber */ private String m_name; /** * The status of this fiber. */ private int m_status; /** * This class is designed to encapsulated a process and its start time. The * start time is based upon the system clock and the runtime is the * difference between the current time and the started time. * * @author <a href="mailto:mike@opennms.org">Mike Davidson </a> * @author <a href="mailto:weave@oculan.com">Brian Weaver </a> * @author <a href="http://www.opennms.org/>OpenNMS </a> * */ private static final class DatedProc { /** * The executable running */ private final String m_cmd; /** * The process returned from the {@link java.lang.Runtime Runtime} * instance. */ private final Process m_proc; /** * The time the process was started. */ private final long m_started; /** * Constructs a new dated process. * * @param cmd * The command used to start the process. * @param p * The running process. */ DatedProc(String cmd, Process p) { m_cmd = cmd; m_proc = p; m_started = System.currentTimeMillis(); } /** * Returns the encapsulated process. * */ Process getProcess() { return m_proc; } /** * Returns the current runtime of the process. */ long getRunTime() { return System.currentTimeMillis() - m_started; } /** * Returns the command being run by the dated process. */ public String toString() { return m_cmd; } } // end class DatedProc /** * This class encapsules a singular run method that is used to * <em>garbage collect</em> expired processes. If a process has exceeded * its maximum runtime then it is killed and removed from the process queue. * * @author <a href="mailto:mike@opennms.org">Mike Davidson </a> * @author <a href="mailto:weave@oculan.com">Brian Weaver </a> * @author <a href="http://www.opennms.org/>OpenNMS </a> * */ private final class Reaper implements Runnable { /** * The reaper execution enviroment. This method scans the process array * and removes expired and completed commands from the array on a * periodic basis. In that respect it is a garbage collection thread for * processes. * */ public void run() { // Wait for a maximum of 15 seconds between checks! // long waitPeriod = m_maxWait / 5; if (waitPeriod > 15000) { waitPeriod = 15000; } ThreadCategory log = ThreadCategory.getInstance(Executor.class); // Begin the checking process. // // Make sure to leave the 'this' keyword associated with the // getClass() call or jikes will complain. The 'this' keyword // removes all ambiguity in the call. // for (;;) { // run and check the queue once about // 1/5 of the maximum run time. // synchronized (m_processes) { Iterator<DatedProc> i = m_processes.iterator(); while (i.hasNext()) { DatedProc dp = i.next(); try { int rc = dp.getProcess().exitValue(); if (log.isDebugEnabled()) { log.debug("Process " + dp + " completed, rc = " + rc); } i.remove(); continue; } catch (IllegalThreadStateException ex) { } // still running if (dp.getRunTime() > m_maxWait) { if (log.isInfoEnabled()) log.info("Process " + dp + " did not complete in the alloted time, terminating."); dp.getProcess().destroy(); i.remove(); } } } synchronized (this) { // the 'this' keyword should not be removed // or else jikes will complain about an ambiguous // call. this.notifyAll(); // sleep for 1/5 of wait time or // 15 seconds, which ever is smaller. // try { // the 'this' keyword should not be removed // or else jikes will complain about an ambiguous // call. this.wait(waitPeriod); } catch (InterruptedException ex) { // this is used as a shutdown mechinism break; } } } // end for(;;) } // end run } // end class Reaper /** * <p> * Converts a single command to an array that can be passed to the * {@link java.lang.Runtime#exec(java.lang.String[]) exec}system call. The * element at index zero of the array is the name of the executable to run. * Indexs [1..length) are the arguments passed to the executable command. * </p> * * <p> * The input command has is white space trimmed before processing. The basic * processing is to split on spaces, except when a double quote or single * quote is encountered. Also backspaces(\) should also be handled correctly * both in and out of the quotes. Shell escapes with <em>$</em> are not * supported. * </p> * * @param cmd * The command to split into an array. * * @return The execution array. * */ private static String[] getExecArguments(String cmd) { ThreadCategory log = ThreadCategory.getInstance(Executor.class); // make sure we get rid of excess white space. // cmd = cmd.trim(); // get the processing elements. // StringBuffer buf = new StringBuffer(); List<String> args = new ArrayList<String>(5); char[] chars = cmd.toCharArray(); boolean dquoted = false; boolean squoted = false; for (int x = 0; x < chars.length; x++) { if (chars[x] == '\\') { if (squoted) { buf.append(chars[x]).append(chars[x + 1]); x += 2; } else { buf.append(chars[++x]); } } else if (chars[x] == '\"' && !squoted) { dquoted = dquoted ? false : true; } else if (chars[x] == '\'' && !dquoted) { squoted = squoted ? false : true; } else if (squoted || dquoted) { buf.append(chars[x]); } else if (chars[x] == ' ') { String arg = buf.toString().trim(); if (log.isDebugEnabled()) { log.debug("getExecArgument: adding argument: " + arg); } args.add(arg); buf.delete(0, buf.length()); // trim off the remaining white space // while (chars[x + 1] == ' ') { x++; } } else { buf.append(chars[x]); } } // Add remaining argument // if (buf.length() > 0) { args.add(buf.toString()); } buf = null; // Convert to string array // String[] results = new String[args.size()]; return args.toArray(results); } /** * Constructs a new action daemon execution environment. The constructor * takes two arguments that define the source of commands to be executed and * the maximum time that a command may run. * * @param execQ * The execution queue * @param maxRunTime * The maximum runtime of a process. * */ Executor(FifoQueue<String> execQ, long maxRunTime, int maxProcesses) { m_processes = Collections.synchronizedList(new LinkedList<DatedProc>()); m_execQ = execQ; m_maxWait = maxRunTime; m_worker = null; m_reaper = null; m_name = "Actiond-Executor"; m_status = START_PENDING; m_reaperRun = null; m_maxProcCount = maxProcesses; } /** * The main worker of the fiber. This method is executed by the encapsualted * thread to read commands from the execution queue and to execute those * commands. If the thread is interrupted or the status changes to * <code>STOP_PENDING</code> then the method will return as quickly as * possible. */ public void run() { ThreadCategory log = ThreadCategory.getInstance(Executor.class); synchronized (this) { m_status = RUNNING; } for (;;) { synchronized (this) { // if stopped or stop pending then break out // if (m_status == STOP_PENDING || m_status == STOPPED) { break; } // if paused or pause pending then block // while (m_status == PAUSE_PENDING || m_status == PAUSED) { m_status = PAUSED; try { wait(); } catch (InterruptedException ex) { // exit break; } } // if resume pending then change to running // if (m_status == RESUME_PENDING) { m_status = RUNNING; } } // check to see if we can execute more // processes. Block until we can. // if (m_maxProcCount == m_processes.size()) { if (log.isDebugEnabled()) log.debug("Number of processes at " + m_maxProcCount + " - being wait for a process to finish or be reaped!"); synchronized (m_reaperRun) { m_reaperRun.notifyAll(); try { m_reaperRun.wait(); } catch (InterruptedException ex) { // exit command break; } } continue; // check status and count again. } // Extract the next command // String cmd = null; try { cmd = m_execQ.remove(1000); if (cmd == null) // status check time { continue; // goto top of loop } } catch (InterruptedException ex) { break; } catch (FifoQueueException ex) { log.warn("The input execution queue has errors, exiting...", ex); break; } // start a new process // if (log.isDebugEnabled()) { log.debug("Parsing cmd args: " + cmd); } String[] execArgs = getExecArguments(cmd); if (execArgs != null && execArgs.length > 0) { try { if (log.isDebugEnabled()) log.debug("Getting ready to execute \'" + cmd + "\'"); Process px = Runtime.getRuntime().exec(execArgs); // Added by Nick Wesselman to attempt to workaround // 1.4.1 JDK bug // http://developer.java.sun.com/developer/bugParade/bugs/4763384.html try { Thread.sleep(1); } catch (InterruptedException e) { // log? } m_processes.add(new DatedProc(cmd, px)); } catch (IOException ex) { log.warn("Failed to execute command: " + cmd, ex); } catch (SecurityException ex) { log.warn("Application not authorized to exec commands!", ex); break; } } } // end infinite loop synchronized (this) { m_status = STOPPED; } } // end run /** * Starts the fiber. If the fiber has already been run or is currently * running then an exception is generated. The status of the fiber is * updated to <code>STARTING</code> and will transisition to <code> * RUNNING</code> * when the fiber finishes initializing and begins processing the * encapsulaed queue. * * @throws java.lang.IllegalStateException * Thrown if the fiber is stopped or has never run. */ public synchronized void start() { if (m_worker != null) { throw new IllegalStateException("The fiber has already be run"); } m_status = STARTING; m_reaperRun = new Reaper(); m_reaper = new Thread(m_reaperRun, getName() + "-Reaper"); m_reaper.setDaemon(true); m_reaper.start(); m_worker = new Thread(this, getName()); m_worker.start(); } /** * Stops a currently running fiber. If the fiber has already been stopped * then the command is silently ignored. If the fiber was never started then * an exception is generated. * * @throws java.lang.IllegalStateException * Thrown if the fiber was never started. */ public synchronized void stop() { if (m_worker == null) { throw new IllegalStateException("The fiber has never been run"); } if (m_status != STOPPED) { m_status = STOP_PENDING; } if (m_reaper.isAlive()) { m_reaper.interrupt(); } if (m_worker.isAlive()) { m_worker.interrupt(); } notifyAll(); } /** * Pauses a currently running fiber. If the fiber was not in a running or * resuming state then the command is silently discarded. If the fiber is * not running or has terminated then an exception is generated. * * @throws java.lang.IllegalStateException * Thrown if the fiber is stopped or has never run. */ public synchronized void pause() { if (m_worker == null || !m_worker.isAlive()) { throw new IllegalStateException("The fiber is not running"); } if (m_status == RUNNING || m_status == RESUME_PENDING) { m_status = PAUSE_PENDING; notifyAll(); } } /** * Resumes the fiber if it is paused. If the fiber was not in a paused or * pause pending state then the request is discarded. If the fiber has not * been started or has already stopped then an exception is generated. * * @throws java.lang.IllegalStateException * Thrown if the fiber is stopped or has never run. */ public synchronized void resume() { if (m_worker == null || !m_worker.isAlive()) { throw new IllegalStateException("The fiber is not running"); } if (m_status == PAUSED || m_status == PAUSE_PENDING) { m_status = RESUME_PENDING; notifyAll(); } } /** * Returns the name of this fiber. * * @return The name of the fiber. */ public String getName() { return m_name; } /** * Returns the current status of the pausable fiber. * * @return The current status of the fiber. * @see org.opennms.core.fiber.PausableFiber * @see org.opennms.core.fiber.Fiber */ public synchronized int getStatus() { if (m_worker != null && !m_worker.isAlive()) { if (m_reaper.isAlive()) m_reaper.interrupt(); m_status = STOPPED; } return m_status; } }