Two new tests cover appd_socket_path():
- appd_socket_path_uses_override_env: WEFT_APPD_SOCKET takes precedence
- appd_socket_path_errors_without_xdg_and_no_override: returns error when
both WEFT_APPD_SOCKET and XDG_RUNTIME_DIR are unset
wsl-test.sh: add --test-threads=1 for weft-appd to prevent WEFT_RUNTIME_BIN
races between the supervisor integration tests.
Before this fix, TerminateApp sent while a process was waiting for its
READY signal was not acted on until the 30-second timeout fired.
abort_rx is now included in the tokio::select! that wraps wait_for_ready,
so the child is killed and AppState::Stopped broadcast as soon as the
abort is received, regardless of where in the startup sequence it fires.
test: supervisor_abort_during_startup_broadcasts_stopped
supervisor_transitions_through_ready_to_stopped now checks both
broadcast messages: AppReady (on READY signal) and AppState::Stopped
(on process exit), covering the path added in 3315b15.
write_ws_port failure is now logged as a warning rather than propagating
an error that would crash the service. Error context strings are added
to create_dir_all and write failures so the warning is actionable.
main.rs: add dispatch_query_installed_returns_installed_apps to verify
the QueryInstalledApps arm returns Response::InstalledApps.
wsl-test.sh: run weft-runtime tests with --test-threads=1 to prevent
the WEFT_APP_STORE env var race between package_store_roots_includes_
system_path and package_store_roots_uses_weft_app_store_when_set.
SessionRegistry::shutdown_all() clears abort_senders, dropping all
oneshot senders. Each supervised process's abort_rx fires, causing
supervise() to kill the child. A 200ms yield after shutdown_all gives
the tokio runtime time to schedule the abort handling before the
process exits and the socket file is removed.
run() now registers a SIGTERM handler (unix-only, cfg-gated) alongside
the existing SIGINT handler. Both break the accept loop and allow the
Unix socket to be removed before exit.
On non-Unix targets the SIGTERM arm uses std::future::pending so the
select! shape is unchanged at the type level.
ws_listener.local_addr().port() is used instead of the configured
ws_port value. This is correct when WEFT_APPD_WS_PORT=0 lets the OS
assign an ephemeral port; the file reflects the real listening port.
ipc.rs: AppReady { session_id, app_id: String }.
runtime.rs: supervise() passes app_id (already in scope as parameter)
when building the AppReady broadcast message.
main.rs: supervisor integration test updated to use .. to ignore
app_id in the AppReady pattern match.
ipc.rs: LaunchAck gains app_id: String field so callers receive the
app identifier alongside the session handle in a single response.
main.rs: dispatch::LaunchApp constructs LaunchAck { session_id, app_id }
using the app_id that was already in scope.
Tests updated: dispatch_launch_returns_ack now asserts app_id value;
dispatch_terminate_known_returns_stopped and
dispatch_query_app_state_returns_starting use .. to ignore app_id.
ipc.rs:
- Add SessionInfo { session_id: u64, app_id: String } struct.
- Change RunningApps { session_ids: Vec<u64> } to
RunningApps { sessions: Vec<SessionInfo> } so callers can display
meaningful app names without a follow-up QueryAppState round-trip.
- Add session_info_roundtrip test.
main.rs:
- Add SessionEntry { app_id: String, state: AppStateKind } to store
app_id alongside state in SessionRegistry.
- launch() stores app_id in the entry.
- running_sessions() replaces running_ids(); returns Vec<SessionInfo>.
- state() reads from SessionEntry.state.
- set_state() writes to SessionEntry.state.
- QueryRunning dispatch uses running_sessions().
- Test registry_running_ids_reflects_live_sessions renamed to
registry_running_sessions_reflects_live_sessions and updated to
assert both session_id and app_id fields.
- dispatch_query_running test asserts app_id values are present.
system-ui.html:
- RUNNING_APPS handler uses msg.sessions[].{session_id,app_id}.
- ensureTaskbarEntry(sessionId, appId): shows the last component of the
reverse-domain app ID as the taskbar label; sets data-app-id attribute;
tooltip shows full app ID and session number.
- LAUNCH_ACK handler passes null for appId (session ID only available
at launch time; app_id arrives in RUNNING_APPS on reconnect).
SessionRegistry now tracks a oneshot abort sender per active session:
- abort_senders: HashMap<u64, oneshot::Sender<()>> field added.
- register_abort(session_id): creates the channel, stores the sender,
returns the receiver to the supervise task.
- terminate(): removes the session state AND drops the abort sender,
closing the channel and triggering the receiver in supervise.
runtime::supervise() now accepts abort_rx: oneshot::Receiver<()>:
- After the READY signal is received, the process-wait loop uses
tokio::select! on child.wait() vs abort_rx.
- On abort: logs intent, calls child.kill(), then sets state Stopped.
- On natural exit: logs exit status, sets state Stopped.
dispatch::LaunchApp: calls register_abort immediately after launch,
passes the receiver to the spawned supervise task.
Integration test updated to pass the abort receiver.
supervisor_transitions_through_ready_to_stopped (unix only):
- Writes a temp shell script that prints 'READY' and exits.
- Sets WEFT_RUNTIME_BIN to the script path; restores env after test.
- Calls runtime::supervise() and verifies final session state is Stopped.
- Verifies AppReady was broadcast via the registry broadcast channel.
- Runs with tokio flavor='current_thread' to avoid concurrent env
mutation. Wraps set_var/remove_var in unsafe blocks (required since
Rust 1.93).
runtime.rs — process lifecycle manager:
- supervise(session_id, app_id, registry): spawns the weft-runtime child
process identified by WEFT_RUNTIME_BIN env var. If unset, logs debug
and returns immediately (no-op until runtime binary is available).
- Child process invoked as: <WEFT_RUNTIME_BIN> <app_id> <session_id>
with stdout/stderr piped, stdin closed.
- wait_for_ready(): reads stdout line-by-line; returns Ok(()) on first
line matching 'READY'; returns Err if stdout closes without it.
- 30-second READY_TIMEOUT via tokio::time::timeout; on expiry, kills
the child and transitions session to Stopped.
- On success: sets session state to Running, broadcasts AppReady to all
connected WebSocket clients via registry broadcast channel.
- drain_stderr(): async task that forwards child stderr lines to tracing
at WARN level for observability.
- On process exit: sets session state to Stopped regardless of exit code.
main.rs — wiring:
- SessionRegistry now owns broadcast::Sender<Response>; Default creates
the channel internally. Added set_state(), subscribe(), broadcast()
methods. Removed standalone broadcast_tx from run(); WS handlers
subscribe via registry.lock().await.subscribe().
- dispatch::LaunchApp spawns a tokio task calling runtime::supervise
immediately after creating the session. supervise is a no-op when
WEFT_RUNTIME_BIN is unset, so existing tests are unaffected.
Cargo.toml: added tokio 'process' and 'time' features.
Implements the weft-appd WebSocket server that allows the system-ui.html
page running inside Servo to send requests and receive push notifications
without requiring custom SpiderMonkey bindings.
ws.rs — WebSocket connection handler:
- Accepts a tokio TcpStream, performs WebSocket handshake via
tokio-tungstenite accept_async.
- Reads JSON Text frames, deserializes as Request (serde_json), calls
dispatch(), sends Response as JSON Text.
- Subscribes to a broadcast::Receiver<Response> for server-push
notifications (APP_READY, etc.); forwards to client via select!.
- Handles close frames, partial errors, and lagged broadcast gracefully.
main.rs — server changes:
- broadcast::channel(16) created at startup; WebSocket handlers
subscribe for push delivery.
- TcpListener bound on 127.0.0.1:7410 (default) or WEFT_APPD_WS_PORT.
- ws_port() / write_ws_port(): port written to
XDG_RUNTIME_DIR/weft/appd.wsport for runtime discovery.
- WS accept branch added to the main select! loop alongside Unix socket.
ipc.rs — Response and AppStateKind now derive Clone (required by
broadcast::Sender<Response>).
system-ui.html — appd WebSocket client:
- appdConnect(): opens ws://127.0.0.1:<port>/appd with exponential
backoff reconnect (1s → 16s max).
- On open: sends QUERY_RUNNING to populate taskbar with live sessions.
- handleAppdMessage(): maps LAUNCH_ACK and RUNNING_APPS to taskbar
entries; APP_READY shows a timed notification; APP_STATE::stopped
removes the taskbar entry.
- WEFT_APPD_WS_PORT window global overrides the default port.
New deps: tokio-tungstenite 0.24, futures-util 0.3 (sink+std),
serde_json 1.
ipc.rs tests (4 tests):
- request_msgpack_roundtrip: LaunchApp serializes and deserializes with
correct field values.
- response_msgpack_roundtrip: LaunchAck round-trips through MessagePack.
- frame_write_read_roundtrip: write_frame encodes a 4-byte LE length
header + body; read_frame decodes the framed request correctly.
- read_frame_eof_returns_none: empty stream returns None without error.
main.rs tests (5 tests):
- registry_launch_increments_id: each launch returns a strictly
increasing session ID.
- registry_terminate_known_session: terminate returns true and state
transitions to NotFound.
- registry_terminate_unknown_returns_false: terminate on missing ID
returns false.
- registry_running_ids_reflects_live_sessions: running_ids returns all
active sessions; terminated sessions are removed.
- registry_state_not_found_for_unknown: querying an unknown session ID
returns AppStateKind::NotFound.
Also extends scripts/wsl-test.sh to run weft-appd tests alongside
weft-compositor tests.
New crate implementing the application daemon entry point:
- crates/weft-appd/Cargo.toml: tokio (current-thread runtime), anyhow,
sd-notify, tracing dependencies
- crates/weft-appd/src/main.rs: async run() resolves IPC socket path
from WEFT_APPD_SOCKET or XDG_RUNTIME_DIR/weft/appd.sock; stubs for
AppRegistry, IpcServer, CompositorClient, RuntimeSupervisor,
CapabilityBroker, ResourceController per WEFT-OS-APPD-DESIGN.md;
sd_notify(READY=1) to be sent after IpcServer bind + CompositorClient
connect
- infra/systemd/weft-appd.service: Type=notify, Requires+After
weft-compositor.service, After servo-shell.service
Also fix two winit backend issues that were present in the working tree:
- remove spurious mut on display binding (never mutated after init)
- wrap std::env::set_var in unsafe block (required since Rust 1.80)