@@ -17,6 +17,7 @@ use propolis_client::handmade::{
17
17
} ,
18
18
Client ,
19
19
} ;
20
+ use regex:: bytes:: Regex ;
20
21
use slog:: { o, Drain , Level , Logger } ;
21
22
use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
22
23
use tokio_tungstenite:: tungstenite:: protocol:: Role ;
@@ -90,6 +91,29 @@ enum Command {
90
91
/// Defaults to the most recent 16 KiB of console output (-16384).
91
92
#[ clap( long, short) ]
92
93
byte_offset : Option < i64 > ,
94
+
95
+ /// If this sequence of bytes is typed, the client will exit.
96
+ /// Defaults to "^]^C" (Ctrl+], Ctrl+C). Note that the string passed
97
+ /// for this argument must be valid UTF-8, and is used verbatim without
98
+ /// any parsing; in most shells, if you wish to include a special
99
+ /// character (such as Enter or a Ctrl+letter combo), you can insert
100
+ /// the character by preceding it with Ctrl+V at the command line.
101
+ #[ clap( long, short, default_value = "\x1d \x03 " ) ]
102
+ escape_string : String ,
103
+
104
+ /// The number of bytes from the beginning of the escape string to pass
105
+ /// to the VM before beginning to buffer inputs until a mismatch.
106
+ /// Defaults to 0, such that input matching the escape string does not
107
+ /// get sent to the VM at all until a non-matching character is typed.
108
+ /// To mimic the escape sequence for exiting SSH ("\n~."),you may pass
109
+ /// `-e '^M~.' --escape-prefix-length=1` such that newlines are sent to
110
+ /// the VM immediately.
111
+ #[ clap( long, default_value = "0" ) ]
112
+ escape_prefix_length : usize ,
113
+
114
+ /// Disable escape string altogether (to exit, use pkill or similar).
115
+ #[ clap( long, short = 'E' ) ]
116
+ no_escape : bool ,
93
117
} ,
94
118
95
119
/// Migrate instance to new propolis-server
@@ -225,60 +249,28 @@ async fn put_instance(
225
249
async fn stdin_to_websockets_task (
226
250
mut stdinrx : tokio:: sync:: mpsc:: Receiver < Vec < u8 > > ,
227
251
wstx : tokio:: sync:: mpsc:: Sender < Vec < u8 > > ,
252
+ mut escape : Option < EscapeSequence > ,
228
253
) {
229
- // next_raw must live outside loop, because Ctrl-A should work across
230
- // multiple inbuf reads.
231
- let mut next_raw = false ;
232
-
233
- loop {
234
- let inbuf = if let Some ( inbuf) = stdinrx. recv ( ) . await {
235
- inbuf
236
- } else {
237
- continue ;
238
- } ;
239
-
240
- // Put bytes from inbuf to outbuf, but don't send Ctrl-A unless
241
- // next_raw is true.
242
- let mut outbuf = Vec :: with_capacity ( inbuf. len ( ) ) ;
243
-
244
- let mut exit = false ;
245
- for c in inbuf {
246
- match c {
247
- // Ctrl-A means send next one raw
248
- b'\x01' => {
249
- if next_raw {
250
- // Ctrl-A Ctrl-A should be sent as Ctrl-A
251
- outbuf. push ( c) ;
252
- next_raw = false ;
253
- } else {
254
- next_raw = true ;
255
- }
256
- }
257
- b'\x03' => {
258
- if !next_raw {
259
- // Exit on non-raw Ctrl-C
260
- exit = true ;
261
- break ;
262
- } else {
263
- // Otherwise send Ctrl-C
264
- outbuf. push ( c) ;
265
- next_raw = false ;
266
- }
254
+ if let Some ( esc_sequence) = & mut escape {
255
+ loop {
256
+ if let Some ( inbuf) = stdinrx. recv ( ) . await {
257
+ // process potential matches of our escape sequence to determine
258
+ // whether we should exit the loop
259
+ let ( outbuf, exit) = esc_sequence. process ( inbuf) ;
260
+
261
+ // Send what we have, even if we're about to exit.
262
+ if !outbuf. is_empty ( ) {
263
+ wstx. send ( outbuf) . await . unwrap ( ) ;
267
264
}
268
- _ => {
269
- outbuf . push ( c ) ;
270
- next_raw = false ;
265
+
266
+ if exit {
267
+ break ;
271
268
}
272
269
}
273
270
}
274
-
275
- // Send what we have, even if there's a Ctrl-C at the end.
276
- if !outbuf. is_empty ( ) {
277
- wstx. send ( outbuf) . await . unwrap ( ) ;
278
- }
279
-
280
- if exit {
281
- break ;
271
+ } else {
272
+ while let Some ( buf) = stdinrx. recv ( ) . await {
273
+ wstx. send ( buf) . await . unwrap ( ) ;
282
274
}
283
275
}
284
276
}
@@ -290,7 +282,10 @@ async fn test_stdin_to_websockets_task() {
290
282
let ( stdintx, stdinrx) = tokio:: sync:: mpsc:: channel ( 16 ) ;
291
283
let ( wstx, mut wsrx) = tokio:: sync:: mpsc:: channel ( 16 ) ;
292
284
293
- tokio:: spawn ( async move { stdin_to_websockets_task ( stdinrx, wstx) . await } ) ;
285
+ let escape = Some ( EscapeSequence :: new ( vec ! [ 0x1d , 0x03 ] , 0 ) . unwrap ( ) ) ;
286
+ tokio:: spawn ( async move {
287
+ stdin_to_websockets_task ( stdinrx, wstx, escape) . await
288
+ } ) ;
294
289
295
290
// send characters, receive characters
296
291
stdintx
@@ -300,33 +295,22 @@ async fn test_stdin_to_websockets_task() {
300
295
let actual = wsrx. recv ( ) . await . unwrap ( ) ;
301
296
assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "test post please ignore" ) ;
302
297
303
- // don't send ctrl-a
304
- stdintx. send ( "\x01 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
298
+ // don't send a started escape sequence
299
+ stdintx. send ( "\x1d " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
305
300
assert_eq ! ( wsrx. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
306
301
307
- // the "t" here is sent "raw" because of last ctrl-a but that doesn't change anything
302
+ // since we didn't enter the \x03, the previous \x1d shows up here
308
303
stdintx. send ( "test" . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
309
304
let actual = wsrx. recv ( ) . await . unwrap ( ) ;
310
- assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "test " ) ;
305
+ assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "\x1d test " ) ;
311
306
312
- // ctrl-a ctrl-c = only ctrl-c sent
313
- stdintx. send ( "\x01 \x03 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
314
- let actual = wsrx. recv ( ) . await . unwrap ( ) ;
315
- assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "\x03 " ) ;
316
-
317
- // same as above, across two messages
318
- stdintx. send ( "\x01 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
307
+ // \x03 gets sent if not preceded by \x1d
319
308
stdintx. send ( "\x03 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
320
- assert_eq ! ( wsrx. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
321
309
let actual = wsrx. recv ( ) . await . unwrap ( ) ;
322
310
assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "\x03 " ) ;
323
311
324
- // ctrl-a ctrl-a = only ctrl-a sent
325
- stdintx. send ( "\x01 \x01 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
326
- let actual = wsrx. recv ( ) . await . unwrap ( ) ;
327
- assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "\x01 " ) ;
328
-
329
- // ctrl-c on its own means exit
312
+ // \x1d followed by \x03 means exit, even if they're separate messages
313
+ stdintx. send ( "\x1d " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
330
314
stdintx. send ( "\x03 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
331
315
assert_eq ! ( wsrx. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
332
316
@@ -337,6 +321,7 @@ async fn test_stdin_to_websockets_task() {
337
321
async fn serial (
338
322
addr : SocketAddr ,
339
323
byte_offset : Option < i64 > ,
324
+ escape : Option < EscapeSequence > ,
340
325
) -> anyhow:: Result < ( ) > {
341
326
let client = propolis_client:: Client :: new ( & format ! ( "http://{}" , addr) ) ;
342
327
let mut req = client. instance_serial ( ) ;
@@ -379,7 +364,9 @@ async fn serial(
379
364
}
380
365
} ) ;
381
366
382
- tokio:: spawn ( async move { stdin_to_websockets_task ( stdinrx, wstx) . await } ) ;
367
+ tokio:: spawn ( async move {
368
+ stdin_to_websockets_task ( stdinrx, wstx, escape) . await
369
+ } ) ;
383
370
384
371
loop {
385
372
tokio:: select! {
@@ -574,7 +561,20 @@ async fn main() -> anyhow::Result<()> {
574
561
}
575
562
Command :: Get => get_instance ( & client) . await ?,
576
563
Command :: State { state } => put_instance ( & client, state) . await ?,
577
- Command :: Serial { byte_offset } => serial ( addr, byte_offset) . await ?,
564
+ Command :: Serial {
565
+ byte_offset,
566
+ escape_string,
567
+ escape_prefix_length,
568
+ no_escape,
569
+ } => {
570
+ let escape = if no_escape || escape_string. is_empty ( ) {
571
+ None
572
+ } else {
573
+ let escape_vector = escape_string. into_bytes ( ) ;
574
+ Some ( EscapeSequence :: new ( escape_vector, escape_prefix_length) ?)
575
+ } ;
576
+ serial ( addr, byte_offset, escape) . await ?
577
+ }
578
578
Command :: Migrate { dst_server, dst_port, dst_uuid, crucible_disks } => {
579
579
let dst_addr = SocketAddr :: new ( dst_server, dst_port) ;
580
580
let dst_client = Client :: new ( dst_addr, log. clone ( ) ) ;
@@ -628,3 +628,109 @@ impl Drop for RawTermiosGuard {
628
628
}
629
629
}
630
630
}
631
+
632
+ struct EscapeSequence {
633
+ bytes : Vec < u8 > ,
634
+ prefix_length : usize ,
635
+
636
+ // the following are member variables because their values persist between
637
+ // invocations of EscapeSequence::process, because the relevant bytes of
638
+ // the things for which we're checking likely won't all arrive at once.
639
+ // ---
640
+ // position of next potential match in the escape sequence
641
+ esc_pos : usize ,
642
+ // buffer for accumulating characters that may be part of an ANSI Cursor
643
+ // Position Report sent from xterm-likes that we should ignore (this will
644
+ // otherwise render any escape sequence containing newlines before its
645
+ // `prefix_length` unusable, if they're received by a shell that sends
646
+ // requests for these reports for each newline received)
647
+ ansi_curs_check : Vec < u8 > ,
648
+ // pattern used for matching partial-to-complete versions of the above.
649
+ // stored here such that it's only instantiated once at construction time.
650
+ ansi_curs_pat : Regex ,
651
+ }
652
+
653
+ impl EscapeSequence {
654
+ fn new ( bytes : Vec < u8 > , prefix_length : usize ) -> anyhow:: Result < Self > {
655
+ let escape_len = bytes. len ( ) ;
656
+ if prefix_length > escape_len {
657
+ anyhow:: bail!(
658
+ "prefix length {} is greater than length of escape string ({})" ,
659
+ prefix_length,
660
+ escape_len
661
+ ) ;
662
+ }
663
+ // matches partial prefixes of 'CSI row ; column R' (e.g. "\x1b[14;30R")
664
+ let ansi_curs_pat = Regex :: new ( "^\x1b (\\ [([0-9]+(;([0-9]+R?)?)?)?)?$" ) ?;
665
+
666
+ Ok ( EscapeSequence {
667
+ bytes,
668
+ prefix_length,
669
+ esc_pos : 0 ,
670
+ ansi_curs_check : Vec :: new ( ) ,
671
+ ansi_curs_pat,
672
+ } )
673
+ }
674
+
675
+ // return the bytes we can safely commit to sending to the serial port, and
676
+ // determine if the user has entered the escape sequence completely.
677
+ // returns true iff the program should exit.
678
+ fn process ( & mut self , inbuf : Vec < u8 > ) -> ( Vec < u8 > , bool ) {
679
+ // Put bytes from inbuf to outbuf, but don't send characters in the
680
+ // escape string sequence unless we bail.
681
+ let mut outbuf = Vec :: with_capacity ( inbuf. len ( ) ) ;
682
+
683
+ for c in inbuf {
684
+ if !self . ignore_ansi_cpr_seq ( & mut outbuf, c) {
685
+ // is this char a match for the next byte of the sequence?
686
+ if c == self . bytes [ self . esc_pos ] {
687
+ self . esc_pos += 1 ;
688
+ if self . esc_pos == self . bytes . len ( ) {
689
+ // Exit on completed escape string
690
+ return ( outbuf, true ) ;
691
+ } else if self . esc_pos <= self . prefix_length {
692
+ // let through incomplete prefix up to the given limit
693
+ outbuf. push ( c) ;
694
+ }
695
+ } else {
696
+ // they bailed from the sequence,
697
+ // feed everything that matched so far through
698
+ if self . esc_pos != 0 {
699
+ outbuf. extend (
700
+ & self . bytes [ self . prefix_length ..self . esc_pos ] ,
701
+ )
702
+ }
703
+ self . esc_pos = 0 ;
704
+ outbuf. push ( c) ;
705
+ }
706
+ }
707
+ }
708
+ ( outbuf, false )
709
+ }
710
+
711
+ // ignore ANSI escape sequence for the Cursor Position Report sent by
712
+ // xterm-likes in response to shells requesting one after each newline.
713
+ // returns true if further processing of character `c` shouldn't apply
714
+ // (i.e. we find a partial or complete match of the ANSI CSR pattern)
715
+ fn ignore_ansi_cpr_seq ( & mut self , outbuf : & mut Vec < u8 > , c : u8 ) -> bool {
716
+ if self . esc_pos > 0
717
+ && self . esc_pos <= self . prefix_length
718
+ && b"\r \n " . contains ( & self . bytes [ self . esc_pos - 1 ] )
719
+ {
720
+ self . ansi_curs_check . push ( c) ;
721
+ if self . ansi_curs_pat . is_match ( & self . ansi_curs_check ) {
722
+ // end of the sequence?
723
+ if c == b'R' {
724
+ outbuf. extend ( & self . ansi_curs_check ) ;
725
+ self . ansi_curs_check . clear ( ) ;
726
+ }
727
+ return true ;
728
+ } else {
729
+ self . ansi_curs_check . pop ( ) ; // we're not `continue`ing
730
+ outbuf. extend ( & self . ansi_curs_check ) ;
731
+ self . ansi_curs_check . clear ( ) ;
732
+ }
733
+ }
734
+ false
735
+ }
736
+ }
0 commit comments