diff --git a/cli/pom.xml b/cli/pom.xml index 3e7458179db..5602beba57e 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -105,7 +105,7 @@ org.jboss.aesh - aesh + aesh-readline diff --git a/cli/src/main/java/org/jboss/as/cli/impl/CLIPrintStream.java b/cli/src/main/java/org/jboss/as/cli/impl/CLIPrintStream.java index 0ccd9cd4805..f8b9ee40d54 100644 --- a/cli/src/main/java/org/jboss/as/cli/impl/CLIPrintStream.java +++ b/cli/src/main/java/org/jboss/as/cli/impl/CLIPrintStream.java @@ -51,6 +51,10 @@ final class CLIPrintStream extends PrintStream { this.delegate = this.baseDelegate = new PrintStream(consoleOutput); } + PrintStream getDelegate() { + return delegate; + } + void captureOutput(PrintStream delegate) { if (this.delegate != this.baseDelegate) { throw new IllegalStateException("Output is already being captured"); diff --git a/cli/src/main/java/org/jboss/as/cli/impl/CommandContextImpl.java b/cli/src/main/java/org/jboss/as/cli/impl/CommandContextImpl.java index 68f62df17e2..663129611f8 100644 --- a/cli/src/main/java/org/jboss/as/cli/impl/CommandContextImpl.java +++ b/cli/src/main/java/org/jboss/as/cli/impl/CommandContextImpl.java @@ -71,16 +71,8 @@ import javax.security.sasl.RealmCallback; import javax.security.sasl.RealmChoiceCallback; import javax.security.sasl.SaslException; +import org.jboss.aesh.util.FileAccessPermission; -import org.jboss.aesh.console.AeshConsoleCallback; -import org.jboss.aesh.console.ConsoleCallback; -import org.jboss.aesh.console.ConsoleOperation; -import org.jboss.aesh.console.Process; -import org.jboss.aesh.console.helper.InterruptHook; -import org.jboss.aesh.console.settings.FileAccessPermission; -import org.jboss.aesh.console.settings.Settings; -import org.jboss.aesh.console.settings.SettingsBuilder; -import org.jboss.aesh.edit.actions.Action; import org.jboss.as.cli.Attachments; import org.jboss.as.cli.CliConfig; import org.jboss.as.cli.CliEvent; @@ -160,6 +152,8 @@ import org.jboss.as.cli.handlers.trycatch.EndTryHandler; import org.jboss.as.cli.handlers.trycatch.FinallyHandler; import org.jboss.as.cli.handlers.trycatch.TryHandler; +import org.jboss.as.cli.impl.ReadlineConsole.Settings; +import org.jboss.as.cli.impl.ReadlineConsole.SettingsBuilder; import org.jboss.as.cli.operation.CommandLineParser; import org.jboss.as.cli.operation.NodePathFormatter; import org.jboss.as.cli.operation.OperationCandidatesProvider; @@ -362,8 +356,10 @@ class CommandContextImpl implements CommandContext, ModelControllerClientFactory configTimeout = config.getCommandTimeout() == null ? DEFAULT_TIMEOUT : config.getCommandTimeout(); setCommandTimeout(configTimeout); resolveParameterValues = config.isResolveParameterValues(); - cliPrintStream = configuration.getConsoleOutput() == null ? new CLIPrintStream() : new CLIPrintStream(configuration.getConsoleOutput()); + // Stdio must be installed prior to have cliPrintStream to capture System.in/out. + // This is required by console implementation. initStdIO(); + cliPrintStream = configuration.getConsoleOutput() == null ? new CLIPrintStream() : new CLIPrintStream(configuration.getConsoleOutput()); try { initCommands(); } catch (CommandLineException e) { @@ -394,9 +390,6 @@ protected void addShutdownHook() { shutdownHook = new CliShutdownHook.Handler() { @Override public void shutdown() { - if (CommandContextImpl.this.console != null) { - CommandContextImpl.this.console.interrupt(); - } terminateSession(); }}; CliShutdownHook.add(shutdownHook); @@ -411,76 +404,28 @@ protected void initBasicConsole(InputStream consoleInput, boolean start) throws assert console == null : "the console has already been initialized"; Settings settings = createSettings(consoleInput); this.console = Console.Factory.getConsole(this, settings); - console.setCallback(initCallback()); - if(start) { - console.start(); - } - } - - private ConsoleCallback initCallback() { - return new CLIAeshConsoleCallback() { - - @Override - public int execute(ConsoleOperation output) throws InterruptedException { - // Track the active process - setActiveProcess(true); - - try { - // Actual work stuff - if (cmdCompleter == null) { - throw new IllegalStateException("The console hasn't been initialized at construction time."); - } - if (output.getBuffer() == null) - terminateSession(); - else { - handleSafe(output.getBuffer().trim()); - if (INTERACT && terminate == RUNNING) { - console.setPrompt(getPrompt()); - } - } - return 0; - - }finally{ - setActiveProcess(false); - } + this.console.setActionCallback((line) -> { + handleSafe(line); + if (console != null) { + console.setPrompt(getPrompt()); + } + }); + if (start) { + try { + console.start(); + } catch (IOException ex) { + throw new CliInitializationException(ex); } - - }; - } - - abstract class CLIAeshConsoleCallback extends AeshConsoleCallback{ - - private Process process; - - public boolean hasActiveProcess() { - return activeProcess; - } - - public void setActiveProcess(boolean activeProcess) { - this.activeProcess = activeProcess; - } - - private boolean activeProcess = false; - - @Override - public void setProcess(org.jboss.aesh.console.Process process){ - this.process = process; - } - - public int getProcessPID(){ - return process.getPID(); } } private Settings createSettings(InputStream consoleInput) { SettingsBuilder settings = new SettingsBuilder(); - if(consoleInput != null) { + if (consoleInput != null) { settings.inputStream(consoleInput); } settings.outputStream(cliPrintStream); - settings.enableExport(false); - settings.disableHistory(!config.isHistoryEnabled()); settings.historyFile(new File(config.getHistoryFileDir(), config.getHistoryFileName())); settings.historySize(config.getHistoryMaxSize()); @@ -491,21 +436,10 @@ private Settings createSettings(InputStream consoleInput) { permissions.setWritableOwnerOnly(true); settings.historyFilePermission(permissions); - settings.parseOperators(false); - settings.parsingQuotes(false); - - settings.interruptHook( - new InterruptHook() { - @Override - public void handleInterrupt(org.jboss.aesh.console.Console console, Action action) { - if(shutdownHook != null ){ - shutdownHook.shutdown(); - }else { - terminateSession(); - } - Thread.currentThread().interrupt(); - } - } + // This is called when Ctrl-C is called. + settings.interruptHook(() -> { + terminateSession(); + } ); return settings.create(); @@ -994,15 +928,17 @@ private String readLine(String prompt, boolean password) throws CommandLineExcep } if (console == null) { - initBasicConsole(null); - } else if(!console.running()) { - console.start(); + initBasicConsole(null, false); } - if (password) { - return console.readLine(prompt, (char) 0x00); - } else { - return console.readLine(prompt); + try { + if (password) { + return console.readLine(prompt, (char) 0x00); + } else { + return console.readLine(prompt); + } + } catch (IOException ex) { + throw new CommandLineException(ex); } } @@ -1601,25 +1537,13 @@ public void interact() { console.setPrompt(getPrompt()); if(!console.running()) { - console.start(); - } - // if console is already running before we have started interacting, we need to - // make sure that the prompt is correctly displayed - else { - console.redrawPrompt(); - } - if(console.isControlled()) { - console.continuous(); - } - - while(/*!isTerminated() && */console.running()){ try { - Thread.sleep(10); - } catch (InterruptedException e) { - e.printStackTrace(); + // This call is blocking. + console.start(); + } catch (IOException ex) { + throw new RuntimeException(ex); } } - INTERACT = false; } @@ -1774,29 +1698,15 @@ public void handle(final Callback[] callbacks) throws IOException, UnsupportedCa @Override public void run() { - boolean success = false; - boolean callContinuous = false; try { if (username == null || password == null) { if (console == null) { initBasicConsole(null, false); } - console.controlled(); - if (!console.running()) { - console.start(); - } else { - callContinuous = true; - } } dohandle(callbacks); - success = true; } catch (IOException | UnsupportedCallbackException | CliInitializationException e) { throw new RuntimeException(e); - } finally { - // in case of success the console will continue after connectController has finished all the initialization required - if (!success || callContinuous) { - console.continuous(); - } } } }); @@ -1834,13 +1744,9 @@ private void dohandle(Callback[] callbacks) throws IOException, UnsupportedCallb if(ERROR_ON_INTERACT) { interactionDisabled(); } - initBasicConsole(null); + initBasicConsole(null, false); } - console.setCompletion(false); - console.getHistory().setUseHistory(false); username = readLine("Username: ", false); - console.getHistory().setUseHistory(true); - console.setCompletion(true); } catch (CommandLineException e) { // the messages of the cause are lost if nested here throw new IOException("Failed to read username: " + e.getLocalizedMessage()); @@ -1862,13 +1768,9 @@ private void dohandle(Callback[] callbacks) throws IOException, UnsupportedCallb if(ERROR_ON_INTERACT) { interactionDisabled(); } - initBasicConsole(null); + initBasicConsole(null, false); } - console.setCompletion(false); - console.getHistory().setUseHistory(false); temp = readLine("Password: ", true); - console.getHistory().setUseHistory(true); - console.setCompletion(true); } catch (CommandLineException e) { // the messages of the cause are lost if nested here throw new IOException("Failed to read password: " + e.getLocalizedMessage()); diff --git a/cli/src/main/java/org/jboss/as/cli/impl/Console.java b/cli/src/main/java/org/jboss/as/cli/impl/Console.java index c0c8fe273d0..079940d0c8e 100644 --- a/cli/src/main/java/org/jboss/as/cli/impl/Console.java +++ b/cli/src/main/java/org/jboss/as/cli/impl/Console.java @@ -1,6 +1,6 @@ /* * JBoss, Home of Professional Open Source. - * Copyright 2015, Red Hat, Inc., and individual contributors + * Copyright 2016, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * @@ -24,20 +24,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; +import java.util.function.Consumer; -import org.jboss.aesh.complete.CompleteOperation; -import org.jboss.aesh.complete.Completion; -import org.jboss.aesh.console.ConsoleCallback; -import org.jboss.aesh.console.Prompt; -import org.jboss.aesh.console.settings.Settings; -import org.jboss.aesh.parser.Parser; import org.jboss.as.cli.CliInitializationException; import org.jboss.as.cli.CommandContext; import org.jboss.as.cli.CommandHistory; import org.jboss.as.cli.CommandLineCompleter; +import org.jboss.as.cli.impl.ReadlineConsole.Settings; /** * @@ -47,12 +41,8 @@ public interface Console { void addCompleter(CommandLineCompleter completer); - boolean isUseHistory(); - CommandHistory getHistory(); - void setCompletion(boolean complete); - void clearScreen(); void printColumns(Collection list); @@ -61,27 +51,15 @@ public interface Console { void printNewLine(); - String readLine(String prompt); + String readLine(String prompt) throws IOException; - String readLine(String prompt, Character mask); + String readLine(String prompt, Character mask) throws IOException; int getTerminalWidth(); int getTerminalHeight(); - /** - * Checks whether the tab-completion is enabled. - * - * @return true if tab-completion is enabled, false - otherwise - */ - boolean isCompletionEnabled(); - - /** - * Enables or disables the tab-completion. - * - * @param completionEnabled true will enable the tab-completion, false will disable it - */ - // void setCompletionEnabled(boolean completionEnabled); + void setActionCallback(Consumer cons); /** * Interrupts blocking readLine method. @@ -90,15 +68,7 @@ public interface Console { */ void interrupt(); - void controlled(); - - boolean isControlled(); - - void continuous(); - - void setCallback(ConsoleCallback consoleCallback); - - void start(); + void start() throws IOException; void stop(); @@ -106,257 +76,19 @@ public interface Console { void setPrompt(String prompt); - void setPrompt(String prompt, Character mask); - - void redrawPrompt(); - static final class Factory { public static Console getConsole(CommandContext ctx, Settings settings) throws CliInitializationException { return getConsole(ctx, null, null, settings); } - public static Console getConsole(final CommandContext ctx, InputStream is, OutputStream os, final Settings settings) throws CliInitializationException { - - org.jboss.aesh.console.Console aeshConsole = null; + public static Console getConsole(final CommandContext ctx, InputStream is, OutputStream os, Settings settings) throws CliInitializationException { + ReadlineConsole console; try { - aeshConsole = new org.jboss.aesh.console.Console(settings); - } catch (Throwable e) { - throw new CliInitializationException("Failed to initialize Aesh console", e); + console = new ReadlineConsole(ctx, settings); + } catch (IOException ex) { + throw new CliInitializationException(ex); } - - final org.jboss.aesh.console.Console finalAeshConsole = aeshConsole; - - Console console = new Console() { - - private CommandContext cmdCtx = ctx; - private org.jboss.aesh.console.Console console = finalAeshConsole; - private CommandHistory history = new HistoryImpl(); - private boolean controlled; - - @Override - public void addCompleter(final CommandLineCompleter completer) { - console.addCompletion(new Completion() { - @Override - public void complete(CompleteOperation co) { - List candidates = new ArrayList<>(); - int offset = completer.complete(cmdCtx, - co.getBuffer(), co.getCursor(), candidates); - co.setOffset(offset); - co.setCompletionCandidates(candidates); - String buffer = cmdCtx.getArgumentsString() == null ? co.getBuffer() : ctx.getArgumentsString() + co.getBuffer(); - if (co.getCompletionCandidates().size() == 1 - && co.getCompletionCandidates().get(0).getCharacters().startsWith(buffer)) - co.doAppendSeparator(true); - else - co.doAppendSeparator(false); - } - }); - } - - @Override - public boolean isUseHistory() { - return !settings.isHistoryDisabled(); - } - - @Override - public CommandHistory getHistory() { - return history; - } - - @Override - public void setCompletion(boolean complete){ - console.setCompletionEnabled(complete); - } - - @Override - public void clearScreen() { - try { - console.clear(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void printColumns(Collection list) { - String[] newList = new String[list.size()]; - list.toArray(newList); - console.getShell().out().println( - Parser.formatDisplayList(newList, - console.getTerminalSize().getHeight(), - console.getTerminalSize().getWidth())); - } - - @Override - public void print(String line) { - console.getShell().out().print(line); - } - - @Override - public void printNewLine() { - console.getShell().out().println(); - } - - @Override - public String readLine(String prompt) { - return read(prompt, null); - } - - @Override - public String readLine(String prompt, Character mask) { - return read(prompt, mask); - } - - private String read(String prompt, Character mask) { - int PID = -1; - - try { - ConsoleCallback callback = console.getConsoleCallback(); - if (callback instanceof CommandContextImpl.CLIAeshConsoleCallback) { - CommandContextImpl.CLIAeshConsoleCallback cliCallback = ((CommandContextImpl.CLIAeshConsoleCallback) callback); - - if (cliCallback.hasActiveProcess()) { - PID = cliCallback.getProcessPID(); - console.putProcessInBackground(PID); - } - - } - Prompt origPrompt = null; - if(!console.getPrompt().getPromptAsString().equals(prompt)) { - origPrompt = console.getPrompt(); - console.setPrompt(new Prompt(prompt, mask)); - redrawPrompt(); - } - try { - return console.getInputLine(); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - if(origPrompt != null) { - console.setPrompt(origPrompt); - } - } - } finally { - if( PID != -1) { - console.putProcessInForeground(PID); - } - } - - // Something is wrong. - return null; - } - - @Override - public int getTerminalWidth() { - return console.getTerminalSize().getWidth(); - } - - @Override - public int getTerminalHeight() { - return console.getTerminalSize().getHeight(); - } - - @Override - public boolean isCompletionEnabled(){ - return settings.isCompletionDisabled(); - } - - @Override - public void interrupt() { - } - - @Override - public void controlled() { - console.controlled(); - controlled = true; - } - - @Override - public void continuous() { - console.continuous(); - controlled = false; - } - - @Override - public boolean isControlled() { - return controlled; - } - - @Override - public void setCallback(ConsoleCallback consoleCallback) { - if(console != null) - console.setConsoleCallback(consoleCallback); - } - - @Override - public void start(){ - if(console != null) - console.start(); - } - - @Override - public void stop() { - if(console != null) { - console.stop(); - } - } - - @Override - public boolean running() { - return console != null && (console.isRunning() || console.hasRunningProcesses()); - } - - @Override - public void setPrompt(String prompt){ - setPrompt(prompt, null); - } - - @Override - public void setPrompt(String prompt, Character mask) { - if(!prompt.equals(console.getPrompt().getPromptAsString())) { - console.setPrompt(new Prompt(prompt, mask)); - } - } - - @Override - public void redrawPrompt() { - console.clearBufferAndDisplayPrompt(); - } - - class HistoryImpl implements CommandHistory { - - @Override - public List asList() { - return console.getHistory().getAll(); - } - - @Override - public boolean isUseHistory() { - return console.getHistory().isEnabled(); - } - - @Override - public void setUseHistory(boolean useHistory) { - if(useHistory){ - console.getHistory().enable(); - }else{ - console.getHistory().disable(); - } - } - - @Override - public void clear() { - console.getHistory().clear(); - } - - @Override - public int getMaxSize() { - return settings.getHistorySize(); - } - } - }; - return console; } } diff --git a/cli/src/main/java/org/jboss/as/cli/impl/ReadlineConsole.java b/cli/src/main/java/org/jboss/as/cli/impl/ReadlineConsole.java new file mode 100644 index 00000000000..4ee80d52bfa --- /dev/null +++ b/cli/src/main/java/org/jboss/as/cli/impl/ReadlineConsole.java @@ -0,0 +1,778 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.as.cli.impl; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.logging.Level; +import org.jboss.aesh.readline.Prompt; +import org.jboss.aesh.readline.Readline; +import org.jboss.aesh.readline.alias.AliasCompletion; +import org.jboss.aesh.readline.alias.AliasManager; +import org.jboss.aesh.readline.alias.AliasPreProcessor; +import org.jboss.aesh.readline.completion.CompleteOperation; +import org.jboss.aesh.readline.completion.Completion; +import org.jboss.aesh.readline.editing.EditModeBuilder; +import org.jboss.aesh.readline.history.FileHistory; +import org.jboss.aesh.terminal.Terminal; +import org.jboss.aesh.terminal.TerminalBuilder; +import org.jboss.aesh.tty.Capability; +import org.jboss.aesh.tty.Connection; +import org.jboss.aesh.tty.Signal; +import org.jboss.aesh.tty.Size; +import org.jboss.aesh.tty.terminal.TerminalConnection; +import org.jboss.aesh.util.ANSI; +import org.jboss.aesh.util.Config; +import org.jboss.aesh.util.FileAccessPermission; +import org.jboss.aesh.util.Parser; +import org.jboss.as.cli.CommandContext; +import org.jboss.as.cli.CommandHistory; +import org.jboss.as.cli.CommandLineCompleter; +import org.jboss.logmanager.Logger; + +/** + * + * @author jdenise@redhat.com + */ +public class ReadlineConsole implements Console { + private static final Logger log = Logger.getLogger(ReadlineConsole.class.getName()); + /** + * Used when an OutputStream has been set. + */ + private static class InProcessConnection implements Connection { + + private Consumer sizeHandler; + private Consumer signalHandler; + private Consumer stdinHandler; + private final Consumer stdOutHandler; + private Consumer closeHandler; + + private final Size size; + + public InProcessConnection(PrintStream output) { + stdOutHandler = ints -> { + if (log.isLoggable(Level.FINER)) { + log.finer("Writing " + Parser.stripAwayAnsiCodes(Parser.fromCodePoints(ints))); + } + output.print(Parser.stripAwayAnsiCodes(Parser.fromCodePoints(ints))); + }; + + size = new Size(80, 20); + } + + @Override + public String terminalType() { + return "CLI console"; + } + + @Override + public Size size() { + return size; + } + + @Override + public Consumer getSizeHandler() { + return sizeHandler; + } + + @Override + public void setSizeHandler(Consumer handler) { + this.sizeHandler = handler; + } + + @Override + public Consumer getSignalHandler() { + return signalHandler; + } + + @Override + public void setSignalHandler(Consumer handler) { + signalHandler = handler; + } + + @Override + public Consumer getStdinHandler() { + return stdinHandler; + } + + @Override + public void setStdinHandler(Consumer handler) { + stdinHandler = handler; + } + + @Override + public Consumer stdoutHandler() { + return stdOutHandler; + } + + @Override + public void setCloseHandler(Consumer closeHandler) { + this.closeHandler = closeHandler; + } + + @Override + public Consumer getCloseHandler() { + return closeHandler; + } + + @Override + public void close() { + if (closeHandler != null) { + closeHandler.accept(null); + } + } + + @Override + public void openBlocking() { + } + + @Override + public void openNonBlocking() { + } + + @Override + public void stopReading() { + } + + @Override + public void suspend() { + } + + @Override + public boolean suspended() { + return false; + } + + @Override + public void awake() { + } + + @Override + public boolean put(Capability capability, Object... params) { + return false; + } + } + + public interface Settings { + + /** + * @return the inStream + */ + InputStream getInStream(); + + /** + * @return the outStream + */ + OutputStream getOutStream(); + + /** + * @return the disableHistory + */ + boolean isDisableHistory(); + + /** + * @return the historyFile + */ + File getHistoryFile(); + + /** + * @return the historySize + */ + int getHistorySize(); + + /** + * @return the permission + */ + FileAccessPermission getPermission(); + + /** + * @return the interrupt + */ + Runnable getInterrupt(); + } + + /** + * This is needed due to GUI use case. The OutputStream is captured after + * the console has been initialised. + */ + private static class CLITerminalConnection extends TerminalConnection { + + private final Consumer interceptor; + CLITerminalConnection(Terminal terminal, CLIPrintStream output) { + super(terminal); + interceptor = new Consumer() { + @Override + public void accept(int[] ints) { + if (log.isLoggable(Level.FINER)) { + log.finer("Writing " + Parser.stripAwayAnsiCodes(Parser.fromCodePoints(ints))); + } + CLITerminalConnection.super.stdoutHandler().accept(ints); + // This is the gui case. + if (output.getDelegate() != System.out) { + if (log.isLoggable(Level.FINER)) { + log.finer("Forwarding to CLIPrintStream delegate"); + } + output.print(Parser.stripAwayAnsiCodes(Parser.fromCodePoints(ints))); + } + } + }; + } + + @Override + public Consumer stdoutHandler() { + return interceptor; + } + } + + private static class SettingsImpl implements Settings { + + private final InputStream inStream; + private final OutputStream outStream; + private final boolean disableHistory; + private final File historyFile; + private final int historySize; + private final FileAccessPermission permission; + private final Runnable interrupt; + + private SettingsImpl(InputStream inStream, + OutputStream outStream, + boolean disableHistory, + File historyFile, + int historySize, + FileAccessPermission permission, + Runnable interrupt) { + this.inStream = inStream; + this.outStream = outStream; + this.disableHistory = disableHistory; + this.historyFile = historyFile; + this.historySize = historySize; + this.permission = permission; + this.interrupt = interrupt; + } + + /** + * @return the inStream + */ + @Override + public InputStream getInStream() { + return inStream; + } + + /** + * @return the outStream + */ + @Override + public OutputStream getOutStream() { + return outStream; + } + + /** + * @return the disableHistory + */ + @Override + public boolean isDisableHistory() { + return disableHistory; + } + + /** + * @return the historyFile + */ + @Override + public File getHistoryFile() { + return historyFile; + } + + /** + * @return the historySize + */ + @Override + public int getHistorySize() { + return historySize; + } + + /** + * @return the permission + */ + @Override + public FileAccessPermission getPermission() { + return permission; + } + + /** + * @return the interrupt + */ + @Override + public Runnable getInterrupt() { + return interrupt; + } + } + + public static class SettingsBuilder { + + private InputStream inStream; + private OutputStream outStream; + private boolean disableHistory; + private File historyFile; + private int historySize; + private FileAccessPermission permission; + private Runnable interrupt; + + public SettingsBuilder inputStream(InputStream inStream) { + this.inStream = inStream; + return this; + } + + public SettingsBuilder outputStream(OutputStream outStream) { + this.outStream = outStream; + return this; + } + + public SettingsBuilder disableHistory(boolean disableHistory) { + this.disableHistory = disableHistory; + return this; + } + + public SettingsBuilder historyFile(File historyFile) { + this.historyFile = historyFile; + return this; + } + + public SettingsBuilder historySize(int historySize) { + this.historySize = historySize; + return this; + } + + public SettingsBuilder historyFilePermission(FileAccessPermission permission) { + this.permission = permission; + return this; + } + + public SettingsBuilder interruptHook(Runnable interrupt) { + this.interrupt = interrupt; + return this; + } + + public Settings create() { + return new SettingsImpl(inStream, outStream, + disableHistory, historyFile, historySize, permission, interrupt); + } + } + + class HistoryImpl implements CommandHistory { + + @Override + public List asList() { + List lst = new ArrayList<>(); + for (int[] l : readlineHistory.getAll()) { + lst.add(Parser.stripAwayAnsiCodes(Parser.fromCodePoints(l))); + } + return lst; + } + + @Override + public boolean isUseHistory() { + return readlineHistory.isEnabled(); + } + + @Override + public void setUseHistory(boolean useHistory) { + if (useHistory) { + readlineHistory.enable(); + } else { + readlineHistory.disable(); + } + + } + + @Override + public void clear() { + readlineHistory.clear(); + } + + @Override + public int getMaxSize() { + return readlineHistory.size(); + } + } + private final CommandContext cmdCtx; + private final List completions = new ArrayList<>(); + private Readline readline; + private final Connection connection; + private final CommandHistory history = new HistoryImpl(); + private final FileHistory readlineHistory; + private String prompt; + private final Settings settings; + private volatile boolean started; + private volatile boolean closed; + private Thread startThread; + private Thread readingThread; + private Consumer callback; + + private final ExecutorService executor = Executors.newFixedThreadPool(1); + + private final AliasManager aliasManager; + private final List>> preProcessors = new ArrayList<>(); + + ReadlineConsole(CommandContext cmdCtx, Settings settings) throws IOException { + this.cmdCtx = cmdCtx; + this.settings = settings; + readlineHistory = new FileHistory(settings.getHistoryFile(), + settings.getHistorySize(), settings.getPermission(), false); + if (settings.isDisableHistory()) { + readlineHistory.disable(); + } else { + readlineHistory.enable(); + } + if (log.isLoggable(Level.FINER)) { + log.finer("History is enabled? " + !settings.isDisableHistory()); + } + connection = newConnection(); + + aliasManager = new AliasManager(new File(Config.getHomeDir() + + Config.getPathSeparator() + ".aesh_aliases"), true, "alias"); + AliasPreProcessor aliasPreProcessor = new AliasPreProcessor(aliasManager); + preProcessors.add(aliasPreProcessor); + completions.add(new AliasCompletion(aliasManager)); + } + + @Override + public void setActionCallback(Consumer callback) { + this.callback = callback; + } + + private Connection newConnection() throws IOException { + // 2 cases, terminal or user provided output. + CLIPrintStream stream = (CLIPrintStream) settings.getOutStream(); + PrintStream wrapped = stream.getDelegate(); + Connection c; + if (wrapped == System.out) { + if (log.isLoggable(Level.FINER)) { + log.finer("Creating terminal connection "); + } + Terminal terminal = TerminalBuilder.builder() + .input(settings.getInStream() == null + ? System.in : settings.getInStream()) + .output(settings.getOutStream() == null ? System.out : settings.getOutStream()) + .nativeSignals(true) + .name("CLI Terminal") + .system(true) + .build(); + c = new CLITerminalConnection(terminal, stream); + if (log.isLoggable(Level.FINER)) { + log.finer("New Terminal " + terminal.getClass()); + } + } else { + if (log.isLoggable(Level.FINER)) { + log.finer("New In process terminal"); + } + c = new InProcessConnection(stream); + } + c.setSignalHandler(signal -> { + if (signal == Signal.INT) { + if (readingThread != null) { + if (log.isLoggable(Level.FINER)) { + log.finer("Interrupting reading thread"); + } + readingThread.interrupt(); + } + if (log.isLoggable(Level.FINER)) { + log.finer("Calling InterruptHandler"); + } + settings.getInterrupt().run(); + } + }); + return c; + } + + @Override + public void addCompleter(CommandLineCompleter completer) { + completions.add(new Completion() { + @Override + public void complete(CompleteOperation co) { + List candidates = new ArrayList<>(); + if (log.isLoggable(Level.FINER)) { + log.finer("Completing " + co.getBuffer()); + } + int offset = completer.complete(cmdCtx, + co.getBuffer(), co.getCursor(), candidates); + co.setOffset(offset); + co.addCompletionCandidates(candidates); + String buffer = cmdCtx.getArgumentsString() == null ? co.getBuffer() : cmdCtx.getArgumentsString() + co.getBuffer(); + if (co.getCompletionCandidates().size() == 1 + && co.getCompletionCandidates().get(0).getCharacters().startsWith(buffer)) { + co.doAppendSeparator(true); + } else { + co.doAppendSeparator(false); + } + if (log.isLoggable(Level.FINER)) { + log.finer("Completion candidates " + co.getCompletionCandidates()); + } + } + }); + } + + @Override + public CommandHistory getHistory() { + return history; + } + + @Override + public void clearScreen() { + connection.stdoutHandler().accept(ANSI.CLEAR_SCREEN); + } + + @Override + public void printColumns(Collection list) { + String[] newList = new String[list.size()]; + list.toArray(newList); + connection.write( + Parser.formatDisplayList(newList, + getHeight(), + getWidth())); + } + + @Override + public void print(String line) { + if (log.isLoggable(Level.FINER)) { + log.finer("Print " + line); + } + connection.write(line); + } + + @Override + public void printNewLine() { + print(Config.getLineSeparator()); + } + + @Override + public String readLine(String prompt) throws IOException { + return readLine(prompt, null); + } + + @Override + public String readLine(String prompt, Character mask) throws IOException { + Readline inputLine = new Readline(); + String[] out = new String[1]; + // Keep a reference on the caller thread in case Ctrl-C is pressed + // and thread need to be interrupted. + readingThread = Thread.currentThread(); + if (log.isLoggable(Level.FINER)) { + log.finer("Prompt " + prompt + " mask " + mask); + } + if (!started) { + if (log.isLoggable(Level.FINER)) { + log.finer("Not started"); + } + // That is the case of the CLI connecting prior to start the terminal. + // No Terminal waiting in Main thread yet. + // We are opening the connection to the terminal until we have read + // something from prompt. Once done, we ask the connection to leave + // allowing us to return what the user typed. + inputLine.readline(connection, new Prompt(prompt, mask), newLine -> { + out[0] = newLine; + connection.stopReading(); + }); + // Open the connection until stopReading called. + connection.openBlocking(); + // stopReading called or connection closed (interrupted). + return out[0]; + } else { + // We must be called from another Thread. connection is reading in Main thread. + // calling readline will wakeup the Main thread that will execute + // the Prompt handling. + // We can safely wait. + if (readingThread == startThread) { + throw new RuntimeException("Can't prompt from the Thread that is " + + "reading terminal input"); + } + // During the execution of a command, the connection is suspended, doesn't + // read on the terminal. We must awake it to have it read the input + // that is going to happen. + connection.awake(); + try { + CountDownLatch latch = new CountDownLatch(1); + inputLine.readline(connection, new Prompt(prompt, mask), newLine -> { + out[0] = newLine; + // Ask the connection to be suspended, no terminal reading during + // command execution. + connection.suspend(); + latch.countDown(); + }); + try { + latch.await(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException(ex); + } + } finally { + readingThread = null; + } + return out[0]; + } + } + + @Override + public int getTerminalWidth() { + return getWidth(); + } + + @Override + public int getTerminalHeight() { + return getHeight(); + } + + private int getHeight() { + if (connection instanceof TerminalConnection) { + return ((TerminalConnection) connection).getTerminal().getHeight(); + } + return connection.size().getHeight(); + } + + private int getWidth() { + if (connection instanceof TerminalConnection) { + return ((TerminalConnection) connection).getTerminal().getWidth(); + } + return connection.size().getWidth(); + } + + @Override + public void interrupt() { + stop(); + } + + @Override + public void start() throws IOException { + if (closed) { + throw new IllegalStateException("Console has already bee closed"); + } + if (!started) { + startThread = Thread.currentThread(); + readline = new Readline(EditModeBuilder.builder().create(), + readlineHistory, null); + started = true; + loop(); + if (log.isLoggable(Level.FINER)) { + log.finer("Started in thread " + startThread.getName() + ". Waiting..."); + } + connection.openBlocking(); + if (log.isLoggable(Level.FINER)) { + log.finer("Done waiting, leaving"); + } + } else if (log.isLoggable(Level.FINER)) { + log.finer("Already started"); + } + } + + private void loop() { + if (log.isLoggable(Level.FINER)) { + log.finer("Set a readline callback with prompt " + prompt); + } + // Console could have been closed during a command execution. + if (!closed) { + readline.readline(connection, new Prompt(prompt), line -> { + // All commands can lead to prompting the user. So require + // to be executed in a dedicated thread. + // This can happen if a security configuration occurs + // on the remote server. + if (log.isLoggable(Level.FINER)) { + log.finer("Executing command " + line + " in a new thread."); + } + if (handleAlias(line)) { + loop(); + return; + } + // This callback is done in the main thread. + // Suspend the connection to avoid it to read on the terminal + // prior to have the executed runnable to be done. + // The command could close the connection and we want it the connection + // to exit immediately. This is what the awake call does. + connection.suspend(); + executor.submit(new Runnable() { + @Override + public void run() { + try { + callback.accept(line); + } finally { + // awake the connection to make it + // read again on the terminal. + // If it has been closed during command execution + // it will not read and exit. + connection.awake(); + loop(); + } + } + }); + }, completions, preProcessors); + } + } + + private boolean handleAlias(String line) { + if (line.startsWith("alias ") || line.equals("alias")) { + String out = aliasManager.parseAlias(line.trim()); + if (out != null) { + connection.write(out); + } + return true; + } else if (line.startsWith("unalias ") || line.equals("unalias")) { + String out = aliasManager.removeAlias(line.trim()); + if (out != null) { + connection.write(out); + } + return true; + } + return false; + } + + @Override + public void stop() { + if (log.isLoggable(Level.FINER)) { + log.finer("Stopping."); + } + if (started) { + readlineHistory.stop(); + aliasManager.persist(); + } + executor.shutdown(); + connection.close(); + closed = true; + } + + @Override + public boolean running() { + return started; + } + + @Override + public void setPrompt(String prompt) { + this.prompt = prompt; + } +} diff --git a/core-feature-pack/pom.xml b/core-feature-pack/pom.xml index a22c0380312..f767f961460 100644 --- a/core-feature-pack/pom.xml +++ b/core-feature-pack/pom.xml @@ -97,7 +97,7 @@ org.jboss.aesh - aesh + aesh-readline diff --git a/core-feature-pack/src/main/resources/modules/system/layers/base/org/jboss/aesh/main/module.xml b/core-feature-pack/src/main/resources/modules/system/layers/base/org/jboss/aesh/main/module.xml index 0d147b941dd..8c7e3b80124 100644 --- a/core-feature-pack/src/main/resources/modules/system/layers/base/org/jboss/aesh/main/module.xml +++ b/core-feature-pack/src/main/resources/modules/system/layers/base/org/jboss/aesh/main/module.xml @@ -28,7 +28,7 @@ - + diff --git a/pom.xml b/pom.xml index 1356e959b67..92f4c3a4ec6 100644 --- a/pom.xml +++ b/pom.xml @@ -94,7 +94,7 @@ 3.1.4 5.0.3 1.11 - 0.66.11 + 1.0-SNAPSHOT 3.0.3 1.1.2.Final 1.5.0.Beta1 @@ -892,8 +892,8 @@ org.jboss.aesh - aesh - ${version.org.jboss.aesh} + aesh-readline + ${version.org.jboss.aesh-readline} junit diff --git a/testsuite/shared/src/main/java/org/jboss/as/test/integration/management/cli/CliProcessBuilder.java b/testsuite/shared/src/main/java/org/jboss/as/test/integration/management/cli/CliProcessBuilder.java index 44d0d70852d..345358fc901 100644 --- a/testsuite/shared/src/main/java/org/jboss/as/test/integration/management/cli/CliProcessBuilder.java +++ b/testsuite/shared/src/main/java/org/jboss/as/test/integration/management/cli/CliProcessBuilder.java @@ -88,8 +88,6 @@ public Process createProcess() { // If uncommented during normal testing, causes all sorts of failures. //cliCommandBuilder.addJavaOption("-agentlib:jdwp=transport=dt_socket,server=n,address=localhost.localdomain:8765,suspend=y"); - cliCommandBuilder.addJavaOption("-Daesh.terminal=org.jboss.aesh.terminal.TestTerminal"); - if (cliConfigFile != null) { cliCommandBuilder.addJavaOption("-Djboss.cli.config=" + cliConfigFile); } else { diff --git a/testsuite/standalone/src/test/java/org/jboss/as/test/integration/management/cli/CliAliasTestCase.java b/testsuite/standalone/src/test/java/org/jboss/as/test/integration/management/cli/CliAliasTestCase.java index 86e4cad40c0..0cbfb5c067e 100644 --- a/testsuite/standalone/src/test/java/org/jboss/as/test/integration/management/cli/CliAliasTestCase.java +++ b/testsuite/standalone/src/test/java/org/jboss/as/test/integration/management/cli/CliAliasTestCase.java @@ -65,7 +65,6 @@ public final void setupUserHome() { @Test public void testValidAliasCommandInteractive() throws Exception { CliProcessWrapper cli = new CliProcessWrapper() - .addCliArgument("-Daesh.terminal=org.jboss.aesh.terminal.TestTerminal") .addJavaOption("-Duser.home=" + temporaryUserHome.getRoot().toPath().toString()) .addCliArgument("-Duser.home=" + temporaryUserHome.getRoot().toPath().toString()); try { @@ -106,7 +105,6 @@ public void testInvalidAliasCommandInteractive() throws Exception { final String INVALID_ALIAS_NAME = "TMP-DEBUG123-INVALID456-ALIAS789"; //does not match [a-zA-Z0-9_]+ regex final String INVALID_ALIAS_COMMAND = "'/subsystem=notfound:invalid-command'"; CliProcessWrapper cli = new CliProcessWrapper() - .addCliArgument("-Daesh.terminal=org.jboss.aesh.terminal.TestTerminal") .addJavaOption("-Duser.home=" + temporaryUserHome.getRoot().toPath().toString()) .addCliArgument("-Duser.home=" + temporaryUserHome.getRoot().toPath().toString()); try { @@ -145,7 +143,6 @@ public void testManuallyAddedAlias() throws Exception { fail(ex.getLocalizedMessage()); } CliProcessWrapper cli = new CliProcessWrapper() - .addCliArgument("-Daesh.terminal=org.jboss.aesh.terminal.TestTerminal") .addJavaOption("-Duser.home=" + temporaryUserHome.getRoot().toPath().toString()) .addCliArgument("-Duser.home=" + temporaryUserHome.getRoot().toPath().toString()); try { @@ -172,7 +169,6 @@ public void testManuallyAddedAlias() throws Exception { public void testAliasPersistence() throws Exception { final File aliasFile = temporaryUserHome.newFile(".aesh_aliases"); CliProcessWrapper cli = new CliProcessWrapper() - .addCliArgument("-Daesh.terminal=org.jboss.aesh.terminal.TestTerminal") .addJavaOption("-Duser.home=" + temporaryUserHome.getRoot().toPath().toString()) .addCliArgument("-Duser.home=" + temporaryUserHome.getRoot().toPath().toString()); try { @@ -198,7 +194,6 @@ public void testAliasPersistence() throws Exception { public void testAliasPersistenceCtrlC() throws Exception { final File aliasFile = temporaryUserHome.newFile(".aesh_aliases"); CliProcessWrapper cli = new CliProcessWrapper() - .addCliArgument("-Daesh.terminal=org.jboss.aesh.terminal.TestTerminal") .addJavaOption("-Duser.home=" + temporaryUserHome.getRoot().toPath().toString()) .addCliArgument("-Duser.home=" + temporaryUserHome.getRoot().toPath().toString()); try { diff --git a/testsuite/standalone/src/test/java/org/jboss/as/test/integration/management/cli/CliConfigTestCase.java b/testsuite/standalone/src/test/java/org/jboss/as/test/integration/management/cli/CliConfigTestCase.java index 26d73bc6345..d004e1d0741 100644 --- a/testsuite/standalone/src/test/java/org/jboss/as/test/integration/management/cli/CliConfigTestCase.java +++ b/testsuite/standalone/src/test/java/org/jboss/as/test/integration/management/cli/CliConfigTestCase.java @@ -60,9 +60,13 @@ public void testEchoCommand() throws Exception { CliProcessWrapper cli = new CliProcessWrapper() .setCliConfig(f.getAbsolutePath()) .addCliArgument("--command=version"); - final String result = cli.executeNonInteractive(); - assertNotNull(result); - assertTrue(result, result.contains("[disconnected /] version")); + try { + final String result = cli.executeNonInteractive(); + assertNotNull(result); + assertTrue(result, result.contains("[disconnected /] version")); + } finally { + cli.destroyProcess(); + } } @Test @@ -71,9 +75,13 @@ public void testNoEchoCommand() throws Exception { CliProcessWrapper cli = new CliProcessWrapper() .setCliConfig(f.getAbsolutePath()) .addCliArgument("--command=version"); - final String result = cli.executeNonInteractive(); - assertNotNull(result); - assertFalse(result, result.contains("[disconnected /] version")); + try { + final String result = cli.executeNonInteractive(); + assertNotNull(result); + assertFalse(result, result.contains("[disconnected /] version")); + } finally { + cli.destroyProcess(); + } } @Test @@ -87,23 +95,27 @@ public void testWorkFlowEchoCommand() throws Exception { TestSuiteEnvironment.getServerAddress() + ":" + TestSuiteEnvironment.getServerPort()) .addCliArgument("--connect"); - final String result = cli.executeNonInteractive(); - assertNotNull(result); - assertTrue(result, result.contains(":read-attribute(name=foo)")); - assertTrue(result, result.contains("/system-property=catch:add(value=bar)")); - assertTrue(result, result.contains("/system-property=finally:add(value=bar)")); - assertTrue(result, result.contains("/system-property=finally2:add(value=bar)")); - assertTrue(result, result.contains("if (outcome == success) of /system-property=catch:read-attribute(name=value)")); - assertTrue(result, result.contains("set prop=Catch\\ block\\ was\\ executed")); - assertTrue(result, result.contains("/system-property=finally:write-attribute(name=value, value=if)")); + try { + final String result = cli.executeNonInteractive(); + assertNotNull(result); + assertTrue(result, result.contains(":read-attribute(name=foo)")); + assertTrue(result, result.contains("/system-property=catch:add(value=bar)")); + assertTrue(result, result.contains("/system-property=finally:add(value=bar)")); + assertTrue(result, result.contains("/system-property=finally2:add(value=bar)")); + assertTrue(result, result.contains("if (outcome == success) of /system-property=catch:read-attribute(name=value)")); + assertTrue(result, result.contains("set prop=Catch\\ block\\ was\\ executed")); + assertTrue(result, result.contains("/system-property=finally:write-attribute(name=value, value=if)")); - assertFalse(result, result.contains("/system-property=catch2:add(value=bar)")); - assertFalse(result, result.contains("set prop=Catch\\ block\\ wasn\\'t\\ executed")); - assertFalse(result, result.contains("/system-property=finally:write-attribute(name=value, value=else)")); + assertFalse(result, result.contains("/system-property=catch2:add(value=bar)")); + assertFalse(result, result.contains("set prop=Catch\\ block\\ wasn\\'t\\ executed")); + assertFalse(result, result.contains("/system-property=finally:write-attribute(name=value, value=else)")); - assertTrue(result, result.contains("/system-property=catch:remove()")); - assertTrue(result, result.contains("/system-property=finally:remove()")); - assertTrue(result, result.contains("/system-property=finally2:remove()")); + assertTrue(result, result.contains("/system-property=catch:remove()")); + assertTrue(result, result.contains("/system-property=finally:remove()")); + assertTrue(result, result.contains("/system-property=finally2:remove()")); + } finally { + cli.destroyProcess(); + } } @Test @@ -116,8 +128,12 @@ public void testConfigTimeoutCommand() throws Exception { + TestSuiteEnvironment.getServerAddress() + ":" + TestSuiteEnvironment.getServerPort()) .addCliArgument("--connect"); - cli.executeInteractive(); - testTimeout(cli, 1); + try { + cli.executeInteractive(); + testTimeout(cli, 1); + } finally { + cli.destroyProcess(); + } } @Test @@ -129,8 +145,12 @@ public void testOptionTimeoutCommand() throws Exception { + TestSuiteEnvironment.getServerPort()) .addCliArgument("--connect") .addCliArgument("--command-timeout=77"); - cli.executeInteractive(); - testTimeout(cli, 77); + try { + cli.executeInteractive(); + testTimeout(cli, 77); + } finally { + cli.destroyProcess(); + } } @Test @@ -142,8 +162,12 @@ public void testNegativeConfigTimeoutCommand() throws Exception { + TestSuiteEnvironment.getServerAddress() + ":" + TestSuiteEnvironment.getServerPort()) .addCliArgument("--connect"); - String output = cli.executeNonInteractive(); - assertTrue(output, output.contains("The command-timeout must be a valid positive integer")); + try { + String output = cli.executeNonInteractive(); + assertTrue(output, output.contains("The command-timeout must be a valid positive integer")); + } finally { + cli.destroyProcess(); + } } @Test @@ -154,8 +178,12 @@ public void testNegativeOptionTimeoutCommand() throws Exception { + TestSuiteEnvironment.getServerPort()) .addCliArgument("--connect") .addCliArgument("--command-timeout=-1"); - String output = cli.executeNonInteractive(); - assertTrue(output, output.contains("The command-timeout must be a valid positive integer")); + try { + String output = cli.executeNonInteractive(); + assertTrue(output, output.contains("The command-timeout must be a valid positive integer")); + } finally { + cli.destroyProcess(); + } } @Test @@ -169,9 +197,13 @@ public void testNonInteractiveCommandTimeout() throws Exception { + TestSuiteEnvironment.getServerAddress() + ":" + TestSuiteEnvironment.getServerPort()) .addCliArgument("--connect"); - final String result = cli.executeNonInteractive(); - assertNotNull(result); - assertTrue(result, result.contains("Timeout exception for run-batch")); + try { + final String result = cli.executeNonInteractive(); + assertNotNull(result); + assertTrue(result, result.contains("Timeout exception for run-batch")); + } finally { + cli.destroyProcess(); + } } private void testTimeout(CliProcessWrapper cli, int config) throws Exception {