
    `gj#                        d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	dZ
dZej                  fddZddZddZddd	Zed
k(  r ej$                   e              yy)u  Parent-death watchdog supervisor for stdio MCP subprocesses.

Problem this fixes (#TBD): a stdio MCP server (e.g. ``npx -y mcp-remote
<url>``) is spawned as a direct child of the Hermes process. Hermes's own
teardown path (``MCPServerTask.shutdown()`` / ``_kill_orphaned_mcp_children``
at final exit) reaps it cleanly on a *graceful* exit. But if the spawning
Hermes process dies hard — ``kill -9``, an OS-level crash, a force-quit of
the TUI/desktop app — that teardown code never runs, and the child (plus any
of its own descendants, e.g. mcp-remote's spawned ``node`` process) is
orphaned. macOS has no direct equivalent of Linux's
``prctl(PR_SET_PDEATHSIG)`` to make the kernel auto-kill a child when its
parent dies, so nothing reaps these until the next Hermes startup's opt-in
``_kill_orphaned_mcp_children()`` sweep — which only runs if something calls
it. Repeated ungraceful session restarts can pile up N orphaned processes,
all racing to hold the same upstream SSE session, producing errors like
"Invalid request parameters" / "Received request before initialization was
complete" on the *legitimate* new connection.

Fix: don't spawn the MCP server command directly. Spawn this supervisor
instead, which:
  1. execs the real command as its own child (own process group via
     ``start_new_session``, so it doesn't inherit the supervisor's
     controlling terminal weirdly and so we can killpg it cleanly);
  2. transparently passes stdin/stdout/stderr through — the MCP stdio
     protocol talks directly over those pipes, so the supervisor must be a
     no-op relay, not a bytes-in-the-middle proxy;
  3. runs a background thread that polls the direct POSIX parent identity:
     compare current ``getppid()`` against the parent PID recorded when the
     wrapper was created;
  4. the instant the original parent is gone, terminates the real child's
     process group (SIGTERM, grace period, then SIGKILL) and exits.

This is intentionally a thin, standard-library-only script so it starts fast
and can't itself become a resource leak.

Usage (see ``tools/mcp_tool.py::_run_stdio``)::

    python3 -m tools.mcp_stdio_watchdog \
        --ppid <original_parent_pid> -- <real_command> <arg1> <arg2> ...
    )annotationsNg       @g      @c                     |       | k7  S )zDReturn whether this process no longer has its original POSIX parent. )original_ppidgetppids     K/root/.hermes/venv/lib/python3.12/site-packages/tools/mcp_stdio_watchdog.py_is_orphanedr	   9   s    9%%    c                Z   t        t        dd      }|(	 | j                          | j                  t               y	 t        j                  | j                        }t        t        dt        j                        }t        j                  |fD ]$  }	  |||       	 | j                  t                y y# t
        t        j                  f$ r | j                          Y yw xY w# t        t
        f$ r Y yw xY w# t        t        t
        f$ r Y  yw xY w# t        j                  $ r Y w xY w)aK  Best-effort SIGTERM-then-SIGKILL of the child's process group.

    This module only ever runs on POSIX (the wrap site in tools/mcp_tool.py
    gates on ``os.name == "posix"``), but guard the POSIX-only primitives
    anyway so an accidental Windows import/execute degrades to a plain
    child kill instead of AttributeError.
    killpgN)timeoutSIGKILL)getattros	terminatewait_TERM_GRACE_SOSError
subprocessTimeoutExpiredkillgetpgidpidProcessLookupErrorsignalSIGTERMPermissionError)procr   pgidsigkillsigs        r   _terminate_process_groupr"   >   s    R4(F~	NNIImI, 	zz$((# fi8G( 		4	IImI,	 223 	IIK	
 (  #OW= 		
 (( 		sF   &B5 C$ 	C9D5)C! C!$C65C69DDD*)D*c                    | j                         Bt        |      rt        |        y t        j                  t
               | j                         Ay y N)pollr	   r"   timesleep_POLL_INTERVAL_S)r   r   s     r   _watchdog_loopr)   _   s<    
))+
&$T*

#$	 ))+
r
   c                0   t        j                  d      }|j                  dt        d       |j                  dt         j                         |j                  |       }t        |j                        }|r|d   d	k(  r|d
d  }|st        dt        j                         yt        j                  |t        j                  t        j                  t        j                  d      fd}t        j                  t        j                   |       t        j                  t        j"                  |       t%        j&                  t(        |j*                  fd      }|j-                          	 j/                         S # t0        $ r t3               Y yw xY w)Nz1Parent-death watchdog for a stdio MCP subprocess.)descriptionz--ppidT)typerequiredcommand)nargsr   z--   z/mcp_stdio_watchdog: no command given after '--')file   )stdinstdoutstderrstart_new_sessionc                L    t               t        j                  d| z          y )N   )r"   sysexit)signumframer   s     r   _forward_shutdownzmain.<locals>._forward_shutdown   s     &vr
   )targetargsdaemon   )argparseArgumentParseradd_argumentint	REMAINDER
parse_argslistr.   printr9   r5   r   Popenr3   r4   r   r   SIGINT	threadingThreadr)   ppidstartr   KeyboardInterruptr"   )argvparserr?   	real_argvr=   watchdogr   s         @r   mainrU   g   sE   $$GF sT:
	););<T"DT\\"IYq\T)abM	?cjjQ
 iizzzzD MM&.."34
MM&--!23DIIH
 NNyy{  &s   .E> >FF__main__)r   rE   returnbool)r   subprocess.PopenrW   None)r   rY   r   rE   rW   rZ   r$   )rQ   zlist[str] | NonerW   rE   )__doc__
__future__r   rB   r   r   r   r9   rL   r&   r(   r   r   r	   r"   r)   rU   __name__r:   r   r
   r   <module>r^      sn   'R #  	   
    .0ZZ &
B%2j zCHHTV r
   