From b6b49371d3ac65f453801615ce44d1cb5fd2eea0 Mon Sep 17 00:00:00 2001 From: Clebert Suconic Date: Fri, 8 May 2026 10:30:38 -0400 Subject: [PATCH] ARTEMIS-6051 Change artemis shell history defaults to ~/.artemis_history It is possible to change the artemis-utility.profile as well --- .../apache/activemq/artemis/cli/Shell.java | 63 ++++++++-- .../activemq/artemis/cli/commands/bin/artemis | 6 + .../cli/commands/etc/artemis-utility.profile | 4 + .../artemis/cli/commands/ShellTest.java | 118 ++++++++++++++++++ 4 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 artemis-cli/src/test/java/org/apache/activemq/artemis/cli/commands/ShellTest.java 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 b98fce02fb9..bf22076ed6f 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 @@ -22,6 +22,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.nio.file.Files; @@ -68,7 +69,7 @@ public class Shell implements Runnable { @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 static final String DEFAULT_HISTORY_FILE = "history-file"; public Shell(CommandLine commandLine) { } @@ -82,7 +83,7 @@ public void run() { connect.setUser(user).setPassword(password).setBrokerURL(brokerURL); connect.run(); } - runShell(false, historyFile); + runShell(false, historyFile, null); } private static ThreadLocal IN_SHELL = ThreadLocal.withInitial(() -> new AtomicBoolean(false)); @@ -106,10 +107,10 @@ public static void setConnected(boolean connected) { } public static void runShell(boolean printBanner) { - runShell(printBanner, null); + runShell(printBanner, null, null); } - public static void runShell(boolean printBanner, File historyFile) { + public static void runShell(boolean printBanner, File historyFile, InputStream pipedInput) { try { setInShell(); @@ -117,8 +118,21 @@ public static void runShell(boolean printBanner, File historyFile) { boolean isInstance = artemisInstance != null; - if (isInstance && historyFile == null) { - historyFile = inquiryDefaultHistory(historyFile, artemisInstance); + if (historyFile == null) { + String historyFilePath = System.getProperty("artemis.shell.history"); + if (historyFilePath == null) { + historyFilePath = System.getenv("ARTEMIS_SHELL_HISTORY"); + } + if (historyFilePath != null) { + historyFile = new File(historyFilePath); + } else { + historyFile = inquiryDefaultHistory(historyFile, artemisInstance); + } + } + + if (pipedInput != null) { + runPipedMode(isInstance, pipedInput); + return; } Supplier workDir = () -> Paths.get(System.getProperty("user.dir")); @@ -168,7 +182,11 @@ public static void runShell(boolean printBanner, File historyFile) { printBanner(); } - if (historyFile != null) { + if (historyFile == null) { + File preferenceFile = new File(artemisInstance + "/etc/" + DEFAULT_HISTORY_FILE); + System.out.println(org.apache.activemq.artemis.cli.Terminal.WARNING_COLOR_UNICODE + "Shell history disabled as recorded in " + preferenceFile.getAbsolutePath() + org.apache.activemq.artemis.cli.Terminal.CLEAR_UNICODE); + System.out.println(); + } else { 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(); } @@ -254,6 +272,7 @@ private static void loadHistory(File historyFile, LineReader reader) { } else { defaultHistoryFile.createNewFile(); } + setHistoryFilePermissions(defaultHistoryFile); } if (historyFile == null) { @@ -324,4 +343,34 @@ private static void setHistoryFilePermissions(File historyFile) { } } + private static void runPipedMode(boolean isInstance, InputStream is) throws Exception { + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + + // Skip empty lines and comments + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + // Exit command + if (line.equals("exit") || line.equals("quit")) { + break; + } + + try { + // Rebuild command for each execution to avoid state issues + CommandLine commandLine = Artemis.buildCommand(isInstance, !isInstance, false); + String[] args = line.split("\\s+"); + commandLine.execute(args); + } catch (Exception e) { + System.err.println("Error executing command: " + line); + e.printStackTrace(); + } + } + } + } + } diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/bin/artemis b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/bin/artemis index b792eb88695..693b1756972 100755 --- a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/bin/artemis +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/bin/artemis @@ -119,6 +119,11 @@ if [ -f "$ARTEMIS_OOME_DUMP" ] ; then mv $ARTEMIS_OOME_DUMP $ARTEMIS_OOME_DUMP.bkp fi +# Set shell history argument if ARTEMIS_SHELL_HISTORY is defined +if [ -n "$ARTEMIS_SHELL_HISTORY" ]; then + SHELL_HISTORY_ARG="-Dartemis.shell.history=${ARTEMIS_SHELL_HISTORY}" +fi + exec "$JAVACMD" \ $LOGGING_ARGS \ $JAVA_ARGS \ @@ -132,6 +137,7 @@ exec "$JAVACMD" \ -Djava.io.tmpdir="$ARTEMIS_INSTANCE/tmp" \ -Ddata.dir="$ARTEMIS_DATA_DIR" \ -Dartemis.instance.etc="$ARTEMIS_INSTANCE_ETC" \ + $SHELL_HISTORY_ARG \ $DEBUG_ARGS \ $JAVA_ARGS_APPEND \ org.apache.activemq.artemis.boot.Artemis "$@" diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-utility.profile b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-utility.profile index a724cd6c496..e23f776e7d8 100644 --- a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-utility.profile +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-utility.profile @@ -19,6 +19,10 @@ ARTEMIS_HOME='${artemis.home}' ARTEMIS_INSTANCE='@artemis.instance@' ARTEMIS_DATA_DIR='${artemis.instance.data}' +if [ -z "$ARTEMIS_SHELL_HISTORY" ]; then + ARTEMIS_SHELL_HISTORY=~/.artemis_history +fi + if [ -z "$LOGGING_ARGS" ]; then LOGGING_ARGS="-Dlog4j2.configurationFile=log4j2-utility.properties" fi diff --git a/artemis-cli/src/test/java/org/apache/activemq/artemis/cli/commands/ShellTest.java b/artemis-cli/src/test/java/org/apache/activemq/artemis/cli/commands/ShellTest.java new file mode 100644 index 00000000000..6060c4d9147 --- /dev/null +++ b/artemis-cli/src/test/java/org/apache/activemq/artemis/cli/commands/ShellTest.java @@ -0,0 +1,118 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.activemq.artemis.cli.Shell; +import org.apache.activemq.artemis.tests.util.ArtemisTestCase; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class ShellTest extends ArtemisTestCase { + + @TempDir + File temporaryFolder; + + @Test + public void testDefaultAllowHistory() throws Exception { + testDefaultHistory(true); + } + + @Test + public void testDefaultDontAllowHistory() throws Exception { + testDefaultHistory(false); + } + + public void testDefaultHistory(boolean allowHistory) throws Exception { + System.setProperty("artemis.instance", temporaryFolder.getAbsolutePath()); + File etcFolder = new File(temporaryFolder, "etc"); + assertTrue(etcFolder.mkdirs()); + + String input; + if (allowHistory) { + input = "Y\n"; + } else { + input = "N\n"; + } + + File shellHistory = new File(etcFolder, Shell.DEFAULT_HISTORY_FILE); + executeShell(input, shellHistory); + + if (!allowHistory) { + String historyContent = new String(Files.readAllBytes(shellHistory.toPath()), StandardCharsets.UTF_8); + assertTrue(historyContent.contains("NO_HISTORY"), "History file should contain NO_HISTORY marker"); + } + } + + private static void executeShell(String input, File outputHistory) throws InterruptedException, IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)); + + try { + CountDownLatch shellFinished = new CountDownLatch(1); + + Thread shellThread = new Thread(() -> { + InputStream originlInput = System.in; + try { + // Run the shell with custom streams for testing + System.setIn(inputStream); + Shell.runShell(false, null, inputStream); + } finally { + shellFinished.countDown(); + System.setIn(originlInput); + } + }); + + shellThread.start(); + + // Wait for shell to finish + assertTrue(shellFinished.await(30, TimeUnit.SECONDS), "Shell did not finish in time"); + shellThread.join(5000); + + assertTrue(outputHistory.exists()); + + // Validate file permissions are 600 (owner read/write only) + Set permissions = Files.getPosixFilePermissions(outputHistory.toPath()); + assertTrue(permissions.contains(PosixFilePermission.OWNER_READ), "File should be readable by owner"); + assertTrue(permissions.contains(PosixFilePermission.OWNER_WRITE), "File should be writable by owner"); + // Explicitly verify group and others cannot read + assertFalse(permissions.contains(PosixFilePermission.GROUP_READ), "File should not be readable by group"); + assertFalse(permissions.contains(PosixFilePermission.GROUP_WRITE), "File should not be writable by group"); + assertFalse(permissions.contains(PosixFilePermission.GROUP_EXECUTE), "File should not be executable by group"); + assertFalse(permissions.contains(PosixFilePermission.OTHERS_READ), "File should not be readable by others"); + assertFalse(permissions.contains(PosixFilePermission.OTHERS_WRITE), "File should not be writable by others"); + assertFalse(permissions.contains(PosixFilePermission.OTHERS_EXECUTE), "File should not be executable by others"); + + } finally { + System.clearProperty("artemis.instance"); + } + } + +}