cz.cuni.amis.planning4j.external.impl.itsimple.ItSimplePlanningProcess.java Source code

Java tutorial

Introduction

Here is the source code for cz.cuni.amis.planning4j.external.impl.itsimple.ItSimplePlanningProcess.java

Source

/*** 
 * This file is part of Java API for AI planning
 * 
 * This file is based on code from project itSIMPLE.
 *
 * Java API for AI planning 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. Other licenses might be available
 * upon written agreement.
 * 
 * Java API for AI planning 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 Java API for AI planning.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Authors:   Tiago S. Vaquero, 
 *          Victor Romero, Martin Cerny.
 **/
package cz.cuni.amis.planning4j.external.impl.itsimple;

import com.jezhumble.javasysmon.JavaSysMon;
import com.jezhumble.javasysmon.OsProcess;
import com.jezhumble.javasysmon.ProcessVisitor;
import cz.cuni.amis.planning4j.PlanningException;
import cz.cuni.amis.planning4j.ActionDescription;
import cz.cuni.amis.planning4j.external.impl.ExternalPlanningResult;
import cz.cuni.amis.planning4j.external.IExternalPlannerExecutor;
import cz.cuni.amis.planning4j.external.IExternalPlanningResult;
import cz.cuni.amis.planning4j.PlanningStatistics;
import cz.cuni.amis.planning4j.external.IExternalPlanningProcess;
import cz.cuni.amis.planning4j.utils.Planning4JUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import java.util.StringTokenizer;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.log4j.Logger;

/**
 * Executes a plan using external planners specified with ItSimple XML format.
 * Instance of this class should not be reused for multiplre runs.
 * Identifiers of the returned plan are treated with {@link Planning4JUtils#normalizeIdentifier(java.lang.String) }
 * @author Martin Cerny
 */
public class ItSimplePlanningProcess implements IExternalPlanningProcess {

    private static final Logger logger = Logger.getLogger(ItSimplePlanningProcess.class);
    /**
     * Timeout to wait for planning process to spawn to get it's pid
     */
    public static final int PROCESS_SPAWN_TIMEOUT = 50;
    protected File plannerExecutableFile;
    private ItSimplePlannerInformation chosenPlanner;
    private File plannerBinariesDirectory;
    private File workingDirectory;
    File domainFile;
    File problemFile;
    long timeInIO;
    Process process = null;
    private boolean cancelled = false;
    private int processPid = -1;
    /**
     * Mutex that forces only one child process to be spawned at a time. This let's us 
     * determine the PID of the process.
     */
    private static final Object spawnProcessMutex = new Object();

    public ItSimplePlanningProcess(ItSimplePlannerInformation chosenPlanner, File plannerBinariesDirectory,
            File workingDirectory, File domainFile, File problemFile, long timeInIO) {
        this.chosenPlanner = chosenPlanner;
        this.plannerBinariesDirectory = plannerBinariesDirectory;
        this.workingDirectory = workingDirectory;

        this.domainFile = domainFile;
        this.problemFile = problemFile;
        this.timeInIO = timeInIO;
    }

    /**
     * Spawns a new planner process and sets {@link #processPid} to it's PID. To work correctly,
     * the code relies on the fact that no other method in this JVM runs processes with names
     * that would be correspond to our planners and
     * that no method kills a process unless it acquires lock on spawnProcessMutex.
     * @param procBuilder
     * @param identifying process name / command line fragment
     * @return 
     */
    private Process spawnPlanner(ProcessBuilder procBuilder, String processCommandFragment) throws IOException {
        synchronized (spawnProcessMutex) {
            JavaSysMon monitor = new JavaSysMon();
            DirectPlannerChildVisitor beforeVisitor = new DirectPlannerChildVisitor(processCommandFragment);
            monitor.visitProcessTree(monitor.currentPid(), beforeVisitor);
            Set<Integer> alreadySpawnedProcesses = beforeVisitor.getPids();

            Process proc = procBuilder.start();

            DirectPlannerChildVisitor afterVisitor = new DirectPlannerChildVisitor(processCommandFragment);

            Set<Integer> newProcesses = afterVisitor.getPids();
            long startTime = System.currentTimeMillis();
            while (newProcesses.isEmpty() && System.currentTimeMillis() - startTime < PROCESS_SPAWN_TIMEOUT) {
                monitor.visitProcessTree(monitor.currentPid(), afterVisitor);
                newProcesses = afterVisitor.getPids();

                newProcesses.removeAll(alreadySpawnedProcesses);
            }

            if (newProcesses.isEmpty()) {
                logger.warn("There is no new planner PID.");
            } else if (newProcesses.size() > 1) {
                logger.warn("Multiple new candidate planner PIDs");
            } else {
                processPid = newProcesses.iterator().next();
            }
            logger.info("Spawned planning process. Pid:" + processPid);
            return proc;
        }
    }

    private void killPlannerByPID() {
        if (processPid < 0) {
            logger.warn("Cannot kill planner by PID. PID not set.");
            return;
        }
        synchronized (spawnProcessMutex) {
            logger.info("Killing planner pid:" + processPid);
            JavaSysMon monitor = new JavaSysMon();
            monitor.killProcessTree(processPid, false);
        }
    }

    private static class DirectPlannerChildVisitor implements ProcessVisitor {

        Set<Integer> pids = new HashSet<Integer>();
        String processNameFragment;

        public DirectPlannerChildVisitor(String processNameFragment) {
            //normalize the fragment, if it contains separators
            this.processNameFragment = processNameFragment.replace('\\', File.separatorChar).replace('/',
                    File.separatorChar);
        }

        @Override
        public boolean visit(OsProcess op, int i) {
            if (op.processInfo().getCommand().contains(processNameFragment)
                    || op.processInfo().getName().equals(processNameFragment)) {
                pids.add(op.processInfo().getPid());
            }
            return false;
        }

        public Set<Integer> getPids() {
            return pids;
        }
    }

    /**
     * Guess whether line might be an action
     * @param line
     * @return 
     */
    protected boolean isLineAction(String line) {
        if (isLineStatistics(line)) {
            //it is surely a statistic
            return false;
        }

        if (line.contains(":")) {
            return true;
        }

        if (line.contains("(") && line.contains(")")) {
            return true;
        }

        return false;
    }

    /**
     * Guess whether line might be a statistic
     * @param line
     * @return 
     */
    protected boolean isLineStatistics(String line) {
        return line.trim().startsWith(";");
    }

    private static enum EConsoleParseState {

        BEGIN, COUNTING_TO_PLAN_START, READING_PLAN, END
    }

    /**
     * Runs the planner and returns the console output
     */
    protected UnprocessedPlanningResult runPlanner(File domain, File problem) {

        //1.Get main planner's parameters and arguments
        ItSimplePlannerSettings settings = chosenPlanner.getSettings();

        String plannerRelativeFile = settings.getExecutableFilePath();
        plannerExecutableFile = new File(plannerBinariesDirectory, plannerRelativeFile);

        if (!plannerExecutableFile.exists()) {
            String toolMessage = "Could not find selected planner '" + plannerRelativeFile + "' in directory "
                    + plannerBinariesDirectory.getAbsolutePath();
            throw new PlanningException(toolMessage);
        }

        List<String> commandArguments = new ArrayList<String>();

        //1.0 Get planner execution file
        commandArguments.add(plannerExecutableFile.getAbsolutePath());

        //1.1 Get domain arguments
        if (!settings.getDomainArgumentName().trim().isEmpty()) {
            commandArguments.add(settings.getDomainArgumentName());
        }
        commandArguments.add(domain.getAbsolutePath()); //domain path

        //1.2 Get problem arguments
        if (!settings.getProblemArgumentName().trim().isEmpty()) {
            commandArguments.add(settings.getProblemArgumentName());
        }
        commandArguments.add(problem.getAbsolutePath()); //problem path

        //1.3 Get additional arguments
        for (PlannerArgument argument : settings.getAdditionalArguments()) {
            //System.out.println(argument.getChildText("name"));
            if (!argument.getName().trim().equals("")) {
                commandArguments.add(argument.getName());
            }
            //if there is a value for the argument then add to the command
            if (!argument.getValue().trim().equals("")) {
                commandArguments.add(argument.getValue());
            }

        }

        //1.4 Get output arguments
        if (settings.isHasOutputFile() && settings.isOutputFileNeedsArgument()) {
            commandArguments.add(settings.getOutputFileArgumentName());
            commandArguments.add(settings.getOutputFile()); //problem path            
        }

        synchronized (this) {
            if (cancelled) {
                return null;
            }
            logger.info("\n>> Calling planner " + chosenPlanner.getName() + " in directory: "
                    + workingDirectory.getAbsolutePath());
            logger.debug("Planner arguments:" + commandArguments);
            //Call the planner
            try {
                ProcessBuilder builder = new ProcessBuilder(commandArguments);
                builder.directory(workingDirectory);
                process = spawnPlanner(builder, plannerRelativeFile);
            } catch (Exception e) {
                String message = "Error while running the planner " + chosenPlanner.getName() + ". ";
                throw new PlanningException(message, e);
            }
        }

        try {
            boolean plannerFoundNoSolution = false;

            Scanner sc = new Scanner(process.getInputStream());
            //Get the planner answer exposed in the console
            if (logger.isDebugEnabled()) {
                logger.debug("Planner console output:");
            }

            StringBuilder consoleOutputBuilder = new StringBuilder();
            List<String> unprocessedPlan = new ArrayList<String>();
            List<String> unprocessedStatistics = new ArrayList<String>();

            //Needed only when parsing plan from console, but need to be initialized here
            EConsoleParseState consoleParseState;
            int numLinesBeforePlan = settings.getConsoleOutputStartsAfterNLines();
            if (settings.getConsoleOutputPlanStartIdentifier() == null
                    || settings.getConsoleOutputPlanStartIdentifier().isEmpty()) {
                consoleParseState = EConsoleParseState.COUNTING_TO_PLAN_START;
            } else {
                consoleParseState = EConsoleParseState.BEGIN;
            }

            while (sc.hasNextLine()) {

                String line = sc.nextLine();
                consoleOutputBuilder.append(line).append("\n");

                for (INoPlanFoundChecker checker : settings.getNoPlanFoundCheckers()) {
                    if (checker.processOutputLine(line)) {
                        plannerFoundNoSolution = true;
                    }
                }

                if (logger.isDebugEnabled()) {
                    logger.debug(line);
                }

                if (!settings.isHasOutputFile()) {
                    if (!line.trim().isEmpty()) {
                        if (logger.isDebugEnabled()) {
                            logger.debug(consoleParseState + ":" + line);
                        }
                        //the plan is part of console output
                        switch (consoleParseState) {
                        case BEGIN: {
                            if (line.contains(settings.getConsoleOutputPlanStartIdentifier())) {
                                if (numLinesBeforePlan > 0) {
                                    consoleParseState = EConsoleParseState.COUNTING_TO_PLAN_START;
                                    numLinesBeforePlan--;
                                } else {
                                    //the plan starts on the same line
                                    int indexPlanStart = line
                                            .indexOf(settings.getConsoleOutputPlanStartIdentifier());
                                    String firstLine = line.substring(indexPlanStart
                                            + settings.getConsoleOutputPlanStartIdentifier().length());
                                    if (!isLineAction(firstLine)) {
                                        unprocessedStatistics.add(firstLine);
                                    } else {
                                        unprocessedPlan.add(firstLine);
                                    }
                                    consoleParseState = EConsoleParseState.READING_PLAN;
                                }
                            }
                            break;
                        }
                        case COUNTING_TO_PLAN_START: {
                            if (numLinesBeforePlan > 0) {
                                numLinesBeforePlan--;
                                break;
                            } else {
                                consoleParseState = EConsoleParseState.READING_PLAN;
                            }
                            //intentional fallthrough!!!
                        }
                        case READING_PLAN: {
                            if (!settings.getConsoleOutputPlanEndIdentifier().isEmpty()
                                    && line.contains(settings.getConsoleOutputPlanEndIdentifier())) {
                                consoleParseState = EConsoleParseState.END;
                            } else {
                                if (!isLineAction(line)) {
                                    unprocessedStatistics.add(line);
                                } else {
                                    unprocessedPlan.add(line);
                                }
                            }
                            break;
                        }
                        case END: {
                            if (isLineStatistics(line)) {
                                unprocessedStatistics.add(line);
                            }
                            break;
                        }

                        }
                    }
                }

            }
            sc.close();

            if (cancelled) {
                return null;
            }

            //Need to clean the stream, otherwise, it would block the process from terminating
            String errorOuput;
            try {
                errorOuput = IOUtils.toString(process.getErrorStream());
            } catch (IOException ex) {
                errorOuput = "Could not get error stream: " + ex.getMessage();
            }

            try {
                if (cancelled) {
                    return null;
                }

                process.waitFor();
                logger.info("\n>> Planner " + chosenPlanner.getName() + " finished execution\n ");
            } catch (InterruptedException ex) {
                if (cancelled) {
                    return null;
                }
                logger.info("Waiting for planner execution interrupted", ex);
                destroyProcess();
                return null;
            }

            process.destroy();

            if (logger.isDebugEnabled()) {
                logger.debug("Planner console output end.");
            }

            if (cancelled) {
                return null;
            }

            int exitCode = process.exitValue();

            for (INoPlanFoundChecker checker : settings.getNoPlanFoundCheckers()) {
                if (checker.processExitCode(exitCode)) {
                    plannerFoundNoSolution = true;
                }
            }

            if (exitCode != 0 && !plannerFoundNoSolution) {
                throw new PlanningException(
                        "Planner terminated with an error - exit code: " + exitCode + ". Planner output:\n "
                                + consoleOutputBuilder.toString() + "\nError output:\n" + errorOuput);
            }

            if (settings.isHasOutputFile()) { //The planner does provide an output file

                String solutionFile = "solution.soln";
                if (!settings.getOutputFile().trim().isEmpty()) {
                    solutionFile = settings.getOutputFile();
                }

                if (settings.getOutputFileAutomaticIncrementSuffix() != null) {
                    //Find the existing file with the highest increment index
                    int i = 1;
                    while (true) {
                        String candidateSolutionFileName = solutionFile + settings
                                .getOutputFileAutomaticIncrementSuffix().replace("#", Integer.toString(i));
                        File candidateSolutionFile = new File(workingDirectory, candidateSolutionFileName);
                        if (candidateSolutionFile.exists()) {
                            solutionFile = candidateSolutionFileName;
                            i++;
                        } else {
                            break;
                        }
                    }
                }

                //Get the planner answer exposed in the solution Output File
                File outputFile = new File(workingDirectory, solutionFile);

                if (outputFile.exists()) {
                    //Get output
                    try {
                        for (String line : FileUtils.readLines(outputFile)) {
                            if (line.trim().isEmpty()) {
                                continue;
                            }
                            if (!isLineAction(line)) {
                                unprocessedStatistics.add(line);
                            } else {
                                unprocessedPlan.add(line);
                            }
                        }
                    } catch (IOException ex) {
                        throw new PlanningException("Could not read planner output", ex);
                    }

                    //remove output solution file (only if the plan create it)
                    outputFile.delete();
                    //TODO check permission
                } else {
                    //if the planner signalled before that it found nothing, the file may  not exits and it's OK
                    if (!plannerFoundNoSolution) {
                        throw new PlanningException("Could not find the planner output solution file! \n");
                    }
                    //System.out.println(toolMessage);
                }

            }

            if (cancelled) {
                return null;
            }

            for (INoPlanFoundChecker checker : settings.getNoPlanFoundCheckers()) {
                if (checker.processUnprocessedPlan(unprocessedPlan)) {
                    plannerFoundNoSolution = true;
                }
            }

            return new UnprocessedPlanningResult(unprocessedPlan, unprocessedStatistics,
                    consoleOutputBuilder.toString(), !plannerFoundNoSolution);

        } finally {
            // delete additional generated files
            for (String generatedFileName : settings.getAdditionalGeneratedFiles()) {
                File file = new File(workingDirectory, generatedFileName);
                if (file.exists()) {
                    // delete the file
                    file.delete();
                }
            }
        }
    }

    /**
     * This method parses the lines of a plan in text format to a XML structure
     * @param plan
     * @return the plan XML structure
     */
    private List<ActionDescription> parsePlanToActionDescription(List<String> plan) {
        List<ActionDescription> result = new ArrayList<ActionDescription>();

        for (int lineIndex = 0; lineIndex < plan.size(); lineIndex++) {
            String line = plan.get(lineIndex);
            try {

                ActionDescription action = new ActionDescription();

                //System.out.println(line);
                int colonIndex = line.indexOf(':');
                String actionInstance;
                if (line.indexOf('(') >= 0) {
                    actionInstance = line.substring(line.indexOf('(') + 1, line.lastIndexOf(')'));
                } else if (colonIndex >= 0) {
                    actionInstance = line.substring(colonIndex + 1);
                } else {
                    logger.warn("Could not determine what is the actual action: " + line);
                    continue;
                }
                StringTokenizer st = new StringTokenizer(actionInstance);

                // the first token is the action name
                String actionName = st.nextToken();

                action.setName(Planning4JUtils.normalizeIdentifier(actionName));

                // the other tokens are the parameters
                List<String> parameterValues = new ArrayList<String>();
                while (st.hasMoreTokens()) {
                    String parameterStr = st.nextToken();
                    parameterValues.add(Planning4JUtils.normalizeIdentifier(parameterStr));
                }
                action.setParameters(parameterValues);

                String startTimeStr;

                // set the startTime name
                if (colonIndex > 0) {
                    startTimeStr = line.substring(0, colonIndex);
                } else {
                    startTimeStr = new StringTokenizer(line).nextToken();
                }

                if (!startTimeStr.startsWith("(")) {
                    try {
                        action.setStartTime(Double.parseDouble(startTimeStr));
                    } catch (NumberFormatException ex) {
                        logger.warn("Could not parse action: " + line);
                        continue;
                    }
                }

                // set the action duration
                String durationStr = "1";
                if (line.indexOf('[') > -1) {
                    durationStr = line.substring(line.indexOf('[') + 1, line.lastIndexOf(']'));
                }
                action.setDuration(Double.parseDouble(durationStr));
                action.setNotes("");

                result.add(action);
            } catch (RuntimeException ex) {
                logger.warn("Could not parse action: " + line, ex);
            }

        }

        return result;
    }

    private PlanningStatistics parseStatistics(List<String> statistic) {

        PlanningStatistics planningStatistics = new PlanningStatistics();

        for (String line : statistic) {

            String keyword;
            String value;
            if (line.indexOf(' ') > -1) {
                // there is a value
                keyword = line.substring(0, line.indexOf(' ')).trim();
                value = line.substring(line.indexOf(' '), line.length()).trim();
            } else {
                keyword = line;
                value = "";
            }

            if (value.isEmpty()) {
                continue;
            }

            if (keyword.equals("Time")) {
                planningStatistics.setTime(Double.parseDouble(value));
            } else if (keyword.equals("ParsingTime")) {
                planningStatistics.setParsingTime(Double.parseDouble(value));
            } else if (keyword.equals("NrActions")) {
                planningStatistics.setNumberOfActions(Integer.parseInt(value));
            } else if (keyword.equals("MakeSpan")) {
                planningStatistics.setMakeSpan(Double.parseDouble(value));
            } else if (keyword.equals("MetricValue")) {
                planningStatistics.setMetricValue(Double.parseDouble(value));
            } else if (keyword.equals("PlanningTechnique")) {
                planningStatistics.setPlanningTechnique(value);
            } else {
                planningStatistics.addAdditionalStat(keyword + " " + value);
            }
        }
        return planningStatistics;

    }

    /**
     * Runs the planner with parameters specified in constructor.
     * @return an xml representation of the plan
     */
    @Override
    public IExternalPlanningResult executePlanner() {
        //set initial time
        long start_time = System.currentTimeMillis() - timeInIO;

        if (cancelled) {
            return null;
        }
        UnprocessedPlanningResult unprocessedResult = runPlanner(domainFile, problemFile);

        if (cancelled) {
            return null;
        }

        PlanningStatistics stats = parseStatistics(unprocessedResult.getStatistics());

        //6. set the plan
        //Element planNode = xmlPlan.getChild("plan");
        List<ActionDescription> actionDescriptions = parsePlanToActionDescription(unprocessedResult.getPlan());

        long time = System.currentTimeMillis() - start_time;

        if (cancelled) {
            return null;
        }

        return new ExternalPlanningResult(unprocessedResult.isPlanningSuccesful(), actionDescriptions,
                unprocessedResult.getConsoleOuptut(), stats, time);
    }

    @Override
    public synchronized void cancel() {
        if (!cancelled) {
            cancelled = true;
            if (process != null) {
                //plannerExecutableFile is set always before the process is set
                destroyProcess();
            }
        }
    }

    public void destroyProcess() {
        if (process != null) {
            try {
                process.exitValue(); //this throws an exception only iff the process has not yet stopped
            } catch (IllegalThreadStateException ingoredException) {
                InputStream is = process.getInputStream();
                InputStream es = process.getErrorStream();

                if (processPid > 0) {
                    killPlannerByPID();
                }

                process.destroy();

                //should read and discard all output
                try {
                    IOUtils.copy(is, new NullOutputStream());
                } catch (IOException ex) {
                    logger.debug("Error consuming output:" + ex.getMessage(), ex);
                }
                try {
                    IOUtils.copy(es, new NullOutputStream());
                } catch (IOException ex) {
                    logger.debug("Error consuming error output:" + ex.getMessage(), ex);
                }
            }

        }
    }

    protected static class UnprocessedPlanningResult {

        List<String> plan;
        List<String> statistics;
        String consoleOuptut;
        boolean planningSuccesful;

        public UnprocessedPlanningResult(List<String> plan, List<String> statistics, String consoleOuptut,
                boolean planningSuccesful) {
            this.plan = plan;
            this.statistics = statistics;
            this.consoleOuptut = consoleOuptut;
            this.planningSuccesful = planningSuccesful;
        }

        public List<String> getPlan() {
            return plan;
        }

        public List<String> getStatistics() {
            return statistics;
        }

        public String getConsoleOuptut() {
            return consoleOuptut;
        }

        public boolean isPlanningSuccesful() {
            return planningSuccesful;
        }
    }
}