Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions client/src/main/java/com/taobao/arthas/client/IOUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -71,4 +72,4 @@ public void run() {
}
}

}
}
35 changes: 29 additions & 6 deletions client/src/main/java/com/taobao/arthas/client/TelnetConsole.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -147,7 +150,7 @@ private static List<String> readLines(File batchFile) {
List<String> list = new ArrayList<String>();
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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -367,7 +378,7 @@ private static int batchModeRun(TelnetClient telnet, List<String> 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();
Expand All @@ -377,7 +388,7 @@ public void run() {
line.appendCodePoint(b);

// 检查到有 [arthas@ 时,意味着可以执行下一个命令了
int index = line.indexOf(PROMPT);
int index = findPromptAtLineStart(line);
if (index >= 0) {
Comment on lines 388 to 392
line.delete(0, index + PROMPT.length());
receviedPromptQueue.put("");
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -43,6 +45,60 @@ void quietBatchModeShouldSendCommandWhenPromptStartsTheStream() throws Exception
}
}

@Test
void batchModeShouldNegotiateBinaryAndSendChineseCommandAsUtf8() throws Exception {
ExecutorService executorService = Executors.newSingleThreadExecutor();
try (ServerSocket serverSocket = new ServerSocket(0)) {
Future<BinaryServerResult> 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<PromptRepaintServerResult> 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);
Expand All @@ -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();
Expand All @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ public TermServer termHandler(Handler<Term> handler) {
@Override
public TermServer listen(Handler<Future<TermServer>> 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);
Comment on lines +49 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid requiring BINARY for every Telnet session

When a Telnet client declines or does not implement the BINARY option, setting both directions as required prevents the Arthas shell from being created: TelnetTtyConnection.checkAccept() only calls the terminal handler after the requested binary directions are acknowledged. This means documented connections such as telnet 127.0.0.1 3658 can hang with no prompt on clients that answer WONT/DONT or ignore BINARY; the same required flags were also added to HttpTelnetTermServer. Please keep a non-binary fallback or make BINARY opportunistic rather than mandatory.

Useful? React with 👍 / 👎.

try {
bootstrap.start(new Consumer<TtyConnection>() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ public TermServer termHandler(Handler<Term> handler) {
@Override
public TermServer listen(Handler<Future<TermServer>> 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<TtyConnection>() {
@Override
Expand Down
Loading
Loading