spawn_file_portal() checks WEFT_FILE_PORTAL_BIN; if set, creates a
per-session Unix socket at /weft/portal-<id>.sock and
spawns weft-file-portal with --allow args derived from the same host
paths that resolve_preopens() produces for the session.
WEFT_FILE_PORTAL_SOCKET is added to the runtime environment so
weft-runtime (and the WASM app) can locate the socket.
The portal process is killed and the socket cleaned up at the end of
supervise(), after the runtime exits and the mount is torn down.
When WEFT_FILE_PORTAL_BIN is unset the behaviour is unchanged.
Add crates/weft-appd/src/mount.rs with MountOrchestrator.
On each app launch, supervise() calls MountOrchestrator::mount_if_needed
which looks for <app_id>.app.img, <app_id>.hash, and <app_id>.roothash
in the app store roots. If all three exist and weft-mount-helper is
available (WEFT_MOUNT_HELPER env or /usr/lib/weft/ default paths), it:
- creates /tmp/weft-mnt-<session_id>/<app_id>/
- invokes weft-mount-helper mount to set up dm-verity and EROFS mount
- sets WEFT_APP_STORE=/tmp/weft-mnt-<session_id> in the child env so
the runtime resolves the package from the mounted read-only image
After the process exits, umount() invokes weft-mount-helper umount and
removes the temporary directory.
Falls back to directory-based install silently if no image is found,
mount-helper is absent, or the mount fails.
Test: find_image_returns_none_when_absent.
supervise() checks /systemd/private to detect an active
user systemd session. When present (and WEFT_DISABLE_CGROUP is unset),
the runtime binary is launched via:
systemd-run --user --scope --wait --collect --slice=weft-apps.slice -p CPUQuota=200% -p MemoryMax=512M -- <bin> ...
This places each app in a transient weft-apps.slice scope with default
resource limits from the blueprint. The --wait flag keeps systemd-run
alive so child.wait()/child.kill() remain correct.
When no user session is present the command is built directly as before.
WEFT_DISABLE_CGROUP=1 bypasses the wrapping unconditionally.
Add capabilities field to weft-pack PackageMeta (optional Vec<String>).
Print cap: lines in weft-pack info output when capabilities are declared.
In weft-appd:
- Make app_store_roots pub(crate) so runtime.rs can use it.
- Add resolve_preopens(app_id) in runtime.rs: reads wapp.toml from the
package store, extracts capabilities, maps each to a (host, guest) pair:
fs:rw:app-data / fs:read:app-data -> ~/.local/share/weft/apps/<id>/data :: /data
fs:rw:xdg-documents / fs:read:xdg-documents -> ~/Documents :: /xdg/documents
Unknown capabilities are logged at debug level and skipped.
- supervise() calls resolve_preopens() and appends --preopen HOST::GUEST
flags before spawning the runtime binary.
weft-runtime now parses optional flags after <app_id> <session_id>:
--preopen HOST::GUEST pre-opens a host directory at GUEST path in the
WASI filesystem (HOST::GUEST or HOST for same path)
--ipc-socket PATH sets WEFT_IPC_SOCKET env var inside the component
wasmtime-runtime path applies preopened dirs via cap_std and WasiCtxBuilder,
and injects WEFT_IPC_SOCKET when --ipc-socket is present. Stub path ignores
both flags.
weft-appd: SessionRegistry gains ipc_socket field (set to the appd Unix
socket path in run()), extracted alongside compositor_tx in dispatch(), and
forwarded to supervise() as ipc_socket_path. supervise() passes
--ipc-socket <path> to the spawned runtime when present.
cap-std added as optional dep under wasmtime-runtime feature.
Adds crates/weft-appd/src/compositor_client.rs: async Tokio client that connects to the
compositor's Unix socket (/weft/compositor.sock or WEFT_COMPOSITOR_SOCKET),
retrying every 2s on failure and 500ms on write error. Incoming CompositorToAppd frames are
decoded and logged (SurfaceReady, ClientDisconnected).
Wires compositor_tx into SessionRegistry. The supervise task now sends AppSurfaceCreated
(with child PID) immediately after process spawn, and AppSurfaceDestroyed when the process
exits. All three existing supervisor tests updated to pass None for compositor_tx.
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
If WEFT_RUNTIME_BIN is set but the binary cannot be spawned (missing,
not executable, etc.), supervise() now transitions the session to
Stopped and broadcasts AppState::Stopped instead of returning an error
that left the session permanently stuck in Starting.
runtime.rs: the READY-timeout early-return path now broadcasts
AppState::Stopped before returning so WebSocket clients see the
session disappear when a module fails to signal readiness within 30s.
runtime.rs: after the child exits (natural exit or abort), supervise()
now broadcasts AppState { session_id, state: Stopped } in the same
lock scope as set_state. WebSocket clients receive the notification
without needing to poll QueryAppState or call TerminateApp.
wait_for_ready() now returns the BufReader<ChildStdout> with the READY
line already consumed. supervise() spawns drain_stdout() on that reader
so any subsequent module output is forwarded to the trace log and the
pipe buffer never fills up.
Without this, a long-running Wasm module that writes to stdout after
printing READY would eventually block waiting on a full pipe.
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.
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.
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.