diff --git a/src/main/java/JobServ/ProcessController.java b/src/main/java/JobServ/ProcessController.java index d1d5383..bceb64c 100644 --- a/src/main/java/JobServ/ProcessController.java +++ b/src/main/java/JobServ/ProcessController.java @@ -43,6 +43,14 @@ class ProcessController { this.outputScanner.useDelimieter("\\A"); } + /* + * getPid() + * returns translated pid of this process + */ + public int getPid() { + return this.pid; + } + /* * getStatus() * returns whether or not the process is running @@ -77,8 +85,7 @@ class ProcessController { /* * getOutput() - * gets new output from stream - * (TODO: investigate whether this would better be done by ) + * gets output from process */ public String getOutput() { String out = ""; diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java new file mode 100644 index 0000000..4acb02e --- /dev/null +++ b/src/main/java/JobServ/ProcessManager.java @@ -0,0 +1,278 @@ +/* + * ProcessManager + * + * v1.0 + * + * May 22, 2019 + */ + +package JobServ; + +import java.util.concurrent.Future; + +/* + * Holds a list of ProcessControllers and controls access to them via mutex + * Additionally, starts and manages a background thread that clears finished processes from the arraylist + */ +class ProcessManager { + // TODO: LOCK_TIMEOUT should be defined in a configuration management system + private final int LOCK_TIMEOUT = 5; // seconds + private ArrayList processQueue; + private Boolean processQueueMutex = false; + private Thread backgroundProcessCleaner; + private ExecutorService threadPool = Executors.newCachedThreadPool(); + + private Callable getLockCallable = new Callable() { + public void Object call() { + while(processQueueMutex){ + continue; // spin! + } + + processQueueMutex = true; + } + } + + /* + * Constructor + * initializes process queue and start the background process checking daemon + */ + public ProcessManager() { + processQueue = new ArrayList(); + // TODO: In a long running server over a large period of time + // It is possible that the streams used to redirect IO in the + // Processes may become a significant use of resources. + // In this case a background thread should be called to periodically + // remove dead ProcessControllers after calling kill() on them. + } + + /* + * newProcess() + * Takes a command and returns the translated pid of a new process + * Returns -1 if getLock fails + */ + public int newProcess(String command) { + /* + * TRADEOFF: Could initialize new ProcessController out here + * Pro: would minimize time spent in critical section + * Con: what if initialization goes through but we dont get the lock + * we would essentially have a dangling untrackable process + * which likely changed system state before it was killed. + */ + + // Enter critical section + try { + this.getLock(); + + } catch (TimeoutException e) { + // (lock was not grabbed) + System.err.println("Timeout starting new job '%s': " + e.getMessage, command); + return -1 + } + + ProcessController newProc = ProcessController(command); + this.processQueue.add(newProc); + + // Exit critical section + this.releaseLock(); + + return newProc.getPid(); + } + + /* + * getProcessStatus() + * returns whether or not a process is running. + * 0: running + * 1: not running + * 2: doesnt exist + * 3: couldnt grab lock + */ + public int getProcessStatus(int pid) { + // Enter critical section + try { + this.getLock(); + + } catch (TimeoutException e) { + // lock could not be grabbed before timeout + System.err.println("Timeout getting process status for %s: " + e.getMessage(), + Integer.toString(pid)); + return 3; + } + + for (ProcessController iter : this.processQueue) { + if (iter.getPid() == pid) { + this.releaseLock(); + // release lock on finding process + return iter.getStatus(); + } + } + + // process must not exist + this.releaseLock(); + return 2; + } + + /* + * getProcessReturn() + * returns a code 0-255, or 256 if process still running + * additionally, returns 257 if lock not grabbable AND + * a 258 if process doesnt exist. + */ + public int getProcessReturn(int pid) { + // Enter Critical section + try { + this.getLock(); + + } catch (TimeoutException e) { + System.err.println("Timeout getting process return for %s: " + e.getMessage(), + Integer.toString(pid)); + return 257; + } + + for (ProcessController iter : this.processQueue) { + if (iter.getPid() == pid) { + this.releaseLock(); + return iter.getReturn(); + } + } + + this.releaseLock(); + return 258; + } + + /* + * getProcessOutput() + * returns output of process 'pid' + * or returns description of error + */ + public String getProcessOutput(int pid) { + try { + this.getLock(); + + } catch (TimeoutException e) { + System.err.println("Timeout getting process output for %s: " + e.getMessage(), + Integer.toString()); + return "[-] ERROR: Timeout grabbing lock to access process information"; + } + + for (ProcessController iter : this.processQueue) { + if (iter.getPid() == pid) { + output = iter.getOutput(); + this.releaseLock(); + return output; + } + } + + this.releaseLock(); + return "[-] ERROR: Process not found" + } + + /* + * killProcess() + * returns false if couldnt grab lock + * ALSO RETURNS TRUE IF PROCESS DOESNT EXIST + */ + public Boolean killProcess(int pid) { + try { + this.getLock(); + + } catch (TimeoutException e) { + System.err.println("Timeout killing process: " + e.getMessage); + return false; + } + + for (ProcessController iter : this.processQueue) { + if (iter.getPid() == pid) { + iter.kill(); + break; + } + } + + this.releaseLock(); + return true; + } + + /* + * cleanProcessQueue() + * represents a background thread that sits and cleans finished processes + */ + private void cleanProcessQueue() { + while(true){ + try { + this.getLock(); + + } catch (TimeoutException e) { + continue; + } + + for (ProcessController iter : this.processQueue) { + if(!iter.getStatus()) { + iter.kill(); + this.processQueue.remove(iter); + } + } + + this.releaseLock(); + Thread.sleep(5000); + } + } + + /* + * getLock() + * Locks access to this.processQueue + * Waits for a predefined timeout period and then grabs the mutex + * Throws TimeoutException when it fails to get the lock. + */ + private synchronized void getLock() throws TimeoutException { + try { + Future future = executor.submit(task); + void result = future.get(this.LOCK_TIMEOUT, TimeUnit.SECONDS); + + } catch (InterruptedException e) { + System.err.println("[!] ERROR: " + e.getMessage()); + throw new TimeoutException(); + // rethrowing a timeout exception tells the calling process that they dont have the lock + + } catch (ExecutionException e) { + System.err.println("[!] ERROR: " + e.getMessage()); + throw new TimeoutException(); + + // cancel the attempt to grab the lock + } finally { + future.cancel(true); + } + + /* + * TODO: touch of tech debt here + * There should honestly be an + * operation retry queue for ops + * That dont get the lock in time. + * + * This would require a scheduler + * that manages a queue of callbacks + * This scheduler would also likely + * mediate access to the ProcessManager + * object for fresh calls as well. + */ + } + + /* + * releaseLock() + * releases mutex so other threads can operate on processqueue + */ + private void releaseLock() { + this.processQueueMutex = false; + } + + /* + * shutdown() + * called (eventually) by the grpc shutdown hook + * (AKA when user hits control c in the shell) + * releases resources held in the processController objects + */ + private void shutdown() { + this.getLock(); + for (ProcessController p : this.processQueue) { + p.kill(); // exit threads, release IO streams, etc. + } + } +}