fix: correctly wait for journalctl processes to not leave zombies when streaming machine logs (#325)

* fix: call cmd.Wait()

Even though the context is cancelled we still need to call cmd.Wait()
after a cmd.Start() to clean up the child (reap) process. Not doing so
results in a zombie journalctl.

I have manually tested this, as I'm still not sure how to e2e test for
this in a simple manner.

Before:

```
root@uncloud2:~# ps aux|grep jou
root         313  0.0  1.2  42272 23352 ?        S<s  Apr20   0:10 /usr/lib/systemd/systemd-journald
root       15751  0.0  0.0      0     0 pts/0    Z+   07:02   0:00 [journalctl] <defunct>
root       15754  0.0  0.0      0     0 pts/0    Z+   07:02   0:00 [journalctl] <defunct>
```

After:

```
root         313  0.0  1.2  42272 23592 ?        S<s  Apr20   0:10 /usr/lib/systemd/systemd-journald
```

* follow() doesnt need wait

We can keep the wait function more contraint, as follow does not need it

Signed-off-by: Miek Gieben <miek@miek.nl>

---------

Signed-off-by: Miek Gieben <miek@miek.nl>
This commit is contained in:
Miek Gieben
2026-04-22 19:00:15 +10:00
committed by GitHub
parent b1897ce719
commit 6fb68c22c5
2 changed files with 7 additions and 6 deletions
+5 -5
View File
@@ -31,9 +31,9 @@ const journalctl = "journalctl"
var commandContext = exec.CommandContext // allow override for test var commandContext = exec.CommandContext // allow override for test
func logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (io.ReadCloser, error) { func logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (io.ReadCloser, func() error, error) {
if !ValidUnit(unit) { if !ValidUnit(unit) {
return nil, fmt.Errorf("journal logs: invalid unit: %s", unit) return nil, nil, fmt.Errorf("journal logs: invalid unit: %s", unit)
} }
args := []string{"-u", unit, "--no-hostname"} args := []string{"-u", unit, "--no-hostname"}
args = append(args, "-n") args = append(args, "-n")
@@ -61,14 +61,14 @@ func logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (io.Rea
cmd := commandContext(ctx, journalctl, args...) cmd := commandContext(ctx, journalctl, args...)
p, err := cmd.StdoutPipe() p, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return nil, err return nil, nil, err
} }
return p, nil return p, cmd.Wait, nil
} }
// follow synchronously follows the io.Reader, writing each new journal entry to channel. // follow synchronously follows the io.Reader, writing each new journal entry to channel.
+2 -1
View File
@@ -16,7 +16,7 @@ func Logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (<-chan
return nil, fmt.Errorf("journal logs: invalid unit: %s", unit) return nil, fmt.Errorf("journal logs: invalid unit: %s", unit)
} }
reader, err := logs(ctx, unit, opts) reader, wait, err := logs(ctx, unit, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -26,6 +26,7 @@ func Logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (<-chan
go func() { go func() {
defer close(outCh) defer close(outCh)
follow(ctx, reader, outCh) follow(ctx, reader, outCh)
wait()
}() }()
return outCh, nil return outCh, nil