[Bug]: Container log file stops receiving output permanently when an attached client dies (MultiWriter aborts fan-out on first EPIPE)
- Dominant language
- Swift
- Stars
- 49.9k
- Forks
- 1.8k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 22
Description
### I have done the following
- [x] I have searched the existing issues
- [ ] If possible, I've reproduced the issue using the 'main' branch of this project
### Steps to reproduce
1. Run a container in the foreground (attached stdio) that logs periodically:
```sh
container run --name freeze alpine:latest sh -c 'while true; do echo "tick $(date -u +%H:%M:%S)"; sleep 2; done'
```
2. 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):
```sh
kill -9
```
3. Confirm the container is still running, then check its logs repeatedly:
```sh
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:
```swift
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:
```swift
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
- [x] I agree to follow this project's Code of Conduct
Contributor guide
Research direction
Start in Sources/Services/RuntimeLinux/Server/RuntimeService.swift and inspect how MultiWriter connects the client stdio and container log handles. Reproduce the failure with the attached container command and kill -9 scenario, then verify that writes continue reaching stdio.log after the client handle fails. Add a regression test if the surrounding test structure supports it.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100