Skip to content

boo kill leaves orphaned grandchildren alive (signals shell only, not process group) #97

Description

@bermudi

Summary

boo kill (and boo kill --all) only signals the PTY's direct child — the shell — with SIGHUP. Grandchild processes that ignore/block SIGHUP, were backgrounded/daemonized, or escaped the shell's job table survive the kill and keep running with the session gone. They hold onto ports, files, etc., and a subsequent session started to replace them fails (e.g. Address already in use).

Mechanism

boo kill sends the quit control command, handled in src/daemon.zig:

} else if (std.mem.eql(u8, cmd, "quit")) {
    self.retireListener();
    conn.send(.ok, "");
    if (self.win) |w| {
        posix.kill(w.child_pid, posix.SIG.HUP) catch {};   // <-- positive pid: shell only
    }
    ...

child_pid is the shell. A positive pid to kill(2) reaches exactly that one process. SIGHUP normally cascades to the shell's jobs via huponexit, so kill appears to work — until a job ignores SIGHUP or has re-parented/re-pgrp'd out, which is exactly what long-running servers and daemons tend to do. The session daemon exits, the listener socket is removed, but the orphaned grandchild keeps its resources.

The PTY child does call setsid() (src/pty.zig), so it is a session/process-group leader and child_pid == pgid.

Reproduction

  1. Start a session and run a SIGHUP-ignoring server bound to a fixed port:
    boo new srv
    # inside the session:
    python3 -c 'import signal,socket; signal.signal(signal.SIGHUP, signal.SIG_IGN); \
      s=socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1); \
      s.bind(("127.0.0.1",8000)); s.listen(1); print("up"); signal.pause()'
    
  2. From another terminal: boo kill srv
  3. Observe the python process is still alive and still holds 127.0.0.1:8000:
    ss -ltnp | grep :8000
    ps -p $(pgrep -f 'signal.pause') -o pid,ppid,cmd
    
    Its PPID is now 1 (reparented to init), confirming it escaped the session.

Expected: the whole process tree started by the session is torn down.
Actual: only the shell dies; the orphaned server survives.

Suggested fix

Signal the process group rather than the single pid:

posix.kill(-w.child_pid, posix.SIG.HUP) catch {};   // -pid == whole pgrp

Since child_pid == pgid (child runs setsid()), -child_pid targets every process in the session's group. This closes the common case (grandchildren still in the shell's group).

Honest limitation

A pgrp signal still cannot reach grandchildren that:

  • were placed in their own process group by the shell's job control,
  • called setsid() themselves / double-forked into a new session,
  • were started via nohup/setsid/disown.

For a true full-tree kill, boo would need to enumerate descendants (/proc/*/stat PPids on Linux, proc_listchildpids on macOS) and signal each pid, escalating TERM → KILL. That is more code and two-platform, but it's the only thing that catches a daemonized grandchild. Worth deciding whether boo promises full-tree teardown or documents the same caveat tmux/screen live with.

Environment

  • boo v0.6.3 (main @ 4af4bdf)
  • Linux (repro), mechanism is platform-independent

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions