@@ -29,6 +29,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
2929use tokio:: net:: TcpStream ;
3030use tokio:: process:: { Child , Command as TokioCommand } ;
3131use tokio_stream:: wrappers:: ReceiverStream ;
32+ use tonic:: Code ;
3233
3334/// Time budget for the local listener to become reachable after `ssh` starts.
3435/// This is a user-visible readiness deadline for both foreground and background
@@ -39,6 +40,10 @@ const FORWARD_LISTENER_PROBE_INTERVAL: Duration = Duration::from_millis(50);
3940/// Per-attempt connect timeout, so one hung probe cannot consume the whole
4041/// grace period.
4142const FORWARD_LISTENER_CONNECT_TIMEOUT : Duration = Duration :: from_millis ( 200 ) ;
43+ /// Time budget for the supervisor relay to register after a fast canonical
44+ /// command has already reported its terminal result.
45+ const TERMINAL_RELAY_REGISTRATION_TIMEOUT : Duration = Duration :: from_secs ( 5 ) ;
46+ const TERMINAL_RELAY_REGISTRATION_INTERVAL : Duration = Duration :: from_millis ( 50 ) ;
4247
4348#[ derive( Clone , Copy , Debug ) ]
4449pub enum Editor {
@@ -79,6 +84,7 @@ async fn ssh_session_config(
7984 name : & str ,
8085 tls : & TlsOptions ,
8186 workspace : & str ,
87+ terminal_relay_registration_timeout : Option < Duration > ,
8288) -> Result < SshSessionConfig > {
8389 let mut client = grpc_client ( server, tls) . await ?;
8490
@@ -94,12 +100,26 @@ async fn ssh_session_config(
94100 . sandbox
95101 . ok_or_else ( || miette:: miette!( "sandbox not found" ) ) ?;
96102
97- let response = client
98- . create_ssh_session ( CreateSshSessionRequest {
99- sandbox_id : sandbox. object_id ( ) . to_string ( ) ,
100- } )
101- . await
102- . into_diagnostic ( ) ?;
103+ let relay_registration_deadline =
104+ terminal_relay_registration_timeout. map ( |timeout| tokio:: time:: Instant :: now ( ) + timeout) ;
105+ let response = loop {
106+ match client
107+ . create_ssh_session ( CreateSshSessionRequest {
108+ sandbox_id : sandbox. object_id ( ) . to_string ( ) ,
109+ } )
110+ . await
111+ {
112+ Ok ( response) => break response,
113+ Err ( status)
114+ if status. code ( ) == Code :: FailedPrecondition
115+ && relay_registration_deadline
116+ . is_some_and ( |deadline| tokio:: time:: Instant :: now ( ) < deadline) =>
117+ {
118+ tokio:: time:: sleep ( TERMINAL_RELAY_REGISTRATION_INTERVAL ) . await ;
119+ }
120+ Err ( status) => return Err ( status) . into_diagnostic ( ) ,
121+ }
122+ } ;
103123 let session = response. into_inner ( ) ;
104124 validate_ssh_session_response ( & session)
105125 . map_err ( |err| miette:: miette!( "gateway returned invalid SSH session response: {err}" ) ) ?;
@@ -257,8 +277,16 @@ async fn sandbox_connect_with_mode(
257277 tls : & TlsOptions ,
258278 replace_process : bool ,
259279 workspace : & str ,
280+ terminal_relay_registration_timeout : Option < Duration > ,
260281) -> Result < i32 > {
261- let session = ssh_session_config ( server, name, tls, workspace) . await ?;
282+ let session = ssh_session_config (
283+ server,
284+ name,
285+ tls,
286+ workspace,
287+ terminal_relay_registration_timeout,
288+ )
289+ . await ?;
262290
263291 let mut command = ssh_base_command ( & session. proxy_command ) ;
264292 if session. main_terminal {
@@ -290,7 +318,7 @@ pub async fn sandbox_connect(
290318 tls : & TlsOptions ,
291319 workspace : & str ,
292320) -> Result < i32 > {
293- sandbox_connect_with_mode ( server, name, tls, true , workspace) . await
321+ sandbox_connect_with_mode ( server, name, tls, true , workspace, None ) . await
294322}
295323
296324pub ( crate ) async fn sandbox_connect_without_exec (
@@ -299,7 +327,24 @@ pub(crate) async fn sandbox_connect_without_exec(
299327 tls : & TlsOptions ,
300328 workspace : & str ,
301329) -> Result < i32 > {
302- sandbox_connect_with_mode ( server, name, tls, false , workspace) . await
330+ sandbox_connect_with_mode ( server, name, tls, false , workspace, None ) . await
331+ }
332+
333+ pub ( crate ) async fn sandbox_connect_terminal_main (
334+ server : & str ,
335+ name : & str ,
336+ tls : & TlsOptions ,
337+ workspace : & str ,
338+ ) -> Result < i32 > {
339+ sandbox_connect_with_mode (
340+ server,
341+ name,
342+ tls,
343+ false ,
344+ workspace,
345+ Some ( TERMINAL_RELAY_REGISTRATION_TIMEOUT ) ,
346+ )
347+ . await
303348}
304349
305350pub async fn sandbox_connect_editor (
@@ -310,7 +355,7 @@ pub async fn sandbox_connect_editor(
310355 tls : & TlsOptions ,
311356 workspace : & str ,
312357) -> Result < ( ) > {
313- let session = ssh_session_config ( server, name, tls, workspace) . await ?;
358+ let session = ssh_session_config ( server, name, tls, workspace, None ) . await ?;
314359 let workspace_root = discover_workspace_root ( & session) . await ?;
315360
316361 let host_alias = host_alias ( name, workspace) ;
@@ -339,7 +384,7 @@ pub async fn sandbox_forward(
339384) -> Result < ( ) > {
340385 openshell_core:: forward:: check_port_available ( spec) ?;
341386
342- let session = ssh_session_config ( server, name, tls, workspace) . await ?;
387+ let session = ssh_session_config ( server, name, tls, workspace, None ) . await ?;
343388
344389 let mut command = TokioCommand :: from ( ssh_base_command ( & session. proxy_command ) ) ;
345390 command
@@ -554,7 +599,7 @@ async fn sandbox_exec_with_mode(
554599 return Err ( miette:: miette!( "no command provided" ) ) ;
555600 }
556601
557- let session = ssh_session_config ( server, name, tls, workspace) . await ?;
602+ let session = ssh_session_config ( server, name, tls, workspace, None ) . await ?;
558603 let mut ssh = ssh_base_command ( & session. proxy_command ) ;
559604
560605 if tty {
@@ -767,7 +812,7 @@ async fn ssh_tar_upload(
767812 tls : & TlsOptions ,
768813 workspace : & str ,
769814) -> Result < ( ) > {
770- let session = ssh_session_config ( server, name, tls, workspace) . await ?;
815+ let session = ssh_session_config ( server, name, tls, workspace, None ) . await ?;
771816
772817 let dest_dir = dest_dir. unwrap_or ( "." ) ;
773818 let escaped_dest = shell_escape ( dest_dir) ;
@@ -1200,7 +1245,7 @@ pub async fn sandbox_sync_down(
12001245 tls : & TlsOptions ,
12011246 workspace : & str ,
12021247) -> Result < ( ) > {
1203- let session = ssh_session_config ( server, name, tls, workspace) . await ?;
1248+ let session = ssh_session_config ( server, name, tls, workspace, None ) . await ?;
12041249 let sandbox_path = resolve_sandbox_source_path ( & session, sandbox_path) . await ?;
12051250 let kind = probe_sandbox_source_kind ( & session, & sandbox_path) . await ?;
12061251
@@ -1482,7 +1527,7 @@ pub async fn sandbox_ssh_proxy_by_name(
14821527 tls : & TlsOptions ,
14831528 workspace : & str ,
14841529) -> Result < ( ) > {
1485- let session = ssh_session_config ( server, name, tls, workspace) . await ?;
1530+ let session = ssh_session_config ( server, name, tls, workspace, None ) . await ?;
14861531 sandbox_ssh_proxy (
14871532 & session. gateway_url ,
14881533 & session. sandbox_id ,
0 commit comments