Skip to content

Commit 75b94aa

Browse files
committed
Fix close race and iframe eval retries
1 parent 15184a1 commit 75b94aa

3 files changed

Lines changed: 103 additions & 13 deletions

File tree

cli/src/native/actions.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2497,6 +2497,15 @@ async fn evaluate_in_same_process_frame(
24972497
)
24982498
.await
24992499
}
2500+
Err(e) if e.contains("has already been declared") => {
2501+
evaluate_in_same_process_frame_once(
2502+
client,
2503+
session_id,
2504+
frame_id,
2505+
&format!("{{ {script} }}"),
2506+
)
2507+
.await
2508+
}
25002509
Err(e) if e.contains("await is only valid") => {
25012510
evaluate_in_same_process_frame_with_top_level_await(
25022511
client, session_id, frame_id, script,

cli/src/native/daemon.rs

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
167167
struct 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
}

cli/src/native/e2e_tests.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6085,6 +6085,25 @@ async fn e2e_frame_scoped_text_wait_eval_and_function_wait() {
60856085
"top-level await should work inside a selected same-process iframe"
60866086
);
60876087

6088+
// Re-declaring the same const across evals must work in a selected
6089+
// same-process iframe too, not just the main frame and OOPIF sessions.
6090+
let resp = execute_command(
6091+
&json!({ "id": "8c", "action": "evaluate",
6092+
"script": "const el = document.getElementById('msg'); el.id" }),
6093+
&mut state,
6094+
)
6095+
.await;
6096+
assert_success(&resp);
6097+
assert_eq!(get_data(&resp)["result"], "msg");
6098+
let resp = execute_command(
6099+
&json!({ "id": "8d", "action": "evaluate",
6100+
"script": "const el = document.getElementById('msg'); el.textContent" }),
6101+
&mut state,
6102+
)
6103+
.await;
6104+
assert_success(&resp);
6105+
assert_eq!(get_data(&resp)["result"], "frame-ready");
6106+
60886107
// Back on the main frame the flag must not leak.
60896108
let resp = execute_command(&json!({ "id": "9", "action": "mainframe" }), &mut state).await;
60906109
assert_success(&resp);

0 commit comments

Comments
 (0)