Skip to content

fix(meet_call): abort scanner on close to unblock 60-second navigation stall#1380

Merged
senamakel merged 2 commits intotinyhumansai:mainfrom
YellowSnnowmann:fix/meet-cleanup-blocks-navigation
May 9, 2026
Merged

fix(meet_call): abort scanner on close to unblock 60-second navigation stall#1380
senamakel merged 2 commits intotinyhumansai:mainfrom
YellowSnnowmann:fix/meet-cleanup-blocks-navigation

Conversation

@YellowSnnowmann
Copy link
Copy Markdown
Contributor

@YellowSnnowmann YellowSnnowmann commented May 8, 2026

Summary

  • The meet-scanner join-automation task held CDP WebSocket connections open for up to 60 s (NAME_INPUT_BUDGET + JOIN_BUTTON_BUDGET) after the Meet window was closed.
  • CEF waits for all active CDP sessions to detach before firing WindowEvent::Destroyed, so the meet-call:closed frontend event was delayed by the same margin — blocking navigation.
  • Fix: meet_scanner::spawn() now returns an AbortHandle. MeetCallState stores it per-call and aborts on CloseRequested (and as a fallback on Destroyed). meet_call_close_window also aborts the scanner before sending the close signal to CEF.

Problem

  • After ending a Google Meet session, users could not navigate elsewhere in the app for up to ~60 seconds.
  • Root cause: the CDP scanner polling loops inside meet_scanner (phase 2 + phase 3, each up to 30 s) kept active WebSocket connections to CEF's debugging endpoint. CEF defers renderer shutdown until all CDP sessions detach, so the window close stalled for the full scanner timeout.
  • See issue Meet cleanup blocks navigation for 60 seconds #1378.

Solution

  • meet_scanner::spawn() changed to return tokio::task::AbortHandle (switched from tauri::async_runtime::spawn to tokio::spawn to expose the handle).
  • MeetCallState gains a scanner_aborts: Mutex<HashMap<String, AbortHandle>> field.
  • Two abort sites:
    1. WindowEvent::CloseRequested — primary path, covers both programmatic window.close() and OS title-bar close. Aborts the scanner immediately so CEF finds no active CDP sessions when it starts renderer shutdown.
    2. WindowEvent::Destroyed — defensive fallback for cases where CloseRequested did not fire.
  • meet_call_close_window also calls abort() before window.close() so CEF receives the close signal with no competing CDP work.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per docs/TESTING-STRATEGY.md
  • Diff coverage ≥ 80% — 3 new unit tests cover changed Rust lines (budget_constants_are_sane, abort_handle_cancels_spawned_task, meet_call_state_scanner_aborts_insert_and_remove)
  • N/A: Coverage matrix updated — behaviour-only change to existing meet-call feature; no new feature IDs
  • N/A: All affected feature IDs from the matrix are listed — no new matrix rows
  • N/A: No new external network dependencies introduced
  • N/A: Manual smoke checklist updated — no release-cut surface change
  • Linked issue closed via Closes #1378 in Related section

Impact

  • macOS / desktop onlymeet_call and meet_scanner are Tauri-shell modules.
  • No behaviour change for calls that complete the join sequence normally (scanner has already exited by then); only the window-close path is different.
  • abort() is idempotent — calling it after the scanner finishes naturally is a safe no-op.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/meet-cleanup-blocks-navigation
  • Commit SHA: $SHA

Validation Run

  • pnpm --filter openhuman-app format:check — N/A: no frontend changes
  • pnpm typecheck — N/A: no frontend changes
  • Focused tests: cargo test --manifest-path app/src-tauri/Cargo.toml meet → 42 passed
  • Rust fmt/check (if changed): cargo fmt --all + cargo check → clean
  • Tauri fmt/check (if changed): cargo check --manifest-path app/src-tauri/Cargo.toml → no errors

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: Meet window closes promptly when the user ends a call; meet-call:closed reaches the frontend in milliseconds instead of up to 60 s.
  • User-visible effect: Navigation to other app tabs/screens is unblocked immediately after ending a Meet session.

Parity Contract

  • Legacy behavior preserved: audio teardown, data-dir cleanup, and meet-call:closed event emission still happen via WindowEvent::Destroyed exactly as before.
  • Guard/fallback/dispatch parity checks: defensive abort in Destroyed handler ensures the scanner is always cancelled even if CloseRequested is not fired by the OS.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: this one
  • Resolution: N/A

Summary by CodeRabbit

  • Bug Fixes

    • Improved Meet call window lifecycle to avoid long navigation stalls and ensure scanners are stopped when windows close or are destroyed
    • Automatic cleanup of per-call resources and data on window teardown
  • New Features

    • Added cancellation support for Meet join automation so ongoing join processes can be aborted promptly

…n stall

The meet_scanner join-automation task held CDP WebSocket connections open
for up to NAME_INPUT_BUDGET + JOIN_BUTTON_BUDGET (60 s total) after the
Meet window closed. CEF waited for all active CDP sessions to detach before
firing WindowEvent::Destroyed, delaying the meet-call:closed frontend event
by the same margin and blocking navigation. Fixes tinyhumansai#1378.

Changes:
- meet_scanner::spawn() returns an AbortHandle instead of () so callers
  can cancel the task.
- MeetCallState gains a scanner_aborts map keyed by request_id.
- meet_call_open_window stores the AbortHandle and aborts on CloseRequested
  (covers both programmatic close and OS title-bar close). A defensive
  abort on Destroyed handles the rare case where CloseRequested didn't fire.
- meet_call_close_window aborts the scanner before calling window.close()
  so CEF receives the close signal with no active CDP sessions.
- 3 new tests: budget_constants_are_sane, abort_handle_cancels_spawned_task,
  meet_call_state_scanner_aborts_insert_and_remove.
@YellowSnnowmann YellowSnnowmann requested a review from a team May 8, 2026 13:36
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba45ddf4-8e67-4c13-9d4b-a39ebc99ede1

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab93eb and cb751ab.

📒 Files selected for processing (1)
  • app/src-tauri/src/meet_call/mod.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src-tauri/src/meet_call/mod.rs

📝 Walkthrough

Walkthrough

This PR fixes a 60-second navigation stall by making meet_scanner tasks cancellable: meet_scanner::spawn() returns an AbortHandle; MeetCallState stores handles per request_id; CloseRequested aborts the scanner; Destroyed removes state, emits meet-call:closed, stops audio, and performs async CEF cleanup; meet_call_close_window aborts before closing.

Changes

Scanner Lifecycle Abort Management

Layer / File(s) Summary
Scanner API Contract
app/src-tauri/src/meet_scanner/mod.rs
spawn() returns tokio::task::AbortHandle by switching from tauri::async_runtime::spawn to tokio::spawn. Module documentation expanded with cancellation requirements.
Abort Handle State Management
app/src-tauri/src/meet_call/mod.rs
MeetCallState adds scanner_aborts: Mutex<HashMap<String, AbortHandle>> field; replaces #[derive(Default)] with explicit impl Default delegating to new().
Lifecycle Handler Wiring
app/src-tauri/src/meet_call/mod.rs
meet_call_open_window stores captured AbortHandle in scanner_aborts. CloseRequested handler aborts scanner before renderer teardown. Destroyed handler removes state, emits meet-call:closed, stops meet-audio loop, and asynchronously removes per-call CEF data directory with defensive abort fallback.
Explicit Window Close
app/src-tauri/src/meet_call/mod.rs
meet_call_close_window sanitizes request_id, aborts stored scanner task, and calls window.close() while preserving Ok(true/false) semantics and cleaning stale inner entries when lookup fails.
Tests & Documentation
app/src-tauri/src/meet_call/mod.rs, app/src-tauri/src/meet_scanner/mod.rs
New tests validate scanner_aborts consume-once behavior, MeetCallState::default() initialization, AbortHandle cancellation semantics, and combined polling budget under 120 seconds. Module docs expanded with teardown and timeout sections.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Possibly related PRs

  • tinyhumansai/openhuman#1189: Both PRs modify scanner spawn APIs to return tokio::AbortHandle and add lifecycle bookkeeping to abort scanner tasks during window teardown.

Suggested reviewers

  • senamakel

"I'm a rabbit with a tiny key,
I hold the handle, set it free.
When windows close I pull the cord,
No sixty seconds stuck aboard.
Hop—navigation's swift and cheery!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary fix: aborting the scanner task on Meet window close to unblock the 60-second navigation stall mentioned in #1378.
Linked Issues check ✅ Passed The PR directly addresses all coding objectives from #1378: scanner abort on close unblocks navigation, teardown is non-blocking, timeout is bounded via 60s CDP behavior, logs diagnostic info, and adds tests validating scanner abort and map cleanup.
Out of Scope Changes check ✅ Passed All changes are scoped to meet-call and meet-scanner modules to implement scanner abort lifecycle management. No unrelated modifications to other system components detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/src-tauri/src/meet_call/mod.rs (1)

229-291: 💤 Low value

Consider whether synchronous remove_dir_all inside async spawn is optimal.

The Destroyed handler spawns an async task but then calls std::fs::remove_dir_all, which is a blocking filesystem operation. While this moves the blocking call off the UI thread (good), it still blocks a Tokio worker thread.

For a directory that CEF may have written significant data to, this could briefly starve other async tasks. Consider using tokio::fs::remove_dir_all for fully async I/O:

♻️ Suggested improvement
 tauri::async_runtime::spawn(async move {
-    if let Err(err) = std::fs::remove_dir_all(&dir_to_purge) {
+    if let Err(err) = tokio::fs::remove_dir_all(&dir_to_purge).await {
         log::debug!(
             "[meet-call] data-dir cleanup skipped request_id={request_id_for_purge} dir={} err={err}",
             dir_to_purge.display()
         );
     }
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src-tauri/src/meet_call/mod.rs` around lines 229 - 291, The cleanup task
currently uses blocking std::fs::remove_dir_all inside
tauri::async_runtime::spawn (see dir_to_purge / request_id_for_purge and the
std::fs::remove_dir_all call), which can block a Tokio worker; change it to
perform nonblocking IO by calling tokio::fs::remove_dir_all(&dir_to_purge).await
inside the async move spawned closure (or, if tokio::fs is unavailable, wrap the
existing call in tokio::task::spawn_blocking), and propagate/log errors the same
way so the log message still reports request_id_for_purge and
dir_to_purge.display() on failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src-tauri/src/meet_call/mod.rs`:
- Around line 324-330: The code currently returns Ok(false) when
app.get_webview_window(&label) is None but leaves the stale entry in
state.inner; update the handler so that when app.get_webview_window(&label)
returns None you first remove the stale mapping from state.inner (e.g., call
remove(&request_id) on the same lock used earlier) before returning Ok(false),
referencing the existing variables request_id, label, state.inner and the call
to app.get_webview_window(&label).

---

Nitpick comments:
In `@app/src-tauri/src/meet_call/mod.rs`:
- Around line 229-291: The cleanup task currently uses blocking
std::fs::remove_dir_all inside tauri::async_runtime::spawn (see dir_to_purge /
request_id_for_purge and the std::fs::remove_dir_all call), which can block a
Tokio worker; change it to perform nonblocking IO by calling
tokio::fs::remove_dir_all(&dir_to_purge).await inside the async move spawned
closure (or, if tokio::fs is unavailable, wrap the existing call in
tokio::task::spawn_blocking), and propagate/log errors the same way so the log
message still reports request_id_for_purge and dir_to_purge.display() on
failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3e40db51-634b-4674-8301-0f3d18b2dc51

📥 Commits

Reviewing files that changed from the base of the PR and between 26ff73a and 0ab93eb.

⛔ Files ignored due to path filters (1)
  • app/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • app/src-tauri/src/meet_call/mod.rs
  • app/src-tauri/src/meet_scanner/mod.rs

Comment thread app/src-tauri/src/meet_call/mod.rs
…ri registry

When app.get_webview_window returns None but the request_id still exists in
state.inner, clean up the orphaned map entry before returning Ok(false).
Addresses CodeRabbit suggestion on PR tinyhumansai#1380.
@senamakel senamakel merged commit e58b5ab into tinyhumansai:main May 9, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Meet cleanup blocks navigation for 60 seconds

2 participants