This is an automated email from the ASF dual-hosted git repository. clebertsuconic pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/artemis.git
commit 6b1ada8aca2f2d8a78d8230dfc55147ff0027cb5 Author: Clebert Suconic <[email protected]> AuthorDate: Sat Mar 14 14:12:24 2026 -0400 ARTEMIS-5954 Artemis Shell History --- .../org/apache/activemq/artemis/cli/Shell.java | 129 ++++++++++++++++++++- .../artemis/cli/commands/InputAbstract.java | 80 +------------ .../cli/commands/util/input/InputReader.java | 52 +++++++++ .../cli/commands/util/input/JLineInputReader.java | 44 +++++++ .../cli/commands/util/input/SystemInputReader.java | 59 ++++++++++ 5 files changed, 285 insertions(+), 79 deletions(-) diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Shell.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Shell.java index 3c627e49d0..b98fce02fb 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Shell.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Shell.java @@ -17,14 +17,26 @@ package org.apache.activemq.artemis.cli; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import org.apache.activemq.artemis.cli.commands.ActionContext; import org.apache.activemq.artemis.cli.commands.Connect; import org.apache.activemq.artemis.cli.commands.messages.ConnectionAbstract; +import org.apache.activemq.artemis.cli.commands.util.input.SystemInputReader; import org.jline.console.SystemRegistry; import org.jline.console.impl.SystemRegistryImpl; import org.jline.reader.EndOfFileException; @@ -36,6 +48,7 @@ import org.jline.reader.UserInterruptException; import org.jline.reader.impl.DefaultParser; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; +import org.jspecify.annotations.Nullable; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.shell.jline3.PicocliCommands; @@ -52,6 +65,11 @@ public class Shell implements Runnable { @CommandLine.Option(names = "--password", description = "It will be used for an initial connection if set.") protected String password; + @CommandLine.Option(names = "--history", description = "File where shell history is being stored.") + protected File historyFile; + + private static final String DEFAULT_HISTORY_FILE = "history-file"; + public Shell(CommandLine commandLine) { } @@ -64,7 +82,7 @@ public class Shell implements Runnable { connect.setUser(user).setPassword(password).setBrokerURL(brokerURL); connect.run(); } - runShell(false); + runShell(false, historyFile); } private static ThreadLocal<AtomicBoolean> IN_SHELL = ThreadLocal.withInitial(() -> new AtomicBoolean(false)); @@ -88,10 +106,20 @@ public class Shell implements Runnable { } public static void runShell(boolean printBanner) { + runShell(printBanner, null); + } + + public static void runShell(boolean printBanner, File historyFile) { try { setInShell(); - boolean isInstance = System.getProperty("artemis.instance") != null; + String artemisInstance = System.getProperty("artemis.instance"); + + boolean isInstance = artemisInstance != null; + + if (isInstance && historyFile == null) { + historyFile = inquiryDefaultHistory(historyFile, artemisInstance); + } Supplier<Path> workDir = () -> Paths.get(System.getProperty("user.dir")); @@ -117,12 +145,18 @@ public class Shell implements Runnable { systemRegistry.setCommandRegistries(picocliCommands); systemRegistry.register("help", picocliCommands); - LineReader reader = LineReaderBuilder.builder() + LineReaderBuilder readerBuilder = LineReaderBuilder.builder() .terminal(terminal) .completer(systemRegistry.completer()) .parser(parser) - .variable(LineReader.LIST_MAX, 50) // max tab completion candidates - .build(); + .variable(LineReader.LIST_MAX, 50); // max tab completion candidates + + if (historyFile != null) { + readerBuilder.variable(LineReader.HISTORY_FILE, historyFile.toPath()); + } + + LineReader reader = readerBuilder.build(); + factory.setTerminal(terminal); if (ActionContext.system() != null) { ActionContext.system().lineReader = reader; @@ -134,6 +168,11 @@ public class Shell implements Runnable { printBanner(); } + if (historyFile != null) { + System.out.println(org.apache.activemq.artemis.cli.Terminal.WARNING_COLOR_UNICODE + "Shell history being saved at " + historyFile.getAbsolutePath() + org.apache.activemq.artemis.cli.Terminal.CLEAR_UNICODE); + System.out.println(); + } + System.out.println("For a list of commands, type " + org.apache.activemq.artemis.cli.Terminal.WARNING_COLOR_UNICODE + "help" + org.apache.activemq.artemis.cli.Terminal.CLEAR_UNICODE + " or press " + org.apache.activemq.artemis.cli.Terminal.WARNING_COLOR_UNICODE + "<TAB>" + org.apache.activemq.artemis.cli.Terminal.CLEAR_UNICODE + ":"); System.out.println("Type " + org.apache.activemq.artemis.cli.Terminal.WARNING_COLOR_UNICODE + "exit" + org.apache.activemq.artemis.cli.Terminal.CLEAR_UNICODE + " or press " + org.apache.activemq.artemis.cli.Terminal.WARNING_COLOR_UNICODE + "<CTRL-D>" + org.apache.activemq.artemis.cli.Terminal.CLEAR_UNICODE + " to leave the session:"); @@ -141,11 +180,14 @@ public class Shell implements Runnable { String line; while (true) { try { + // load the history on each loop because other instances may be changing it as well + loadHistory(historyFile, reader); // We build a new command every time, as they could have state from previous executions systemRegistry.setCommandRegistries(new PicocliCommands(Artemis.buildCommand(isInstance, !isInstance, false))); systemRegistry.cleanUp(); line = reader.readLine(getPrompt(), rightPrompt, (MaskingCallback) null, null); systemRegistry.execute(line); + saveHistory(reader, historyFile); } catch (InterruptedException e) { e.printStackTrace(); // Ignore @@ -171,6 +213,67 @@ public class Shell implements Runnable { } + private static void saveHistory(LineReader reader, File historyFile) { + try { + setHistoryFilePermissions(historyFile); + reader.getHistory().save(); + } catch (Throwable e) { + System.err.println("Error saving history of shell : " + e.getMessage()); + } + } + + private static void loadHistory(File historyFile, LineReader reader) { + if (historyFile != null) { + try { + if (historyFile.length() > 0) { + reader.getHistory().load(); + } + } catch (IOException e) { + System.err.println("Could not load history from " + historyFile + ": " + e.getMessage()); + } + } + } + + private static @Nullable File inquiryDefaultHistory(File historyFile, String artemisInstance) { + final String NO_HISTORY = "NO_HISTORY"; + SystemInputReader inputReader = new SystemInputReader(); + File defaultHistoryFile = new File(artemisInstance + "/etc/" + DEFAULT_HISTORY_FILE); + try { + if (!defaultHistoryFile.exists()) { + defaultHistoryFile.createNewFile(); + } + + if (defaultHistoryFile.exists()) { + if (defaultHistoryFile.length() == 0) { + String input = inputReader.inputLoop(null, "Allow shell history? (Y/N)", s -> s != null && (s.toUpperCase().equals("Y") || s.toUpperCase().equals("N"))).toUpperCase(); + if (input.equals("N")) { + historyFile = null; + try (PrintStream fileOutputStream = new PrintStream(new FileOutputStream(defaultHistoryFile))) { + fileOutputStream.println(NO_HISTORY); + } + } else { + defaultHistoryFile.createNewFile(); + } + } + + if (historyFile == null) { + try (BufferedReader fileReader = new BufferedReader(new InputStreamReader(new FileInputStream(defaultHistoryFile)))) { + String line = fileReader.readLine(); + if (line != null && line.equals(NO_HISTORY)) { + // the user selected no history in the past + historyFile = null; + } else { + historyFile = defaultHistoryFile; + } + } + } + } + } catch (IOException e) { + // no history on this case then + } + return historyFile; + } + private static void printBanner() { System.out.print(org.apache.activemq.artemis.cli.Terminal.INFO_COLOR_UNICODE); try { @@ -205,4 +308,20 @@ public class Shell implements Runnable { PROMPT.set(org.apache.activemq.artemis.cli.Terminal.INPUT_COLOR_UNICODE + prompt + " > " + org.apache.activemq.artemis.cli.Terminal.CLEAR_UNICODE); } + private static void setHistoryFilePermissions(File historyFile) { + try { + Path path = historyFile.toPath(); + if (!Files.exists(path)) { + historyFile.createNewFile(); + } + if (Files.exists(path)) { + Set<PosixFilePermission> perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(path, perms); + } + } catch (Exception e) { + } + } + } diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/InputAbstract.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/InputAbstract.java index a49452125d..602d81cdf3 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/InputAbstract.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/InputAbstract.java @@ -17,68 +17,13 @@ package org.apache.activemq.artemis.cli.commands; -import java.io.InputStream; -import java.io.PrintStream; -import java.util.Scanner; - -import org.apache.activemq.artemis.cli.Terminal; -import org.jline.reader.LineReader; +import org.apache.activemq.artemis.cli.commands.util.input.InputReader; +import org.apache.activemq.artemis.cli.commands.util.input.JLineInputReader; +import org.apache.activemq.artemis.cli.commands.util.input.SystemInputReader; import picocli.CommandLine.Option; public class InputAbstract extends ActionAbstract { - public interface InputReader { - String readLine(String prompt); - String readPassword(String prompt); - } - - private class ScanReader implements InputReader { - ScanReader(InputStream inputStream, PrintStream promptStream) { - this.scanner = new Scanner(inputStream); - this.promptStream = promptStream; - } - - Scanner scanner; - PrintStream promptStream; - - - @Override - public String readLine(String prompt) { - promptStream.println(prompt); - return scanner.nextLine(); - } - - @Override - public String readPassword(String prompt) { - promptStream.println(prompt); - char[] typedPassword = System.console().readPassword(); - if (typedPassword == null) { - return null; - } else { - return new String(typedPassword); - } - } - } - - - private class JLineReader implements InputReader { - JLineReader(LineReader reader) { - this.reader = reader; - } - - public LineReader reader; - - @Override - public String readLine(String prompt) { - return reader.readLine(Terminal.INPUT_COLOR_UNICODE + prompt + ":" + Terminal.CLEAR_UNICODE); - } - - @Override - public String readPassword(String prompt) { - return reader.readLine(Terminal.INPUT_COLOR_UNICODE + prompt + ":" + Terminal.CLEAR_UNICODE, '*'); - } - } - InputReader lineReader; @@ -154,20 +99,7 @@ public class InputAbstract extends ActionAbstract { return silentDefault; } - String inputStr; - boolean valid = false; - getActionContext().out.println(); - do { - getActionContext().out.println(propertyName + ":"); - inputStr = lineReader.readLine(prompt); - - if (!acceptNull && inputStr.trim().isEmpty()) { - getActionContext().out.println("Invalid Entry!"); - } else { - valid = true; - } - } - while (!valid); + String inputStr = lineReader.inputLoop(propertyName, prompt, s -> s != null && !s.isEmpty()); return inputStr.trim(); } @@ -206,9 +138,9 @@ public class InputAbstract extends ActionAbstract { super.execute(context); if (context.lineReader != null) { - this.lineReader = new JLineReader(context.lineReader); + this.lineReader = new JLineInputReader(context.lineReader); } else { - this.lineReader = new ScanReader(context.in, context.out); + this.lineReader = new SystemInputReader(context.in, context.out); } return null; diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/input/InputReader.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/input/InputReader.java new file mode 100644 index 0000000000..1a272ce44f --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/input/InputReader.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.activemq.artemis.cli.commands.util.input; + +import java.util.function.Function; + +public abstract class InputReader { + public abstract String readLine(String prompt); + public abstract String readPassword(String prompt); + + protected abstract void printLine(String line); + + public String inputLoop(String propertyName, String prompt, Function<String, Boolean> inputValidator) { + String inputStr; + boolean valid = false; + + printLine(""); + + do { + if (propertyName != null) { + printLine(propertyName + ":"); + } + + inputStr = readLine(prompt); + + if (inputValidator == null || inputValidator.apply(inputStr)) { + valid = true; + } else { + valid = false; + } + } + while (!valid); + + return inputStr; + } + +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/input/JLineInputReader.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/input/JLineInputReader.java new file mode 100644 index 0000000000..ae150e8e61 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/input/JLineInputReader.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.activemq.artemis.cli.commands.util.input; + +import org.apache.activemq.artemis.cli.Terminal; +import org.jline.reader.LineReader; + +public class JLineInputReader extends InputReader { + private final LineReader reader; + + public JLineInputReader(LineReader reader) { + this.reader = reader; + } + + @Override + public String readLine(String prompt) { + return reader.readLine(Terminal.INPUT_COLOR_UNICODE + prompt + ":" + Terminal.CLEAR_UNICODE); + } + + @Override + public String readPassword(String prompt) { + return reader.readLine(Terminal.INPUT_COLOR_UNICODE + prompt + ":" + Terminal.CLEAR_UNICODE, '*'); + } + + @Override + protected void printLine(String line) { + System.out.println(line); + } +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/input/SystemInputReader.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/input/SystemInputReader.java new file mode 100644 index 0000000000..978c059716 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/input/SystemInputReader.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.activemq.artemis.cli.commands.util.input; + +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Scanner; + +public class SystemInputReader extends InputReader { + public SystemInputReader() { + this(System.in, System.out); + } + + public SystemInputReader(InputStream inputStream, PrintStream promptStream) { + this.scanner = new Scanner(inputStream); + this.promptStream = promptStream; + } + + private final Scanner scanner; + private final PrintStream promptStream; + + + @Override + public String readLine(String prompt) { + promptStream.println(prompt); + return scanner.nextLine(); + } + + @Override + public String readPassword(String prompt) { + promptStream.println(prompt); + char[] typedPassword = System.console().readPassword(); + if (typedPassword == null) { + return null; + } else { + return new String(typedPassword); + } + } + + @Override + protected void printLine(String line) { + promptStream.println(line); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
