Skip to content

aw-notify exits fatally when aw-server is unreachable at startup (crash-loops under desktop managers) #47

Closed

Description

@TimeToBuildBob

Summary

When aw-notify starts while aw-server is not yet accepting connections, it exits fatally instead of waiting/retrying. Under the desktop module manager (aw-tauri/aw-qt) this becomes a crash loop: the module is restarted with 2s/4s/8s backoff, gives up after 3 attempts, and the user gets a "module quit after 3 auto-restart attempts" dialog. On desktop the notification module is commonly (auto)started alongside aw-server, so a server that is slow to bind is enough to trigger it.

Evidence (user report)

ActivityWatch/activitywatch#1526 — v0.14.0 and v0.14.1, Linux AppImage:

  • Connection refused when aw-notify polls /api/0/info.
  • Crash dialog: module quit after 3 auto-restart attempts within 10 minutes.
  • No new aw-notify log file is written for the crash.
  • The standalone aw-notify-rs v0.2.0 binary works for the same user.

Root cause

src/main.rs → run_app():

let client = aw_client_rust::blocking::AwClient::new("127.0.0.1", port, client_name)
    .map_err(|e| anyhow!("Failed to create client: {}", e))?;
client.get_info()?;                       // <-- any connection error here is fatal
let config = try_load_server_config(&client, config);
if !config.enabled { /* opt-in gate */ return Ok(()); }

get_info()? propagates on connection-refused; main() then logs Fatal error: … and returns Err, so the process exits non-zero. The surrounding comment in the pre-opt-in code said "Wait for server to be ready (like Python's wait_for_start)", but the Rust rewrite has no actual wait/retry (the Python implementation did).

Suggested fix

Either of these would make startup robust:

  1. Bounded wait-for-server at startup — poll get_info() with a short backoff for a bounded window (e.g. ~10–15s) before treating it as fatal. This window matters because the desktop manager's own retry budget is only 3 attempts, so the module must outlast a slow server bind itself.
  2. Make the startup probe non-fatal — log the failure and continue; the existing server_monitor_thread already tracks SERVER_AVAILABLE and reconnects.

Option 1 matches the original Python wait_for_start behaviour and is the smallest change.

Repro

# With no aw-server listening on :5600
aw-notify --output-only
# -> "Fatal error: …" and a non-zero exit (no retry)

Filed from the ActivityWatch triage flow after user report ActivityWatch/activitywatch#1526. Tagging so the desktop crash-loop symptom is tracked separately from the opt-in architecture work in ActivityWatch/activitywatch#1435.

Activity

  1. TimeToBuildBob commented on Oct 15, 2026

    @TimeToBuildBob
    ContributorAuthor

    Already covered by #46; no competing PR needed. I reproduced the immediate fatal exit on master 8c5c1b1, then built and tested #46 at 15545489a660fb34e41c4481f414f87b9d123900 locally.

    Verification: all 11 black-box opt-in/startup/heartbeat tests passed (including delayed server bind, transient 503 readiness failures, retry exhaustion, and settings read only after readiness), plus 20 Rust unit tests and 1 doctest. Clippy with warnings denied and formatting checks passed. The daemon retries /info five times with 1/2/4/8-second backoff; manual check-ins deliberately remain fail-fast.

    Leaving this issue open until the fix lands. This verifies the startup race in the standalone binary, not that a released desktop bundle has been repaired. Also, the 15 seconds is the backoff budget, not a strict wall-clock deadline: requests retain the client timeout.

  2. TimeToBuildBob commented on Oct 15, 2026

    @TimeToBuildBob
    ContributorAuthor

    Verified fixed on master by #46, squash commit 87e83cc. Its Git tree (69e957a0fb76cdc83c4d6d10ee0597139a883298) exactly matches the PR head I tested above: delayed bind, transient 503 readiness failures, retry exhaustion, and settings read only after readiness are covered. Master Continuous Integration run 38800228072 and Lint and Format run 38800228074 both passed.

    Closing this startup-only issue. This does not establish that a released desktop bundle includes the fix; ActivityWatch/activitywatch#1526 remains separate because disabled-module exits also need correct manager handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions