blob: 6418cd294adce58858100919982bd88f6d17c6ca [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 Kichwa Coders and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* Contributors:
* Jonah Graham (Kichwa Coders) - initial API and implementation
*******************************************************************************/
package org.eclipse.ease.lang.python.py4j.internal;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.core.variables.IStringVariableManager;
import org.eclipse.core.variables.VariablesPlugin;
import org.eclipse.ease.AbstractReplScriptEngine;
import org.eclipse.ease.Logger;
import org.eclipse.ease.Script;
import org.eclipse.ease.ScriptEngineException;
import org.eclipse.ease.ScriptExecutionException;
import org.eclipse.ease.debugging.ScriptStackTrace;
import org.eclipse.ease.tools.RunnableWithResult;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.widgets.Display;
import org.osgi.framework.Bundle;
import py4j.ClientServer;
import py4j.ClientServer.ClientServerBuilder;
import py4j.JavaServer;
public class Py4jScriptEngine extends AbstractReplScriptEngine {
/**
* The ID of the Engine, to match the declaration in the plugin.xml
*/
public static final String ENGINE_ID = "org.eclipse.ease.lang.python.py4j.engine";
// TODO: Add preference for this
private static final int PYTHON_STARTUP_TIMEOUT_SECONDS = 10;
// TODO: Add preference for this
private static final int PYTHON_SHUTDOWN_TIMEOUT_SECONDS = 10;
/**
* Path within this plug-in to the main python file.
*/
private static final String PYSRC_EASE_PY4J_MAIN_PY = "/pysrc/ease_py4j_main.py";
/**
* The ID of the py4j sources plug-in, needs to match the name of the dependent plug-in.
*/
private static final String PY4J_PYTHON_BUNDLE_ID = "py4j-python";
private ClientServer fGatewayServer;
protected IPythonSideEngine fPythonSideEngine;
private Process fPythonProcess;
private Thread fInputGobbler, fErrorGobbler;
private CountDownLatch fPythonStartupComplete;
/**
* Standard StreamGobbler.
*/
private static class StreamGobbler implements Runnable {
private final InputStream fReader;
private final OutputStream fWriter;
private final String fStreamName;
public StreamGobbler(final InputStream stream, final OutputStream output, final String streamName) {
fReader = stream;
fWriter = output;
fStreamName = streamName;
}
@Override
public void run() {
try {
final byte[] bytes = new byte[512];
int readCount;
while ((readCount = fReader.read(bytes)) >= 0) {
try {
fWriter.write(bytes, 0, readCount);
} catch (final IOException e) {
Logger.error(Activator.PLUGIN_ID, "Failed to write data read from Python's " + fStreamName + " stream.", e);
}
}
} catch (final IOException e) {
Logger.error(Activator.PLUGIN_ID, "Failed to read data from Python's " + fStreamName + " stream.", e);
}
}
}
public Py4jScriptEngine() {
super("Python (Py4J)");
}
@Override
protected void setupEngine() throws ScriptEngineException {
try {
fPythonStartupComplete = new CountDownLatch(1);
fGatewayServer = new ClientServerBuilder(this).javaPort(0).pythonPort(0).build();
fGatewayServer.startServer(true);
final int javaListeningPort = ((JavaServer) fGatewayServer.getJavaServer()).getListeningPort();
fPythonProcess = startPythonProcess(javaListeningPort);
fInputGobbler = new Thread(new StreamGobbler(fPythonProcess.getInputStream(), getOutputStream(), "stdout"),
"EASE py4j engine output stream gobbler");
fInputGobbler.start();
fErrorGobbler = new Thread(new StreamGobbler(fPythonProcess.getErrorStream(), getErrorStream(), "stderr"), "EASE py4j engine error stream gobbler");
fErrorGobbler.start();
// TODO Handle python's stdin (fPythonProcess.getOutputStream())
if (!fPythonStartupComplete.await(PYTHON_STARTUP_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
throw new ScriptEngineException("Python process did not start within " + PYTHON_STARTUP_TIMEOUT_SECONDS + " seconds");
}
} catch (final ScriptEngineException e) {
teardownEngine();
throw e;
} catch (final Exception e) {
teardownEngine();
throw new ScriptEngineException("Failed to start Python process. Please check the setting for the Python interpreter"
+ " in Preferences -> Scripting -> Python Scripting:\n" + e.getMessage(), e);
}
}
private Process startPythonProcess(final int javaListeningPort) throws IOException, MalformedURLException, URISyntaxException, CoreException {
final ProcessBuilder pb = new ProcessBuilder();
final List<String> prependsPythonPath = new ArrayList<>();
prependsPythonPath.add(getPy4jPythonSrc().toString());
// Add EASE Python directory to python path
final Bundle bundle = Platform.getBundle("org.eclipse.ease.lang.python");
try {
URL url = bundle.getEntry("pysrc");
url = FileLocator.toFileURL(url);
final URI uri = new URI(url.getProtocol(), url.getPath(), null);
prependsPythonPath.add(new File(uri).getAbsolutePath());
} catch (final IllegalStateException e) {
Logger.error(Activator.PLUGIN_ID, "Cannot get entry pysrc because the plugin has not been initialized properly.", e);
} catch (final IOException | URISyntaxException e) {
Logger.error(Activator.PLUGIN_ID, "Cannot append additional Python modules to search path.", e);
}
final IPreferenceStore preferenceStore = Activator.getDefault().getPreferenceStore();
String interpreter = preferenceStore.getString(Py4JScriptEnginePrefConstants.INTERPRETER);
final boolean ignorePythonEnvVariables = preferenceStore.getBoolean(Py4JScriptEnginePrefConstants.IGNORE_PYTHON_ENV_VARIABLES);
final IStringVariableManager variableManager = VariablesPlugin.getDefault().getStringVariableManager();
interpreter = variableManager.performStringSubstitution(interpreter);
pb.command().add(interpreter);
pb.command().add("-u");
if (ignorePythonEnvVariables) {
pb.command().add("-E");
}
pb.command().add(getPy4jEaseMainPy().toString());
pb.command().add(Integer.toString(javaListeningPort));
pb.command().addAll(prependsPythonPath);
final Process start = pb.start();
return start;
}
private File getPy4jPythonSrc() throws IOException {
final File py4jPythonBundleFile = FileLocator.getBundleFile(Platform.getBundle(PY4J_PYTHON_BUNDLE_ID));
final File py4jPythonSrc = new File(py4jPythonBundleFile, "/src");
final File py4j = new File(py4jPythonSrc, "py4j");
if (!py4j.exists() || !py4j.isDirectory()) {
throw new IOException("Failed to find py4j python directory, expected it here: " + py4j);
}
return py4jPythonSrc;
}
private File getPy4jEaseMainPy() throws MalformedURLException, IOException, URISyntaxException {
final URL url = new URL("platform:/plugin/" + Activator.PLUGIN_ID + PYSRC_EASE_PY4J_MAIN_PY);
final URL fileURL = FileLocator.toFileURL(url);
final File py4jEaseMain = new File(URIUtil.toURI(fileURL));
if (!py4jEaseMain.exists()) {
throw new IOException("Failed to find " + PYSRC_EASE_PY4J_MAIN_PY + ", expected it here: " + py4jEaseMain);
}
return py4jEaseMain;
}
public void pythonStartupComplete(final int pythonPort, final IPythonSideEngine pythonSideEngine) {
final JavaServer javaServer = (JavaServer) fGatewayServer.getJavaServer();
javaServer.resetCallbackClient(javaServer.getCallbackClient().getAddress(), pythonPort);
fPythonSideEngine = pythonSideEngine;
fPythonStartupComplete.countDown();
}
@Override
protected Object execute(final Script script, final Object reference, final String fileName, final boolean uiThread) throws Throwable {
if (uiThread) {
// run in UI thread
final RunnableWithResult<Object> runnable = new RunnableWithResult<Object>() {
@Override
public Object runWithTry() throws Throwable {
return internalExecute(script, fileName);
}
};
Display.getDefault().syncExec(runnable);
return runnable.getResultOrThrow();
} else {
return internalExecute(script, fileName);
}
}
protected Object internalExecute(final Script script, final String fileName) throws Throwable, Exception {
if (hasVariable("argv"))
fPythonSideEngine.executeScript("import sys\nsys.argv = argv\n", "<dynamic>");
IInteractiveReturn interactiveReturn;
if (script.isShellMode()) {
interactiveReturn = fPythonSideEngine.executeInteractive(script.getCode());
} else {
final String code = script.getCode();
interactiveReturn = fPythonSideEngine.executeScript(code, fileName);
}
final Object exception = interactiveReturn.getException();
if (exception instanceof Throwable) {
throw (Throwable) exception;
} else if (exception != null) {
throw new ScriptExecutionException(exception.toString(), 0, null, null, new ScriptStackTrace(), null);
} else {
return interactiveReturn.getResult();
}
}
@Override
public void terminateCurrent() {
// TODO: It isn't possible to safely/reliablily interrupt a thread in
// Python
// For now, as terminateCurrent is called when the running script is
// called,
// we tear down the engine instead. This prevent the script from having
// a chance
// to cleanup.
// XXX: This is an issue solved by PyDev, resolving it here fully is not
// the logical course of action.
if (fPythonProcess != null) {
fPythonProcess.destroyForcibly();
}
}
@Override
protected void teardownEngine() {
// TODO: this clean shutdown isn't working as intended.
// Sometimes (on Linux) the Python process seems to shutdown
// before fully acknowledging the call to teardownEngine, leaving
// us in a worst state than if we shutdown not-cleanly.
// When/if this is resurrected, the fPythonProcess.destroy();
// below should be removed.
// if (fPythonSideEngine != null) {
// // try a clean shutdown
// fPythonSideEngine.teardownEngine();
// }
if (fPythonProcess != null) {
fPythonProcess.destroy();
try {
fPythonProcess.waitFor(PYTHON_SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (final InterruptedException e) {
// finish the teardown, but don't wait around
Thread.currentThread().interrupt();
}
}
if (fGatewayServer != null) {
fGatewayServer.shutdown();
}
if (fPythonProcess != null) {
// The clean shutdown had a chance, now time for a force shutdown
fPythonProcess.destroyForcibly();
}
try {
// Wait until the gobblers have shovelled all their
// inputs before allowing the engine to considered terminated
if (fInputGobbler != null) {
fInputGobbler.join();
}
if (fErrorGobbler != null) {
fErrorGobbler.join();
}
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void registerJar(final URL url) {
throw new UnsupportedOperationException();
}
@Override
protected Object internalGetVariable(final String name) {
return fPythonSideEngine.internalGetVariable(name);
}
@Override
protected Map<String, Object> internalGetVariables() {
return fPythonSideEngine.internalGetVariables();
}
@Override
protected boolean internalHasVariable(final String name) {
return fPythonSideEngine.internalHasVariable(name);
}
@Override
protected void internalSetVariable(final String name, final Object content) {
fPythonSideEngine.internalSetVariable(name, content);
}
@SuppressWarnings("unchecked")
@Override
public <T> T getAdapter(final Class<T> adapter) {
if (adapter.isInstance(fPythonProcess)) {
return (T) fPythonProcess;
}
return super.getAdapter(adapter);
}
@Override
public String toString(Object object) {
if (object == null)
return "None";
return super.toString(object);
}
}