sce.ProcessExecutor.java Source code

Java tutorial

Introduction

Here is the source code for sce.ProcessExecutor.java

Source

/* Smart Cloud/City Engine backend (SCE).
   Copyright (C) 2015 DISIT Lab http://www.disit.org - University of Florence
    
   This program 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 2
   of the License, or (at your option) any later version.
   This program 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 this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */

package sce;

import java.net.*;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Date;
import java.util.Map;
import org.quartz.Job;
import org.quartz.InterruptableJob;
import org.quartz.UnableToInterruptJobException;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.SchedulerException;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import java.util.Iterator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.quartz.SchedulerMetaData;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
//import org.apache.catalina.tribes.util.Arrays;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecuteResultHandler;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.exec.ShutdownHookProcessDestroyer;
import org.apache.commons.exec.environment.EnvironmentUtils;

/*@DisallowConcurrentExecution
 * An annotation that marks a Job
 * class as one that must not have
 * multiple instances executed concurrently
 * (where instance is based-upon a JobDetail 
 * definition - or in other words based upon a JobKey). 
 */

/*@PersistJobDataAfterExecution
 * An annotation that marks a Job class as one that makes
 * updates to its JobDataMap during execution, and wishes the
 * scheduler to re-store the JobDataMap when execution completes.
 * Jobs that are marked with this annotation should also seriously
 * consider using the DisallowConcurrentExecution annotation, to avoid
 * data storage race conditions with concurrently executing job instances.
 * If you use the @PersistJobDataAfterExecution annotation, you should strongly
 * consider also using the @DisallowConcurrentExecution annotation, in order to
 * avoid possible confusion (race conditions) of what data was left stored when two
 * instances of the same job (JobDetail) executed concurrently.
 */
public class ProcessExecutor implements Job, InterruptableJob {

    ExecuteWatchdog watchdog;
    long timeout;
    private String environment;
    private String result;

    public ProcessExecutor() {
    }

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        try {
            JobKey key = context.getJobDetail().getKey();

            JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();

            //set job execution timeout in seconds
            if (jobDataMap.containsKey("#jobTimeout")) {
                this.timeout = Long.parseLong(jobDataMap.getString("#jobTimeout"));
            } //default job execution timeout in seconds (0 means to wait forever)
            else {
                this.timeout = 0;
            }

            //set java environment variables
            if (jobDataMap.containsKey("#environment")) {
                this.environment = jobDataMap.getString("#environment");
            } else {
                this.environment = "";
            }

            //json has the form of an array, whose elements are associative arrays (i.e., 0->("processPath"->"/var/www/html/processBinary"), 1->("parameter1"->"value1")...)
            if (jobDataMap.containsKey("#processParameters")) {
                //read json from request
                JSONParser parser = new JSONParser();
                JSONArray jsonarray = (JSONArray) parser.parse(jobDataMap.getString("#processParameters"));
                int size = jsonarray.size();
                String[] processParameters = null;
                boolean script = false; //set to true if the process is a sh script

                for (int i = 0; i < size; i++) {
                    JSONObject jsonobject = (JSONObject) jsonarray.get(i);
                    Iterator<?> keys = jsonobject.keySet().iterator();
                    while (keys.hasNext()) {
                        String k = (String) keys.next();
                        //this condition is true only for the first element of the jsonobject (i.e., processParameters is istantiated once)
                        if (k.equals("processPath")) {
                            String processPath = (String) jsonobject.get("processPath");
                            if (processPath.endsWith(".sh")) {
                                script = true;
                                processParameters = new String[size + 1];
                                processParameters[i] = "/bin/sh";
                            } else {
                                processParameters = new String[size];
                            }
                        }
                        if (script) {
                            processParameters[i + 1] = (String) jsonobject.get(k);
                        } else {
                            processParameters[i] = (String) jsonobject.get(k);
                        }
                    }
                }

                String r = executeProcess(processParameters);

                //set the result to the job execution context, to be able to retrieve it later (e.g., with a job listener)
                context.setResult(r);

                //if notificationEmail is defined in the job data map, then send a notification email to it
                if (jobDataMap.containsKey("#notificationEmail")) {
                    sendEmail(context, jobDataMap.getString("#notificationEmail"));
                }

                //trigger the linked jobs of the finished job, depending on the job result [true, false]
                jobChain(context);

                //System.out.println("Instance " + key + " of REST Job returns: " + truncateResult(r));
            } else {
                //System.out.println("Instance " + key + " of ProcessExecutor Job returns: process not found");
            }
        } catch (NumberFormatException | ParseException | JobExecutionException e) {
            throw new JobExecutionException(e.getMessage(), e);
        }
    }

    @Override
    public void interrupt() throws UnableToInterruptJobException {
        try {
            if (this.watchdog != null) {
                this.watchdog.destroyProcess();
            }
        } catch (IllegalThreadStateException e) {
            throw new UnableToInterruptJobException(e);
        }
    }

    // use apache commons 1.3
    // https://commons.apache.org/proper/commons-exec/tutorial.html
    public String executeProcess(String[] processParameters) throws JobExecutionException {
        try {
            //Command to be executed
            CommandLine command = new CommandLine(processParameters[0]);

            String[] params = new String[processParameters.length - 1];
            for (int i = 0; i < processParameters.length - 1; i++) {
                params[i] = processParameters[i + 1];
            }

            //Adding its arguments
            command.addArguments(params);

            //set timeout in seconds
            ExecuteWatchdog watchDog = new ExecuteWatchdog(
                    this.timeout == 0 ? ExecuteWatchdog.INFINITE_TIMEOUT : this.timeout * 1000);
            this.watchdog = watchDog;

            //Result Handler for executing the process in a Asynch way
            DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
            //MyResultHandler resultHandler = new MyResultHandler();

            //Using Std out for the output/error stream
            //ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            //PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream);
            //This is used to end the process when the JVM exits
            ShutdownHookProcessDestroyer processDestroyer = new ShutdownHookProcessDestroyer();

            //Our main command executor
            DefaultExecutor executor = new DefaultExecutor();

            //Setting the properties
            executor.setStreamHandler(new PumpStreamHandler(null, null));
            executor.setWatchdog(watchDog);
            //executor.setExitValue(1); // this has to be set if the java code contains System.exit(1) to avoid a FAILED status

            //Setting the working directory
            //Use of recursion along with the ls makes this a long running process
            //executor.setWorkingDirectory(new File("/home"));
            executor.setProcessDestroyer(processDestroyer);

            //if set, use the java environment variables when running the command
            if (!this.environment.equals("")) {
                Map<String, String> procEnv = EnvironmentUtils.getProcEnvironment();
                EnvironmentUtils.addVariableToEnvironment(procEnv, this.environment);
                //Executing the command
                executor.execute(command, procEnv, resultHandler);
            } else {
                //Executing the command
                executor.execute(command, resultHandler);
            }

            //The below section depends on your need
            //Anything after this will be executed only when the command completes the execution
            resultHandler.waitFor();

            /*int exitValue = resultHandler.getExitValue();
             System.out.println(exitValue);
             if (executor.isFailure(exitValue)) {
             System.out.println("Execution failed");
             } else {
             System.out.println("Execution Successful");
             }
             System.out.println(outputStream.toString());*/
            //return outputStream.toString();
            if (watchdog.killedProcess()) {
                throw new JobExecutionException("Job Interrupted", new InterruptedException());
            }
            if (executor.isFailure(resultHandler.getExitValue())) {
                ExecuteException ex = resultHandler.getException();
                throw new JobExecutionException(ex.getMessage(), ex);
            }
            return "1";
        } catch (ExecuteException ex) {
            throw new JobExecutionException(ex.getMessage(), ex);
        } catch (IOException | InterruptedException | JobExecutionException ex) {
            throw new JobExecutionException(ex.getMessage(), ex);
        }
    }
    //this method does not implement a timeout for the process execution and does not require Java 8
    /*public String executeProcess(String[] processParameters) throws JobExecutionException {
     //String[] params = new String[3];
     //params[0] = "D:\\prog.exe";
     //params[1] = picA + ".jpg";
     //params[2] = picB + ".jpg";
     try {
     final Process p = new ProcessBuilder(processParameters).start();
     ThreadCustom thread = new ThreadCustom() {
     @Override
     public void run() {
     String line;
     String r = "";
     try {
     try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
     while ((line = input.readLine()) != null) {
     //System.out.println(line);
     r += line;
     }
     }
     setResult(r); //set the result
     } catch (IOException e) {
     setResult("Process not read: " + e);
     setException(e); //set the exception to be thrown below
     }
     }
     };
     //set the new process
     this.process = p;
        
     thread.start();
     int res = p.waitFor();
     thread.join();
     if (res != 0) {
     setResult("Process failed with status: " + res);
        
     int len;
     if ((len = p.getErrorStream().available()) > 0) {
     byte[] buf = new byte[len];
     p.getErrorStream().read(buf);
     System.err.println("Command error:\t\"" + new String(buf) + "\"");
     }
     }
     //return "Process started with status: " + result;
     if (thread.getException() != null) {
     throw new JobExecutionException(thread.getException());
     }
     return getResult();
     } catch (IOException | InterruptedException e) {
     //e.printStackTrace();
     //return e.getMessage();
     throw new JobExecutionException(e);
     }
     }*/
    //this method (with the class CallableProcess) implements a timeout for the process execution and does not require Java 8
    //http://bryox.blogspot.it/2011/12/execute-command-line-process-from-java.html
    /*public String executeProcess(String[] processParameters) throws JobExecutionException {
     ExecutorService service = Executors.newSingleThreadExecutor();
     ThreadCustom thread = null;
     try {
     final Process p = new ProcessBuilder(processParameters).start();
     thread = new ThreadCustom() {
     @Override
     public void run() {
     String line;
     String r = "";
     try {
     try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
     while ((line = input.readLine()) != null) {
     r += line;
     setResult(r);
     }
     }
     //setResult(r); //set the result
     } catch (IOException e) {
     setResult("Process not read: " + e);
     setException(e); //set the exception to be thrown below
     }
     }
     };
     //set the new process
     this.process = p;
        
     thread.start();
     //thread.wait();
     thread.join(this.timeout * 1000L); //a timeout of 0 means to wait forever 
        
     Callable<Integer> call = new CallableProcess(this.process);
     Future<Integer> future = service.submit(call);
     int exitValue = future.get(this.timeout, TimeUnit.SECONDS);
     if (exitValue != 0) {
     thread.interrupt();
     throw new JobExecutionException("Process did not exit correctly");
     }
     } catch (ExecutionException e) {
     if (thread != null && thread.isAlive()) {
     thread.interrupt();
     }
     throw new JobExecutionException("Process failed to execute", e);
     } catch (TimeoutException e) {
     if (this.process != null) {
     this.process.destroy();
     }
     if (thread != null && thread.isAlive()) {
     thread.interrupt();
     }
     throw new JobExecutionException("Process timed out", e);
     } catch (IOException | InterruptedException e) {
     if (thread != null && thread.isAlive()) {
     thread.interrupt();
     }
     throw new JobExecutionException("Process interrupted", e);
     } finally {
     service.shutdown();
     }
     if (thread != null && thread.isAlive()) {
     thread.interrupt();
     }
     //System.out.println("Thread status: " + (thread != null ? thread.getState().toString() : ""));
     return getResult();
     }
        
     private static class CallableProcess implements Callable {
        
     private final Process p;
        
     public CallableProcess(Process process) {
     p = process;
     }
        
     @Override
     public Integer call() throws Exception {
     return p.waitFor();
     }
     }*/

    //send a notification email upon job completion
    public void sendEmail(JobExecutionContext context, String email) throws JobExecutionException {
        try {
            Date d = new Date();
            String message = "Job was executed at: " + d.toString() + "\n";
            SchedulerMetaData schedulerMetadata = context.getScheduler().getMetaData();
            //Get the scheduler instance id
            message += "Scheduler Instance Id: " + schedulerMetadata.getSchedulerInstanceId() + "\n";
            //Get the scheduler name
            message += "Scheduler Name: " + schedulerMetadata.getSchedulerName() + "\n";
            try {
                //Get the scheduler ip
                Enumeration<NetworkInterface> n = NetworkInterface.getNetworkInterfaces();
                message += "Scheduler IP: ";
                for (; n.hasMoreElements();) {
                    NetworkInterface e = n.nextElement();
                    //System.out.println("Interface: " + e.getName());
                    Enumeration<InetAddress> a = e.getInetAddresses();
                    for (; a.hasMoreElements();) {
                        InetAddress addr = a.nextElement();
                        message += !addr.getHostAddress().equals("127.0.0.1") ? addr.getHostAddress() + " " : "";
                    }
                }
                message += "\n";
            } catch (SocketException e) {
                throw new JobExecutionException(e);
            }
            //Returns the result (if any) that the Job set before its execution completed (the type of object set as the result is entirely up to the particular job).
            //The result itself is meaningless to Quartz, but may be informative to JobListeners or TriggerListeners that are watching the job's execution. 
            message += "Result: "
                    + (context.getResult() != null ? truncateResult((String) context.getResult()) : "") + "\n";
            //Get the unique Id that identifies this particular firing instance of the trigger that triggered this job execution. It is unique to this JobExecutionContext instance as well. 
            message += "Fire Instance Id: "
                    + (context.getFireInstanceId() != null ? context.getFireInstanceId() : "") + "\n";
            //Get a handle to the Calendar referenced by the Trigger instance that fired the Job. 
            message += "Calendar: " + (context.getCalendar() != null ? context.getCalendar().getDescription() : "")
                    + "\n";
            //The actual time the trigger fired. For instance the scheduled time may have been 10:00:00 but the actual fire time may have been 10:00:03 if the scheduler was too busy. 
            message += "Fire Time: " + (context.getFireTime() != null ? context.getFireTime() : "") + "\n";
            //the job name
            message += "Job Name: "
                    + (context.getJobDetail().getKey() != null ? context.getJobDetail().getKey().getName() : "")
                    + "\n";
            //the job group
            message += "Job Group: "
                    + (context.getJobDetail().getKey() != null ? context.getJobDetail().getKey().getGroup() : "")
                    + "\n";
            //The amount of time the job ran for (in milliseconds). The returned value will be -1 until the job has actually completed (or thrown an exception), and is therefore generally only useful to JobListeners and TriggerListeners.
            //message += "RunTime: " + context.getJobRunTime() + "\n";
            //the next fire time
            message += "Next Fire Time: "
                    + (context.getNextFireTime() != null && context.getNextFireTime().toString() != null
                            ? context.getNextFireTime().toString()
                            : "")
                    + "\n";
            //the previous fire time
            message += "Previous Fire Time: "
                    + (context.getPreviousFireTime() != null && context.getPreviousFireTime().toString() != null
                            ? context.getPreviousFireTime().toString()
                            : "")
                    + "\n";
            //refire count
            message += "Refire Count: " + context.getRefireCount() + "\n";

            //job data map
            message += "\nJob data map: \n";
            JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
            for (Map.Entry<String, Object> entry : jobDataMap.entrySet()) {
                message += entry.getKey() + " = " + entry.getValue() + "\n";
            }
            Mail.sendMail("SCE notification", message, email, null, null);
        } catch (SchedulerException e) {
            throw new JobExecutionException(e);
        }
    }

    //trigger the linked jobs of the finished job, depending on the job result [true, false]
    public void jobChain(JobExecutionContext context) throws JobExecutionException {
        try {
            //get the job data map
            JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
            //get the finished job result (true/false)
            String resultjob = context.getResult() != null ? (String) context.getResult() : null;

            if (jobDataMap.containsKey("#nextJobs") && resultjob != null) {
                //read json from request
                JSONParser parser = new JSONParser();
                Object obj = parser.parse(jobDataMap.getString("#nextJobs"));
                JSONArray jsonArray = (JSONArray) obj;
                for (int i = 0; i < jsonArray.size(); i++) {
                    JSONObject jsonobject = (JSONObject) jsonArray.get(i);
                    String operator = (String) jsonobject.get("operator");
                    String res = (String) jsonobject.get("result");
                    String nextJobName = (String) jsonobject.get("jobName");
                    String nextJobGroup = (String) jsonobject.get("jobGroup");

                    //if condition is satisfied, trigger the new job it does exist in the scheduler
                    if ((operator.equals("==") && !isNumeric(resultjob) && !isNumeric(res)
                            && res.equalsIgnoreCase(resultjob))
                            || (operator.equals("==") && isNumeric(resultjob) && isNumeric(res)
                                    && Double.parseDouble(resultjob) == Double.parseDouble(res))
                            || (operator.equals("!=") && !isNumeric(resultjob) && !isNumeric(res)
                                    && !res.equalsIgnoreCase(resultjob))
                            || (operator.equals("!=") && isNumeric(resultjob) && isNumeric(res)
                                    && Double.parseDouble(resultjob) != Double.parseDouble(res))
                            || (operator.equals("<") && isNumeric(resultjob) && isNumeric(res)
                                    && Double.parseDouble(resultjob) < Double.parseDouble(res))
                            || (operator.equals(">") && isNumeric(resultjob) && isNumeric(res)
                                    && Double.parseDouble(resultjob) > Double.parseDouble(res))
                            || (operator.equals("<=") && isNumeric(resultjob) && isNumeric(res)
                                    && Double.parseDouble(resultjob) <= Double.parseDouble(res))
                            || (operator.equals(">=") && isNumeric(resultjob) && isNumeric(res)
                                    && Double.parseDouble(resultjob) >= Double.parseDouble(res))) {
                        //if nextJobName contains email(s), then send email(s) with obtained result, instead of triggering jobs
                        if (nextJobName.contains("@") && nextJobGroup.equals(" ")) {
                            sendEmail(context, nextJobName);
                        } //else trigger next job
                        else if (context.getScheduler().checkExists(JobKey.jobKey(nextJobName, nextJobGroup))) {
                            context.getScheduler().triggerJob(JobKey.jobKey(nextJobName, nextJobGroup));
                        }
                    }
                }
            }
        } catch (SchedulerException | ParseException e) {
            throw new JobExecutionException(e);
        }
    }

    //test if a string is numeric
    public boolean isNumeric(String str) {
        try {
            Double.parseDouble(str);
        } catch (NumberFormatException e) {
            return false;
        }
        return true;
    }

    public String getResult() {
        return this.result;
    }

    public void setResult(String result) {
        this.result = result;
    }

    //return a truncated result if too long for displaying
    public String truncateResult(String result) {
        if (result != null) {
            return result.length() > 30 ? result.substring(0, 29) + "..." : result;
        }
        return result;
    }

    /**
     * File Permissions using File and PosixFilePermission
     *
     * @throws IOException
     */
    public void setFilePermissions() throws IOException {
        File file = new File("/Users/temp.txt");

        //set application user permissions to 455
        file.setExecutable(false);
        file.setReadable(false);
        file.setWritable(true);

        //change permission to 777 for all the users
        //no option for group and others
        file.setExecutable(true, false);
        file.setReadable(true, false);
        file.setWritable(true, false);

        //using PosixFilePermission to set file permissions 777
        Set<PosixFilePermission> perms = new HashSet<>();
        //add owners permission
        perms.add(PosixFilePermission.OWNER_READ);
        perms.add(PosixFilePermission.OWNER_WRITE);
        perms.add(PosixFilePermission.OWNER_EXECUTE);
        //add group permissions
        perms.add(PosixFilePermission.GROUP_READ);
        perms.add(PosixFilePermission.GROUP_WRITE);
        perms.add(PosixFilePermission.GROUP_EXECUTE);
        //add others permissions
        perms.add(PosixFilePermission.OTHERS_READ);
        perms.add(PosixFilePermission.OTHERS_WRITE);
        perms.add(PosixFilePermission.OTHERS_EXECUTE);

        Files.setPosixFilePermissions(Paths.get("/Users/pankaj/run.sh"), perms);
    }
}