#!/usr/bin/env python3
"""Run a command fully detached, so it survives this session ending.

WHY THIS EXISTS

`skills/garden-days/references/failure-modes.md` already records the symptom:
"harness teardown kills background tasks; nohup+disown survives most but not
all". This is the "not all" half, and the cost is worse than a lost render.

A killed Veo render leaves its batch at `running` with its job also `running`.
`resume` looks for PENDING jobs, finds none, and reports "already completed" —
success on stdout, no film. Two renders were killed mid-flight in one session;
the first left one wedged batch, the second left four. (`vclaw veo unwedge`
now clears them, but not producing the wedge is better.)

WHY nohup + disown IS NOT ENOUGH

`nohup` only ignores SIGHUP, and `disown` only removes the job from the
shell's table. Neither changes the child's PROCESS GROUP, so a harness that
tears down by signalling the whole group — which is the usual way — takes the
child with it regardless.

`setsid()` puts the child in a new session with a new process group and no
controlling terminal, so a group-directed signal cannot reach it. macOS has no
`setsid` binary, so it has to be done in-process, and it only works in a child
that is not already a process-group leader — hence the first fork.

The second fork is what orphans the grandchild: its parent exits immediately,
it is re-parented to init, and nothing in the original session holds a handle
to it. Verified: a render launched this way completed on its own while a
watcher process around it was killed.

USAGE
    detach.py <logfile> <command> [args...]
    detach.py --pidfile <path> <logfile> <command> [args...]

Prints the detached PID and exits 0 immediately. Everything the command writes
to stdout and stderr goes to <logfile> (appended, line-buffered by the OS).
Use the pidfile to check liveness later: `kill -0 $(cat <path>)`.
"""
from __future__ import annotations

import os
import sys


def main(argv: list[str]) -> int:
    pidfile: str | None = None
    if argv and argv[0] == "--pidfile":
        if len(argv) < 2:
            sys.stderr.write("detach.py: --pidfile needs a path\n")
            return 2
        pidfile = argv[1]
        argv = argv[2:]

    if len(argv) < 2:
        sys.stderr.write(
            "usage: detach.py [--pidfile <path>] <logfile> <command> [args...]\n"
        )
        return 2

    log, cmd = argv[0], argv[1:]

    # Fail before forking rather than in a grandchild nobody is watching: once
    # detached, an exec failure is invisible except as an empty log.
    try:
        log_fd = os.open(log, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
    except OSError as error:
        sys.stderr.write(f"detach.py: cannot open log {log}: {error}\n")
        return 2
    os.close(log_fd)

    read_fd, write_fd = os.pipe()  # grandchild reports its pid back up

    first = os.fork()
    if first > 0:
        os.close(write_fd)
        os.waitpid(first, 0)  # reap the intermediate child, never the grandchild
        with os.fdopen(read_fd) as pipe:
            pid = pipe.read().strip()
        if pid:
            if pidfile:
                try:
                    with open(pidfile, "w", encoding="utf-8") as handle:
                        handle.write(f"{pid}\n")
                except OSError as error:
                    sys.stderr.write(f"detach.py: could not write pidfile: {error}\n")
            print(pid)
            return 0
        sys.stderr.write("detach.py: child did not report a pid\n")
        return 1

    # --- intermediate child ---------------------------------------------------
    os.close(read_fd)
    os.setsid()  # new session, new process group, no controlling terminal

    second = os.fork()
    if second > 0:
        os.write(write_fd, str(second).encode())
        os.close(write_fd)
        os._exit(0)  # orphan the grandchild; init adopts it

    # --- grandchild -----------------------------------------------------------
    os.close(write_fd)
    fd = os.open(log, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
    os.dup2(fd, 1)
    os.dup2(fd, 2)
    if fd > 2:
        os.close(fd)
    devnull = os.open(os.devnull, os.O_RDONLY)
    os.dup2(devnull, 0)  # never inherit a terminal stdin — a read would block forever
    if devnull > 2:
        os.close(devnull)

    try:
        os.execvp(cmd[0], cmd)
    except OSError as error:
        os.write(2, f"detach.py: cannot exec {cmd[0]}: {error}\n".encode())
        os._exit(127)
    return 0  # unreachable


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
