Skip to content

🖥️ 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) and exec (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 type logs/attach/exec. An exec grant 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/rows set the initial PTY geometry for an exec session (default 80×24 until the client's first resize frame); ignored by logs/attach.
  • MVP implementation (DMN-007): the logs session — 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). The attach session — Docker only for now (Engine API attach, 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).
  • exec session (DMN-082): an interactive shell inside a Docker container, independent of attach — it does not join the console hub and is never fanned out to other viewers; each exec connection gets its own process via the Engine API (create_exec + start_exec with a PTY, attach_stdin/stdout/stderr). If the token's command is empty, the daemon probes /bin/bash then falls back to /bin/sh — whichever exists in the image. Frames in both directions are tagged binary, one leading byte matching the platform relay's wsapi/protocol.go byte-for-byte: 0x00 = data (the raw stdin/stdout payload follows), 0x01 = resize, client→daemon only (a 4-byte payload: big-endian cols then rows), applied via the Engine API's resize_exec. An unrecognized tag is dropped rather than failing the connection. exec is Docker-only for now; systemd/process report the same "not supported yet" error as attach. 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): attach sessions 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 API attach) closes together with the last client. logs sessions are independent by construction: every connection is its own follow stream with its own tail, 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. Docker logs: since = the container's State.StartedAt, cut exactly on each line's nanosecond timestamp. Docker attach: the live attach is opened without the Engine's logs replay (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 by StartedAt and 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 the app.log offset on every start (app.log.run-offset) and the console follows from it (tail -c +N -F, the last tail lines 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 plain tail applies. 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> (synonym asc 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 the docker group 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.console controls access to the application console (🔐 access-control).

DMN-007, DMN-082, DMN-088, DMN-116, FE-006, FE-061 in ROADMAP.md.

Released under the MIT License.