Skip to content

fix(daemon): no terminal on the install container, and SteamCMD is why - #123

Merged
aaldersondev merged 1 commit into
mainfrom
fix/install-container-tty
Aug 25, 2026
Merged

fix(daemon): no terminal on the install container, and SteamCMD is why#123
aaldersondev merged 1 commit into
mainfrom
fix/install-container-tty

Conversation

@aaldersondev

Copy link
Copy Markdown
Contributor

The report

A Garry's Mod install, twice, ending on:

Connecting anonymously to Steam Public...OK
Waiting for client config...OK
Waiting for user info...OK
ERROR! Failed to install app '4020' (Missing configuration)
[Hopper] Installation failed (code 8).

What it is

The tty the daemon gives the install container.

Measured on one machine, interleaved so that neither time nor a Steam outage can explain it:

Container Result
with -t Missing configuration, 7 runs out of 7
without -t downloads all 6.87 GB, 3 runs out of 3

TERM is not the variable — TERM=xterm without a tty downloads, an empty TERM with one fails. Nothing a script can do escapes it either: stdin from /dev/null, stdout down a pipe, and setsid to drop the controlling terminal were all still refused. Ruled out along the way: the node's own configuration (allowedOrigins, MTU, enable_icc), the capability set, the memory and pid limits, and disk space — 1.9 GB for a 6.87 GB depot gives state is 0x202, which is a different message.

This is not one game. 104 of the 274 published eggs read for this catalogue install from SteamCMD. A tty was a wall in front of every one of them, with a message that names neither the panel nor the terminal.

What changes

Tty: false on the install container.

The cost is the attach stream, which is then multiplexed rather than plain: eight bytes of header before each piece of output, frames that straddle socket reads, stdout and stderr interleaved. DockerFrameReader reads it back into the one text stream a tty used to give. It decodes through a StringDecoder, which also closes a seam that was already there — chunk.toString('utf8') split a multi-byte character into two replacement characters at every chunk boundary.

Its fallback is narrow and deliberate: a header that makes no sense turns the rest of the stream into text. It should never happen, since the daemon creates the containers it attaches to — but an install console filling with control characters is the worst failure available on the screen somebody is watching to find out why their install is stuck.

The installer's tests now emit framed output, so they exercise the path that runs rather than that fallback. Verified by mutation: making the reader pass bytes through fails eight unit tests and one installer test.

And a retry, which is a different question

6.87 GB is a long time for a connection to stay up, and Steam resumes from what it has staged under steamapps/downloading. So the Source install makes three attempts, then names which of three things happened:

SteamCMD says What it means
Missing configuration The install container had a terminal — this node's daemon is older than the panel that sent the script. Update it.
state is 0x202 Out of room; the install wants about twice the finished size free.
No subscription The app id is not anonymously installable.

That first row is the version-skew case this fix creates: the panel sends the script, the daemon creates the container, and a node that has not been updated still has the tty.

Replaces #122

That PR blamed a passing Steam mood, on a comparison with an uncontrolled variable — the two containers differed by a tty, not only by ten minutes. Closed with the measurements.

🤖 Generated with Claude Code

A Garry's Mod installation ended on

  Connecting anonymously to Steam Public...OK
  Waiting for user info...OK
  ERROR! Failed to install app '4020' (Missing configuration)

and exit 8, before a byte of depot. It was not this machine's doing and it
was not a passing Steam mood: it was the tty the daemon gave the install
container.

Measured eight times on one machine, interleaved so that neither time nor a
Steam outage can explain it — seven failures out of seven with a tty, three
successes out of three without. `TERM` is not the variable: `TERM=xterm`
without a tty downloads, an empty `TERM` with one fails. And nothing a script
can do gets out from under it — stdin from /dev/null, stdout down a pipe, and
`setsid` to drop the controlling terminal were all still refused.

It is not one game either. 104 of the 274 published eggs read for this
catalogue install from SteamCMD, so a tty was a wall in front of every one of
them, carrying a message that names neither the panel nor the terminal.

So the install container is created with `Tty: false`. What that costs is the
attach stream, which is then multiplexed rather than plain: eight bytes of
header before each piece of output, frames that straddle socket reads, and
stdout and stderr interleaved. `DockerFrameReader` reads it back into the one
text stream a tty used to give, decoding through a StringDecoder — which also
closes the seam `chunk.toString('utf8')` had at every chunk boundary, where a
multi-byte character split in two came out as two replacement characters.

Its fallback is deliberate and narrow: a header that makes no sense turns the
rest of the stream into plain text. It should never happen, since the daemon
creates the containers it attaches to — but an install console filling with
control characters is the worst failure available here, on the screen
somebody is watching to find out why their installation is stuck. The
installer's own tests now emit framed output, so they exercise the path that
runs rather than that fallback; breaking the reader fails eight unit tests
and one installer test.

The Source templates gain a retry, which is a different question and a
smaller one: 6.87 GB is a long time for a connection to stay up, and Steam
resumes from what it has staged under steamapps/downloading. Three attempts,
then a diagnosis, because none of the three fixes is guessable from what
SteamCMD prints — `Missing configuration` now means this node's daemon is
older than the panel that sent the script, `state is 0x202` means the volume
is out of room (measured by giving it 1.9 GB for a 6.87 GB depot), and `No
subscription` means the app id is not anonymously installable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ReX2JSG6VKYShanE1F69KP
@aaldersondev
aaldersondev merged commit 5fa3224 into main Aug 25, 2026
3 checks passed
@aaldersondev
aaldersondev deleted the fix/install-container-tty branch August 25, 2026 05:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant