package org.myrobotlab.service; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import org.myrobotlab.codec.CodecUri; import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.InvokerUtils; import org.myrobotlab.framework.Message; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.ServiceType; import org.myrobotlab.framework.Status; import org.myrobotlab.framework.StreamGobbler; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.interfaces.ServiceInterface; import org.slf4j.Logger; /** * Cli - This is a command line interface to MyRobotLab. It supports some shell * like commands such as "cd", and "ls" Use the command "help" to display help. * * should be a singleton in a process. This does not seem to work on * cygwin/windows, but it does work in a command prompt. Linux/Mac can use this * via a Terminal / Console window. * * @author GroG * */ public class Cli extends Service { private static final long serialVersionUID = 1L; public final static Logger log = LoggerFactory.getLogger(Cli.class); public final static String cd = "cd"; public final static String pwd = "pwd"; public final static String ls = "ls"; public final static String help = "help"; public final static String question = "?"; transient private HashMap<String, Pipe> pipes = new HashMap<String, Pipe>(); // my "real" std:in & std:out transient Decoder in; transient OutputStream os; ArrayList<String> history = new ArrayList<String>(); // transient FileOutputStream fos; String cwd = "/"; String prompt = "#"; // active relay - could be list - but lets start simple String attached = null; // transient OutputStream attachedIn = null; transient OutputStream attachedIn = null; transient StreamGobbler attachedOut = null; // ================= Decoder Begin ================= // FIXME - tab to autoComplete ! // FIXME - needs refactor / merge with StreamGobbler // FIXME - THIS CONCEPT IS SOOOOOO IMPORTANT // - its a Central Point Controller - where input (any InputStream) can send // data to be decoded on a very common API e.g. (proto // scheme)(host)/api/inputEncoding/responseEncoding/instance/(method)/(params...) // Agent + (RemoteAdapter/WebGui/Netosphere) + Cli(command processor part // with InStream/OutStream) - is most Big-Fu ! public class Decoder extends Thread { // public String cwd = "/"; CHANGED THIS - it now is GLOBAL - :P String name; transient Cli cli; transient InputStream is; // TODO ecoding defaults & methods to change // FIXME - need reference to OutputStream to return String inputEncoding = CodecUtils.TYPE_URI; // REST JSON String outputEncoding = CodecUtils.TYPE_JSON; // JSON / JSON MSG public Decoder(Cli cli, String name, InputStream is) { super(String.format("cli-decoder-%s", name)); this.cli = cli; this.name = name; this.is = is; } StringBuffer sb = new StringBuffer(); @Override public void run() { try { writePrompt(); String line = null; int c = 0; while ((c = is.read()) != -1) { // handle up arrow - tab autoComplet - and regular \n // if not one of these - then append the next character if (c != '\n') { sb.append((char)c); continue; } else { line = sb.toString(); sb.setLength(0); } line = line.trim(); // order of precedence // 1. execute cli methods // 2. execute service methods // 3. execute Runtime methods if (line.length() == 0) { writePrompt(); continue; } // FIXME - don't need a buffered writer here // everything should be OutputStream if (attachedIn != null) { if ("detach".equals(line)) { // multiple in future mabye detach(); continue; } // relaying command to another process try { attachedIn.write(String.format("%s\n",line).getBytes()); attachedIn.flush(); } catch (Exception e) { log.error("std:in ---(agent)---X---> process ({})", name); log.info("detaching... "); detach(); } // writePrompt(); continue; } if (line.startsWith(cd)) { String path = "/"; if (line.length() > 2) { // FIXME - cheesy - "look" for relative directories // ! if (!line.contains("/")) { path = "/" + line.substring(3); } else { path = line.substring(3); } } cli.cd(path); } else if (line.startsWith(help)) { // TODO dump json command object // which has a map of commands } else if (line.startsWith(pwd)) { out(String.format("%s\n", cwd).getBytes()); } else if (line.startsWith(ls)) { String path = cwd; // <-- path = if (line.length() > 3) { path = line.substring(3); } path = path.trim(); // absolute path always cli.ls(path); } else if (line.startsWith("lp")) { // cli.lp(path??); // cli.lp(); } else { String path = null; if (line.startsWith("/")) { path = String.format("/%s%s", CodecUtils.PREFIX_API, line); } else { path = String.format("/%s%s%s", CodecUtils.PREFIX_API, cwd, line); } log.info(path); try { Object ret = null; // Object ret = InvokerUtils.invoke(path); // InvokerUtils removed - need more access & control Message msg = CodecUri.decodePathInfo(path); ServiceInterface si = Runtime.getService(msg.name); if (si == null) { ret = Status.error("could not find service %s", msg.name); } else { ret = si.invoke(msg.method, msg.data); } if (ret != null && ret instanceof Serializable) { // configurable use log or system.out ? // FIXME - make getInstance configurable // Encoder // reference !!! out(CodecUtils.toJson(ret).getBytes()); } } catch (Exception e) { Logging.logError(e); } } writePrompt(); } // while read line } catch (IOException e) { log.error("leaving Decoder"); Logging.logError(e); } /* * DON'T CLOSE - WE MAY WANT TO RE-ATTACH finally { * FileIO.closeStream(is); } */ log.info("LEAVING STDIN READING!"); } } // ================= Decoder End ================= public void writePrompt() throws IOException { out(getPrompt().getBytes()); } public String getPrompt() { return String.format("%s:%s%s ", Runtime.getInstance().getName(), cwd, prompt); } public Object process(String line) throws IOException { // FIXME - must read char by char to process up-arrow history commands // in.read() line = line.trim(); if (line.length() == 0) { writePrompt(); return null; } if (attachedIn != null) { if ("detach".equals(line)) { // multiple in future mabye detach(); return null; } // relaying command to another process attachedIn.write(String.format("%s\n",line).getBytes()); attachedIn.flush(); // writePrompt(); return null; } if (line.startsWith(cd)) { String path = "/"; if (line.length() > 2) { // FIXME - cheesy - "look" for relative directories // ! if (!line.contains("/")) { path = "/" + line.substring(3); } else { path = line.substring(3); } } cd(path); } else if (line.startsWith(help)) { // TODO dump json command object // which has a map of commands } else if (line.startsWith(pwd)) { out(cwd.getBytes()); } else if (line.startsWith(ls)) { String path = cwd; // <-- path = if (line.length() > 3) { path = line.substring(3); } path = path.trim(); // absolute path always ls(path); } else if (line.startsWith("lp")) { // cli.lp(path??); // cli.lp(); } else { String path = null; if (line.startsWith("/")) { path = String.format("/%s%s", CodecUtils.PREFIX_API, line); } else { path = String.format("/%s%s%s", CodecUtils.PREFIX_API, cwd, line); } log.info(path); try { // if service is local - we can trasact Message msg = CodecUri.decodePathInfo(path); Object ret = null; if (Runtime.getService(msg.name).isLocal()) { ret = InvokerUtils.invoke(path); } else { // FIXME - sendBlocking is not getting a return ret = sendBlocking(msg.name, msg.method, msg.data); } if (ret != null && ret instanceof Serializable) { // configurable use log or system.out ? // FIXME - make getInstance configurable // Encoder // reference !!! out(CodecUtils.toJson(ret).getBytes()); } /* * Old Way Message msg = Encoder.decodePathInfo(path); if (msg != null) * { info("incoming msg[%s]", msg); * * // get service - is this a security breech ? ServiceInterface si = * Runtime.getService(msg.name); Object ret = si.invoke(msg.method, * msg.data); * * // want message ? or just data ? // configurable ... // if you data * with tags - you might as well do // message ! // - return only * callbacks this way -> // si.in(msg); if (ret != null && ret * instanceof Serializable) { // configurable use log or system.out ? // * FIXME - make getInstance configurable // Encoder // reference !!! * out(Encoder.toJson(ret).getBytes()); } } */ } catch (Exception e) { Logging.logError(e); } } writePrompt(); return null; } public class Pipe { public String name; public transient InputStream out; public transient OutputStream in; public Pipe(String name, InputStream out, OutputStream in) { this.name = name; this.out = out; this.in = in; } } /** * Command Line Interpreter - used for processing encoded (default RESTful) * commands from std in and returning results in (default JSON) encoded return * messages. * * Has the ability to pipe to another process - if attached to another process * handle, and the ability to switch between many processes * * @param n */ public Cli(String n) { super(n); } /** * add an i/o pair to this cli for the possible purpose attaching * * @param name * @param process * @return */ public void add(String name, InputStream out, OutputStream in) { pipes.put(name, new Pipe(name, out, in)); } public boolean attach() { return attach(null); } /** * attach to another processes' Cli * * @param name * @return */ public boolean attach(String name) { if (pipes.size() == 1) { // only 1 choice for (String key : pipes.keySet()) { name = key; } } if (!pipes.containsKey(name)) { error("%s not found", name); return false; } Pipe pipe = pipes.get(name); attached = name; // stdin will now be relayed and not interpreted attachedIn = pipe.in; // need to fire up StreamGobbler // (new Process) --- stdout --> (Agent Process) StreamGobbler ---> // stdout ArrayList<OutputStream> outRelay = new ArrayList<OutputStream>(); if (os != null) { outRelay.add(os); } /* if (fos != null) { outRelay.add(fos); } */ attachedOut = new StreamGobbler(pipe.out, outRelay, name); attachedOut.start(); // grab input output from foreign process // introduce - hello - get response check with // timer - because if a Cli is not there // we cant attach to it return true; } public void attachStdIO() { if (in == null) { in = new Decoder(this, "stdin", System.in); in.start(); } else { log.info("stdin already attached"); } // if I'm not an agent then just writing to System.out is fine // because all of it will be relayed to an Agent if I'm spawned // from an Agent.. or // If I'm without an Agent I'll just do the logging I was directed // to on the command line if (os == null) { os = System.out; } else { log.info("stdout already attached"); } try { // if I'm an agent I'll do dual logging /* if (fos == null && Runtime.isAgent()) { fos = new FileOutputStream("agent.log"); } */ } catch (Exception e) { Logging.logError(e); } } public String cd(String path) { cwd = path; return path; } private void detach() throws IOException { out(String.format("detaching from %s", attached).getBytes()); attached = null; attachedIn = null; if (attachedOut != null) { attachedOut.interrupt(); } attachedOut = null; } public void detachStdIO() { if (in != null) { in.interrupt(); } } public String echo(String msg) { return msg; } /* * public ArrayList<ProcessData> lp(){ return * Runtime.getAgent().getProcesses(); } */ /** * FIXME !!! return Object[] and let Cli command processor handle encoding for * return * * path is always absolute never relative * * @param path * @throws IOException */ public void ls(String path) throws IOException { String[] parts = path.split("/"); if (path.equals("/")) { // FIXME don't do this here !!! out(String.format("%s\n", CodecUtils.toJson(Runtime.getServiceNames()).toString()).getBytes()); } else if (parts.length == 2 && !path.endsWith("/")) { // FIXME don't do this here !!! out(String.format("%s\n", CodecUtils.toJson(Runtime.getService(parts[1])).toString()).getBytes()); } else if (parts.length == 2 && path.endsWith("/")) { ServiceInterface si = Runtime.getService(parts[1]); // FIXME don't do this here !!! out(String.format("%s\n", CodecUtils.toJson(si.getDeclaredMethodNames()).toString()).getBytes()); } // if path == /serviceName - json return ? Cool ! // if path /serviceName/ - method return } public void out(byte[] data) throws IOException { // if (Runtime.isAgent()) { if (os != null) { os.write(data); os.flush(); } /* if (fos != null) { fos.write(data); fos.flush(); } */ // } invoke("stdout", data); } public String stdout(byte[] data) { if (data != null) return new String(data); else { return ""; } } public void out(String str) throws IOException { out(str.getBytes()); } @Override public void startService() { super.startService(); attachStdIO(); } @Override public void stopService() { super.stopService(); try { if (in != null) { in.interrupt(); } in = null; if (os != null) { os.close(); } os = null; /* if (fos != null) { fos.close(); } fos = null; */ } catch (Exception e) { Logging.logError(e); } } /** * This static method returns all the details of the class without it having * to be constructed. It has description, categories, dependencies, and peer * definitions. * * @return ServiceType - returns all the data * */ static public ServiceType getMetaData() { ServiceType meta = new ServiceType(Cli.class.getCanonicalName()); meta.addDescription("command line interpreter interface"); meta.addCategory("framework"); return meta; } public static void main(String[] args) { LoggingFactory.init("ERROR"); try { Cli cli = (Cli) Runtime.start("cli", "Cli"); cli.process("help"); /* * cli.ls("/"); cli.ls("/cli"); cli.ls("/cli/"); */ // cli.test(); } catch (Exception e) { Logging.logError(e); } } }