@@ -160,8 +160,8 @@ pub async fn run_daemon(session: &str) {
160160}
161161
162162/// Sidecar files that make this daemon discoverable by CLI invocations.
163- /// `handle_connection` unlinks them the moment a `close` command has been
164- /// answered: ensure_daemon's liveness check is socket connectivity, so a
163+ /// `handle_connection` unlinks them before a `close` response is written:
164+ /// ensure_daemon's liveness check is socket connectivity, so a
165165/// fast follow-up command would otherwise connect to the dying daemon, get
166166/// its browser launched there, and lose it when the daemon exits.
167167struct DaemonSidecars {
@@ -490,28 +490,55 @@ async fn handle_connection<S>(
490490 }
491491
492492 let is_close = cmd. get ( "action" ) . and_then ( |v| v. as_str ( ) ) == Some ( "close" ) ;
493+ let mut notify_close = false ;
493494
494495 let response = {
495496 let mut s = state. lock ( ) . await ;
496- execute_command ( & cmd, & mut s) . await
497+ // A command that was accepted before `close` unlinked the
498+ // socket can still be queued behind the state mutex. Drop
499+ // it after the close command marks this daemon invisible:
500+ // the client treats EOF as transient, reconnects, sees no
501+ // sidecars, and respawns a fresh daemon.
502+ if sidecars. is_closing ( ) && !is_close {
503+ return ;
504+ }
505+
506+ let response = execute_command ( & cmd, & mut s) . await ;
507+ if is_close
508+ && response
509+ . get ( "success" )
510+ . and_then ( |v| v. as_bool ( ) )
511+ . unwrap_or ( false )
512+ && response
513+ . get ( "data" )
514+ . and_then ( |d| d. get ( "closed" ) )
515+ . and_then ( |v| v. as_bool ( ) )
516+ . unwrap_or ( false )
517+ {
518+ if let Some ( ref path) = stream_file_cleanup {
519+ let _ = fs:: remove_file ( path) ;
520+ }
521+ // Unlink the socket/pid/version sidecars before
522+ // releasing the state lock. Any connection already
523+ // queued behind this close will observe `is_closing`
524+ // above instead of launching work in a dying daemon.
525+ sidecars. begin_close ( ) ;
526+ notify_close = true ;
527+ }
528+ response
497529 } ;
498530
499531 let mut resp = serde_json:: to_string ( & response) . unwrap_or_default ( ) ;
500532 resp. push ( '\n' ) ;
501533 if writer. write_all ( resp. as_bytes ( ) ) . await . is_err ( ) {
534+ if notify_close {
535+ close_notify. notify_one ( ) ;
536+ return ;
537+ }
502538 break ;
503539 }
504540
505- if is_close {
506- if let Some ( ref path) = stream_file_cleanup {
507- let _ = fs:: remove_file ( path) ;
508- }
509- // Unlink the socket/pid/version sidecars BEFORE the grace
510- // sleep. ensure_daemon probes the socket path, so a fast
511- // follow-up command would otherwise reach this dying
512- // daemon, auto-launch its browser here, and lose it when
513- // the daemon exits below.
514- sidecars. begin_close ( ) ;
541+ if notify_close {
515542 // Signal the daemon loop to exit gracefully instead of
516543 // calling process::exit(), which skips destructors and
517544 // can leave Chrome processes orphaned (issue #1113).
@@ -809,4 +836,39 @@ mod tests {
809836
810837 let _ = fs:: remove_dir_all ( & dir) ;
811838 }
839+
840+ #[ tokio:: test]
841+ async fn test_closing_daemon_drops_non_close_command_without_response ( ) {
842+ use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
843+
844+ let ( mut client, server) = tokio:: io:: duplex ( 1024 ) ;
845+ let state = std:: sync:: Arc :: new ( tokio:: sync:: Mutex :: new ( DaemonState :: new ( ) ) ) ;
846+ let close_notify = std:: sync:: Arc :: new ( tokio:: sync:: Notify :: new ( ) ) ;
847+ let sidecars = std:: sync:: Arc :: new ( DaemonSidecars :: new ( Vec :: new ( ) ) ) ;
848+ sidecars. begin_close ( ) ;
849+
850+ let task = tokio:: spawn ( handle_connection (
851+ server,
852+ state,
853+ None ,
854+ None ,
855+ close_notify,
856+ sidecars,
857+ ) ) ;
858+
859+ client
860+ . write_all ( br#"{"id":"queued","action":"url"}"# )
861+ . await
862+ . unwrap ( ) ;
863+ client. write_all ( b"\n " ) . await . unwrap ( ) ;
864+
865+ let mut buf = [ 0u8 ; 16 ] ;
866+ let read = tokio:: time:: timeout ( Duration :: from_secs ( 1 ) , client. read ( & mut buf) )
867+ . await
868+ . expect ( "closing daemon should drop queued command promptly" )
869+ . expect ( "duplex read should not fail" ) ;
870+ assert_eq ! ( read, 0 , "client should see EOF and respawn" ) ;
871+
872+ task. await . unwrap ( ) ;
873+ }
812874}
0 commit comments