/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.enterprise.client; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StreamTokenizer; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.script.ScriptEngine; import javax.script.ScriptException; import jline.ArgumentCompletor; import jline.Completor; import jline.ConsoleReader; import jline.MultiCompletor; import jline.SimpleCompletor; import mazz.i18n.Msg; import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import org.rhq.bindings.ScriptEngineFactory; import org.rhq.bindings.util.PackageFinder; import org.rhq.core.domain.auth.Subject; import org.rhq.enterprise.client.commands.ClientCommand; import org.rhq.enterprise.client.commands.ScriptCommand; import org.rhq.enterprise.client.script.CommandLineParseException; import org.rhq.enterprise.client.utility.CLIMetadataProvider; import org.rhq.enterprise.client.utility.CodeCompletionCompletorWrapper; import org.rhq.enterprise.client.utility.DummyCodeCompletion; import org.rhq.enterprise.clientapi.RemoteClient; import org.rhq.scripting.CodeCompletion; import org.rhq.scripting.ScriptEngineInitializer; /** * @author Greg Hinkle * @author Simeon Pinder */ public class ClientMain { // I18N messaging private static final Msg MSG = ClientI18NFactory.getMsg(); // Stored command map. Key to instance that handles that command. private static Map<String, ClientCommand> commands = new HashMap<String, ClientCommand>(); public static final int DEFAULT_CONSOLE_WIDTH = 80; // JLine console reader private ConsoleReader consoleReader; // for feedback to user. private PrintWriter outputWriter; // Local storage of credentials for this session/client private String transport = null; private String host = null; private int port; private String user; private String pass; private String language; private ArrayList<String> notes = new ArrayList<String>(); private RemoteClient remoteClient; // The subject that will be used to carry out all requested actions private Subject subject; private CodeCompletion codeCompletion; private boolean interactiveMode = true; private Recorder recorder = new NoOpRecorder(); private ScriptEngine engine; private ScriptEngineInitializer scriptEngineInitializer; private class StartupConfiguration { public boolean askForPassword; public boolean displayUsage; public List<String> commandsToExec; public boolean invalidArgs; public boolean showVersionAndExit; public boolean showDetailedVersion; public void process() throws Exception { if (invalidArgs) { displayUsage(); throw new IllegalArgumentException(MSG.getMsg(ClientI18NResourceKeys.BAD_ARGS)); } if (displayUsage) { displayUsage(); } if (askForPassword) { setPass(getConsoleReader().readLine("password: ", (char) 0)); } if (isInteractiveMode()) { String version = showDetailedVersion ? Version.getProductNameAndVersionBuildInfo() : Version .getProductNameAndVersion(); outputWriter.println(version); if (showVersionAndExit) { // If -v was the only option specified, exit after printing the version. System.exit(0); } } if (getUser() != null && getPass() != null) { ClientCommand loginCmd = getCommands().get("login"); List<String> argsList = new ArrayList<String>(6); // 6 args at most argsList.add("login"); argsList.add(getUser()); argsList.add(getPass()); String host = getHost(); int port = getPort(); String transport = getTransport(); if (host != null) { argsList.add(host); if (port != 0) { argsList.add(String.valueOf(port)); if (transport != null) { argsList.add(transport); } } } loginCmd.execute(ClientMain.this, argsList.toArray(new String[argsList.size()])); if (!loggedIn()) { if (isInteractiveMode()) { return; } else { System.exit(1); } } } if (commandsToExec != null && !commandsToExec.isEmpty()) { getCommands().get("exec").execute(ClientMain.this, commandsToExec.toArray(new String[commandsToExec.size()])); } } } public static void main(String[] args) { initCommands(); try { new ClientMain().run(args); } catch (Exception e) { e.printStackTrace(); System.exit(1); } } private void run(String[] args) throws Exception { // capture startup arguments and setup the properties //from them StartupConfiguration config = processArguments(args); //initialize the CLI initialize(); //process the arguments now that we are initialized config.process(); if (isInteractiveMode()) { // begin client access loop inputLoop(); } } private static void initCommands() { for (Class<ClientCommand> commandClass : ClientCommand.COMMANDS) { ClientCommand command; try { command = commandClass.newInstance(); commands.put(command.getPromptCommandString(), command); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } private void initScriptCommand() { ScriptCommand sc = (ScriptCommand) commands.get("exec"); sc.initClient(this); } private void initCodeCompletion() { this.codeCompletion.setScriptContext(getScriptEngine().getContext()); this.codeCompletion.setMetadataProvider(new CLIMetadataProvider()); } public ClientMain() { // initialize the printwriter to system.out for console conversations outputWriter = new PrintWriter(System.out, true); } private void initialize() throws IOException { // this.inputReader = new BufferedReader(new // InputStreamReader(System.in)); // initialize the printwriter to system.out for console conversations this.outputWriter = new PrintWriter(System.out, true); //ScriptCommand is super special because it handles executing all the code for us initScriptCommand(); if (isInteractiveMode()) { // Initialize JLine console elements. consoleReader = new jline.ConsoleReader(); // Setup the command line completers for listed actions for the user before login // completes initial commands available Completor commandCompletor = new SimpleCompletor(commands.keySet().toArray(new String[commands.size()])); // completes help arguments (basically, help <command>) Completor helpCompletor = new ArgumentCompletor(new Completor[] { new SimpleCompletor("help"), new SimpleCompletor(commands.keySet().toArray(new String[commands.size()])) }); this.codeCompletion = ScriptEngineFactory.getCodeCompletion(getLanguage()); if (codeCompletion == null) { //the language module for this language doesn't support code completion //let's provide a dummy one. codeCompletion = new DummyCodeCompletion(); } initCodeCompletion(); consoleReader.addCompletor(new MultiCompletor(new Completor[] { new CodeCompletionCompletorWrapper(codeCompletion, outputWriter, consoleReader), helpCompletor, commandCompletor })); // enable pagination consoleReader.setUsePagination(true); } } public String getUserInput(String prompt) { String input_string = ""; while ((input_string != null) && (input_string.trim().length() == 0)) { if (prompt == null) { if (!loggedIn()) { prompt = "unconnected$ "; } else { // Modify the prompt to display host:port(logged-in-user) String loggedInUser = ""; if ((getSubject() != null) && (getSubject().getName() != null)) { loggedInUser = getSubject().getName(); } if (loggedInUser.trim().length() > 0) { prompt = loggedInUser + "@" + host + ":" + port + "$ "; } else { prompt = host + ":" + port + "$ "; } } } try { outputWriter.flush(); input_string = consoleReader.readLine(prompt); } catch (Exception e) { input_string = null; } } return input_string; } public ConsoleReader getConsoleReader() { return consoleReader; } /** * Indicates whether the 'Subject', used for all authenticated actions, is currently logged in. * * @return flag indicating status of realtime check. */ public boolean loggedIn() { return subject != null && remoteClient != null && remoteClient.isLoggedIn(); } /** * This enters in an infinite loop. Because this never returns, the current thread never dies and hence the agent * stays up and running. The user can enter agent commands at the prompt - the commands are sent to the agent as if * the user is a remote client. */ private void inputLoop() { // we need to start a new thread and run our loop in it; otherwise, our // shutdown hook doesn't work Thread inputLoopThread = new Thread(new LoopRunnable()); inputLoopThread.setName("RHQ Client Prompt Input Thread"); inputLoopThread.setDaemon(false); inputLoopThread.start(); } public boolean executePromptCommand(String[] args) throws Exception { String cmd = args[0]; if (commands.containsKey(cmd)) { ClientCommand command = commands.get(cmd); if (shouldDisplayHelp(args)) { outputWriter.println("Usage: " + command.getSyntax()); outputWriter.println(command.getDetailedHelp()); return true; } try { boolean response = command.execute(this, args); processNotes(outputWriter); outputWriter.println(""); return response; } catch (CommandLineParseException e) { outputWriter.println(command.getPromptCommandString() + ": " + e.getMessage()); outputWriter.println("Usage: " + command.getSyntax()); } catch (ArrayIndexOutOfBoundsException e) { outputWriter.println(command.getPromptCommandString() + ": An incorrect number of arguments was specified."); outputWriter.println("Usage: " + command.getSyntax()); } } else { boolean result = commands.get("exec").execute(this, args); if (loggedIn()) { this.codeCompletion.setScriptContext(getScriptEngine().getContext()); } return result; } return true; } private boolean shouldDisplayHelp(String[] args) { return args.length >= 2 && (args[1].equals("-h") || args[1].equals("--help")); } /** * Meant to display small note/helpful ui messages to the user as feedback from the previous command. * * @param outputWriter2 * reference to printWriter. */ private void processNotes(PrintWriter outputWriter2) { if ((outputWriter2 != null) && (notes.size() > 0)) { for (String line : notes) { outputWriter2.println("-> " + line); } notes.clear(); } } /** * Given a command line, this will parse each argument and return the argument array. * * @param cmdLine * the command line * @return the array of command line arguments */ public String[] parseCommandLine(String cmdLine) { if (cmdLine == null) { return new String[] { "" }; } ByteArrayInputStream in = new ByteArrayInputStream(cmdLine.getBytes()); StreamTokenizer strtok = new StreamTokenizer(new InputStreamReader(in)); List<String> args = new ArrayList<String>(); boolean keep_going = true; boolean isScriptFileCommand = false; boolean isNamedArgs = false; // we don't want to parse numbers and we want ' to be a normal word // character strtok.ordinaryChars('0', '9'); strtok.ordinaryChar('.'); strtok.ordinaryChar('-'); strtok.ordinaryChar('\''); strtok.wordChars(33, 127); // parse the command line while (keep_going) { int nextToken; try { // if we are executing a script file and have reached the arguments, we // want to reset the tokenizer's syntax so that handle single and double // quotes correctly. if (isScriptFileCommand && args.size() > 2 && args.get(args.size() - 2).equals("-f")) { strtok.resetSyntax(); strtok.ordinaryChars('0', '9'); strtok.ordinaryChar('.'); strtok.ordinaryChar('-'); strtok.quoteChar('\''); strtok.quoteChar('"'); strtok.wordChars(33, 33); strtok.wordChars(35, 38); strtok.wordChars(40, 127); } nextToken = strtok.nextToken(); } catch (IOException e) { nextToken = StreamTokenizer.TT_EOF; } if (nextToken == java.io.StreamTokenizer.TT_WORD) { if (args.size() > 0 && strtok.sval.equals("-f")) { isScriptFileCommand = true; } args.add(strtok.sval); if (strtok.sval.equals("--args-style=named")) { isNamedArgs = true; } } else if (nextToken == '\"' || nextToken == '\'') { args.add(strtok.sval); } else if ((nextToken == java.io.StreamTokenizer.TT_EOF) || (nextToken == java.io.StreamTokenizer.TT_EOL)) { keep_going = false; } } if (isNamedArgs) { List<String> newArgs = new ArrayList<String>(); int namedArgsIndex = args.indexOf("--args-style=named"); for (int i = 0; i <= namedArgsIndex; ++i) { newArgs.add(args.get(i)); } String namedArg = null; for (int i = namedArgsIndex + 1; i < args.size(); ++i) { if (namedArg == null && args.get(i).endsWith("=")) { namedArg = args.get(i); } else if (namedArg != null) { newArgs.add(args.get(i - 1) + args.get(i)); namedArg = null; } else { newArgs.add(args.get(i)); } } return newArgs.toArray(new String[newArgs.size()]); } return args.toArray(new String[args.size()]); } private void displayUsage() { outputWriter .println("rhq-cli.sh [-h] [-u user] [-p pass] [-P] [-s host] [-t port] [-v] [-f file]|[-c command]"); } StartupConfiguration processArguments(String[] args) throws IllegalArgumentException, IOException { StartupConfiguration config = new StartupConfiguration(); String sopts = "-:hu:p:Ps:t:r:c:f:v"; LongOpt[] lopts = { new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h'), new LongOpt("user", LongOpt.REQUIRED_ARGUMENT, null, 'u'), new LongOpt("password", LongOpt.REQUIRED_ARGUMENT, null, 'p'), new LongOpt("prompt", LongOpt.OPTIONAL_ARGUMENT, null, 'P'), new LongOpt("host", LongOpt.REQUIRED_ARGUMENT, null, 's'), new LongOpt("port", LongOpt.REQUIRED_ARGUMENT, null, 't'), new LongOpt("transport", LongOpt.REQUIRED_ARGUMENT, null, 'r'), new LongOpt("command", LongOpt.REQUIRED_ARGUMENT, null, 'c'), new LongOpt("file", LongOpt.NO_ARGUMENT, null, 'f'), new LongOpt("version", LongOpt.NO_ARGUMENT, null, 'v'), new LongOpt("language", LongOpt.REQUIRED_ARGUMENT, null, 'l'), new LongOpt("args-style", LongOpt.REQUIRED_ARGUMENT, null, -2) }; Getopt getopt = new Getopt("Cli", args, sopts, lopts, false); int code; List<String> execCmdLine = new ArrayList<String>(); execCmdLine.add("exec"); while ((code = getopt.getopt()) != -1) { switch (code) { case ':': case '?': { config.invalidArgs = true; break; } case 1: { // this catches non-option arguments which can be passed when running a script in non-interactive mode // with -f or running a single command in non-interactive mode with -c. execCmdLine.add(getopt.getOptarg()); break; } case 'h': { config.displayUsage = true; break; } case 'u': { this.user = getopt.getOptarg(); break; } case 'p': { this.pass = getopt.getOptarg(); break; } case 'P': { config.askForPassword = true; break; } case 'c': { interactiveMode = false; execCmdLine.add(getopt.getOptarg()); break; } case 'f': { interactiveMode = false; execCmdLine.add("-f"); execCmdLine.add(getopt.getOptarg()); break; } case -2: { execCmdLine.add("--args-style=" + getopt.getOptarg()); break; } case 's': { setHost(getopt.getOptarg()); break; } case 'r': { setTransport(getopt.getOptarg()); break; } case 't': { String portArg = getopt.getOptarg(); try { setPort(Integer.parseInt(portArg)); } catch (Exception e) { outputWriter.println("Invalid port [" + portArg + "]"); System.exit(1); } break; } case 'v': { config.showDetailedVersion = true; if (args.length == 1) { config.showVersionAndExit = true; } break; } case 'l': this.language = getopt.getOptarg(); break; } } if (!interactiveMode) { config.commandsToExec = execCmdLine; } return config; } public RemoteClient getRemoteClient() { return remoteClient; } public void setRemoteClient(RemoteClient remoteClient) { this.remoteClient = remoteClient; initScriptCommand(); if (isInteractiveMode()) { initCodeCompletion(); } } public Subject getSubject() { return subject; } public void setSubject(Subject subject) { this.subject = subject; } public String getTransport() { return transport; } public void setTransport(String transport) { this.transport = transport; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } public String getPass() { return pass; } public void setPass(String pass) { this.pass = pass; } public PrintWriter getPrintWriter() { return outputWriter; } public void setPrintWriter(PrintWriter writer) { this.outputWriter = writer; } public String getLanguage() { return this.language == null ? "javascript" : this.language; } public int getConsoleWidth() { //the console reader might be null when this method is asked for the output //width in non-interactive mode where we don't attach to stdin. return this.consoleReader == null ? DEFAULT_CONSOLE_WIDTH : this.consoleReader.getTermwidth(); } public ScriptEngine getScriptEngine() { if (engine == null) { try { engine = ScriptEngineFactory.getScriptEngine(getLanguage(), new PackageFinder(Arrays.asList(getLibDir())), null); if (engine == null) { throw new IllegalStateException("The scripting language '" + getLanguage() + "' could not be loaded."); } scriptEngineInitializer = ScriptEngineFactory.getInitializer(getLanguage()); } catch (ScriptException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return engine; } public String getUsefulErrorMessage(ScriptException e) { return scriptEngineInitializer.extractUserFriendlyErrorMessage(e); } public Map<String, ClientCommand> getCommands() { return commands; } /** * This method allows ClientCommands to insert a small note to be displayed after the command has been executed. A * note can be an indication of a problem that was handled or a note about some option that should be changed. * * These notes are meant to be terse, and pasted/purged at the end of every command execution. * * @param note the note to be displayed, e.g. "There were errors retrieving some data from the server objects. See * System Admin." */ public void addMenuNote(String note) { if ((note != null) && (note.trim().length() > 0)) { notes.add(note); } } public boolean isInteractiveMode() { return interactiveMode; } public Recorder getRecorder() { return recorder; } public void setRecorder(Recorder recorder) { this.recorder = recorder; } private static File getLibDir() { String cwd = System.getProperty("user.dir"); return new File(cwd, "lib"); } private class LoopRunnable implements Runnable { @Override public void run() { while (true) { String cmd; cmd = getUserInput(null); try { recorder.record(cmd); } catch (IOException e) { e.printStackTrace(); } try { // parse the command into separate arguments and execute String[] cmd_args = parseCommandLine(cmd); boolean can_continue = executePromptCommand(cmd_args); // break the input loop if the prompt command told us to exit // if we are not in daemon mode, this really will end up killing the agent if (!can_continue) { break; } } catch (Throwable t) { // outputWriter.println(ThrowableUtil.getAllMessages(t)); t.printStackTrace(outputWriter); // LOG.debug(t, // AgentI18NResourceKeys.COMMAND_FAILURE_STACK_TRACE); } } } } }