Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[Bug]: container logs -f exits immediately with no output when the container's log file is empty

Chiusa Adatta ai principianti
#2,374 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Una pull request collegata è già stata integrata.

  • #2377 di @i098 — integrata

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
78/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
swift
Ambito
cli

Direzione di ricerca

Il bug si trova in tail() in Sources/ContainerCommands/Container/ContainerLogs.swift: il ritorno anticipato su un risultato nil di readToEnd() viene eseguito prima del ramo di follow. Inizia riproducendo il problema con la sequenza minima container run -d seguita da container logs -f descritta nell'issue. È completato quando -f su un log di zero byte si blocca e trasmette le scritture successive, mentre la modalità senza follow e -f su un log non vuoto continuano a funzionare come prima.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Problem

container logs -f exits immediately — no output, no follow, exit code 0 — when the container's log file is still empty at call time. A caller that starts tailing right after container run -d (the normal pattern for a supervised dev service) silently gets nothing, and tooling that treats "the log tail exited" as "the task is done" can go on to tear down a perfectly healthy container.

Real-world impact

A Taskfile-based dev stack ran container run -d --rm --name postgres -e ... -v postgres:/var/lib/postgresql -p 5432:5432 postgres:18-alpine, then tailed with container logs -f postgres, with defer: container stop postgres registered for Ctrl+C cleanup. With a freshly auto-created volume, postgres writes nothing to its log while the container VM boots and initdb runs; the tail hit the zero-byte log and exited within the same second (the deferred container stop fired immediately after, with no log lines in between, and no error reported — i.e. exit 0). The result: postgres was killed ~1s after coming up.

Evidence that postgres did come up in the "failed" run — from the next run's recovery log on the same volume (the volume had been deleted before the failed run, so the timestamp must be from that run):

2026-10-09 15:03:08.142 UTC [27] LOG:  database system was interrupted; last known up at 2026-10-09 14:27:23 UTC
2026-10-09 15:03:08.223 UTC [27] LOG:  database system was not properly shut down; automatic recovery in progress

14:27:23 is the failed run (stack start 14:27:21, container start step completed at +2s). Re-running the same task dev against the now-initialized volume works: the entrypoint's first line ("Skipping initialization") reaches the log long before initdb would on a fresh volume, the tail's initial read finds it, prints, and follows normally. The only difference between the failing and succeeding runs is whether the volume was fresh.

Minimal repro
container run -d --rm --name logempty alpine sh -c 'sleep 10; echo hello'
sleep 0.5
time container logs -f logempty; echo "exit=$?"
container stop logempty

Expected per the code path below: container logs -f returns within a second, prints nothing, exit=0 — even though the container is still running and emits hello 10s later. Expected per the flag's contract ("Follow log output"): block, then stream hello when it appears.

Root cause

container logs reads the whole log file before ever considering follow:

  • The full-file read in tail(): ContainerLogs.swift#L89-L95 — guard let data = try fh.readToEnd() else { return }, with the in-code comment noting the nil case is a zero-byte read.
  • FileHandle.readToEnd() returns nil for a zero-byte file — see read(upToCount:), which maps length == 0 to nil in the open-source mirror of Foundation (FileHandle.swift#L504-L515); the same nil-on-empty behavior on Darwin Foundation is documented by the comment in the linked tail() code.
  • The early return sits before the follow branch (ContainerLogs.swift#L105-L109), so -f never reaches followFile.
  • followFile is already built for an empty start — its first act is seekToEnd() (ContainerLogs.swift#L112-L113) — so nothing downstream prevents following an empty log.
  • The server side just hands back read handles to the on-disk log files (ContainersService.swift#L754-L755), so this is a purely client-side sequencing bug.

Not a duplicate: #1967 and #2022 are about -n line truncation, #2009 is about the on-disk log freezing after an attached client dies. This is about -f not following at all when the file starts empty.

Code evidence is against main at 48f902dc (2026-10-08); the real-world impact was observed on macOS with a current container CLI — the code path above is deterministic for any build containing the linked tail() implementation.

Proposed solution

Make the early return apply only to non-follow mode, so -f falls through to followFile even when the log is empty:

} else if !follow {
    // Fast path if all they want is the full file.
    guard let data = try fh.readToEnd() else {
        return
    }
    guard let str = String(data: data, encoding: .utf8) else {
        throw ContainerizationError(
            .internalError,
            message: "failed to convert container logs to utf8"
        )
    }
    print(str.trimmingCharacters(in: .newlines))
}

fflush(stdout)
if follow {
    setbuf(stdout, nil)
    try await Self.followFile(fh: fh)
}

Why this over the alternatives:

  • CLI-side retry/poll (re-invoke logs -f until the file becomes non-empty) fixes the symptom for one caller but adds a busy-wait to the CLI, does not help consumers of the ContainerClient.logs API who tail the file themselves, and still leaves the flag failing to honor its contract.
  • Server-side (block the containerLogs XPC response until the first byte arrives) would change the semantics of every log retrieval — including non-follow, where an empty log legitimately means "nothing yet" — and adds latency where the on-disk file is already the natural stream source.
  • Document as working-as-intended contradicts the flag's own help text ("Follow log output") and the direction of #1752, which asks for "follow/stream behavior that works reliably for orchestrators reading multiple containers concurrently." Docker's equivalent documents --follow as: "The docker logs --follow command will continue streaming the new output from the container's STDOUT and STDERR" (Docker CLI reference).

Behavioral delta: container logs -f on an empty log will now block and stream instead of exiting 0. Blocking on empty input is the standard -f/tail -f contract, and the follow stream already ends when the log file is truncated or the seek fails (ContainerLogs.swift#L116-L125), so a stopped/removed container still ends the stream.

Acceptance criteria
  • container logs -f on a container whose log file is currently empty blocks, streams lines as they are written, and exits only when the stream ends (repro above: hello appears after 10s)
  • container logs without -f on an empty log is unchanged (no output, exit 0)
  • container logs -f on a non-empty log is unchanged (prints existing content, then follows)
  • Regression test: follow on a zero-byte log file streams subsequent writes (unit or CLITest)
Lingua principale
Swift
Stelle
50.9k
Fork
1.8k
Merge medio
1g 3h
PR unite (30g)
30

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di apple/container

Tutte le issue di apple/container

Issue simili

Altre issue su Swift

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.