Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/process-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Backend process recovery

Berd stores one compact, owner-private recovery record per backend under the app data directory's `processes/berd-serve` folder. Legacy shared-temp JSON records are never read or migrated. Upgrading can leave an older backend for manual cleanup or a reboot.

Publication retains the file it created through startup and shutdown. A terminal newline commits a single-line payload; readers reject incomplete writes. Windows publishes and deletes through retained file handles and revalidates the record and directory ACLs before deletion. Unix creates the final name exclusively and never unlinks by pathname: there is no portable atomic operation that deletes only the retained file when another process can replace its name. Partial and completed records therefore remain as recovery evidence on Unix, including after a clean shutdown.

Stale recovery requires an exact owner identity and an orphaned backend identity. Linux probes through a retained `/proc` directory descriptor and signals that same process with `pidfd_send_signal`; unsupported kernels retain evidence without falling back to a numeric PID signal. Windows checks and terminates through one process handle. macOS cannot yet establish the required executable identity, so it retains evidence without signaling stale records. Normal shutdown still terminates and reaps the owned child on all platforms.

A failed startup retains any unreaped child in memory. Another startup must finish that teardown before it can spawn a replacement; the app exit path also retries it. A child already reaped by readiness checks counts as confirmed exited.

Recovery streams directory entries off the async runtime through a buffer of at most 256 paths. Enumeration and process cleanup have a two-second startup budget. Scans continue beyond the first buffer while time remains. Exhausting the budget leaves unprocessed evidence for a later attempt; recovery does not guarantee a full scan of an arbitrarily large directory. Retained Unix records can consume disk space over time.

## Validation

`just tauri-test` and the Windows-native `just ci-windows` gate include these focused suites:

- `services::acp::process_record_store`: publication substitution, partial writes, ACL changes, exact deletion, directory parsing, and scan buffering.
- `services::acp::goose_serve::recovery_tests`: failed startup, already-reaped children, retry ownership, signal escalation, and retained evidence.
- `services::process::`: process identity and the Linux exit-between-probe-and-signal regression.

Run the native Linux and Windows CI jobs before merging; compilation on macOS cannot validate those operating-system operations.
6 changes: 6 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ _tauri-test-skill-marketplace:

[unix]
_tauri-test-unix:
just _tauri-cargo-unix test --lib services::acp::process_record_store
just _tauri-cargo-unix test --lib services::acp::goose_serve::recovery_tests
just _tauri-cargo-unix test --lib services::process::
# rust-cache can restore Sherpa's generated cache directory without its native libraries.
if [ "$(uname -s)" = "Linux" ]; then rm -rf src-tauri/target/sherpa-onnx-prebuilt; fi
just _tauri-cargo-unix test -p tauri-plugin-berdctl --features server
Expand All @@ -251,6 +254,9 @@ _tauri-test-skill-marketplace:

[windows]
_tauri-test-windows:
just _tauri-cargo-windows test --lib services::acp::process_record_store
just _tauri-cargo-windows test --lib services::acp::goose_serve::recovery_tests
just _tauri-cargo-windows test --lib services::process::
just _tauri-cargo-windows test -p tauri-plugin-berdctl --features server
just _tauri-cargo-windows test -p berdctl
just _tauri-cargo-windows test --lib telemetry
Expand Down
5 changes: 5 additions & 0 deletions scripts/windows/CI-Windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ Invoke-CargoCheck -ArgumentList @(
"test", "--lib", "commands::system::tests::windows_chrome_launch_"
) -Label "cargo test Windows Chrome launch"

# SEC-007 recovery needs native handle, ACL and process-lifetime coverage.
foreach ($filter in @("services::acp::process_record_store", "services::acp::goose_serve::recovery_tests", "services::process::")) {
Invoke-CargoCheck -ArgumentList @("test", "--lib", $filter) -Label "cargo test $filter"
}

# Clippy compiles both configurations, so separate `cargo check` calls only
# repeat the same compile coverage.
Invoke-CargoCheck -ArgumentList @(
Expand Down
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ ignore = "0.4.25"
fern = "0.7"
infer = "0.19.0"
libc = "0.2"
errno = "0.3"
log = "0.4.29"
mime_guess = "2"
futures-util = "0.3"
Expand Down Expand Up @@ -88,10 +89,16 @@ zip = { version = "2", default-features = false, features = ["deflate"] }
[target.'cfg(windows)'.dependencies]
keyring = { version = "3.6.3", default-features = false, features = ["windows-native"] }
windows-sys = { version = "0.59", features = [
"Wdk_Foundation",
"Wdk_Storage_FileSystem",
"Win32_Foundation",
"Win32_Globalization",
"Win32_Security",
"Win32_Security_Authorization",
"Win32_Storage_FileSystem",
"Win32_System_Com",
"Win32_System_IO",
"Win32_System_Memory",
"Win32_System_Threading",
"Win32_UI_Shell",
] }
Expand Down
4 changes: 3 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,9 @@ pub fn run() {
.stop_for_app_exit();
app.state::<services::remote_backend::RemoteBackendRegistry>()
.kill_all_tunnels();
services::acp::goose_serve::GooseServeProcess::kill_singleton();
tauri::async_runtime::block_on(
services::acp::goose_serve::GooseServeProcess::kill_singleton(),
);
}
#[cfg(target_os = "macos")]
RunEvent::Reopen { .. } => {
Expand Down
Loading