Repository navigation
Mast v0.7.7: what runs in the app container, and who started it - #56
Merged
Merged
Conversation
On Plasma under Wayland the titlebar's minimise, maximise and close did nothing — no hover, no click — and the bar stood taller than every other window's. Neither was GTK's doing: tao 0.35 replaced the window's titlebar on Wayland with its own GTK header bar, wrapped in an event box set above its children, so every pointer event landed on the box and none reached the buttons. Dragging still worked only because the window handles that itself. And being a header bar, it asked for client-side decorations, so KWin never drew its own. tao 0.36 (tauri-apps/tao#1218) dropped that header bar and leaves the choice to GTK, which asks for server-side decorations through KDE's protocol wherever the compositor offers it. Tauri 2.12 carries tao 0.37, so this moves Tauri to 2.12.3 and @tauri-apps/api with it — the release build refuses mismatched Rust and JS minors. Run against this Plasma session with WAYLAND_DEBUG, 0.7.4 sends request_mode(1), client-side; this build sends request_mode(2) and KWin answers mode(2), drawing a Breeze titlebar over a window whose geometry is now the content alone. Desktops that tile still lose the titlebar via linux_env::wants_no_titlebar, whose comment claimed GTK3 had no server-side decoration support on Wayland at all — it does, and the comment now says who draws the bar where.
CI's stable toolchain moved to Rust 1.99, whose new double_must_use lint fires on the #[must_use] async-trait 0.1.91 puts on every generated method — six errors at RuntimeAdapter's attribute, none of them in code written here, and enough to stop clippy before it reached any crate that depends on mast-docker. async-trait 0.1.92 exists to resolve exactly that lint (dtolnay/async-trait#303). With it, clippy 1.99 runs clean across the whole workspace with -D warnings.
Stopping a project command that runs through Sail — `sail artisan dev`, `sail npm run dev` — reported success and stopped nothing. The stop script finds the command's processes by the MAST_COMMAND_RUN marker in /proc/<pid>/environ, and it ran as root. Reading another user's environ takes CAP_SYS_PTRACE, which Docker's default capability set leaves out, so as root every one of the command's sail-owned processes was unreadable, the marker was never found, and the script exited 0. Since nothing was flagged as left behind, the next Start, Restart or auto-start launched a second copy beside the first. On a real project that left two `artisan dev` stacks in one container — two Horizon masters, two queue listeners, two schedulers running every task twice — and the second stack's Vite, failing on the port the first still held, deleted public/hot on its way out, so pages fell back to an old build while the command showed as running. The stop now runs as the user the command was started as, which can read and signal all of its own processes. The existing test ran the script as the processes' owner and so could never see this; a new Docker-gated test runs the real stop argv against an Alpine container with the command owned by uid 1000. Against the root stop it fails with the command still running; with this change it stops the command and leaves another run's process alone.
Stopping as the command's own user still missed the processes that matter most. Sail's PHP binary carries a file capability (cap_net_bind_service, so `artisan serve` can bind port 80), and running a binary with file capabilities makes the process non-dumpable: its /proc/<pid>/environ passes to root, readable by no one without CAP_SYS_PTRACE — not even `sail`. So the environment marker only ever found the sh and node processes around a command. For `sail artisan dev`, stopping those would have orphaned Horizon, the scheduler, the queue listener, Reverb and Pail; for a command that is PHP from the start, such as `sail artisan horizon`, it found nothing at all. Each run now starts through a small sh wrapper that records its PID and start time in /tmp/mast-runs/<run> inside the container, then execs the command, so the PID is the command's own. The stop takes the recorded process (if that PID still has the recorded start time, so a reused number is never signalled), anything carrying the run's marker, and every descendant of either, read from /proc/<pid>/stat, which anyone can read. A completed stop removes the record. The Docker test now starts its runs through their real argv, wrapper included, with a setgid copy of busybox standing in for Sail's PHP — a non-dumpable process whose environment its own user cannot read — and a child under it. Matching by environment alone, it fails with both still running; with the record and the tree it stops them and leaves another run alone. Dry-run in a real Sail container against a leftover `artisan dev` stack, it finds the 22 marked processes and the 7 PHP processes beneath them that the marker cannot see, and nothing else.
A record now reads `pid start name`, so one left behind by an earlier Mast session — a run this process never started and has no operation for — can still say which command it was, not just which PID. The name is reduced to a single field (anything outside [A-Za-z0-9._-] becomes `_`), and the stop reads only the first two fields, so anything added after them later cannot shift what it compares.
Mast watched containers, but everything that matters for a Laravel app runs as processes inside one, and every `docker exec` leaves a tree there that outlives its client. Quitting Mast, the Stop that silently failed before 0.7.6, an agent's `sail artisan horizon` — each left trees nothing showed. A real container held two `artisan dev` stacks (3 GB and 0.5 GB): two Horizon masters, two schedulers running every task twice, and a Vite whose exit deleted the other's public/hot, so pages served an old build while every chip was green. Each reconcile pass now walks /proc once per running app container and groups the processes into trees: whatever a `docker exec` started, or init adopted, and everything beneath it. ProjectSummary carries them as container_runs, each with an owner, age, memory and the daemon kinds inside. Ownership comes from what Mast itself leaves, never from environ, which Sail's PHP processes do not let anyone read: run records name Mast's commands (this session's, or an earlier one's), Mast's registry claims its supervised daemons, and the rest were started outside Mast or orphaned. The container's own startup is left out. Process chips now count copies (ProcessState.instances), and running means a process that is the daemon. The old substring check matched a dev-stack runner's arguments and showed a Horizon that had died as running — true on a second project here today. Advisories put fixes beside the problem. A daemon that must run once running more than once is one advisory per set of copies, offering to stop each copy Mast is not running. public/hot is compared with VITE_PORT and probed, and pages loading an old build, another server's assets or a dead address each say so, with restart or remove-hot to fix it. public/ is watched, filtered to `hot`, so the card follows the file. Action::StopContainerRun stops one tree by root PID and start time, as root (CAP_KILL is enough, and the tree comes from /proc/<pid>/stat), so a stale listing can never reach a process that has reused the number. Running a Sail command first stops any copy of that same command an earlier Mast session left, so relaunching Mast no longer doubles an auto-started stack; nothing started outside Mast is stopped without a click. The Docker test helpers move to test_docker for the inventory's own real-container test, which runs the scan and the tree stop in busybox: ownership from a record, a mismatched start time stopping nothing, and a stop leaving the neighbouring tree alone. ADR-0011 records the model.
A new row on the project card, "In the app container", has one chip per process tree: Mast's own commands green, anything else amber — a copy an earlier Mast session left, something a terminal or an agent started, an orphan. Each says who started it and how much memory it holds, so a forgotten 3 GB stack beside a 0.5 GB one stands out without opening anything. Its menu gives the full command line, process count, age and the daemons inside, what the owner means, and, for anything Mast is not running, Stop. Advisories render with the card's warnings, each fix beside its sentence: Stop for a duplicate copy (with its age, since two copies of one stack share a name), Restart for the dev command, and Remove public/hot through the same preview-and-confirm dialog as every other repair. Process chips show ×2 in amber when a daemon runs twice. The store keeps which runs are stopping, so a run stopped from its chip shows as stopping in its advisory too.
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #55, which is stacked on #54. Merge those first; this diff then narrows to its own three commits.
What
"In the app container" on the project card. One chip per process tree running in the app container beyond its own startup. Each
docker execleaves such a tree, and it outlives its client.Who started it, from evidence Mast leaves rather than
environ, which Sail's non-dumpable PHP processes don't let anyone read:The container's own startup (supervisord and its
artisan serve) is left out.Duplicates. Horizon, the scheduler, Reverb, Vite or Pail running more than once becomes one advisory per set of copies. It explains the cost ("scheduled tasks run twice") and offers to stop each copy Mast isn't running. Process chips show ×2 in amber.
Process chips now show the truth. A daemon is running when a process is that daemon. The old substring check matched a dev-stack runner's arguments, so a Horizon that had died still showed as running. That's the case right now on a second project here.
Vite and
public/hot. Each case gets an advisory with a fix:The fixes are Restart for the dev command, and Remove
public/hotthrough the usual preview dialog.public/is now watched, filtered tohot.No more doubling on relaunch. Running a Sail command first stops any copy of that exact command an earlier Mast session left running, and says so in the output. Nothing started outside Mast is stopped without a click.
Action::StopContainerRunstops one tree by root PID and start time, so a stale listing can't reach a reused PID.The design is recorded in ADR-0011.
Verified
mast status --json:kusina.design: twoartisan devstacks (3 GB, 30 processes, up 6 h; 0.5 GB, 17 processes, up 3 h), Horizon ×2 and Scheduler ×2. One advisory names both stacks, and the Vite advisory names127.0.0.1:5288against port 5180. The pre-0.7.6 stacks have no records, so they show as started outside Mast, which is honest.api.thinksolar.local: Horizon and the scheduler correctly read as not running. Its stack has 13 processes left, after both panes died. 0.7.4 shows Horizon green there.ProjectCardrendered with that live data in headless Chrome: the advisories with their fixes, the ×2 chips, the run chips with memory, and an open run menu.cargo test --workspacepasses.cargo clippy --workspace --all-targets -D warningsis clean on Rust 1.96 and 1.99.vp checkis clean, andvp testpasses 198 tests.Not in this PR
thinksolar'sdevhas, and its chip is green.