Skip to content

Commit ba3d76a

Browse files
author
lif
committed
Make the mechanism for escaping discoverable and customizable
1 parent 92508d5 commit ba3d76a

File tree

1 file changed

+73
-65
lines changed

1 file changed

+73
-65
lines changed

bin/propolis-cli/src/main.rs

Lines changed: 73 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
use std::ffi::OsString;
12
use std::fs::File;
23
use std::io::BufReader;
34
use std::path::{Path, PathBuf};
45
use std::{
56
net::{IpAddr, SocketAddr, ToSocketAddrs},
6-
os::unix::prelude::AsRawFd,
7+
os::unix::prelude::{AsRawFd, OsStringExt},
78
time::Duration,
89
};
910

@@ -90,6 +91,15 @@ enum Command {
9091
/// Defaults to the most recent 16 KiB of console output (-16384).
9192
#[clap(long, short)]
9293
byte_offset: Option<i64>,
94+
95+
/// If this sequence of bytes is typed, the client will exit.
96+
/// Defaults to ^]^C (Ctrl+], Ctrl+C).
97+
#[clap(long, short, default_value = "\x1d\x03")]
98+
escape_string: OsString,
99+
100+
/// Disable escape string altogether (to exit, use pkill or similar).
101+
#[clap(long, short = 'E')]
102+
no_escape: bool,
93103
},
94104

95105
/// Migrate instance to new propolis-server
@@ -221,60 +231,56 @@ async fn put_instance(
221231
async fn stdin_to_websockets_task(
222232
mut stdinrx: tokio::sync::mpsc::Receiver<Vec<u8>>,
223233
wstx: tokio::sync::mpsc::Sender<Vec<u8>>,
234+
escape_vector: Option<Vec<u8>>,
224235
) {
225-
// next_raw must live outside loop, because Ctrl-A should work across
226-
// multiple inbuf reads.
227-
let mut next_raw = false;
236+
if let Some(esc_sequence) = &escape_vector {
237+
// esc_pos must live outside loop, because escape string should work
238+
// across multiple inbuf reads.
239+
let mut esc_pos = 0;
228240

229-
loop {
230-
let inbuf = if let Some(inbuf) = stdinrx.recv().await {
231-
inbuf
232-
} else {
233-
continue;
234-
};
241+
loop {
242+
let inbuf = if let Some(inbuf) = stdinrx.recv().await {
243+
inbuf
244+
} else {
245+
continue;
246+
};
235247

236-
// Put bytes from inbuf to outbuf, but don't send Ctrl-A unless
237-
// next_raw is true.
238-
let mut outbuf = Vec::with_capacity(inbuf.len());
239-
240-
let mut exit = false;
241-
for c in inbuf {
242-
match c {
243-
// Ctrl-A means send next one raw
244-
b'\x01' => {
245-
if next_raw {
246-
// Ctrl-A Ctrl-A should be sent as Ctrl-A
247-
outbuf.push(c);
248-
next_raw = false;
249-
} else {
250-
next_raw = true;
251-
}
252-
}
253-
b'\x03' => {
254-
if !next_raw {
255-
// Exit on non-raw Ctrl-C
248+
// Put bytes from inbuf to outbuf, but don't send characters in the
249+
// escape string sequence unless we bail.
250+
let mut outbuf = Vec::with_capacity(inbuf.len());
251+
252+
let mut exit = false;
253+
for c in inbuf {
254+
if c == esc_sequence[esc_pos] {
255+
esc_pos += 1;
256+
if esc_pos == esc_sequence.len() {
257+
// Exit on completed escape string
256258
exit = true;
257259
break;
258-
} else {
259-
// Otherwise send Ctrl-C
260-
outbuf.push(c);
261-
next_raw = false;
262260
}
263-
}
264-
_ => {
261+
} else {
262+
// they bailed from the sequence,
263+
// feed everything that matched so far through
264+
if esc_pos != 0 {
265+
outbuf.extend(&esc_sequence[..esc_pos])
266+
}
267+
esc_pos = 0;
265268
outbuf.push(c);
266-
next_raw = false;
267269
}
268270
}
269-
}
270271

271-
// Send what we have, even if there's a Ctrl-C at the end.
272-
if !outbuf.is_empty() {
273-
wstx.send(outbuf).await.unwrap();
274-
}
272+
// Send what we have, even if we're about to exit.
273+
if !outbuf.is_empty() {
274+
wstx.send(outbuf).await.unwrap();
275+
}
275276

276-
if exit {
277-
break;
277+
if exit {
278+
break;
279+
}
280+
}
281+
} else {
282+
while let Some(buf) = stdinrx.recv().await {
283+
wstx.send(buf).await.unwrap();
278284
}
279285
}
280286
}
@@ -286,7 +292,10 @@ async fn test_stdin_to_websockets_task() {
286292
let (stdintx, stdinrx) = tokio::sync::mpsc::channel(16);
287293
let (wstx, mut wsrx) = tokio::sync::mpsc::channel(16);
288294

289-
tokio::spawn(async move { stdin_to_websockets_task(stdinrx, wstx).await });
295+
let escape_vector = Some(vec![0x1d, 0x03]);
296+
tokio::spawn(async move {
297+
stdin_to_websockets_task(stdinrx, wstx, escape_vector).await
298+
});
290299

291300
// send characters, receive characters
292301
stdintx
@@ -296,33 +305,22 @@ async fn test_stdin_to_websockets_task() {
296305
let actual = wsrx.recv().await.unwrap();
297306
assert_eq!(String::from_utf8(actual).unwrap(), "test post please ignore");
298307

299-
// don't send ctrl-a
300-
stdintx.send("\x01".chars().map(|c| c as u8).collect()).await.unwrap();
308+
// don't send a started escape sequence
309+
stdintx.send("\x1d".chars().map(|c| c as u8).collect()).await.unwrap();
301310
assert_eq!(wsrx.try_recv(), Err(TryRecvError::Empty));
302311

303-
// the "t" here is sent "raw" because of last ctrl-a but that doesn't change anything
312+
// since we didn't enter the \x03, the previous \x1d shows up here
304313
stdintx.send("test".chars().map(|c| c as u8).collect()).await.unwrap();
305314
let actual = wsrx.recv().await.unwrap();
306-
assert_eq!(String::from_utf8(actual).unwrap(), "test");
307-
308-
// ctrl-a ctrl-c = only ctrl-c sent
309-
stdintx.send("\x01\x03".chars().map(|c| c as u8).collect()).await.unwrap();
310-
let actual = wsrx.recv().await.unwrap();
311-
assert_eq!(String::from_utf8(actual).unwrap(), "\x03");
315+
assert_eq!(String::from_utf8(actual).unwrap(), "\x1dtest");
312316

313-
// same as above, across two messages
314-
stdintx.send("\x01".chars().map(|c| c as u8).collect()).await.unwrap();
317+
// \x03 gets sent if not preceded by \x1d
315318
stdintx.send("\x03".chars().map(|c| c as u8).collect()).await.unwrap();
316-
assert_eq!(wsrx.try_recv(), Err(TryRecvError::Empty));
317319
let actual = wsrx.recv().await.unwrap();
318320
assert_eq!(String::from_utf8(actual).unwrap(), "\x03");
319321

320-
// ctrl-a ctrl-a = only ctrl-a sent
321-
stdintx.send("\x01\x01".chars().map(|c| c as u8).collect()).await.unwrap();
322-
let actual = wsrx.recv().await.unwrap();
323-
assert_eq!(String::from_utf8(actual).unwrap(), "\x01");
324-
325-
// ctrl-c on its own means exit
322+
// \x1d followed by \x03 means exit, even if they're separate messages
323+
stdintx.send("\x1d".chars().map(|c| c as u8).collect()).await.unwrap();
326324
stdintx.send("\x03".chars().map(|c| c as u8).collect()).await.unwrap();
327325
assert_eq!(wsrx.try_recv(), Err(TryRecvError::Empty));
328326

@@ -333,6 +331,7 @@ async fn test_stdin_to_websockets_task() {
333331
async fn serial(
334332
addr: SocketAddr,
335333
byte_offset: Option<i64>,
334+
escape_vector: Option<Vec<u8>>,
336335
) -> anyhow::Result<()> {
337336
let client = propolis_client::Client::new(&format!("http://{}", addr));
338337
let mut req = client.instance_serial();
@@ -375,7 +374,9 @@ async fn serial(
375374
}
376375
});
377376

378-
tokio::spawn(async move { stdin_to_websockets_task(stdinrx, wstx).await });
377+
tokio::spawn(async move {
378+
stdin_to_websockets_task(stdinrx, wstx, escape_vector).await
379+
});
379380

380381
loop {
381382
tokio::select! {
@@ -569,7 +570,14 @@ async fn main() -> anyhow::Result<()> {
569570
}
570571
Command::Get => get_instance(&client).await?,
571572
Command::State { state } => put_instance(&client, state).await?,
572-
Command::Serial { byte_offset } => serial(addr, byte_offset).await?,
573+
Command::Serial { byte_offset, escape_string, no_escape } => {
574+
let escape_vector = if no_escape || escape_string.is_empty() {
575+
None
576+
} else {
577+
Some(escape_string.into_vec())
578+
};
579+
serial(addr, byte_offset, escape_vector).await?
580+
}
573581
Command::Migrate { dst_server, dst_port, dst_uuid } => {
574582
let dst_addr = SocketAddr::new(dst_server, dst_port);
575583
let dst_client = Client::new(dst_addr, log.clone());

0 commit comments

Comments
 (0)