1
+ use std:: ffi:: OsString ;
1
2
use std:: fs:: File ;
2
3
use std:: io:: BufReader ;
3
4
use std:: path:: { Path , PathBuf } ;
4
5
use std:: {
5
6
net:: { IpAddr , SocketAddr , ToSocketAddrs } ,
6
- os:: unix:: prelude:: AsRawFd ,
7
+ os:: unix:: prelude:: { AsRawFd , OsStringExt } ,
7
8
time:: Duration ,
8
9
} ;
9
10
@@ -17,6 +18,7 @@ use propolis_client::handmade::{
17
18
} ,
18
19
Client ,
19
20
} ;
21
+ use regex:: bytes:: Regex ;
20
22
use slog:: { o, Drain , Level , Logger } ;
21
23
use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
22
24
use tokio_tungstenite:: tungstenite:: protocol:: Role ;
@@ -90,6 +92,29 @@ enum Command {
90
92
/// Defaults to the most recent 16 KiB of console output (-16384).
91
93
#[ clap( long, short) ]
92
94
byte_offset : Option < i64 > ,
95
+
96
+ /// If this sequence of bytes is typed, the client will exit.
97
+ /// Defaults to "^]^C" (Ctrl+], Ctrl+C). Note that the string passed
98
+ /// for this argument is used verbatim without any parsing; in most
99
+ /// shells, if you wish to include a special character (such as Enter
100
+ /// or a Ctrl+letter combo), you can insert the character by preceding
101
+ /// it with Ctrl+V at the command line.
102
+ #[ clap( long, short, default_value = "\x1d \x03 " ) ]
103
+ escape_string : OsString ,
104
+
105
+ /// The number of bytes from the beginning of the escape string to pass
106
+ /// to the VM before beginning to buffer inputs until a mismatch.
107
+ /// Defaults to 0, such that input matching the escape string does not
108
+ /// get sent to the VM at all until a non-matching character is typed.
109
+ /// To mimic the escape sequence for exiting SSH (Enter, tilde, dot),
110
+ /// you may pass `-e '^M~.' --escape-prefix-length=1` such that normal
111
+ /// Enter presses are sent to the VM immediately.
112
+ #[ clap( long, default_value = "0" ) ]
113
+ escape_prefix_length : usize ,
114
+
115
+ /// Disable escape string altogether (to exit, use pkill or similar).
116
+ #[ clap( long, short = 'E' ) ]
117
+ no_escape : bool ,
93
118
} ,
94
119
95
120
/// Migrate instance to new propolis-server
@@ -225,60 +250,89 @@ async fn put_instance(
225
250
async fn stdin_to_websockets_task (
226
251
mut stdinrx : tokio:: sync:: mpsc:: Receiver < Vec < u8 > > ,
227
252
wstx : tokio:: sync:: mpsc:: Sender < Vec < u8 > > ,
253
+ escape_vector : Option < Vec < u8 > > ,
254
+ escape_prefix_length : usize ,
228
255
) {
229
- // next_raw must live outside loop, because Ctrl-A should work across
230
- // multiple inbuf reads.
231
- let mut next_raw = false ;
256
+ if let Some ( esc_sequence) = & escape_vector {
257
+ // esc_pos must live outside loop, because escape string should work
258
+ // across multiple inbuf reads.
259
+ let mut esc_pos = 0 ;
232
260
233
- loop {
234
- let inbuf = if let Some ( inbuf) = stdinrx. recv ( ) . await {
235
- inbuf
236
- } else {
237
- continue ;
238
- } ;
261
+ // matches partial increments of "\x1b[14;30R"
262
+ let ansi_curs_pat =
263
+ Regex :: new ( "^\x1b (\\ [([0-9]+(;([0-9]+R?)?)?)?)?$" ) . unwrap ( ) ;
264
+ let mut ansi_curs_check = Vec :: new ( ) ;
239
265
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 ;
266
+ loop {
267
+ let inbuf = if let Some ( inbuf) = stdinrx. recv ( ) . await {
268
+ inbuf
269
+ } else {
270
+ continue ;
271
+ } ;
272
+
273
+ // Put bytes from inbuf to outbuf, but don't send characters in the
274
+ // escape string sequence unless we bail.
275
+ let mut outbuf = Vec :: with_capacity ( inbuf. len ( ) ) ;
276
+
277
+ let mut exit = false ;
278
+ for c in inbuf {
279
+ // ignore ANSI escape sequence for the cursor position
280
+ // response sent by xterm-alikes in response to shells
281
+ // requesting one after receiving a newline.
282
+ if esc_pos > 0
283
+ && esc_pos <= escape_prefix_length
284
+ && b"\r \n " . contains ( & esc_sequence[ esc_pos - 1 ] )
285
+ {
286
+ ansi_curs_check. push ( c) ;
287
+ if ansi_curs_pat. is_match ( & ansi_curs_check) {
288
+ // end of the sequence?
289
+ if c == b'R' {
290
+ outbuf. extend ( & ansi_curs_check) ;
291
+ ansi_curs_check. clear ( ) ;
292
+ }
293
+ continue ;
253
294
} else {
254
- next_raw = true ;
295
+ ansi_curs_check. pop ( ) ; // we're not `continue`ing
296
+ outbuf. extend ( & ansi_curs_check) ;
297
+ ansi_curs_check. clear ( ) ;
255
298
}
256
299
}
257
- b'\x03' => {
258
- if !next_raw {
259
- // Exit on non-raw Ctrl-C
300
+
301
+ if c == esc_sequence[ esc_pos] {
302
+ esc_pos += 1 ;
303
+ if esc_pos == esc_sequence. len ( ) {
304
+ // Exit on completed escape string
260
305
exit = true ;
261
306
break ;
262
- } else {
263
- // Otherwise send Ctrl-C
307
+ } else if esc_pos <= escape_prefix_length {
308
+ // let through incomplete prefix up to the given limit
264
309
outbuf. push ( c) ;
265
- next_raw = false ;
266
310
}
267
- }
268
- _ => {
311
+ } else {
312
+ // they bailed from the sequence,
313
+ // feed everything that matched so far through
314
+ if esc_pos != 0 {
315
+ outbuf. extend (
316
+ & esc_sequence[ escape_prefix_length..esc_pos] ,
317
+ )
318
+ }
319
+ esc_pos = 0 ;
269
320
outbuf. push ( c) ;
270
- next_raw = false ;
271
321
}
272
322
}
273
- }
274
323
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
- }
324
+ // Send what we have, even if we're about to exit .
325
+ if !outbuf. is_empty ( ) {
326
+ wstx. send ( outbuf) . await . unwrap ( ) ;
327
+ }
279
328
280
- if exit {
281
- break ;
329
+ if exit {
330
+ break ;
331
+ }
332
+ }
333
+ } else {
334
+ while let Some ( buf) = stdinrx. recv ( ) . await {
335
+ wstx. send ( buf) . await . unwrap ( ) ;
282
336
}
283
337
}
284
338
}
@@ -290,7 +344,10 @@ async fn test_stdin_to_websockets_task() {
290
344
let ( stdintx, stdinrx) = tokio:: sync:: mpsc:: channel ( 16 ) ;
291
345
let ( wstx, mut wsrx) = tokio:: sync:: mpsc:: channel ( 16 ) ;
292
346
293
- tokio:: spawn ( async move { stdin_to_websockets_task ( stdinrx, wstx) . await } ) ;
347
+ let escape_vector = Some ( vec ! [ 0x1d , 0x03 ] ) ;
348
+ tokio:: spawn ( async move {
349
+ stdin_to_websockets_task ( stdinrx, wstx, escape_vector, 0 ) . await
350
+ } ) ;
294
351
295
352
// send characters, receive characters
296
353
stdintx
@@ -300,33 +357,22 @@ async fn test_stdin_to_websockets_task() {
300
357
let actual = wsrx. recv ( ) . await . unwrap ( ) ;
301
358
assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "test post please ignore" ) ;
302
359
303
- // don't send ctrl-a
304
- stdintx. send ( "\x01 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
360
+ // don't send a started escape sequence
361
+ stdintx. send ( "\x1d " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
305
362
assert_eq ! ( wsrx. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
306
363
307
- // the "t" here is sent "raw" because of last ctrl-a but that doesn't change anything
364
+ // since we didn't enter the \x03, the previous \x1d shows up here
308
365
stdintx. send ( "test" . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
309
366
let actual = wsrx. recv ( ) . await . unwrap ( ) ;
310
- assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "test" ) ;
311
-
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 " ) ;
367
+ assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "\x1d test" ) ;
316
368
317
- // same as above, across two messages
318
- stdintx. send ( "\x01 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
369
+ // \x03 gets sent if not preceded by \x1d
319
370
stdintx. send ( "\x03 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
320
- assert_eq ! ( wsrx. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
321
371
let actual = wsrx. recv ( ) . await . unwrap ( ) ;
322
372
assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "\x03 " ) ;
323
373
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
374
+ // \x1d followed by \x03 means exit, even if they're separate messages
375
+ stdintx. send ( "\x1d " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
330
376
stdintx. send ( "\x03 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
331
377
assert_eq ! ( wsrx. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
332
378
@@ -337,6 +383,8 @@ async fn test_stdin_to_websockets_task() {
337
383
async fn serial (
338
384
addr : SocketAddr ,
339
385
byte_offset : Option < i64 > ,
386
+ escape_vector : Option < Vec < u8 > > ,
387
+ escape_prefix_length : usize ,
340
388
) -> anyhow:: Result < ( ) > {
341
389
let client = propolis_client:: Client :: new ( & format ! ( "http://{}" , addr) ) ;
342
390
let mut req = client. instance_serial ( ) ;
@@ -379,7 +427,23 @@ async fn serial(
379
427
}
380
428
} ) ;
381
429
382
- tokio:: spawn ( async move { stdin_to_websockets_task ( stdinrx, wstx) . await } ) ;
430
+ let escape_len = escape_vector. as_ref ( ) . map ( |x| x. len ( ) ) . unwrap_or ( 0 ) ;
431
+ if escape_prefix_length > escape_len {
432
+ anyhow:: bail!(
433
+ "prefix length {} is greater than length of escape string ({})" ,
434
+ escape_prefix_length,
435
+ escape_len
436
+ ) ;
437
+ }
438
+ tokio:: spawn ( async move {
439
+ stdin_to_websockets_task (
440
+ stdinrx,
441
+ wstx,
442
+ escape_vector,
443
+ escape_prefix_length,
444
+ )
445
+ . await
446
+ } ) ;
383
447
384
448
loop {
385
449
tokio:: select! {
@@ -574,7 +638,20 @@ async fn main() -> anyhow::Result<()> {
574
638
}
575
639
Command :: Get => get_instance ( & client) . await ?,
576
640
Command :: State { state } => put_instance ( & client, state) . await ?,
577
- Command :: Serial { byte_offset } => serial ( addr, byte_offset) . await ?,
641
+ Command :: Serial {
642
+ byte_offset,
643
+ escape_string,
644
+ escape_prefix_length,
645
+ no_escape,
646
+ } => {
647
+ let escape_vector = if no_escape || escape_string. is_empty ( ) {
648
+ None
649
+ } else {
650
+ Some ( escape_string. into_vec ( ) )
651
+ } ;
652
+ serial ( addr, byte_offset, escape_vector, escape_prefix_length)
653
+ . await ?
654
+ }
578
655
Command :: Migrate { dst_server, dst_port, dst_uuid, crucible_disks } => {
579
656
let dst_addr = SocketAddr :: new ( dst_server, dst_port) ;
580
657
let dst_client = Client :: new ( dst_addr, log. clone ( ) ) ;
0 commit comments