I have done the following
Steps to reproduce
- Run a container in the foreground (attached stdio) that logs periodically:
container run --name freeze alpine:latest sh -c 'while true; do echo "tick $(date -u +%H:%M:%S)"; sleep 2; done'
- From another terminal, kill the attached CLI process without giving it a chance to clean up (simulates a client crash, terminal close, SSH drop, or supervisor restart of any program holding the attach):
kill -9 <pid of the `container run` process>
- Confirm the container is still running, then check its logs repeatedly:
container ls # freeze ... running
container logs freeze # last line is from the moment of the kill
sleep 10
container logs freeze # identical output — nothing new, ever
Observed (timestamps from a real run): CLI killed at 21:41:21 UTC → container logs frozen at tick 21:41:22 permanently, while the container keeps running and emitting every 2 seconds. The bundle's stdio.log on disk stops growing at that moment, so this is the write path, not container logs' read path.
Problem description
RuntimeService wires an attached container's stdout/stderr through MultiWriter(handles: [clientStdioHandle, containerLogHandle]) (Sources/Services/RuntimeLinux/Server/RuntimeService.swift), and MultiWriter.write aborts the whole fan-out on the first failing handle:
func write(_ data: Data) throws {
for handle in handles {
try handle.write(contentsOf: data)
}
}
The client's stdio handle comes first. Once the attached client dies, every write to that handle fails (EPIPE), the loop throws before reaching the log file handle, and stdio.log never receives another byte for the container's remaining lifetime — silently, with the container otherwise healthy.
Real-world impact: any long-running container started attached loses all logging from the moment its original client goes away for any reason. We hit this in production-like use through a Docker API bridge over container (socktainer/Whalebridge): a PostgreSQL container's logs froze mid-day and docker logs served the same stale buffer for hours; the trigger was the bridge daemon being restarted while the container ran on. The repro above shows the same failure with nothing but the container CLI itself.
Expected behavior: the death of one output consumer should not disable the others. MultiWriter.write should isolate per-handle failures — e.g. attempt every handle and swallow (or collect) individual errors, and ideally drop a handle after a persistent failure so the container's own log file keeps working:
func write(_ data: Data) throws {
for handle in handles {
do { try handle.write(contentsOf: data) } catch { /* drop or note dead handle */ }
}
}
(Ordering the log file first would narrow the window but not fix it; per-handle isolation does.)
Happy to submit a PR with the fix plus a regression test if that's welcome.
Environment
- OS: macOS 27.0 (Apple silicon)
- Xcode: Xcode 26
- Container: container CLI version 1.1.0 (build: release, commit: 5973b9c)
Code of Conduct
I have done the following
Steps to reproduce
container run --name freeze alpine:latest sh -c 'while true; do echo "tick $(date -u +%H:%M:%S)"; sleep 2; done'Observed (timestamps from a real run): CLI killed at
21:41:21UTC →container logsfrozen attick 21:41:22permanently, while the container keeps running and emitting every 2 seconds. The bundle'sstdio.logon disk stops growing at that moment, so this is the write path, notcontainer logs' read path.Problem description
RuntimeServicewires an attached container's stdout/stderr throughMultiWriter(handles: [clientStdioHandle, containerLogHandle])(Sources/Services/RuntimeLinux/Server/RuntimeService.swift), andMultiWriter.writeaborts the whole fan-out on the first failing handle:The client's stdio handle comes first. Once the attached client dies, every write to that handle fails (EPIPE), the loop throws before reaching the log file handle, and
stdio.lognever receives another byte for the container's remaining lifetime — silently, with the container otherwise healthy.Real-world impact: any long-running container started attached loses all logging from the moment its original client goes away for any reason. We hit this in production-like use through a Docker API bridge over
container(socktainer/Whalebridge): a PostgreSQL container's logs froze mid-day anddocker logsserved the same stale buffer for hours; the trigger was the bridge daemon being restarted while the container ran on. The repro above shows the same failure with nothing but thecontainerCLI itself.Expected behavior: the death of one output consumer should not disable the others.
MultiWriter.writeshould isolate per-handle failures — e.g. attempt every handle and swallow (or collect) individual errors, and ideally drop a handle after a persistent failure so the container's own log file keeps working:(Ordering the log file first would narrow the window but not fix it; per-handle isolation does.)
Happy to submit a PR with the fix plus a regression test if that's welcome.
Environment
Code of Conduct