Skip to content

Commit 0ac25a7

Browse files
authored
fix: reap child processes on session stop/kill to prevent zombies (#8)
captureOutput goroutine could exit via the done channel without calling cmd.Wait(), leaving terminated child processes as zombies in the process table. Moved cmd.Wait() and p.Close() into a defer so the child is always reaped regardless of exit path.
1 parent bbd4c20 commit 0ac25a7

File tree

1 file changed

+25
-23
lines changed

1 file changed

+25
-23
lines changed

internal/daemon/server.go

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,30 @@ func (s *Server) captureOutput(name string, h *sessionHandle) {
477477

478478
f := p.File()
479479

480+
defer func() {
481+
cmd.Wait()
482+
p.Close()
483+
484+
s.mu.Lock()
485+
defer s.mu.Unlock()
486+
487+
h.pty = nil
488+
h.cmd = nil
489+
h.done = nil
490+
h.frameDetector = nil
491+
h.responder = nil
492+
493+
h.state = StateStopped
494+
now := time.Now()
495+
h.stoppedAt = &now
496+
497+
if meta, err := s.storage.LoadMeta(name); err == nil {
498+
meta.State = StateStopped
499+
meta.StoppedAt = &now
500+
s.storage.SaveMeta(name, meta)
501+
}
502+
}()
503+
480504
if detector != nil {
481505
defer func() {
482506
if pending := detector.Flush(); len(pending) > 0 {
@@ -513,31 +537,9 @@ func (s *Server) captureOutput(name string, h *sessionHandle) {
513537
}
514538
}
515539
if err != nil && !isTimeout(err) {
516-
break
540+
return
517541
}
518542
}
519-
520-
cmd.Wait()
521-
p.Close()
522-
523-
s.mu.Lock()
524-
defer s.mu.Unlock()
525-
526-
h.pty = nil
527-
h.cmd = nil
528-
h.done = nil
529-
h.frameDetector = nil
530-
h.responder = nil
531-
532-
h.state = StateStopped
533-
now := time.Now()
534-
h.stoppedAt = &now
535-
536-
if meta, err := s.storage.LoadMeta(name); err == nil {
537-
meta.State = StateStopped
538-
meta.StoppedAt = &now
539-
s.storage.SaveMeta(name, meta)
540-
}
541543
}
542544

543545
func isTimeout(err error) bool {

0 commit comments

Comments
 (0)