/* * RapidMiner * * Copyright (C) 2001-2011 by Rapid-I and the contributors * * Complete list of developers available at our web site: * * http://rapid-i.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.tools; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import com.rapidminer.RapidMiner; import com.rapidminer.io.process.XMLTools; /** When started for the first time, listens on a given socket. * If started for the second time, contacts this socket and * passes command line options to this socket. * The port number on which we listen is stored in a file in the users * home directory. * * In order to use this class, first try to contact another instance by calling * {@link #sendToOtherInstanceIfUp(String...)}. If true is returned, commands were * sent to the other instance and we can terminate. If false is returned, the other * instance is not running. In that case, call {@link #installListener(RemoteControlHandler)}. * Now, when another instance is started, callbacks are made to the {@link RemoteControlHandler} * passed. Precisely this is done when calling {@link #defaultLaunchWithArguments(String[], RemoteControlHandler)}. * * @author Simon Fischer * */ public class LaunchListener { /** Callbacks will be made to this interface when another client contacts us. */ public static interface RemoteControlHandler { /** Callback method called when another client starts. */ boolean handleArguments(String[] args); } private static final String FAILED = "<failed/>"; private static final String UNKNOWN_COMMAND = "<unknown-command/>"; private static final String REJECTED = "<rejected/>"; private static final String OK = "<ok/>"; private static final String HELLO_MESSAGE = "<hi>I am RapidMiner. I understand a bit of XML.</hi>"; private static final Logger LOGGER = Logger.getLogger(LaunchListener.class.getName()); private static final LaunchListener INSTANCE = new LaunchListener(); private RemoteControlHandler handler; private LaunchListener() { } private File getSocketFile() { return FileSystemService.getUserConfigFile("socket"); } public static LaunchListener getInstance() { return INSTANCE; } private void installListener(final RemoteControlHandler handler) throws IOException { // port 0 = let system assign port // backlog 1 = we don't expect simultaneous requests final ServerSocket serverSocket = new ServerSocket(0, 1, InetAddress.getLocalHost()); final int port = serverSocket.getLocalPort(); final File socketFile = getSocketFile(); LOGGER.info("Listening for other instances on port "+port+". Writing "+socketFile+"."); PrintStream socketOut = new PrintStream(socketFile); socketOut.println(""+port); socketOut.close(); RapidMiner.addShutdownHook(new Runnable() { @Override public void run() { LOGGER.config("Deleting "+socketFile); socketFile.delete(); } }); Thread listenerThread = new Thread("Launch-Listener") { @Override public void run() { LaunchListener.this.handler = handler; while (true) { Socket client; try { client = serverSocket.accept(); // We don't spawn another thread here. // Assume no malicious client and communication is quick. talkToSecondClient(client); } catch (IOException e) { LogService.getRoot().log(Level.WARNING, "Error accepting socket connection: "+e, e); } } } }; listenerThread.setDaemon(true); listenerThread.start(); } private void talkToSecondClient(Socket client) { try { LOGGER.info("Second client launched."); PrintStream out = new PrintStream(client.getOutputStream()); out.println(HELLO_MESSAGE); Document doc = XMLTools.parse(client.getInputStream()); LOGGER.config("Read XML document from other client: "); final String command = doc.getDocumentElement().getTagName(); if ("args".equals(command)) { NodeList argsElems = doc.getDocumentElement().getElementsByTagName("arg"); List<String> args = new LinkedList<String>(); for (int i = 0; i < argsElems.getLength(); i++) { args.add(argsElems.item(i).getTextContent()); } if (handler != null) { LOGGER.config("Handling <args> command from other client."); try { if (handler.handleArguments(args.toArray(new String[args.size()]))) { out.println(OK); } else { out.println(REJECTED); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Error executing remote control command: "+e, e); out.println(FAILED); } } else { LOGGER.warning("Other client sent <args> command, but I don't have a handler installed."); out.println(FAILED); } } else { out.println(UNKNOWN_COMMAND); LOGGER.warning("Unknown command from second client: <"+command+">."); } // BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); // String line; // while ((line = in.readLine()) != null) { // LOGGER.info("Other client says: "+line); // out.println("You said: "+line); // } client.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to talk to client: "+e, e); } catch (SAXException e) { LOGGER.log(Level.WARNING, "I don't understand what the other client is trying to say: "+e, e); } } private Socket getOtherInstance() { File socketFile = getSocketFile(); if (!socketFile.exists()) { LOGGER.config("Socket file "+socketFile+" does not exist. Assuming I am the first instance."); return null; } int port; BufferedReader in = null; try { in = new BufferedReader(new FileReader(socketFile)); String portStr = in.readLine(); port = Integer.parseInt(portStr); } catch (Exception e) { LOGGER.log(Level.WARNING, "Failed to read socket file '"+socketFile+"': "+ e, e); return null; } finally { try { in.close(); } catch (IOException e) { } } LOGGER.config("Checking for running instance on port "+port+"."); try { return new Socket("localhost", port); } catch (UnknownHostException e) { LOGGER.config("Name localhost cannot be resolved. Assuming we are the first instance."); return null; } catch (IOException e) { LOGGER.config("Got exception "+e+". Assuming we are the first instance."); return null; } } // private boolean isOtherInstanceUp() { // final Socket other = getOtherInstance(); // if (other != null) { // boolean isRM; // try { // BufferedReader in = new BufferedReader(new InputStreamReader(other.getInputStream())); // isRM = readHelloMessage(in); // other.close(); // } catch (IOException e) { // LOGGER.log(Level.WARNING, "Failed to other instance: "+e, e); // return false; // } // return isRM; // } else { // return false; // } // } private boolean readHelloMessage(BufferedReader in) throws IOException { boolean isRM; String line = in.readLine(); if (HELLO_MESSAGE.equals(line)) { LOGGER.config("Found other RapidMiner instance."); isRM = true; } else { LOGGER.config("Read unknown string from other instance: "+line); isRM = false; } return isRM; } private boolean sendToOtherInstanceIfUp(String ... args) { final Socket other = getOtherInstance(); if (other == null) { return false; } try { BufferedReader in = new BufferedReader(new InputStreamReader(other.getInputStream())); boolean isRM = readHelloMessage(in); if (!isRM) { return false; } else { LOGGER.config("Sending arguments to other RapidMiner instance: "+Arrays.toString(args)); Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); Element root = doc.createElement("args"); doc.appendChild(root); for (String arg : args) { Element argElem = doc.createElement("arg"); argElem.setTextContent(arg); root.appendChild(argElem); } XMLTools.stream(doc, other.getOutputStream(), null); return true; } } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to talk to other instance: "+e, e); return false; } catch (ParserConfigurationException e) { LOGGER.log(Level.WARNING, "Cannot create XML document: "+e, e); return false; } catch (XMLException e) { LOGGER.log(Level.WARNING, "Cannot create XML document: "+e, e); return false; } finally { try { other.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to close socket: "+e, e); } } } /** Sends the arguments to the other client, if up. * @return true if other client is not up, so we must continue launching our APP. * */ public static boolean defaultLaunchWithArguments(String[] args, RemoteControlHandler handler) throws IOException { //LogService.getRoot(); ParameterService.init(); if (!getInstance().sendToOtherInstanceIfUp(args)) { getInstance().installListener(handler); return true; } else { return false; } } }