Commit graph

23 commits

Author SHA1 Message Date
67f54c39be fix(ci): restore cross-platform and linux checks 2026-03-13 13:44:44 +01:00
2beebf13d2 style: apply rustfmt to all workspace crates 2026-03-13 10:38:01 +01:00
4d0089a107 feat: appd IPC relay, WIT interfaces, UI kit, gesture routing, and CI hardening
- weft-appd: per-session IPC socket paths; bidirectional Wasm-HTML JSON relay
  via spawn_ipc_relay; SO_PEERCRED UID check on Unix socket connections;
  PanelGesture request and NavigationGesture broadcast for compositor gestures
- weft-runtime: weft:app/ipc, weft:app/fetch, weft:app/notifications WIT
  interfaces; IpcState non-blocking Unix socket host functions; ureq-backed
  net:fetch host function (net-fetch feature); notify-send notifications host
- weft-file-portal: spawn a thread per accepted connection for concurrent access
- weft-app-shell: weft-system:// URL translation; WEFT UI Kit UserScript
  injection; resolve_weft_system_url helper
- weft-servo-shell: forward compositor navigation gestures to weft-appd
  WebSocket as PanelGesture; WEFT UI Kit UserScript injection
- infra/shell: weft-ui-kit.js with 11 custom elements (weft-button, weft-card,
  weft-dialog, weft-icon, weft-list, weft-list-item, weft-menu, weft-menu-item,
  weft-progress, weft-input, weft-label); system-ui.html handles
  NAVIGATION_GESTURE messages and dispatches weft:navigation-gesture CustomEvent
- infra/systemd: add missing env vars to weft-appd.service; correct
  servo-shell.service binary path and system-ui.html argument
- .github/workflows/ci.yml: exclude weft-servo-shell and weft-app-shell from
  cross-platform job; add them to linux-only job with libsystemd-dev dependency
2026-03-12 12:49:45 +01:00
a401510b88 feat(appd): per-app process isolation via weft-app-shell
Add weft-app-shell binary: takes <app_id> <session_id> args, connects to
zweft_shell_manager_v1 as an application window, resolves the app UI URL,
and runs a single Servo WebView in an isolated process. Prints READY to
stdout after the window is initialised so weft-appd can track the session
lifecycle.

weft-appd runtime.rs: after weft-runtime emits READY, spawn weft-app-shell
(WEFT_APP_SHELL_BIN env var) alongside it. The app shell is killed when the
session ends via abort or natural runtime exit.

weft-servo-shell: remove in-process app WebView management. The shell now
manages the system UI WebView only; all app rendering happens in dedicated
weft-app-shell processes.
2026-03-12 10:58:45 +01:00
a3f60910d5 fix(appd): remove abort_sender on READY timeout early-return path
The READY timeout branch killed the process and returned Ok(()) without
calling remove_abort_sender(), leaving a spent entry in abort_senders.
Add the call to keep it consistent with all other exit paths.
2026-03-11 19:01:59 +01:00
0fea8f58e4 fix(appd): mark session Stopped when WEFT_RUNTIME_BIN is absent
Previously the supervisor returned Ok(()) without updating the session
state, leaving it in Starting indefinitely. Now it sets state to
Stopped, removes the abort sender, and broadcasts AppState:stopped
before returning, consistent with every other early-exit path.
2026-03-11 18:43:16 +01:00
6f43490192 fix(appd): remove abort_sender entry when session supervisor exits
When a runtime process exits naturally (not via TerminateApp) the
oneshot Sender remained in abort_senders until shutdown_all().
Add remove_abort_sender() and call it at the normal exit path in
supervise() to release the entry immediately.
2026-03-11 18:38:35 +01:00
8eb7211998 refactor(appd): consolidate portal cleanup through kill_portal in normal exit path 2026-03-11 18:21:43 +01:00
0877bd970d fix(appd): kill file portal process on all early-return paths in supervise()
Runtime spawn failure, READY timeout, and abort-during-startup all
previously returned without killing the portal child process or removing
its socket file. Extracted kill_portal() to consolidate cleanup and
call it in each of those three paths.
2026-03-11 18:20:16 +01:00
92920984bd feat(appd): auto-launch weft-file-portal per session
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.
2026-03-11 18:09:38 +01:00
d2cb693c55 feat(appd): MountOrchestrator -- EROFS+dm-verity image mount on app launch
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.
2026-03-11 15:47:23 +01:00
71b7bdf657 feat(appd): wrap runtime in systemd-run cgroup scope when user session is active
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.
2026-03-11 15:25:04 +01:00
c5a47a05b4 feat(appd,pack): capability dispatch -- map wapp.toml capabilities to --preopen args
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.
2026-03-11 15:20:51 +01:00
b2ac279dc5 feat(runtime): add --preopen and --ipc-socket CLI arguments
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.
2026-03-11 15:10:11 +01:00
6b428e5a47 feat(appd): add compositor IPC client; send AppSurfaceCreated/Destroyed on session lifecycle
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.
2026-03-11 14:40:55 +01:00
71597580ba fix(appd): abort TerminateApp during startup phase promptly
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
2026-03-11 12:30:21 +01:00
7a07e46c55 fix(appd): broadcast AppState::Stopped on runtime spawn failure
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.
2026-03-11 11:45:17 +01:00
d6de84b4c7 fix(appd): broadcast AppState::Stopped on READY timeout
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.
2026-03-11 11:19:09 +01:00
3315b158db feat(appd): broadcast AppState::Stopped when supervised process exits
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.
2026-03-11 11:16:28 +01:00
68e1f82ca7 fix(appd): drain module stdout after READY signal to prevent pipe stall
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.
2026-03-11 11:14:18 +01:00
dbe44bd0e0 feat(appd): include app_id in AppReady broadcast
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.
2026-03-11 10:50:41 +01:00
1e4ced9a39 feat(appd): implement TerminateApp process signaling via abort channel
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.
2026-03-11 09:37:09 +01:00
86d0011016 feat(appd): implement runtime supervisor with process spawning and READY signal
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.
2026-03-11 09:17:20 +01:00