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
@@ -90,6 +91,15 @@ 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).
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 ,
93
103
} ,
94
104
95
105
/// Migrate instance to new propolis-server
@@ -221,60 +231,56 @@ async fn put_instance(
221
231
async fn stdin_to_websockets_task (
222
232
mut stdinrx : tokio:: sync:: mpsc:: Receiver < Vec < u8 > > ,
223
233
wstx : tokio:: sync:: mpsc:: Sender < Vec < u8 > > ,
234
+ escape_vector : Option < Vec < u8 > > ,
224
235
) {
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 ;
228
240
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
+ } ;
235
247
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
256
258
exit = true ;
257
259
break ;
258
- } else {
259
- // Otherwise send Ctrl-C
260
- outbuf. push ( c) ;
261
- next_raw = false ;
262
260
}
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 ;
265
268
outbuf. push ( c) ;
266
- next_raw = false ;
267
269
}
268
270
}
269
- }
270
271
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
+ }
275
276
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 ( ) ;
278
284
}
279
285
}
280
286
}
@@ -286,7 +292,10 @@ async fn test_stdin_to_websockets_task() {
286
292
let ( stdintx, stdinrx) = tokio:: sync:: mpsc:: channel ( 16 ) ;
287
293
let ( wstx, mut wsrx) = tokio:: sync:: mpsc:: channel ( 16 ) ;
288
294
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
+ } ) ;
290
299
291
300
// send characters, receive characters
292
301
stdintx
@@ -296,33 +305,22 @@ async fn test_stdin_to_websockets_task() {
296
305
let actual = wsrx. recv ( ) . await . unwrap ( ) ;
297
306
assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "test post please ignore" ) ;
298
307
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 ( ) ;
301
310
assert_eq ! ( wsrx. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
302
311
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
304
313
stdintx. send ( "test" . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
305
314
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" ) ;
312
316
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
315
318
stdintx. send ( "\x03 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
316
- assert_eq ! ( wsrx. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
317
319
let actual = wsrx. recv ( ) . await . unwrap ( ) ;
318
320
assert_eq ! ( String :: from_utf8( actual) . unwrap( ) , "\x03 " ) ;
319
321
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 ( ) ;
326
324
stdintx. send ( "\x03 " . chars ( ) . map ( |c| c as u8 ) . collect ( ) ) . await . unwrap ( ) ;
327
325
assert_eq ! ( wsrx. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
328
326
@@ -333,6 +331,7 @@ async fn test_stdin_to_websockets_task() {
333
331
async fn serial (
334
332
addr : SocketAddr ,
335
333
byte_offset : Option < i64 > ,
334
+ escape_vector : Option < Vec < u8 > > ,
336
335
) -> anyhow:: Result < ( ) > {
337
336
let client = propolis_client:: Client :: new ( & format ! ( "http://{}" , addr) ) ;
338
337
let mut req = client. instance_serial ( ) ;
@@ -375,7 +374,9 @@ async fn serial(
375
374
}
376
375
} ) ;
377
376
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
+ } ) ;
379
380
380
381
loop {
381
382
tokio:: select! {
@@ -569,7 +570,14 @@ async fn main() -> anyhow::Result<()> {
569
570
}
570
571
Command :: Get => get_instance ( & client) . await ?,
571
572
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
+ }
573
581
Command :: Migrate { dst_server, dst_port, dst_uuid } => {
574
582
let dst_addr = SocketAddr :: new ( dst_server, dst_port) ;
575
583
let dst_client = Client :: new ( dst_addr, log. clone ( ) ) ;
0 commit comments