/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.scripting.python; import java.io.BufferedReader; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.Serializable; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.ObjectMapper; import de.rcenvironment.core.component.api.ComponentConstants; import de.rcenvironment.core.component.datamanagement.api.CommonComponentHistoryDataItem; import de.rcenvironment.core.component.datamanagement.api.ComponentDataManagementService; import de.rcenvironment.core.component.execution.api.ComponentContext; import de.rcenvironment.core.component.execution.api.ConsoleRow; import de.rcenvironment.core.component.execution.api.ConsoleRowUtils; import de.rcenvironment.core.component.model.endpoint.api.EndpointDefinition.InputExecutionContraint; import de.rcenvironment.core.datamodel.types.api.BooleanTD; import de.rcenvironment.core.datamodel.types.api.DirectoryReferenceTD; import de.rcenvironment.core.datamodel.types.api.FileReferenceTD; import de.rcenvironment.core.datamodel.types.api.FloatTD; import de.rcenvironment.core.datamodel.types.api.IntegerTD; import de.rcenvironment.core.datamodel.types.api.MatrixTD; import de.rcenvironment.core.datamodel.types.api.ShortTextTD; import de.rcenvironment.core.datamodel.types.api.SmallTableTD; import de.rcenvironment.core.datamodel.types.api.VectorTD; import de.rcenvironment.core.scripting.ScriptDataTypeHelper; import de.rcenvironment.core.utils.common.JsonUtils; import de.rcenvironment.core.utils.common.TempFileServiceAccess; import de.rcenvironment.core.utils.common.legacy.FileSupport; import de.rcenvironment.core.utils.common.textstream.TextStreamWatcher; import de.rcenvironment.core.utils.executor.LocalApacheCommandLineExecutor; /** * Implementation of {@link ScriptEngine} for the python language. * * @author Sascha Zur * @author Jascha Riedel (#14029) */ public class PythonScriptEngine implements ScriptEngine { private static final String SIMPLEJSON = "simplejson.zip"; private static final String RESOURCES = "/resources/"; private static final String PYTHON_BRIDGE = "RCE_Channel.py"; private static final String RUN_SCRIPT = "Run_python_script_in_rce.py"; private static final Log LOGGER = LogFactory.getLog(PythonScriptEngine.class); private static final String ESCAPED_DOUBLE_QUOTE = "\""; private static final int EXIT_CODE_FAILURE = 1; private static ComponentDataManagementService componentDatamanagementService; private File tempDir; private ScriptContext context; private LocalApacheCommandLineExecutor executor; private final ObjectMapper mapper = JsonUtils.getDefaultObjectMapper(); private Map<String, Serializable> output = new HashMap<>(); private final List<File> tempFiles = new LinkedList<>(); private List<String> closeOutputChannelsList = new LinkedList<>(); private TextStreamWatcher stdoutWatcher; private TextStreamWatcher stderrWatcher; private Map<String, Object> stateOutput; /** * This latch is used to ensure that a cancellation request is not performed during the preparation of the script execution is executed, * but after the initialization is completed. */ private CountDownLatch initializationSignal; /** * Set to true, if the script engine was canceled before it is properly initialized. */ private boolean canceledBeforeInitialization = false; /** * Creates a new executor. * * @param dataItem {@link ScriptComponentHistoryDataItem} object for this script execution */ public synchronized void createNewExecutor(CommonComponentHistoryDataItem dataItem) { if (canceledBeforeInitialization) { LOGGER.error("Failed to create executor for python, since the SriptEngine was already canceled."); return; } // This method is synchronized to avoid a race condition with cancel try { executor = new LocalApacheCommandLineExecutor(null); initializationSignal = new CountDownLatch(1); } catch (IOException e) { LOGGER.error("Failed to create executor for python."); } } @Override public Bindings createBindings() { return null; } @Override public Object eval(String script) throws ScriptException { // find temp directory for all intermediate files try { tempDir = TempFileServiceAccess.getInstance().createManagedTempDir("python"); tempFiles.add(tempDir); } catch (final IOException e) { LOGGER.error("Could not create managed temp directory, falling back to default"); try { final File tmp = File.createTempFile("prefix", "suffix"); tempDir = tmp.getParentFile(); tmp.delete(); // not needed } catch (final IOException e1) { LOGGER.error("Failed to fall back."); throw new ScriptException("Unable to create temp file and directory"); } } writeInputForPython(); // run script try { createTemporaryPythonScript(script); } catch (IOException e) { LOGGER.error("Failed to create temporary Python script."); } executor.setWorkDir(tempDir); final String command = ESCAPED_DOUBLE_QUOTE + ((String) context.getAttribute(PythonComponentConstants.PYTHON_INSTALLATION)) + ESCAPED_DOUBLE_QUOTE + " -u " + tempDir.getAbsolutePath() + File.separator + RUN_SCRIPT; LOGGER.debug("PythonExecutor executes command: " + command); int exitCode = 0; try { executor.start(command); prepareOutputForRun(); // as soon as we reach this position the execution can be interrupted initializationSignal.countDown(); try { exitCode = executor.waitForTermination(); stdoutWatcher.waitForTermination(); stderrWatcher.waitForTermination(); } catch (InterruptedException e) { LOGGER.error("ProgramBlocker: InterruptedException " + e.getMessage()); return EXIT_CODE_FAILURE; } catch (CancellationException e) { LOGGER.debug("Execution canceled while waiting for termination of TextStreamWatcher."); return EXIT_CODE_FAILURE; } } catch (IOException e) { LOGGER.error("Something during Python execution failed. See exception for details", e); } readOutputFromPython(); return exitCode; } private void prepareOutputForRun() { stdoutWatcher = ConsoleRowUtils.logToWorkflowConsole(((ComponentContext) context.getAttribute(PythonComponentConstants.COMPONENT_CONTEXT)) .getLog(), executor.getStdout(), ConsoleRow.Type.TOOL_OUT, null, false); stderrWatcher = ConsoleRowUtils.logToWorkflowConsole(((ComponentContext) context.getAttribute(PythonComponentConstants.COMPONENT_CONTEXT)) .getLog(), executor.getStderr(), ConsoleRow.Type.TOOL_ERROR, null, false); } private void writeInputForPython() { ComponentContext compContext = (ComponentContext) context.getAttribute(PythonComponentConstants.COMPONENT_CONTEXT); Map<String, Object> inputsToWrite = new HashMap<>(); for (String inputName : compContext.getInputsWithDatum()) { switch (compContext.getInputDataType(inputName)) { case FileReference: FileReferenceTD fileReference = (FileReferenceTD) compContext.readInput(inputName); File fileInputDir = new File(tempDir, inputName); tempFiles.add(fileInputDir); File file = new File(fileInputDir, fileReference.getFileName()); try { componentDatamanagementService.copyFileReferenceTDToLocalFile(compContext, fileReference, file); } catch (IOException e) { throw new RuntimeException("Failed to load input file from the data management", e); } inputsToWrite.put(inputName, file.getAbsolutePath().toString().replaceAll("\\\\", "/")); break; case DirectoryReference: DirectoryReferenceTD directoryReference = (DirectoryReferenceTD) compContext.readInput(inputName); File dirInputDir = new File(tempDir, inputName); tempFiles.add(dirInputDir); File dir = new File(dirInputDir, directoryReference.getDirectoryName()); try { componentDatamanagementService.copyDirectoryReferenceTDToLocalDirectory(compContext, (DirectoryReferenceTD) compContext.readInput(inputName), dirInputDir); } catch (IOException e) { throw new RuntimeException("Failed to load input directory from the data management", e); } inputsToWrite.put(inputName, dir.getAbsolutePath().toString().replaceAll("\\\\", "/")); break; case Boolean: boolean bool = (((BooleanTD) compContext.readInput(inputName)).getBooleanValue()); if (bool) { inputsToWrite.put(inputName, true); } else { inputsToWrite.put(inputName, false); } break; case ShortText: inputsToWrite.put(inputName, ((ShortTextTD) compContext.readInput(inputName)).getShortTextValue()); break; case Integer: inputsToWrite.put(inputName, ((IntegerTD) compContext.readInput(inputName)).getIntValue()); break; case Float: if (compContext.readInput(inputName) instanceof FloatTD) { inputsToWrite.put(inputName, ((FloatTD) compContext.readInput(inputName)).getFloatValue()); } else if (compContext.readInput(inputName) instanceof IntegerTD) { inputsToWrite.put(inputName, ((IntegerTD) compContext.readInput(inputName)).getIntValue()); } break; case Vector: VectorTD vector = (VectorTD) compContext.readInput(inputName); Object[] resultVector = new Object[vector.getRowDimension()]; for (int j = 0; j < vector.getRowDimension(); j++) { resultVector[j] = vector.getFloatTDOfElement(j).getFloatValue(); } inputsToWrite.put(inputName, resultVector); break; case Matrix: MatrixTD matrix = (MatrixTD) compContext.readInput(inputName); if (matrix.getRowDimension() > 1) { Object[][] result = new Object[matrix.getRowDimension()][matrix.getColumnDimension()]; for (int i = 0; i < result.length; i++) { for (int j = 0; j < result[0].length; j++) { result[i][j] = ScriptDataTypeHelper.getObjectOfEntryForPythonOrJython(matrix.getFloatTDOfElement(i, j)); } } inputsToWrite.put(inputName, result); } else { Object[] result = new Object[matrix.getColumnDimension()]; for (int j = 0; j < matrix.getColumnDimension(); j++) { result[j] = ScriptDataTypeHelper.getObjectOfEntryForPythonOrJython(matrix.getFloatTDOfElement(0, j)); } inputsToWrite.put(inputName, result); } break; case SmallTable: SmallTableTD table = (SmallTableTD) compContext.readInput(inputName); if (table.getRowCount() > 1) { Object[][] result = new Object[table.getRowCount()][table.getColumnCount()]; for (int i = 0; i < table.getRowCount(); i++) { for (int j = 0; j < table.getColumnCount(); j++) { result[i][j] = ScriptDataTypeHelper.getObjectOfEntryForPythonOrJython(table.getTypedDatumOfCell(i, j)); } } inputsToWrite.put(inputName, result); } else { Object[] result = new Object[table.getColumnCount()]; for (int j = 0; j < table.getColumnCount(); j++) { result[j] = ScriptDataTypeHelper.getObjectOfEntryForPythonOrJython(table.getTypedDatumOfCell(0, j)); } inputsToWrite.put(inputName, result); } break; default: inputsToWrite.put(inputName, "None"); // Should not happen break; } } List<String> inputsNotConnected = new LinkedList<>(); for (String input : compContext.getInputsNotConnected()) { if (compContext.getInputMetaDataValue(input, ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT) != null && (compContext.getInputMetaDataValue(input, ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT).equals( InputExecutionContraint.RequiredIfConnected.name()) || compContext.getInputMetaDataValue(input, ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT).equals( InputExecutionContraint.NotRequired.name()))) { inputsNotConnected.add(input); } } for (String input : compContext.getInputs()) { if (compContext.getInputMetaDataValue(input, ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT) != null && compContext.getInputMetaDataValue(input, ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT).equals( InputExecutionContraint.NotRequired.name()) && !compContext.getInputsWithDatum().contains(input)) { inputsNotConnected.add(input); } } try { mapper.writeValue(new File(tempDir.getAbsolutePath(), "pythonInput.rced"), inputsToWrite); mapper.writeValue(new File(tempDir.getAbsolutePath(), "pythonInputReqIfConnected.rced"), inputsNotConnected); mapper.writeValue(new File(tempDir.getAbsolutePath(), "pythonStateVariables.rces"), context.getAttribute(PythonComponentConstants.STATE_MAP)); mapper.writeValue(new File(tempDir.getAbsolutePath(), "pythonRunNumber.rcen"), context.getAttribute(PythonComponentConstants.RUN_NUMBER)); } catch (JsonGenerationException e) { LOGGER.error(e.getMessage()); } catch (JsonMappingException e) { LOGGER.error(e.getMessage()); } catch (IOException e) { LOGGER.error(e.getMessage()); } List<String> outputNames = new LinkedList<>(); for (String outputName : compContext.getOutputs()) { outputNames.add(outputName); } try { mapper.writeValue(new File(tempDir.getAbsolutePath() + File.separator + "outputs.rceo"), outputNames); } catch (JsonGenerationException e) { LOGGER.error(e.getMessage()); } catch (JsonMappingException e) { LOGGER.error(e.getMessage()); } catch (IOException e) { LOGGER.error(e.getMessage()); } } @SuppressWarnings("unchecked") private void readOutputFromPython() throws ScriptException { try { if (new File(tempDir.getAbsolutePath() + File.separator + "pythonOutput.rced").exists()) { output = mapper.readValue(new File(tempDir.getAbsolutePath() + File.separator + "pythonOutput.rced"), output.getClass()); } if (new File(tempDir.getAbsolutePath() + File.separator + "pythonCloseOutputChannelsList.rced").exists()) { closeOutputChannelsList = mapper.readValue(new File(tempDir.getAbsolutePath() + File.separator + "pythonCloseOutputChannelsList.rced"), closeOutputChannelsList.getClass()); } stateOutput = new HashMap<>(); if (new File(tempDir.getAbsolutePath() + File.separator + "pythonStateOutput.rces").exists()) { stateOutput = mapper.readValue(new File(tempDir.getAbsolutePath() + File.separator + "pythonStateOutput.rces"), stateOutput.getClass()); } } catch (IOException e) { throw new ScriptException(e); } } public Map<String, Object> getStateOutput() { return stateOutput; } /** * This creates a temporary file containing the wrapped python script. * * @return The file handle to execute later with the python interpreter * @throws IOException For any file error */ private void createTemporaryPythonScript(String script) throws IOException { final File temp = new File(tempDir, "userscript.py"); final FileWriter writer = new FileWriter(temp); script = StringUtils.replace(script, "\r\n", "\n"); writer.write(script); writer.close(); File wrapperMain = new File(tempDir, RUN_SCRIPT); try (InputStream wrapperScriptInputMain = PythonScriptEngine.class.getResourceAsStream(RESOURCES + RUN_SCRIPT)) { FileUtils.copyInputStreamToFile(wrapperScriptInputMain, wrapperMain); File wrapperBridge = new File(tempDir, PYTHON_BRIDGE); try (InputStream wrapperScriptInputBridge = PythonScriptEngine.class.getResourceAsStream(RESOURCES + PYTHON_BRIDGE)) { FileUtils.copyInputStreamToFile(wrapperScriptInputBridge, wrapperBridge); try (InputStream simpleJsonFiles = PythonScriptEngine.class.getResourceAsStream(RESOURCES + SIMPLEJSON)) { FileSupport.unzip(simpleJsonFiles, tempDir); } } } } @Override public Object eval(Reader reader) throws ScriptException { String script = ""; BufferedReader br = new BufferedReader(reader); String line; try { line = br.readLine(); while (line != null) { script += line; } } catch (IOException e) { LOGGER.error("Could not read script"); } return eval(script); } @Override public Object eval(String script, ScriptContext contextIn) throws ScriptException { context = contextIn; return eval(script); } @Override public Object eval(Reader reader, ScriptContext contextIn) throws ScriptException { context = contextIn; return eval(reader); } @Override public Object eval(String script, Bindings n) throws ScriptException { return eval(script); } @Override public Object eval(Reader reader, Bindings n) throws ScriptException { return eval(reader); } @Override public Object get(String key) { return output.get(key); } public List<String> getCloseOutputChannelsList() { return closeOutputChannelsList; } @Override public Bindings getBindings(int scope) { return null; } @Override public ScriptContext getContext() { return context; } @Override public ScriptEngineFactory getFactory() { return null; } @Override public void put(String key, Object value) { context.setAttribute(key, value, 0); } @Override public void setBindings(Bindings bindings, int scope) { } @Override public void setContext(ScriptContext context) { this.context = context; } public LocalApacheCommandLineExecutor getExecutor() { return executor; } /** * Disposes all created help files. */ public void dispose() { try { if (tempDir != null) { TempFileServiceAccess.getInstance().disposeManagedTempDirOrFile(tempDir); } if (tempFiles != null) { for (File f : tempFiles) { if (f.exists()) { FileUtils.forceDelete(f); } } } } catch (IOException e) { LOGGER.error(e.getMessage()); } } protected void bindComponentDataManagementService(ComponentDataManagementService compDataManagementService) { componentDatamanagementService = compDataManagementService; } /** * Cancels the execution of the Python script. */ public synchronized void cancel() { // This method is synchronized to avoid a race condition with createNewExecutor if (initializationSignal == null) { canceledBeforeInitialization = true; return; } try { initializationSignal.await(); } catch (InterruptedException e) { LOGGER.debug("Interrupted while waiting for the initialization to finish.", e); LOGGER.debug("Cancelling the cancellation."); return; } stdoutWatcher.cancel(); stderrWatcher.cancel(); executor.cancel(); } }