feat(terminal): implement synchronized binary input handling for XTerminal
- Added `sendSessionBinaryInputWithSync` function to broadcast binary input to sync peers. - Updated `XTerminal` component to utilize the new function for handling binary input with peer synchronization. - Enhanced tests to verify the correct broadcasting of binary input to multiple peers without text bookkeeping. - Introduced `escape_telnet_application_data` function to handle IAC byte escaping for telnet transport. - Added tests for the new escaping functionality to ensure proper data handling in telnet sessions.
This commit is contained in:
1 parent
280272aaaf
commit
334817dbff
6 files changed
+69
-3
No files matched your search
@@ -58,6 +58,21 @@ fn unescape_iac_iac(data: &[u8]) -> Vec<u8> {
|
||||
visible
|
||||
}
|
||||
|
||||
fn escape_telnet_application_data(data: &[u8], raw_tcp_cli: bool) -> Vec<u8> {
|
||||
if raw_tcp_cli {
|
||||
return data.to_vec();
|
||||
}
|
||||
|
||||
let mut escaped = Vec::with_capacity(data.len());
|
||||
for &byte in data {
|
||||
escaped.push(byte);
|
||||
if byte == IAC {
|
||||
escaped.push(IAC);
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
|
||||
/// Strip IAC sequences from raw data, returning only user-visible bytes.
|
||||
/// Calls `on_negotiate` for each IAC command/option pair encountered.
|
||||
fn strip_telnet_commands(data: &[u8], on_negotiate: &mut impl FnMut(u8, u8)) -> Vec<u8> {
|
||||
|
||||
@@ -400,6 +400,7 @@ async fn telnet_session_task(
|
||||
let mut write_failed = None;
|
||||
if raw {
|
||||
let send_data = prepare_terminal_write_input(data, &encoding, true, false);
|
||||
let send_data = escape_telnet_application_data(&send_data, config.raw_tcp_cli);
|
||||
if let Err(e) = writer.write_all(&send_data).await {
|
||||
write_failed = Some(e);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ mod tests {
|
||||
DO, IAC, OPT_NAWS, OPT_SUPPRESS_GO_AHEAD, TelnetEnterMode, TelnetLineEditor,
|
||||
TelnetAutoLogin, TelnetAutoLoginAction, TelnetAutoLoginConfig,
|
||||
TelnetAutoLoginCredentials, TelnetSessionConfig, WILL, maybe_build_naws,
|
||||
await_telnet_connection, negotiate_response, normalize_enter_bytes, split_write_chunks,
|
||||
await_telnet_connection, escape_telnet_application_data, negotiate_response, normalize_enter_bytes, split_write_chunks,
|
||||
strip_telnet_commands,
|
||||
};
|
||||
use crate::error::AppError;
|
||||
@@ -84,6 +84,19 @@ mod tests {
|
||||
assert!(responses.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_input_escapes_iac_only_for_telnet_transport() {
|
||||
let mouse_report = b"\x1b[M \xff\x80";
|
||||
assert_eq!(
|
||||
escape_telnet_application_data(mouse_report, false),
|
||||
b"\x1b[M \xff\xff\x80"
|
||||
);
|
||||
assert_eq!(
|
||||
escape_telnet_application_data(mouse_report, true),
|
||||
mouse_report
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_naws_false_prevents_naws_resize_payload() {
|
||||
let config = TelnetSessionConfig {
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
type SendSessionInputOptions,
|
||||
type SessionInputPreview,
|
||||
sendSessionBinaryInput,
|
||||
sendSessionBinaryInputWithSync,
|
||||
sendSessionInput,
|
||||
sendSessionInputWithSync,
|
||||
} from "@/lib/sessionInput";
|
||||
@@ -2362,7 +2363,12 @@ export default function XTerminal({
|
||||
requestWake("input");
|
||||
}
|
||||
if (disconnectedRef.current) return;
|
||||
void sendSessionBinaryInput(sessionIdRef.current, data).catch(() => {});
|
||||
const peers = syncPeerSessionIdsRef.current;
|
||||
void (
|
||||
peers?.length
|
||||
? sendSessionBinaryInputWithSync(sessionIdRef.current, data, peers)
|
||||
: sendSessionBinaryInput(sessionIdRef.current, data)
|
||||
).catch(() => {});
|
||||
});
|
||||
|
||||
const resizeDisposable = terminal.onResize(({ cols, rows }) => {
|
||||
|
||||
@@ -5,7 +5,12 @@ const mocks = vi.hoisted(() => ({
|
||||
}));
|
||||
vi.mock("./invoke", () => ({ invoke: mocks.invoke }));
|
||||
|
||||
import { sendSessionBinaryInput, sendSessionInput, sendSessionInputWithSync } from "./sessionInput";
|
||||
import {
|
||||
sendSessionBinaryInput,
|
||||
sendSessionBinaryInputWithSync,
|
||||
sendSessionInput,
|
||||
sendSessionInputWithSync,
|
||||
} from "./sessionInput";
|
||||
|
||||
describe("sendSessionInputWithSync command confirmation", () => {
|
||||
beforeEach(() => {
|
||||
@@ -37,6 +42,21 @@ describe("sendSessionInputWithSync command confirmation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("broadcasts binary input to sync peers without text bookkeeping", async () => {
|
||||
await sendSessionBinaryInputWithSync("primary", "\x1b[M \xff", [
|
||||
"peer-a",
|
||||
"peer-b",
|
||||
]);
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledTimes(3);
|
||||
for (const sessionId of ["primary", "peer-a", "peer-b"]) {
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("write_bytes_to_session", {
|
||||
sessionId,
|
||||
data: [0x1b, 0x5b, 0x4d, 0x20, 0xff],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("registers each peer candidate before synchronized command input", async () => {
|
||||
await sendSessionInputWithSync(
|
||||
"primary",
|
||||
|
||||
@@ -182,6 +182,17 @@ export async function sendSessionBinaryInput(sessionId: string, data: string): P
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendSessionBinaryInputWithSync(
|
||||
sessionId: string,
|
||||
data: string,
|
||||
peerSessionIds: string[],
|
||||
): Promise<void> {
|
||||
await sendSessionBinaryInput(sessionId, data);
|
||||
await Promise.allSettled(
|
||||
peerSessionIds.map((sid) => sendSessionBinaryInput(sid, data)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send input to a session and broadcast to all sync-group peers.
|
||||
* Peers do not emit frontend preview/history UI, but an executable command
|
||||
|
||||
Reference in new issue
Block a user