Skip to content

[Bug]: Container log file stops receiving output permanently when an attached client dies (MultiWriter aborts fan-out on first EPIPE) #2009

Description

@cap10morgan

I have done the following

  • 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:
    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):
    kill -9 <pid of the `container run` process>
  3. 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 agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions