From b8424121cdc26148c2c3c8e43b7779c7dc507f4c Mon Sep 17 00:00:00 2001 From: hengyunabc Date: Sat, 18 Jul 2026 18:57:17 +0800 Subject: [PATCH 1/4] feat: support unicode input in terminal --- .../java/com/taobao/arthas/client/IOUtil.java | 5 +- .../taobao/arthas/client/TelnetConsole.java | 35 +++- .../client/TelnetConsoleBatchModeTest.java | 197 +++++++++++++++++- .../shell/term/impl/TelnetTermServer.java | 6 +- .../impl/httptelnet/HttpTelnetTermServer.java | 6 +- .../shell/term/impl/TermImplUnicodeTest.java | 47 +++++ .../httptelnet/HttpTelnetTermServerTest.java | 115 ++++++++++ pom.xml | 2 +- 8 files changed, 400 insertions(+), 13 deletions(-) create mode 100644 core/src/test/java/com/taobao/arthas/core/shell/term/impl/TermImplUnicodeTest.java create mode 100644 core/src/test/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpTelnetTermServerTest.java diff --git a/client/src/main/java/com/taobao/arthas/client/IOUtil.java b/client/src/main/java/com/taobao/arthas/client/IOUtil.java index a1a360b7f50..9621bffc75f 100644 --- a/client/src/main/java/com/taobao/arthas/client/IOUtil.java +++ b/client/src/main/java/com/taobao/arthas/client/IOUtil.java @@ -5,6 +5,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Writer; +import java.nio.charset.StandardCharsets; /*** * This is a utility class providing a reader/writer capability required by the @@ -42,7 +43,7 @@ public void run() { @Override public void run() { try { - InputStreamReader reader = new InputStreamReader(remoteInput); + InputStreamReader reader = new InputStreamReader(remoteInput, StandardCharsets.UTF_8); while (true) { int singleChar = reader.read(); if (singleChar == -1) { @@ -71,4 +72,4 @@ public void run() { } } -} \ No newline at end of file +} diff --git a/client/src/main/java/com/taobao/arthas/client/TelnetConsole.java b/client/src/main/java/com/taobao/arthas/client/TelnetConsole.java index e6141d9573e..ce09d1ffe25 100644 --- a/client/src/main/java/com/taobao/arthas/client/TelnetConsole.java +++ b/client/src/main/java/com/taobao/arthas/client/TelnetConsole.java @@ -4,11 +4,12 @@ import java.awt.event.ActionListener; import java.io.BufferedReader; import java.io.File; -import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -17,7 +18,9 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.net.telnet.InvalidTelnetOptionException; +import org.apache.commons.net.telnet.SimpleOptionHandler; import org.apache.commons.net.telnet.TelnetClient; +import org.apache.commons.net.telnet.TelnetOption; import org.apache.commons.net.telnet.TelnetOptionHandler; import org.apache.commons.net.telnet.WindowSizeOptionHandler; @@ -147,7 +150,7 @@ private static List readLines(File batchFile) { List list = new ArrayList(); BufferedReader br = null; try { - br = new BufferedReader(new FileReader(batchFile)); + br = Files.newBufferedReader(batchFile.toPath(), StandardCharsets.UTF_8); String line = br.readLine(); while (line != null) { list.add(line); @@ -285,6 +288,14 @@ public static int process(String[] args, ActionListener eotEventCallback) throws : new TelnetClient(); telnet.setConnectTimeout(DEFAULT_CONNECTION_TIMEOUT); + // Telnet BINARY 让 terminal 的 UTF-8 字节在两个方向都保持完整。 + TelnetOptionHandler binaryOpt = new SimpleOptionHandler(TelnetOption.BINARY, true, true, true, true); + try { + telnet.addOptionHandler(binaryOpt); + } catch (InvalidTelnetOptionException e) { + // ignore + } + // send init terminal size TelnetOptionHandler sizeOpt = new WindowSizeOptionHandler(width, height, true, true, false, false); try { @@ -367,7 +378,7 @@ private static int batchModeRun(TelnetClient telnet, List commands, fina public void run() { try { StringBuilder line = new StringBuilder(); - BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); + BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); int b = -1; while (true) { b = in.read(); @@ -377,7 +388,7 @@ public void run() { line.appendCodePoint(b); // 检查到有 [arthas@ 时,意味着可以执行下一个命令了 - int index = line.indexOf(PROMPT); + int index = findPromptAtLineStart(line); if (index >= 0) { line.delete(0, index + PROMPT.length()); receviedPromptQueue.put(""); @@ -406,19 +417,31 @@ public void run() { } } // send command to server - outputStream.write((command + " | plaintext\n").getBytes()); + outputStream.write((command + " | plaintext\n").getBytes(StandardCharsets.UTF_8)); outputStream.flush(); } // 读到最后一个命令执行后的 prompt ,可以直接发 quit命令了。 receviedPromptQueue.take(); - outputStream.write("quit\n".getBytes()); + outputStream.write("quit\n".getBytes(StandardCharsets.UTF_8)); outputStream.flush(); System.out.println(); return STATUS_OK; } + private static int findPromptAtLineStart(StringBuilder output) { + int fromIndex = 0; + int index; + while ((index = output.indexOf(PROMPT, fromIndex)) >= 0) { + if (index == 0 || output.charAt(index - 1) == '\n') { + return index; + } + fromIndex = index + PROMPT.length(); + } + return -1; + } + private static String usage(CLI cli) { StringBuilder usageStringBuilder = new StringBuilder(); UsageMessageFormatter usageMessageFormatter = new UsageMessageFormatter(); diff --git a/client/src/test/java/com/taobao/arthas/client/TelnetConsoleBatchModeTest.java b/client/src/test/java/com/taobao/arthas/client/TelnetConsoleBatchModeTest.java index a145021f061..79692984e3f 100644 --- a/client/src/test/java/com/taobao/arthas/client/TelnetConsoleBatchModeTest.java +++ b/client/src/test/java/com/taobao/arthas/client/TelnetConsoleBatchModeTest.java @@ -1,5 +1,7 @@ package com.taobao.arthas.client; +import org.apache.commons.net.telnet.TelnetCommand; +import org.apache.commons.net.telnet.TelnetOption; import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; @@ -43,6 +45,60 @@ void quietBatchModeShouldSendCommandWhenPromptStartsTheStream() throws Exception } } + @Test + void batchModeShouldNegotiateBinaryAndSendChineseCommandAsUtf8() throws Exception { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try (ServerSocket serverSocket = new ServerSocket(0)) { + Future serverResult = executorService.submit(() -> runBinaryServer(serverSocket)); + + int status = TelnetConsole.process(new String[] { + "--quiet", + "-c", + "echo 中文", + "-t", + "1000", + "127.0.0.1", + String.valueOf(serverSocket.getLocalPort()) + }); + + BinaryServerResult result = serverResult.get(2, TimeUnit.SECONDS); + assertThat(status).isEqualTo(TelnetConsole.STATUS_OK); + assertThat(result.clientWillBinary).isTrue(); + assertThat(result.clientDoBinary).isTrue(); + assertThat(result.command).isEqualTo("echo 中文 | plaintext"); + assertThat(result.quit).isEqualTo("quit"); + } finally { + executorService.shutdownNow(); + } + } + + @Test + void batchModeShouldIgnorePromptRepaintWhileEnteringChineseCommand() throws Exception { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try (ServerSocket serverSocket = new ServerSocket(0)) { + Future serverResult = + executorService.submit(() -> runPromptRepaintServer(serverSocket)); + + int status = TelnetConsole.process(new String[] { + "--quiet", + "-c", + "echo 中文", + "-t", + "2000", + "127.0.0.1", + String.valueOf(serverSocket.getLocalPort()) + }); + + PromptRepaintServerResult result = serverResult.get(3, TimeUnit.SECONDS); + assertThat(status).isEqualTo(TelnetConsole.STATUS_OK); + assertThat(result.command).isEqualTo("echo 中文 | plaintext"); + assertThat(result.quitSentBeforeCommandCompleted).isFalse(); + assertThat(result.quit).isEqualTo("quit"); + } finally { + executorService.shutdownNow(); + } + } + private static ServerResult runPromptFirstServer(ServerSocket serverSocket) throws IOException { try (Socket socket = serverSocket.accept()) { socket.setSoTimeout(2000); @@ -59,6 +115,62 @@ private static ServerResult runPromptFirstServer(ServerSocket serverSocket) thro } } + private static BinaryServerResult runBinaryServer(ServerSocket serverSocket) throws IOException { + try (Socket socket = serverSocket.accept()) { + socket.setSoTimeout(2000); + OutputStream outputStream = socket.getOutputStream(); + TelnetApplicationReader reader = new TelnetApplicationReader(socket.getInputStream()); + + outputStream.write(new byte[] { + (byte) TelnetCommand.IAC, (byte) TelnetCommand.DO, (byte) TelnetOption.BINARY, + (byte) TelnetCommand.IAC, (byte) TelnetCommand.WILL, (byte) TelnetOption.BINARY + }); + write(outputStream, "[arthas@123]$ "); + String command = reader.readLine(); + + write(outputStream, "ok\n[arthas@123]$ "); + String quit = reader.readLine(); + + return new BinaryServerResult( + reader.clientWillBinary, reader.clientDoBinary, command, quit); + } + } + + private static PromptRepaintServerResult runPromptRepaintServer(ServerSocket serverSocket) throws IOException { + try (Socket socket = serverSocket.accept()) { + socket.setSoTimeout(2000); + OutputStream outputStream = socket.getOutputStream(); + TelnetApplicationReader reader = new TelnetApplicationReader(socket.getInputStream()); + + outputStream.write(new byte[] { + (byte) TelnetCommand.IAC, (byte) TelnetCommand.DO, (byte) TelnetOption.BINARY, + (byte) TelnetCommand.IAC, (byte) TelnetCommand.WILL, (byte) TelnetOption.BINARY + }); + write(outputStream, "[arthas@123]$ "); + String command = reader.readLine(); + + write(outputStream, "\r\033[K[arthas@123]$ echo 中"); + socket.setSoTimeout(250); + String quit = null; + boolean quitSentBeforeCommandCompleted = false; + try { + quit = reader.readLine(); + quitSentBeforeCommandCompleted = "quit".equals(quit); + } catch (java.net.SocketTimeoutException ignored) { + // 重绘中的 prompt 不应唤醒批处理命令边界。 + } + + write(outputStream, "文\r\nok\r\n[arthas@123]$ "); + if (quit == null) { + socket.setSoTimeout(2000); + quit = reader.readLine(); + } + + return new PromptRepaintServerResult( + command, quit, quitSentBeforeCommandCompleted); + } + } + private static void write(OutputStream outputStream, String value) throws IOException { outputStream.write(value.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); @@ -77,12 +189,93 @@ private static String readLine(InputStream inputStream) throws IOException { } private static class ServerResult { - private final String command; - private final String quit; + final String command; + final String quit; private ServerResult(String command, String quit) { this.command = command; this.quit = quit; } } + + private static class BinaryServerResult extends ServerResult { + private final boolean clientWillBinary; + private final boolean clientDoBinary; + + private BinaryServerResult( + boolean clientWillBinary, + boolean clientDoBinary, + String command, + String quit) { + super(command, quit); + this.clientWillBinary = clientWillBinary; + this.clientDoBinary = clientDoBinary; + } + } + + private static class PromptRepaintServerResult extends ServerResult { + private final boolean quitSentBeforeCommandCompleted; + + private PromptRepaintServerResult( + String command, + String quit, + boolean quitSentBeforeCommandCompleted) { + super(command, quit); + this.quitSentBeforeCommandCompleted = quitSentBeforeCommandCompleted; + } + } + + private static class TelnetApplicationReader { + private final InputStream inputStream; + private boolean clientWillBinary; + private boolean clientDoBinary; + + private TelnetApplicationReader(InputStream inputStream) { + this.inputStream = inputStream; + } + + private String readLine() throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int value; + while ((value = inputStream.read()) != -1) { + if (value == TelnetCommand.IAC) { + readTelnetCommand(buffer); + } else if (value == '\n') { + break; + } else if (value != '\r' && value != 0) { + buffer.write(value); + } + } + return new String(buffer.toByteArray(), StandardCharsets.UTF_8); + } + + private void readTelnetCommand(ByteArrayOutputStream buffer) throws IOException { + int command = inputStream.read(); + if (command == TelnetCommand.IAC) { + buffer.write(TelnetCommand.IAC); + } else if (command == TelnetCommand.DO + || command == TelnetCommand.DONT + || command == TelnetCommand.WILL + || command == TelnetCommand.WONT) { + int option = inputStream.read(); + if (option == TelnetOption.BINARY) { + clientWillBinary |= command == TelnetCommand.WILL; + clientDoBinary |= command == TelnetCommand.DO; + } + } else if (command == TelnetCommand.SB) { + skipSubnegotiation(); + } + } + + private void skipSubnegotiation() throws IOException { + int previous = -1; + int value; + while ((value = inputStream.read()) != -1) { + if (previous == TelnetCommand.IAC && value == TelnetCommand.SE) { + return; + } + previous = value; + } + } + } } diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/TelnetTermServer.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/TelnetTermServer.java index 585c2b9cf0a..b5333a3c654 100644 --- a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/TelnetTermServer.java +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/TelnetTermServer.java @@ -43,7 +43,11 @@ public TermServer termHandler(Handler handler) { @Override public TermServer listen(Handler> listenHandler) { // TODO: charset and inputrc from options - bootstrap = new NettyTelnetTtyBootstrap().setHost(hostIp).setPort(port); + bootstrap = new NettyTelnetTtyBootstrap() + .setHost(hostIp) + .setPort(port) + .setInBinary(true) + .setOutBinary(true); try { bootstrap.start(new Consumer() { @Override diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpTelnetTermServer.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpTelnetTermServer.java index 97f52e43491..0895c89ec2d 100644 --- a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpTelnetTermServer.java +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpTelnetTermServer.java @@ -51,7 +51,11 @@ public TermServer termHandler(Handler handler) { @Override public TermServer listen(Handler> listenHandler) { // TODO: charset and inputrc from options - bootstrap = new NettyHttpTelnetTtyBootstrap(workerGroup, httpSessionManager).setHost(hostIp).setPort(port); + bootstrap = new NettyHttpTelnetTtyBootstrap(workerGroup, httpSessionManager) + .setHost(hostIp) + .setPort(port) + .setInBinary(true) + .setOutBinary(true); try { bootstrap.start(new Consumer() { @Override diff --git a/core/src/test/java/com/taobao/arthas/core/shell/term/impl/TermImplUnicodeTest.java b/core/src/test/java/com/taobao/arthas/core/shell/term/impl/TermImplUnicodeTest.java new file mode 100644 index 00000000000..05f355f9fd6 --- /dev/null +++ b/core/src/test/java/com/taobao/arthas/core/shell/term/impl/TermImplUnicodeTest.java @@ -0,0 +1,47 @@ +package com.taobao.arthas.core.shell.term.impl; + +import io.termd.core.function.Consumer; +import io.termd.core.tty.TtyConnection; +import io.termd.core.util.Helper; +import io.termd.core.util.Vector; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TermImplUnicodeTest { + + @Test + void readlineShouldPreserveChineseInput() { + AtomicReference> stdinHandler = new AtomicReference>(); + List output = new ArrayList(); + TtyConnection connection = mock(TtyConnection.class); + when(connection.size()).thenReturn(new Vector(80, 24)); + when(connection.getStdinHandler()).thenAnswer(invocation -> stdinHandler.get()); + when(connection.stdoutHandler()).thenReturn(codePoints -> { + for (int codePoint : codePoints) { + output.add(codePoint); + } + }); + doAnswer(invocation -> { + stdinHandler.set(invocation.getArgument(0)); + return null; + }).when(connection).setStdinHandler(any()); + + TermImpl term = new TermImpl(connection); + AtomicReference line = new AtomicReference(); + term.readline("$ ", line::set); + + stdinHandler.get().accept(Helper.toCodePoints("echo 中文\r")); + + assertThat(line).hasValue("echo 中文"); + assertThat(output).doesNotContain(7); + } +} diff --git a/core/src/test/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpTelnetTermServerTest.java b/core/src/test/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpTelnetTermServerTest.java new file mode 100644 index 00000000000..f1789afae0d --- /dev/null +++ b/core/src/test/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpTelnetTermServerTest.java @@ -0,0 +1,115 @@ +package com.taobao.arthas.core.shell.term.impl.httptelnet; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.term.TermServer; +import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager; +import io.netty.util.concurrent.DefaultEventExecutorGroup; +import io.netty.util.concurrent.EventExecutorGroup; +import io.termd.core.telnet.TelnetConnection; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; + +class HttpTelnetTermServerTest { + + private static final int BINARY_OPTION = 0; + private static final int NAWS_OPTION = 31; + + @Test + void shouldRequestBinaryInBothDirections() throws Exception { + int port = findFreePort(); + EventExecutorGroup workerGroup = new DefaultEventExecutorGroup(1); + HttpTelnetTermServer server = new HttpTelnetTermServer( + "127.0.0.1", port, 3000, workerGroup, new HttpSessionManager()); + server.termHandler(term -> { + }); + AtomicReference> listenResult = new AtomicReference>(); + + try { + server.listen(listenResult::set); + assertThat(listenResult.get()).isNotNull(); + assertThat(listenResult.get().succeeded()).isTrue(); + + try (Socket socket = new Socket("127.0.0.1", port)) { + socket.setSoTimeout(1000); + OutputStream output = socket.getOutputStream(); + output.write(new byte[] { + TelnetConnection.BYTE_IAC, TelnetConnection.BYTE_WILL, (byte) NAWS_OPTION + }); + output.flush(); + + BinaryNegotiation negotiation = readBinaryNegotiation(socket.getInputStream(), output); + + assertThat(negotiation.serverRequestsBinaryInput).isTrue(); + assertThat(negotiation.serverOffersBinaryOutput).isTrue(); + } + } finally { + server.close(result -> { + }); + workerGroup.shutdownGracefully().syncUninterruptibly(); + } + } + + private static BinaryNegotiation readBinaryNegotiation(InputStream input, OutputStream output) + throws Exception { + BinaryNegotiation result = new BinaryNegotiation(); + try { + while (!result.complete()) { + int value = input.read(); + if (value == -1) { + break; + } + if (value != (TelnetConnection.BYTE_IAC & 0xff)) { + continue; + } + int command = input.read(); + int option = input.read(); + if (option != BINARY_OPTION) { + continue; + } + if (command == (TelnetConnection.BYTE_DO & 0xff)) { + result.serverRequestsBinaryInput = true; + output.write(new byte[] { + TelnetConnection.BYTE_IAC, + TelnetConnection.BYTE_WILL, + (byte) BINARY_OPTION + }); + output.flush(); + } else if (command == (TelnetConnection.BYTE_WILL & 0xff)) { + result.serverOffersBinaryOutput = true; + output.write(new byte[] { + TelnetConnection.BYTE_IAC, + TelnetConnection.BYTE_DO, + (byte) BINARY_OPTION + }); + output.flush(); + } + } + } catch (SocketTimeoutException ignored) { + // 由断言报告缺失的协商方向。 + } + return result; + } + + private static int findFreePort() throws Exception { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } + + private static class BinaryNegotiation { + private boolean serverRequestsBinaryInput; + private boolean serverOffersBinaryOutput; + + private boolean complete() { + return serverRequestsBinaryInput && serverOffersBinaryOutput; + } + } +} diff --git a/pom.xml b/pom.xml index a08af4fd7b8..889182d28c4 100644 --- a/pom.xml +++ b/pom.xml @@ -111,7 +111,7 @@ com.alibaba.middleware termd-core - 1.1.7.15 + 1.1.7.16-SNAPSHOT com.alibaba.middleware From 48ce19701be3fa5116f866dc2158cb67fe5ee1e4 Mon Sep 17 00:00:00 2001 From: hengyunabc Date: Sat, 18 Jul 2026 19:04:32 +0800 Subject: [PATCH 2/4] fix: preserve unicode in command history --- .../taobao/arthas/core/util/FileUtils.java | 11 ++++------- .../arthas/core/util/FileUtilsTest.java | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/taobao/arthas/core/util/FileUtils.java b/core/src/main/java/com/taobao/arthas/core/util/FileUtils.java index 05f8237f84d..dbdc6742b60 100644 --- a/core/src/main/java/com/taobao/arthas/core/util/FileUtils.java +++ b/core/src/main/java/com/taobao/arthas/core/util/FileUtils.java @@ -8,6 +8,7 @@ import java.io.*; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -94,7 +95,6 @@ private static boolean isAuthCommand(String command) { return command != null && command.trim().startsWith(ArthasConstants.AUTH); } - private static final int[] AUTH_CODEPOINTS = Helper.toCodePoints(ArthasConstants.AUTH); /** * save the command history to the given file, data will be overridden. * @param history the command history, each represented by an int array @@ -105,12 +105,10 @@ public static void saveCommandHistory(List history, File file) { for (int[] command : history) { String commandStr = Helper.fromCodePoints(command); if (isAuthCommand(commandStr)) { - command = AUTH_CODEPOINTS; + commandStr = ArthasConstants.AUTH; } - for (int i : command) { - out.write(i); - } + out.write(commandStr.getBytes(StandardCharsets.UTF_8)); out.write('\n'); } } catch (IOException e) { @@ -123,7 +121,7 @@ public static List loadCommandHistory(File file) { BufferedReader br = null; List history = new ArrayList<>(); try { - br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); + br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)); String line; while ((line = br.readLine()) != null) { history.add(Helper.toCodePoints(line)); @@ -226,4 +224,3 @@ public static boolean isDirectoryOrNotExist(String path) { return !file.exists() || file.isDirectory(); } } - diff --git a/core/src/test/java/com/taobao/arthas/core/util/FileUtilsTest.java b/core/src/test/java/com/taobao/arthas/core/util/FileUtilsTest.java index fbbc7cd599a..22513d7568a 100644 --- a/core/src/test/java/com/taobao/arthas/core/util/FileUtilsTest.java +++ b/core/src/test/java/com/taobao/arthas/core/util/FileUtilsTest.java @@ -1,6 +1,7 @@ package com.taobao.arthas.core.util; import com.taobao.arthas.core.testtool.TestUtils; +import io.termd.core.util.Helper; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -11,6 +12,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.List; import static org.hamcrest.CoreMatchers.allOf; @@ -106,6 +109,22 @@ public void testLoadCommandHistory() throws IOException { Assert.assertArrayEquals(command1, content.get(0)); } + @Test + public void testSaveAndLoadUnicodeCommandHistory() throws IOException { + String command = "echo 中文 " + new String(Character.toChars(0x20000)); + int[] codePoints = Helper.toCodePoints(command); + File targetFile = temporaryFolder.newFile("unicode-history.txt"); + + FileUtils.saveCommandHistory(TestUtils.newArrayList(codePoints), targetFile); + + Assert.assertArrayEquals( + (command + '\n').getBytes(StandardCharsets.UTF_8), + Files.readAllBytes(targetFile.toPath())); + List history = FileUtils.loadCommandHistory(targetFile); + Assert.assertEquals(1, history.size()); + Assert.assertArrayEquals(codePoints, history.get(0)); + } + From df8443a13eeedb51b081b6f8d521eedb040360e0 Mon Sep 17 00:00:00 2001 From: hengyunabc Date: Sun, 19 Jul 2026 00:40:22 +0800 Subject: [PATCH 3/4] build: use termd 1.1.7.16 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 889182d28c4..fa528432446 100644 --- a/pom.xml +++ b/pom.xml @@ -111,7 +111,7 @@ com.alibaba.middleware termd-core - 1.1.7.16-SNAPSHOT + 1.1.7.16 com.alibaba.middleware From 00f5813a29e01a9715771b186fef9f4e10c6e6c6 Mon Sep 17 00:00:00 2001 From: hengyunabc Date: Sun, 19 Jul 2026 01:15:10 +0800 Subject: [PATCH 4/4] docs: update Unicode input FAQ --- site/docs/doc/faq.md | 4 +++- site/docs/en/doc/faq.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/site/docs/doc/faq.md b/site/docs/doc/faq.md index a3469dd0d25..9b68777cac1 100644 --- a/site/docs/doc/faq.md +++ b/site/docs/doc/faq.md @@ -104,7 +104,9 @@ watch OuterClass$InnerClass ### 输入中文/Unicode 字符 -把中文/Unicode 字符转为`\u`表示方法: +从 Arthas 4.3.2 开始,Terminal 支持直接输入和编辑中文及其他 Unicode 字符。 + +Arthas 4.3.1 及更早版本不支持直接输入中文/Unicode 字符,可以将字符转换为`\u`表示: ```bash ognl '@java.lang.System@out.println("Hello \u4e2d\u6587")' diff --git a/site/docs/en/doc/faq.md b/site/docs/en/doc/faq.md index 9b335e71a78..b2afdec70dd 100644 --- a/site/docs/en/doc/faq.md +++ b/site/docs/en/doc/faq.md @@ -104,7 +104,9 @@ For classes generated by lambda, will be skipped because the JVM itself does not ### Enter Unicode characters -Convert Unicode characters to `\u` representation: +Starting with Arthas 4.3.2, the terminal supports directly entering and editing Chinese and other Unicode characters. + +In Arthas 4.3.1 and earlier, convert Unicode characters to the `\u` representation: ```bash ognl '@java.lang.System@out.println("Hello \u4e2d\u6587")'