/* * Copyright 2015 Nokia Solutions and Networks * Licensed under the Apache License, Version 2.0, * see license.txt file for details. */ package org.rf.ide.core.executor; import static com.google.common.collect.Lists.newArrayList; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.Proxy; import java.net.ServerSocket; import java.net.URL; import java.util.Base64; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import org.apache.ws.commons.util.NamespaceContextImpl; import org.apache.xmlrpc.XmlRpcException; import org.apache.xmlrpc.client.XmlRpcClient; import org.apache.xmlrpc.client.XmlRpcClientConfigImpl; import org.apache.xmlrpc.client.XmlRpcSun15HttpTransportFactory; import org.apache.xmlrpc.common.TypeFactoryImpl; import org.apache.xmlrpc.common.XmlRpcController; import org.apache.xmlrpc.common.XmlRpcStreamConfig; import org.apache.xmlrpc.parser.NullParser; import org.apache.xmlrpc.parser.TypeParser; import org.apache.xmlrpc.serializer.NullSerializer; import org.apache.xmlrpc.serializer.TypeSerializer; import org.apache.xmlrpc.serializer.TypeSerializerImpl; import org.rf.ide.core.executor.RobotRuntimeEnvironment.RobotEnvironmentException; import org.rf.ide.core.jvmutils.process.OSProcessHelper; import org.rf.ide.core.jvmutils.process.OSProcessHelper.ProcessHelperException; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.io.Files; /** * @author mmarzec */ @SuppressWarnings("PMD.GodClass") class RobotCommandRpcExecutor implements RobotCommandExecutor { private static final int CONNECTION_TIMEOUT = 30; private final String interpreterPath; private final SuiteExecutor interpreterType; private final File scriptFile; private Process serverProcess; private boolean isExternal = false; private XmlRpcClient client; RobotCommandRpcExecutor(final String interpreterPath, final SuiteExecutor interpreterType, final File scriptFile) { this.interpreterPath = interpreterPath; this.interpreterType = interpreterType; this.scriptFile = scriptFile; } void waitForEstablishedConnection() { if (new File(interpreterPath).exists() && scriptFile.exists()) { isExternal = RedSystemProperties.shouldConnectToRunningServer(); if (isExternal) { client = createClient(RedSystemProperties.getSessionServerAddress()); } else { final int port = findFreePort(); serverProcess = createPythonServerProcess(interpreterPath, scriptFile, port); client = createClient("127.0.0.1:" + port); } waitForConnectionToServer(CONNECTION_TIMEOUT); } else { throw new RobotCommandExecutorException("Could not setup python server on file: " + interpreterPath); } } private Process createPythonServerProcess(final String interpreterPath, final File scriptFile, final int port) { try { final List<String> command = newArrayList(interpreterPath, scriptFile.getPath(), String.valueOf(port)); final Process process = new ProcessBuilder(command).start(); final Semaphore semaphore = new Semaphore(0); startStdOutReadingThread(process, semaphore); startStdErrReadingThread(process, semaphore); return process; } catch (final IOException e) { throw new RobotCommandExecutorException("Could not setup python server on file: " + interpreterPath, e); } } private void startStdOutReadingThread(final Process process, final Semaphore semaphore) { new Thread(() -> { for (final PythonProcessListener listener : getListeners()) { listener.processStarted(interpreterPath, process); } semaphore.release(); final InputStream inputStream = process.getInputStream(); try (final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { String line = reader.readLine(); while (line != null) { for (final PythonProcessListener listener : getListeners()) { listener.lineRead(serverProcess, line); } line = reader.readLine(); } } catch (final IOException e) { // that fine } finally { for (final PythonProcessListener listener : getListeners()) { listener.processEnded(serverProcess); } } }).start(); } private List<PythonProcessListener> getListeners() { return newArrayList(PythonInterpretersCommandExecutors.getInstance().getListeners()); } private void startStdErrReadingThread(final Process process, final Semaphore semaphore) { new Thread(() -> { try { semaphore.acquire(); } catch (final InterruptedException e) { // that fine } final InputStream inputStream = process.getErrorStream(); try (final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { String line = reader.readLine(); while (line != null) { for (final PythonProcessListener listener : getListeners()) { listener.errorLineRead(serverProcess, line); } line = reader.readLine(); } } catch (final IOException e) { // that fine } }).start(); } private static int findFreePort() { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); } catch (final IOException e) { throw new RobotCommandExecutorException("Unable to find empty port for XmlRpc server", e); } } private XmlRpcClient createClient(final String hostAndPort) { final XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl(); try { config.setServerURL(new URL("http://" + hostAndPort)); config.setConnectionTimeout((int) TimeUnit.SECONDS.toMillis(CONNECTION_TIMEOUT)); config.setReplyTimeout((int) TimeUnit.SECONDS.toMillis(CONNECTION_TIMEOUT)); } catch (final MalformedURLException e) { // can't happen here } final XmlRpcClient client = new XmlRpcClient(); final XmlRpcSun15HttpTransportFactory transportFactory = new XmlRpcSun15HttpTransportFactory(client); transportFactory.setProxy(Proxy.NO_PROXY); client.setTransportFactory(transportFactory); client.setConfig(config); client.setTypeFactory(new XmlRpcTypeFactoryWithNil(client)); return client; } private void waitForConnectionToServer(final int timeoutInSec) { final long start = System.currentTimeMillis(); while (true) { try { callRpcFunction("checkServerAvailability"); break; } catch (final XmlRpcException e) { try { Thread.sleep(200); } catch (final InterruptedException ie) { // we'll try once again } } if (System.currentTimeMillis() - start > (timeoutInSec * 1000)) { kill(); break; } } } boolean isAlive() { return !isExternal() && serverProcess.isAlive(); } boolean isExternal() { return isExternal; } void kill() { if (isAlive()) { try { new OSProcessHelper().destroyProcessTree(serverProcess); } catch (final ProcessHelperException e) { e.printStackTrace(); } serverProcess.destroyForcibly(); try { serverProcess.waitFor(); } catch (final InterruptedException e) { throw new RobotCommandExecutorException("Unable to kill rpc server", e); } } } @Override public Map<String, Object> getVariables(final String filePath, final List<String> fileArguments) { try { final Map<String, Object> variables = new LinkedHashMap<>(); final Map<?, ?> varToValueMapping = (Map<?, ?>) callRpcFunction("getVariables", filePath, fileArguments); for (final Entry<?, ?> entry : varToValueMapping.entrySet()) { variables.put((String) entry.getKey(), entry.getValue()); } return variables; } catch (final XmlRpcException e) { e.printStackTrace(); throw new RobotEnvironmentException( "Unable to communicate with XML-RPC server. File " + filePath + " with arguments " + fileArguments, e); } } @Override public Map<String, Object> getGlobalVariables() { try { final Map<String, Object> variables = new LinkedHashMap<>(); final Map<?, ?> varToValueMapping = (Map<?, ?>) callRpcFunction("getGlobalVariables"); for (final Entry<?, ?> entry : varToValueMapping.entrySet()) { variables.put((String) entry.getKey(), entry.getValue()); } return variables; } catch (final XmlRpcException e) { throw new RobotEnvironmentException("Unable to communicate with XML-RPC server", e); } } @Override public List<String> getStandardLibrariesNames() { try { final List<String> libraries = newArrayList(); final Object[] libs = (Object[]) callRpcFunction("getStandardLibrariesNames"); for (final Object o : libs) { libraries.add((String) o); } return libraries; } catch (final XmlRpcException e) { throw new RobotEnvironmentException("Unable to communicate with XML-RPC server", e); } } @Override public String getStandardLibraryPath(final String libName) { try { return (String) callRpcFunction("getStandardLibraryPath", libName); } catch (final XmlRpcException e) { throw new RobotEnvironmentException("Unable to communicate with XML-RPC server", e); } } @Override public String getRobotVersion() { try { return (String) callRpcFunction("getRobotVersion"); } catch (final XmlRpcException e) { throw new RobotEnvironmentException("Unable to communicate with XML-RPC server", e); } } @Override public void createLibdocForStdLibrary(final String resultFilePath, final String libName, final String libPath) { createLibdoc(resultFilePath, libName, libPath, new EnvironmentSearchPaths()); } @Override public void createLibdocForThirdPartyLibrary(final String resultFilePath, final String libName, final String libPath, final EnvironmentSearchPaths additionalPaths) { createLibdoc(resultFilePath, libName, libPath, additionalPaths); } private void createLibdoc(final String resultFilePath, final String libName, final String libPath, final EnvironmentSearchPaths additionalPaths) { try { final String base64EncodedLibFileContent = (String) callRpcFunction("createLibdoc", libName, newArrayList(additionalPaths.getExtendedPythonPaths(interpreterType)), newArrayList(additionalPaths.getClassPaths())); final byte[] bytes = Base64.getDecoder().decode(base64EncodedLibFileContent); if (bytes.length > 0) { final File libdocFile = new File(resultFilePath); if (!libdocFile.exists()) { libdocFile.createNewFile(); } Files.write(bytes, libdocFile); } } catch (final XmlRpcException | IOException e) { final String additional = libPath.isEmpty() ? "" : ". Library path '" + libPath + "', result file '" + resultFilePath + "'"; throw new RobotEnvironmentException( "Unable to generate library specification file for library '" + libName + "'" + additional, e); } } @Override public List<File> getModulesSearchPaths() { try { final List<File> libraries = newArrayList(); final Object[] paths = (Object[]) callRpcFunction("getModulesSearchPaths"); for (final Object o : paths) { if (!"".equals(o)) { libraries.add(new File((String) o)); } } return libraries; } catch (final XmlRpcException e) { throw new RobotEnvironmentException("Unable to obtain modules search path", e); } } @Override public Optional<File> getModulePath(final String moduleName, final EnvironmentSearchPaths additionalPaths) { try { final String path = (String) callRpcFunction("getModulePath", moduleName, newArrayList(additionalPaths.getExtendedPythonPaths(interpreterType)), newArrayList(additionalPaths.getClassPaths())); return Optional.of(new File(path)); } catch (final XmlRpcException e) { throw new RobotEnvironmentException("Unable to find path of '" + moduleName + "' module", e); } } @Override public List<String> getClassesFromModule(final File moduleLocation, final String moduleName, final EnvironmentSearchPaths additionalPaths) { try { final List<String> classes = newArrayList(); final Object[] libs = (Object[]) callRpcFunction("getClassesFromModule", moduleLocation.getAbsolutePath(), moduleName, newArrayList(additionalPaths.getExtendedPythonPaths(interpreterType)), newArrayList(additionalPaths.getClassPaths())); for (final Object o : libs) { classes.add((String) o); } return classes; } catch (final XmlRpcException e) { throw new RobotEnvironmentException("Unable to find classes in module " + moduleLocation.getAbsolutePath(), e); } } @Override public Boolean isVirtualenv() { try { return (Boolean) callRpcFunction("isVirtualenv"); } catch (final XmlRpcException e) { throw new RobotEnvironmentException("Unable to check if is virtualenv.", e); } } private Object callRpcFunction(final String functionName, final Object... arguments) throws XmlRpcException { final Object rpcResult = client.execute(functionName, arguments); return resultOrException(rpcResult); } private static Object resultOrException(final Object rpcCallResult) { final Map<?, ?> result = (Map<?, ?>) rpcCallResult; Preconditions.checkArgument(result.size() == 2); Preconditions.checkArgument(result.containsKey("result")); Preconditions.checkArgument(result.containsKey("exception")); if (result.get("exception") != null) { final String exception = (String) result.get("exception"); final String indent = Strings.repeat(" ", 12); final String indentedException = indent + exception.replaceAll("\n", "\n" + indent); throw new RobotEnvironmentException("RED python session problem. Following exception has been thrown by " + "python service:\n" + indentedException); } return result.get("result"); } @SuppressWarnings("serial") static class RobotCommandExecutorException extends RuntimeException { RobotCommandExecutorException(final String message) { super(message); } RobotCommandExecutorException(final String message, final Throwable cause) { super(message, cause); } } private static class XmlRpcTypeFactoryWithNil extends TypeFactoryImpl { // Value null is not a part of xml-rpc specification, it is an // extension, so apache library // handles it with namespace added (<ex:nil>); unfortunately many other // libraries does not // handle <ex:nil>, but handles <nil> instead, which is not even a part // of specification. // This is so common that it is de-facto standard. This class is // responsible for handling // <nil> tags public XmlRpcTypeFactoryWithNil(final XmlRpcController controller) { super(controller); } @Override public TypeParser getParser(final XmlRpcStreamConfig config, final NamespaceContextImpl context, final String uri, final String localName) { if (NullSerializer.NIL_TAG.equals(localName) || NullSerializer.EX_NIL_TAG.equals(localName)) { return new NullParser(); } else { return super.getParser(config, context, uri, localName); } } @Override public TypeSerializer getSerializer(final XmlRpcStreamConfig config, final Object object) throws SAXException { if (object == null) { return new TypeSerializerImpl() { @Override public void write(final ContentHandler handler, final Object o) throws SAXException { handler.startElement("", VALUE_TAG, VALUE_TAG, ZERO_ATTRIBUTES); handler.startElement("", NullSerializer.NIL_TAG, NullSerializer.NIL_TAG, ZERO_ATTRIBUTES); handler.endElement("", NullSerializer.NIL_TAG, NullSerializer.NIL_TAG); handler.endElement("", VALUE_TAG, VALUE_TAG); } }; } else { return super.getSerializer(config, object); } } } }