🖥️ WebSocket application console (daemon)
📌 Description
The daemon exposes a WebSocket console for a specific application: a terminal and live log stream in the browser (attach to a container or to a native application's journal).
🎯 Scenarios
- 🧑💻 A developer opens the application's "Console" tab — sees live output and types commands into the application's stdin (e.g. a Minecraft server console).
- 🔎 In the "Logs" tab they find the moment of a crash using timestamps and a filter.
🏗️ Technical design
🖥️ Application tabs in the UI
Every application in the platform has two tabs:
- Console — real time: a stream of the Docker application's output or the native application's runtime process; at the bottom — an input line for the application's stdin. This is an interactive session (attach), not just viewing.
- Logs — history: every line carries a timestamp, text search/filtering, sorting by time, range selection; a "show in context" jump from a search result.
🎨 Terminal
The terminal is modern and convenient: xterm.js, full ANSI color support and control sequences, automatic sizing (resize → PTY), copy/paste, buffer search, scrollback history, font/theme selection (inherits the UI theme).
⚙️ Transport
- WebSocket console: a server inside the daemon; sessions
logs(a read-only stream: docker logs -f / journald follow, with timestamps),attach(the application's PTY/stdin, bidirectional) andexec(an independent shell inside the application, DMN-082 — see below). The protocol is binary frames (stdin/stdout/resize) as in ttyd/gotty. - Access via a temporary token: a WebSocket connection to the console opens only with a temporary token, issued through the daemon API (
AppService.IssueConsoleToken/POST /v1/apps/{id}/console-token): TTL 30 seconds, single-use, bound to the application and the session typelogs/attach/exec. Anexecgrant additionally carries the command to run (empty = probe for a shell, see below). The platform obtains the token automatically when the console tab opens (the backend checks permissions → requests a token from the daemon → hands it to the browser); in standalone mode the CLI issues it (asc app console-token <id>). - Endpoint:
GET /v1/console?token=<token>[&tail=N][&cols=N&rows=N]— outside the API's bearer authentication (a browser cannot set headers on a WS handshake); the only protection is the single-use token; an invalid/used token → 401 before the upgrade.cols/rowsset the initial PTY geometry for anexecsession (default 80×24 until the client's first resize frame); ignored bylogs/attach. - MVP implementation (DMN-007): the
logssession — text frames, source by runtime: for Docker — log streaming via the Engine API (follow+ timestamps over the unix socket); for systemd —journalctl -f -o short-iso; for process —tail -F app.log(the subprocess is killed when the client disconnects). Theattachsession — Docker only for now (Engine APIattach, binary stdin/stdout frames); systemd/process need a PTY at launch — that arrives with the ttyd-style binary protocol (resize etc.) during the UI work (FE-006). execsession (DMN-082): an interactive shell inside a Docker container, independent ofattach— it does not join the console hub and is never fanned out to other viewers; eachexecconnection gets its own process via the Engine API (create_exec+start_execwith a PTY,attach_stdin/stdout/stderr). If the token's command is empty, the daemon probes/bin/bashthen falls back to/bin/sh— whichever exists in the image. Frames in both directions are tagged binary, one leading byte matching the platform relay'swsapi/protocol.gobyte-for-byte:0x00= data (the raw stdin/stdout payload follows),0x01= resize, client→daemon only (a 4-byte payload: big-endiancolsthenrows), applied via the Engine API'sresize_exec. An unrecognized tag is dropped rather than failing the connection.execis Docker-only for now; systemd/process report the same "not supported yet" error asattach. Known limitation: the Engine API's exec has no kill — an abrupt socket drop leaves the daemon only able to close stdin, not force the process to exit; documented here rather than worked around, since a proper fix needs Docker to support it.- Multiple connections to one application (tabs/users):
attachsessions are multi-client following the wings pattern — the daemon creates one shared source session (hub) per application and its output is fanned out to all connections via a broadcast channel; a lagging client loses old chunks but does not slow down the rest. The hub keeps a replay buffer of the last ~128 KiB of output — a new tab immediately sees recent output, and all clients' stdin converges into the container's single pipe. The source (Engine APIattach) closes together with the last client.logssessions are independent by construction: every connection is its own follow stream with its owntail, so multiple tabs work there too. - Current run only (DMN-116): every runtime appends each run to one shared log (the container's Engine log, the journal,
app.log), so a console session starts its backlog at the start of the application's current run — after a start or restart the console shows only what this run printed, never the output of earlier runs. Dockerlogs:since= the container'sState.StartedAt, cut exactly on each line's nanosecond timestamp. Dockerattach: the live attach is opened without the Engine'slogsreplay (that replay is every run the container ever had); the backlog — the last 1000 messages of the current run — is read from the log afterwards, bounded byStartedAtand the moment the attach went live, so the two meet with no gap and no duplicates. systemd:journalctl _SYSTEMD_INVOCATION_ID=<the unit's current InvocationID>. process: the driver records theapp.logoffset on every start (app.log.run-offset) and the console follows from it (tail -c +N -F, the lasttaillines of the run). compose:docker compose logs --since <the earliest StartedAt of the project's running containers>. Where the run start cannot be determined (never started, a process started by an older daemon) the plaintailapplies. The browser terminal is fully reset for every new session, so a restart replaces the old output instead of stacking under it. - CLI:
asc attach <id>(synonymasc app attach <id>) — an interactive application console right from the server terminal: the CLI process's stdin/stdout are piped into the application. With a daemon running it goes through the daemon's own console (DMN-043): the CLI asks for a console token over the unix socket and opens the same WebSocket session the browser uses, so a regular user needs neither membership in thedockergroup nor access to the root-owned app tree — the peer uid the daemon reads from the socket is the whole authorization. Without a daemon (standalone install) the CLI goes straight to the Engine API instead. Either way Docker fans the output out to everyone attached (CLI + browser tabs). Docker applications only for now; systemd/process — together with PTY (FE-006). Disconnect — Ctrl+C, the application keeps running. - Routing: browser ↔ nodeservice (wss) ↔ daemon tunnel ↔ console module.
- Permissions:
apps.consolecontrols access to the application console (🔐 access-control).
🔗 Related tasks
DMN-007, DMN-082, DMN-088, DMN-116, FE-006, FE-061 in ROADMAP.md.