@@ -20,10 +20,14 @@ use std::os::unix::process::CommandExt;
2020use std:: path:: { Path , PathBuf } ;
2121use std:: process:: { Command , Stdio } ;
2222use std:: sync:: Arc ;
23+ use std:: time:: Duration ;
2324use tokio:: io:: { AsyncRead , AsyncReadExt , AsyncWrite , AsyncWriteExt , BufReader } ;
2425use tokio:: net:: TcpStream ;
26+ use tokio:: process:: Command as TokioCommand ;
2527use tokio_rustls:: TlsConnector ;
2628
29+ const FOREGROUND_FORWARD_STARTUP_GRACE_PERIOD : Duration = Duration :: from_secs ( 2 ) ;
30+
2731#[ derive( Clone , Copy , Debug ) ]
2832pub enum Editor {
2933 Vscode ,
@@ -309,9 +313,11 @@ pub async fn sandbox_forward(
309313) -> Result < ( ) > {
310314 let session = ssh_session_config ( server, name, tls) . await ?;
311315
312- let mut command = ssh_base_command ( & session. proxy_command ) ;
316+ let mut command = TokioCommand :: from ( ssh_base_command ( & session. proxy_command ) ) ;
313317 command
314318 . arg ( "-N" )
319+ . arg ( "-o" )
320+ . arg ( "ExitOnForwardFailure=yes" )
315321 . arg ( "-L" )
316322 . arg ( format ! ( "{port}:127.0.0.1:{port}" ) ) ;
317323
@@ -326,10 +332,18 @@ pub async fn sandbox_forward(
326332 . stdout ( Stdio :: inherit ( ) )
327333 . stderr ( Stdio :: inherit ( ) ) ;
328334
329- let status = tokio:: task:: spawn_blocking ( move || command. status ( ) )
330- . await
331- . into_diagnostic ( ) ?
332- . into_diagnostic ( ) ?;
335+ let status = if background {
336+ command. status ( ) . await . into_diagnostic ( ) ?
337+ } else {
338+ let mut child = command. spawn ( ) . into_diagnostic ( ) ?;
339+ match tokio:: time:: timeout ( FOREGROUND_FORWARD_STARTUP_GRACE_PERIOD , child. wait ( ) ) . await {
340+ Ok ( status) => status. into_diagnostic ( ) ?,
341+ Err ( _) => {
342+ eprintln ! ( "{}" , foreground_forward_started_message( name, port) ) ;
343+ child. wait ( ) . await . into_diagnostic ( ) ?
344+ }
345+ }
346+ } ;
333347
334348 if !status. success ( ) {
335349 return Err ( miette:: miette!( "ssh exited with status {status}" ) ) ;
@@ -351,6 +365,14 @@ pub async fn sandbox_forward(
351365 Ok ( ( ) )
352366}
353367
368+ fn foreground_forward_started_message ( name : & str , port : u16 ) -> String {
369+ format ! (
370+ "{} Forwarding port {port} to sandbox {name}\n Access at: http://127.0.0.1:{port}/\n Press Ctrl+C to stop\n {}" ,
371+ "✓" . green( ) . bold( ) ,
372+ "Hint: pass --background to start forwarding without blocking your terminal" . dimmed( ) ,
373+ )
374+ }
375+
354376async fn sandbox_exec_with_mode (
355377 server : & str ,
356378 name : & str ,
@@ -1105,4 +1127,16 @@ mod tests {
11051127 let text = format ! ( "{err}" ) ;
11061128 assert ! ( text. contains( "openshell-test-missing-binary is not installed or not on PATH" ) ) ;
11071129 }
1130+
1131+ #[ test]
1132+ fn foreground_forward_started_message_includes_port_and_stop_hint ( ) {
1133+ let message = foreground_forward_started_message ( "demo" , 8080 ) ;
1134+ assert ! ( message. contains( "Forwarding port 8080 to sandbox demo" ) ) ;
1135+ assert ! ( message. contains( "Access at: http://127.0.0.1:8080/" ) ) ;
1136+ assert ! ( message. contains( "sandbox demo" ) ) ;
1137+ assert ! ( message. contains( "Press Ctrl+C to stop" ) ) ;
1138+ assert ! ( message. contains(
1139+ "Hint: pass --background to start forwarding without blocking your terminal"
1140+ ) ) ;
1141+ }
11081142}
0 commit comments