1+ use std:: ffi:: OsString ;
12use std:: fs:: File ;
23use std:: io:: BufReader ;
34use std:: path:: { Path , PathBuf } ;
45use 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(
221231async 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( ) , "\x1d test" ) ;
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() {
333331async 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