
    .cj<                       U d 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dlZddl	Z	ddl
Z
ddlmZmZ ddlmZ ddlmZmZ ddlmZmZmZ ddlmZ ej        dk    rddlZnddlZd	Zd
ZdZej        dk    Z e            Z dZ!da"dZ#dZ$ e	j%                    Z&i Z'e(e)e*e+e+f         e)e,e)edf         ee-         f         f         e.d<    ej/        e0          Z1 G d de          Z2defdZ3	 dddde-de,de,dee2         fdZ4defdZ5dee*z  defdZ6d ee*z  d!ee*z  de+fd"Z7defd#Z8dd$ee         defd%Z9defd&Z:defd'Z;de*fd(Z<d)Z=d*edee*         fd+Z>d,d-d.e-d/e+ddfd0Z?d1e*de*fd2Z@d3e*d1e*defd4ZAd.e-dee-         fd5ZBd.e-dee-         fd6ZCd.e-dee*         fd7ZDd8e*dz  de*dz  fd9ZEd8e*dz  de+fd:ZFd8e*dz  de+fd;ZGd.e-de+fd<ZHd=e(e*ef         de+fd>ZId?edee*         fd@ZJd8e*d?ede+fdAZKddBd=e(e*ef         d.e-dCee         de+fdDZLde(fdEZMde(e*ef         fdFZNdedee(e*ef                  fdGZOdedHe(e*ef         ddfdIZPdd$ee         dee(         fdJZQddKee         dee(e*ef                  fdLZRd=ee(e*ef                  dee-         fdMZSddNZTdede)e+ee-         ee-         f         fdOZUd$edPe+de)edf         fdQZVd$edRe+ddfdSZWddTZXde+fdUZYd.e-de+fdVZZddWZ[de+fdXZ\ddYZ]ddKee         de+fdZZ^dd[Z_e e e e e e e e e d\	d]ed^ed_ed`edaedbedceddedeeddfdfZ`ddee         dee(e*ef                  fdgZadhZbebfd=ee(e*ef                  die-de+fdjZcd=ee(e*ef                  de+fdkZddlede-fdmZe efdnh          Zgdoe+d]ed`ede+fdpZhdoe+d]ede+fdqZi	 dddBdree(e*ef                  dCee         dee-         fdsZjddtZkdd3e*d1e*duee(e*ef                  de)e+ee(e*ef                  f         fdvZld3e*d1e*ddfdwZmdddxdyee-         dzee-         de-fd{Znd|Zod}Zpd~Zqd}Zrddee         defdZsdefdZtde*die-de+fdZudede*de*die-de+f
dZvde dde-dee         dede+fdZwde+fdZxddee         ddfdZyd=e(e*ef         dee)e-e-ef                  fdZzdye-dze-de*fdZ{dye-dze-de-de,de)e+e+f         f
dZ|d.e-de}fdZ~ddde}de-de,de-fdZdddd=e(e*ef         de-de-dee-         fdZddddye-dze-dede-de-dee-         fdZde-de+fdZde+fdZde+fdZddZ	 dddd$ee         dRe+dee-         fdZ	 dde$dd$ee         dRe+de,dee-         fdZ	 dddd$ee         dRe+de+fdZdS )u  
Gateway runtime status helpers.

Provides PID-file based detection of whether the gateway daemon is running,
used by send_message's check_fn to gate availability in the CLI.

The PID file lives at ``{HERMES_HOME}/gateway.pid``.  HERMES_HOME defaults to
``~/.hermes`` but can be overridden via the environment variable.  This means
separate HERMES_HOME directories naturally get separate PID files — a property
that will be useful when we add named profiles (multiple agents running
concurrently under distinct configurations).
    N)datetimetimezone)Path)get_hermes_home!_get_platform_default_hermes_home)Any
NamedTupleOptionalatomic_json_writewin32hermes-gatewayzgateway_state.jsonzgateway-lockszgateway.lock   g      ?._gateway_running_pid_cachec                   2    e Zd ZU dZeed<   eed<   eed<   dS )	StormInfozResult of a respawn-storm check: how many starts, over what window, and
    the backoff the caller should sleep to break the storm.countwindow_s	backoff_sN)__name__
__module____qualname____doc__int__annotations__float     0/home/ice/.hermes/hermes-agent/gateway/status.pyr   r   5   s<         ? ? JJJOOOr   r   returnc                  $    t                      dz  S )u   Path to the append-only gateway-start ledger used by the respawn-storm
    breaker. Distinct from ``restart_loop.json`` (the auto-resume guard) — no
    collision.zgateway-starts.log)r   r   r   r   _get_starts_log_pathr"   >   s     333r            ^@g     r@)backoff_cap_s
max_startsr   r%   c          
      N   	 t                      }|j                            dd           t          j        t
          j                                                  g }|                                ru|	                    d          
                                D ]L}|                                }|s	 |                    t          |                     =# t          $ r Y Iw xY w|                               fd|D             }t          | dz  d          }|| d         }|                    d	          }	|	                    d
                    d |D                       d
z   d           t'          j        |	|           t+          |          | k    rSt-          |ddt-          t+          |          | z
  d          z  z            }
t/          t+          |          |
          S dS # t0          $ r&}t2                              d|           Y d}~dS d}~ww xY w)a  Record this gateway start and report whether a respawn storm is underway.

    Appends the current UTC timestamp to the starts-log, prunes entries older
    than ``window_s``, and ring-buffers the file so it can't grow unbounded.
    Returns a :class:`StormInfo` when more than ``max_starts`` starts landed in
    the window (with an exponential backoff capped at ``backoff_cap_s``), else
    ``None``.

    Best-effort: any bookkeeping failure is logged and swallowed so a broken
    ledger can never crash gateway startup.
    Tparentsexist_okutf-8encodingc                 &    g | ]}|z
  k    |S r   r   ).0tsnowr   s     r   
<listcomp>z0record_start_and_check_storm.<locals>.<listcomp>g   s'    @@@38x+?+?"+?+?+?r      (   Nz.tmp
c              3   4   K   | ]}t          |          V  d S N)repr)r/   r0   s     r   	<genexpr>z/record_start_and_check_storm.<locals>.<genexpr>p   s(      222d2hh222222r         @      )r   r   r   z8respawn-storm breaker bookkeeping failed (non-fatal): %s)r"   parentmkdirr   r1   r   utc	timestampexists	read_text
splitlinesstripappendr   
ValueErrormaxwith_suffix
write_textjoinosreplacelenminr   	Exceptionloggerdebug)r&   r   r%   pathexistinglinerecentkeepto_writetmpbackoff_er1   s    `          @r   record_start_and_check_stormr[   E   sP   +#%%$666l8<((2244 ";;== 	88CCEE  zz|| OOE$KK0000!   H 	 A@@@@x@@@ :>2&&TEFF#v&&II2222222T9G 	 	
 	
 	
 	
3v;;##sa3s6{{Z/G+K+K&KL G 3v;;WUUUUt   F	
 	
 	
 ttttt	s=   B1G4 6"CG4 
C&#G4 %C&&DG4 4
H$>HH$c                      t           j                            dd                                          } | rt	          |           S t                      S )a  Return the process-level HERMES_HOME, skipping context-local overrides.

    Gateway identity files (PID, lock, runtime status, takeover/stop markers)
    must always live in the directory the gateway process was launched with.
    ``get_hermes_home()`` honors ``_HERMES_HOME_OVERRIDE`` contextvar used for
    per-session profile dispatch, which would route these files into the wrong
    profile directory when a profile-context task happens to be active at write
    time.  See issue #56986.
    HERMES_HOME )rK   environgetrD   r   r   )vals    r   _get_process_hermes_homerb      sE     *..
+
+
1
1
3
3C
 Cyy,...r   rR   c                 l    t          |                                                               d          S )zFReturn a stable absolute HERMES_HOME path for persisted identity data.F)strict)r   
expanduserresolverR   s    r   _canonical_hermes_homerh      s+    ::  ""**%*888r   leftrightc                     t           j                            t          t	          |                               t           j                            t          t	          |                              k    S )zBCompare HERMES_HOME paths with the host platform's case semantics.)rK   rR   normcasestrrh   )ri   rj   s     r   _same_hermes_homern      sZ    7C 6t < <==>>"'BRBR"5))**C C  r   c                  (    t                      } | dz  S )z@Return the path to the gateway PID file, respecting HERMES_HOME.gateway.pid)rb   homes    r   _get_pid_pathrs      s    #%%D-r   pid_pathc                 j    | |                      t                    S t                      }|t          z  S )z1Return the path to the runtime gateway lock file.)	with_name_GATEWAY_LOCK_FILENAMErb   )rt   rr   s     r   _get_gateway_lock_pathrx      s4    !!"8999#%%D(((r   c                  N    t                                          t                    S )z5Return the persisted runtime health/status file path.)rs   rv   _RUNTIME_STATUS_FILEr   r   r   _get_runtime_status_pathr{      s    ??$$%9:::r   c                      t          j        d          } | rt          |           S t          t          j        dt          j                    dz  dz                      }|dz  t          z  S )zBReturn the machine-local directory for token-scoped gateway locks.HERMES_GATEWAY_LOCK_DIRXDG_STATE_HOMEz.localstatehermes)rK   getenvr   rr   _LOCKS_DIRNAME)override
state_homes     r   _get_lock_dirr      s`    y233H H~~bi 0$)++2H72RSSTTJ >11r   c                  b    t          j        t          j                                                  S r7   )r   r1   r   r?   	isoformatr   r   r   _utc_now_isor      s     <%%//111r   g   6Avaluec                 b   t          | t                    rdS t          | t                    r|                                 }|                    d          r|dd         dz   }	 t          j        |          }n# t          $ r Y dS w xY w|j         |	                    t          j                  }|                                S t          | t          t          f          rt          |           }||k    s |t          d          t          d          fv rdS t          j        t          j                                                  }|t"          k     s	||dz   k    rdS 	 t          j        |t          j        	                                          S # t&          t(          t          f$ r Y dS w xY wdS )
u
  Coerce a persisted ``updated_at`` value to an RFC3339 string or ``None``.

    The ``gateway_state.json`` writers all emit RFC3339 via :func:`_utc_now_iso`,
    but the file can also be produced by legacy gateways (which wrote unix
    epoch floats), hand edits, or partial corruption. Every read/emit surface
    (``/api/status``'s ``gateway_updated_at``, the gateway's
    ``/health/detailed`` ``updated_at``) promises consumers ``string | null``
    (see ``web/src/lib/api.ts``), so this funnel enforces that contract:

    - ``str``: accepted iff :meth:`datetime.fromisoformat` parses it (a
      trailing ``Z`` is tolerated). Naive timestamps are coerced to UTC.
      Returns the canonical ``datetime.isoformat()`` rendering.
    - ``int`` / ``float``: treated as unix epoch **seconds** and converted to
      a UTC ISO string. Implausible values — before 2000-01-01, more than a
      day in the future, or non-finite — return ``None``.
    - ``bool``: returns ``None``. Although ``bool`` is an ``int`` subclass,
      ``True``/``False`` as a timestamp is always garbage (epoch 0/1 would be
      rejected by the range guard anyway); rejecting explicitly keeps the
      behaviour documented rather than incidental.
    - anything else (``None``, dict, list, ...): ``None``.
    N)Zzz+00:00)tzinfoinfz-infiQ )tz)
isinstanceboolrm   rD   endswithr   fromisoformatrF   r   rL   r   r?   r   r   r   r1   r@   _EPOCH_MIN_PLAUSIBLEfromtimestampOverflowErrorOSError)r   rawparsedsecondsr1   s        r   normalize_updated_atr      s   , % t% "kkmm<<
## 	&crc(X%C	+C00FF 	 	 	44	= ^^8<^88F!!!%#u&& 
,,gU5\\5==,I!I!I4l8<((2244)))WsU{-B-B4	)'hlCCCMMOOOw
3 	 	 	44	4s$   $A9 9
BB1F F,+F,Fforcepidr   c                   |rt           rddlm} 	 t          j        ddt          |           ddgddd |            	          }n0# t          $ r# t          j        | t          j
                   Y d
S w xY w|j        dk    r6|j        p|j        pd                                }t          |pd|            d
S |st          j
        nt!          t          dt          j
                  }t          j        | |           d
S )zTerminate a PID with platform-appropriate force semantics.

    POSIX uses SIGTERM/SIGKILL. Windows uses taskkill /T /F for true force-kill
    because os.kill(..., SIGTERM) is not equivalent to a tree-killing hard stop.
    r   )windows_hide_flagstaskkillz/PIDz/Tz/FT
   )capture_outputtexttimeoutcreationflagsNr^   ztaskkill failed for PID SIGKILL)_IS_WINDOWShermes_cli._subprocess_compatr   
subprocessrunrm   FileNotFoundErrorrK   killsignalSIGTERM
returncodestderrstdoutrD   r   getattr)r   r   r   resultdetailssigs         r   terminate_pidr      s,       	EDDDDD
	^VSXXtT:#0022  FF ! 	 	 	GC(((FF	 !!};;BBDDG'E%E%E%EFFF %
U&..769fn+U+UCGCs   3A )A21A2identityc                     t          j        |                     d                                                    d d         S )Nr+      )hashlibsha256encode	hexdigest)r   s    r   _scope_hashr     s3    >(//'2233==??DDr   scopec                 J    t                      |  dt          |           dz  S )N-z.lock)r   r   )r   r   s     r   _get_scope_lock_pathr     s*    ??DDH(=(=DDDDDr   c                    t          d|  d          }	 t          |                    d                                          d                   S # t          t
          t          t          t          f$ r Y nw xY w	 ddl	}t          t          |                    |                                           dz                      S # t          $ r Y dS w xY w)	u  Return a stable per-process start-time fingerprint, or None.

    Used as a PID-reuse guard: a ``(pid, start_time)`` pair uniquely identifies
    a process, so a recycled PID (same number, different process) yields a
    different value and is never mistaken for the original.

    On Linux this is field 22 of ``/proc/<pid>/stat`` (start time in clock
    ticks since boot, an int).  On platforms without ``/proc`` (macOS, Windows)
    we fall back to ``psutil.Process(pid).create_time()`` — a float epoch
    timestamp — quantized to an int (centiseconds) for stable equality.

    The two sources are never mixed on a single platform: ``/proc`` always
    succeeds first on Linux, and always fails on macOS/Windows so psutil is
    always used there.  Because the guard only compares the value recorded at
    spawn against the live value *on the same host*, the differing units across
    platforms are irrelevant — only same-source equality matters.
    /proc//statr+   r,      r   Nd   )r   r   rB   splitr   
IndexErrorPermissionErrorrF   r   psutilroundProcesscreate_timerO   )r   	stat_pathr   s      r   _get_process_start_timer     s    $ (c((())I9&&&88>>@@DEEEz?JP   5,,88::S@AABBB   tts$   :A #A65A6:AC 
CCc                      t          |           S )zBPublic wrapper for retrieving a process start time when available.)r   )r   s    r   get_process_start_timer   A  s    "3'''r   c                    t          d|  d          }	 |                                }|r=|                    dd                              dd                                          S n# t
          t          t          f$ r Y nw xY wt          s	 t          j
        dd	t          |           d
dgddd          }|j        dk    r2|j                                        r|j                                        S n# t          t          j        f$ r Y nw xY w	 ddl}|                    |           }|                                }|rd                    |          S n# t&          $ r Y nw xY wdS )zReturn the process command line as a space-separated string.

    On Linux, reads /proc/<pid>/cmdline directly.  On macOS and other
    platforms without /proc, falls back to ``ps -p <pid> -o command=``.
    On Windows (no /proc, no ps), uses psutil.
    r   z/cmdline        r+   ignore)errorsps-p-ozcommand=Tr#   r   r   r   r   N )r   
read_bytesrL   decoderD   r   r   r   r   r   r   rm   r   r   TimeoutExpiredr   r   cmdlinerJ   rO   )r   cmdline_pathr   r   r   proccmdline_partss          r   _read_process_cmdliner   F  s    ....//LW%%''  	W;;w--44WX4NNTTVVV	W 8     
	^tSXXtZ8#	  F  A%%&-*=*=*?*?%}**,,,23 	 	 	D	~~c"" 	+88M***	+    4s7   A) )BBA&C6 6DDAE 
E%$E%commandc                    | sdS 	 t          j        | d          }n$# t          $ r |                                 }Y nw xY wd |D             }|sdS |D ]C}|dk    s|                    d          r dS |                    dd	          d
         }|dv r dS Dd                    |          }d|v pd|v pt          d |D                       }|sdS g }d}|D ]N}|rd}|dv rd}|                    d          s|                    d          r9|                    |           Ot          |          D ]1\  }	}|dk    r|	d	z   t          |          k    r dS ||	d	z            c S dS )ao  Return the Hermes gateway lifecycle subcommand from a command line.

    Lifecycle decisions (is the gateway up? did restart relaunch it?) must not
    fire on loose substring matches.  The previous ``"... gateway" in cmdline``
    test also matched ``hermes_cli.main gateway status`` and even unrelated
    processes like ``python -m tui_gateway`` -- which made ``restart()`` race
    against a still-draining old process and ``status``/``start`` report false
    positives.  This requires the actual ``gateway`` subcommand followed by
    ``run`` (or one of the gateway-dedicated entrypoints), excluding the other
    ``gateway`` management subcommands and any process that merely contains the
    word "gateway".

    Tokenizes quote-aware (``shlex``) so quoted Windows paths with spaces
    (``"C:\Program Files\...\hermes-gateway.exe"``) survive, and strips
    ``--profile``/``-p`` selectors from anywhere in argv -- Hermes's
    ``_apply_profile_override`` removes them before argparse, so the profile
    flag (and a profile literally named ``gateway``) can legally appear on
    either side of the ``gateway`` subcommand.
    NF)posixc                     g | ]=}|                     d                               dd                                          >S )z"'\/)rD   rL   lowerr/   ts     r   r2   z/_gateway_command_subcommand.<locals>.<listcomp>  s>    LLLAaggenn$$T3//5577LLLr   zgateway/run.pyz/gateway/run.pyr   r      r   )r   zhermes-gateway.exer   zhermes_cli.mainzhermes_cli/main.pyc              3   R   K   | ]"}|                     d d          d         dv V  #dS )r   r   r   )r   z
hermes.exeN)rsplitr   s     r   r9   z._gateway_command_subcommand.<locals>.<genexpr>  s;      PPAqxxQ#'??PPPPPPr   )z	--profiler   Tz
--profile=z-p=gateway)shlexr   rF   r   r   rJ   any
startswithrE   	enumeraterM   )
r   
raw_tokenstokenstokenbasenamejoinedhas_gateway_entryfiltered	skip_nextis
             r   _gateway_command_subcommandr  p  s   (  t%[666

 % % %]]__


% MLLLLF t   $$$7H(I(I$55<<Q''+???55 @ XXfFV# 	Q6)	QPPPPPPP 
  t
 HI 	 	 	I'''IL)) 	U-=-=e-D-D 	h''  5Iq5CMM!!55A4s    >>c                 (    t          |           dk    S )zAReturn True only for a real ``gateway run`` process command line.r   r  r   s    r   looks_like_gateway_command_liner    s    &w//588r   c                 $    t          |           dv S )a9  Return True for command lines that can host the gateway runtime.

    ``gateway restart`` is normally a management command, not the gateway
    runtime. On hosts without a service manager, though, the manual restart
    fallback executes ``run_gateway()`` in that same process, so its argv stays
    as ``gateway restart`` while it owns the webhook port and writes runtime
    state. Keep the public ``looks_like_gateway_command_line()`` strict, and
    use this broader matcher only when validating Hermes-owned runtime records
    or no-supervisor cleanup scans.
    >   r   restartr  r  s    r   'looks_like_gateway_runtime_command_liner    s     'w//3EEEr   c                 F    t          |           }|sdS t          |          S )zBReturn True when the live PID still looks like the Hermes gateway.F)r   r  )r   r   s     r   _looks_like_gateway_processr
    s*    #C((G u*7333r   recordc                     |                      d          t          k    rdS |                      d          }t          |t                    r|sdS d                    d |D                       }t          |          S )zMValidate gateway identity from PID-file metadata when cmdline is unavailable.kindFargvr   c              3   4   K   | ]}t          |          V  d S r7   )rm   )r/   parts     r   r9   z-_record_looks_like_gateway.<locals>.<genexpr>  s(      22Ts4yy222222r   )r`   _GATEWAY_KINDr   listrJ   r  )r  r  r   s      r   _record_looks_like_gatewayr    s{    zz&]**u::fDdD!!  uhh22T22222G27;;;r   profile_homec                 4    | j         j        dk    r| j        S dS )ai  Return the profile id a HERMES_HOME directory represents, or None.

    A named profile's home is ``<root>/profiles/<name>`` (immediate parent is
    ``profiles``).  The root/default home (``~/.hermes`` or ``$HERMES_HOME``)
    has no such parent, so it maps to the default profile (``None`` here, which
    callers treat as "the bare, flag-less gateway").
    profilesN)r=   name)r  s    r   _profile_name_for_homer    s#     :--  4r   c                    |                                  }t          |          }t          |                                           }|/|dk    r)|                                 }d| |v pd| |v pd| |v S d|v sd|v rdS d|v r	d| |vrdS dS )	u  Return True when a gateway command line belongs to ``profile_home``.

    Mirrors ``hermes_cli.gateway._matches_current_profile`` so the dashboard's
    cross-profile liveness fallback scopes a live PID to the *right* profile.
    In a per-profile container, one profile's stale ``gateway_state.json`` can
    record a PID that the OS has since recycled onto a DIFFERENT profile's live
    gateway.  That recycled PID's command line still ``looks_like_gateway`` —
    so without a profile check the dead profile is reported running.  A named
    profile gateway carries ``-p <name>``/``--profile <name>`` (or, rarely, an
    explicit ``HERMES_HOME=<path>``) on its argv; the default/root gateway runs
    bare with no profile flag.
    Ndefaultz
--profile z-p zhermes_home=z -p FT)r   r  rm   )r   r  
command_lcprofile_namehome_lc
profile_lcs         r    _command_line_belongs_to_profiler    s     J),77L,%%''GLI$=$=!''))
%%%3 6!Z!!Z/6'g'':5	
 z!!Vz%9%9u##(@w(@(@
(R(Ru4r   expected_homer!  c                    t          |          }|r't          |          sdS |t          ||          sdS dS t          |           S )u6  Return True when a live PID still identifies as this gateway record.

    Prefer the live command line whenever it is readable. Runtime status files
    can outlive the gateway process they describe; if PID reuse leaves the same
    PID occupied by an s6 supervisor/log process, the stale record's argv should
    not make that unrelated process count as a running gateway.

    When ``expected_home`` is provided (the dashboard enumerating a specific
    profile's state file), the readable live command line must additionally
    belong to *that* profile — otherwise a PID recycled onto a different
    profile's live gateway would make the dead profile look alive.  When the
    live command line cannot be read (Windows/permission), fall back to the
    persisted record so cross-platform behavior is preserved.
    FNT)r   r  r  r  )r  r   r!  live_cmdlines       r    _record_matches_live_gateway_pidr$    sc    ( )--L 6|DD 	5$-M-.
 .
$ 5t%f---r   c            
          t          j                    t          t          t          j                  t          t          j                              t          t          t                                          dS )N)r   r  r  
start_timehermes_home)
rK   getpidr  r  sysr  r   rm   rh   rb   r   r   r   _build_pid_recordr*  4  sV    y{{SX-bikk::
 12J2L2LMMNN
 
 
r   c            	      r    t                      } |                     dd ddi t                      d           | S )NstartingFr   )gateway_stateexit_reasonrestart_requestedactive_agents	platforms
updated_at)r*  updater   )payloads    r   _build_runtime_status_recordr5  B  sJ    !!GNN#""nn     Nr   c                 D   |                                  sd S 	 |                     d                                          }n# t          t          f$ r Y d S w xY w|sd S 	 t          j        |          }n# t
          j        $ r Y d S w xY wt          |t                    r|nd S )Nr+   r,   )
rA   rB   rD   r   UnicodeDecodeErrorjsonloadsJSONDecodeErrorr   dict)rR   r   r4  s      r   _read_json_filer<  O  s    ;;== tnngn..4466'(    tt	
  t*S//   tt $//977T9s#   (A AAA3 3BBr4  c                 ,    t          | |d d           d S )N),:)indent
separatorsr   )rR   r4  s     r   _write_json_filerB  b  s    dGDZHHHHHHr   c                    | pt                      } |                                 sd S 	 |                                                                 }n# t          t
          f$ r Y d S w xY w|sd S 	 t          j        |          }n9# t          j        $ r' 	 dt          |          icY S # t          $ r Y Y d S w xY ww xY wt          |t                    rd|iS t          |t                    r|S d S Nr   )rs   rA   rB   rD   r   r7  r8  r9  r:  r   rF   r   r;  )rt   r   r4  s      r   _read_pid_recordrE  f  s1   *=??H?? t  ""((**'(    tt  t*S//   	3s88$$$$ 	 	 	444	 '3  w'4   4sA   &A A$#A$,B B7B$!B7$
B3.B72B33B7	lock_pathc                 <    t          | pt                                S r7   )rE  rx   )rF  s    r   _read_gateway_lock_recordrH    s    IA)?)A)ABBBr   c                 r    | sd S 	 t          | d                   S # t          t          t          f$ r Y d S w xY wrD  )r   KeyError	TypeErrorrF   )r  s    r   _pid_from_recordrL    sO     t6%=!!!i,   tts    66c                  x    t           5  t                                           d d d            d S # 1 swxY w Y   d S r7   )_gateway_running_pid_cache_lockr   clearr   r   r   _clear_running_pid_cacherP    s~    	( + +"((***+ + + + + + + + + + + + + + + + + +s   /33c                 n    	 |                                  }n# t          $ r Y dS w xY wd|j        |j        fS )N)FNNT)statr   st_mtime_nsst_size)rR   sts     r   _file_cache_signaturerV    sK    #YY[[ # # #"""#"."*--s    
%%include_runtime_statusc                    t          |           t          t          |                     g}|r.|                    t          t                                           t	          |          S r7   )rV  rx   rE   r{   tuple)rt   rW  partss      r   _running_pid_cache_signaturer[    sc     	h''4X>>??E  H*+C+E+EFFGGG<<r   cleanup_stalec                    |sdS t                       	 |                     d           n# t          $ r Y nw xY w	 t          |                               d           dS # t          $ r Y dS w xY w)a  Delete a stale gateway PID file (and its sibling lock metadata).

    Called from ``get_running_pid()`` after the runtime lock has already been
    confirmed inactive, so the on-disk metadata is known to belong to a dead
    process.  Unlike ``remove_pid_file()`` (which defensively refuses to delete
    a PID file whose ``pid`` field differs from ``os.getpid()`` to protect
    ``--replace`` handoffs), this path force-unlinks both files so the next
    startup sees a clean slate.
    NT
missing_ok)rP  unlinkrO   rx   rt   r\  s     r   _cleanup_invalid_pid_pathrb    s      4((((   x((//4/@@@@@   s   + 
88#A! !
A/.A/c                 2   |                      d           |                                  t          j        t	                      |            |                                  	 t          j        |                                            d S # t          $ r Y d S w xY w)Nr   )
seektruncater8  dumpr*  flushrK   fsyncfilenor   handles    r   _write_gateway_lock_recordrl    s    
KKNNN
OOI!!6***
LLNNN
!!!!!   s    &B 
BBc                    	 t           r|                     dt          j                   |                                 dk    r)|                     d           |                                  |                     t                     t          j	        | 
                                t          j        d           n>t          j        | 
                                t          j        t          j        z             dS # t           t"          f$ r Y dS w xY w)Nr   r5   r   TF)r   rd  rK   SEEK_ENDtellwriterg  _WINDOWS_LOCK_OFFSETmsvcrtlockingri  LK_NBLCKfcntlflockLOCK_EXLOCK_NBBlockingIOErrorr   rj  s    r   _try_acquire_file_lockrz    s     	HKK2;'''{{}}!!T"""KK,---N6==??FOQ????K)FGGGtW%   uus   C3C7 7DDc                 .   	 ddl }	 |                    t          |                                                     |j        k    rdS n&# t          |dd          $ r Y dS t          $ r Y nw xY wt          |                    t          |                               S # t          $ r Y nw xY wt          r	 ddl}|j        j        }|j        |j        _        |j        |j        _        |j        |j        _        d}d}d}d	}d
}|                    ||z  dt          |                     }	|	s&|                                }
|
|k    rdS |
|k    rdS dS 	 |                    |	d          }||k    |                    |	           S # |                    |	           w xY w# t*          t,          f$ r Y dS w xY w	 t/          dt          |            d                              d                                          }t5          |          dk    r|d         dk    rdS n# t6          $ r 	 t9          j        ddddt=          t          |                     gddd
          }|j        dk    r/|j         !                                "                    d          rY dS n# t          $ r Y nw xY wY ntF          tH          t*          f$ r Y nw xY w	 tK          j&        t          |           d           dS # tN          $ r Y dS tH          $ r Y dS t*          $ r Y dS w xY w)u  Cross-platform "is this PID alive" check that does NOT kill the target.

    CRITICAL on Windows: Python's ``os.kill(pid, 0)`` is NOT a no-op like it
    is on POSIX. CPython's Windows implementation
    (``Modules/posixmodule.c::os_kill_impl``) treats ``sig=0`` as
    ``CTRL_C_EVENT`` because the two values collide at the C level, and
    routes it through ``GenerateConsoleCtrlEvent(0, pid)`` — which sends
    a Ctrl+C to the entire console process group containing the target
    PID, not just the PID itself. Any caller that wanted to "check if
    this PID is alive" via ``os.kill(pid, 0)`` on Windows was silently
    killing that process (and often unrelated processes in the same
    console group). Long-standing Python quirk; see bpo-14484.

    Implementation: prefer :mod:`psutil` (hard dependency — the canonical
    cross-platform answer, maintained by Giampaolo Rodolà, uses
    ``OpenProcess + GetExitCodeProcess`` on Windows internally). Fall back
    to a hand-rolled ctypes ``OpenProcess`` / ``WaitForSingleObject`` pair
    on Windows + ``os.kill(pid, 0)`` on POSIX if psutil is somehow
    unavailable — e.g. stripped-down install or import error during the
    scaffold phase before ``psutil`` is pip-installed.
    r   NFNoSuchProcessr   i   r   i  W   r#   Tr   r   r+   r,   r;   r   r   r   zstate=r   r   )(r   r   r   statusSTATUS_ZOMBIEr   rO   r   
pid_existsImportErrorr   ctypeswindllkernel32c_void_pOpenProcessrestypec_uintWaitForSingleObjectGetLastErrorCloseHandler   AttributeErrorr   rB   r   rM   r   r   r   rm   r   r   rD   r   r   r   rK   r   ProcessLookupError)r   r   r  r  !PROCESS_QUERY_LIMITED_INFORMATIONSYNCHRONIZEWAIT_TIMEOUTERROR_INVALID_PARAMETERERROR_ACCESS_DENIEDrk  errwait_resultstat_fieldsrs                 r   _pid_existsr    s   ,	~~c#hh''..00F4HHHu Iv33 	 	 	55 	 	 	D	F%%c#hh//000    D	MMM}-H ,2?H (39=H(0,2MH!)06-"K%L&(#"#))1K?C F  ++--111 5---4u-&::61EE #l2$$V,,,,$$V,,,,( 	 	 	55		-c#hh---..88'8JJPPRR  ;!##A#(=(=u  	 	 	
N44SXX?#'	   <1$$)9)9)D)DS)I)I$ 55   OW5 	 	 	D			GCHHa   4! 	 	 	55 	 	 	44 	 	 	55	s   B =A B A*B 	A*'B )A**1B 
B)(B)5BF F F -F FF F21F26A%H 
K)A.JK
J(%K'J((K-KK
"K. .
L;	L	LLc                 6   	 t           rN|                     t                     t          j        |                                 t          j        d           d S t          j        |                                 t          j	                   d S # t          $ r Y d S w xY w)Nr   )r   rd  rq  rr  rs  ri  LK_UNLCKru  rv  LOCK_UNr   rj  s    r   _release_file_lockr  T  s     	8KK,---N6==??FOQ?????K77777   s   AB
 1B
 

BBc                     t           dS t                      } | j                            dd           	 t	          | dd          }n^# t
          $ rQ 	 |                                  n# t          $ r Y Y dS w xY w	 t	          | dd          }n# t          $ r Y Y dS w xY wY nw xY wt          |          s|	                                 dS t          |           |a t                       dS )zClaim the cross-process runtime lock for the gateway.

    Unlike the PID file, the lock is owned by the live process itself. If the
    process dies abruptly, the OS releases the lock automatically.
    NTr(   a+r+   r,   F)_gateway_lock_handlerx   r=   r>   openr   r`  r   rz  closerl  rP  )rR   rk  s     r   acquire_gateway_runtime_lockr  _  s2    't!##DKdT222dD7333   	KKMMMM 	 	 	555		$w777FF 	 	 	555	 F "&)) uv&&&!4sY   A 
B#A('B#(
A72B#6A77B#;BB#
BB#BB#"B#c                      t           } | dS da t          |            	 |                                  n# t          $ r Y nw xY wt	                       dS )z<Release the gateway runtime lock when owned by this process.N)r  r  r  r   rP  rj  s    r   release_gateway_runtime_lockr    sm     "F~v   s   3 
A A c                 P   | pt                      }t          |t                      k    rdS |                                sdS 	 t          |dd          }n7# t          $ r* 	 |                                 n# t          $ r Y nw xY wY dS w xY w	 t          |          r8t          |           	 	 |	                                 dS # t          $ r Y dS w xY w	 	 |	                                 dS # t          $ r Y dS w xY w# 	 |	                                 w # t          $ r Y w w xY wxY w)zFReturn True when some process currently owns the gateway runtime lock.NTFr  r+   r,   )
rx   r  rA   r  r   r`  r   rz  r  r  )rF  resolved_lock_pathrk  s      r   is_gateway_runtime_lock_activer    s    #>&<&>&>',>BXBZBZ,Z,Zt$$&& u($AAA 	 	 	
	%%'''' 	 	 	D	uu		!&)) 	v&&&	LLNNNNN 	 	 	DD		 	LLNNNNN 	 	 	DD		LLNNNN 	 	 	D	s   A 
B
!A65B
6
B B
BB
	B
C> .C 
CCC- -
C;:C;>D% DD%
D"D%!D""D%c                  B   t                      } | j                            dd           t          j        t                                }	 t          j        | t          j        t          j	        z  t          j
        z            }n# t          $ r  w xY w	 t          j        |dd          5 }|                    |           ddd           n# 1 swxY w Y   t                       dS # t          $ r* 	 |                     d           n# t"          $ r Y nw xY w w xY w)zWrite the current process PID and metadata to the gateway PID file.

    Uses atomic O_CREAT | O_EXCL creation so that concurrent --replace
    invocations race: exactly one process wins and the rest get
    FileExistsError.
    Tr(   wr+   r,   Nr^  )rs   r=   r>   r8  dumpsr*  rK   r  O_CREATO_EXCLO_WRONLYFileExistsErrorfdopenrp  rP  rO   r`  r   )rR   r  fdfs       r   write_pid_filer    sm    ??DKdT222Z)++,,FWT2:	1BK?@@   	Yr3111 	QGGFOOO	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 """""   	KK4K(((( 	 	 	D	s`   9B BC* -CC* CC* CC* *
D5DD
DDDD)	r-  r.  r/  r0  platformplatform_state
error_codeerror_messageserved_profilesr-  r.  r/  r0  r  r  r  r  r  c        	            t                      }	t          |	          pt                      }
t                      }|
                    di            |d         |
d<   |d         |
d<   |d         |
d<   |d         |
d<   t                      |
d<   | t          ur| |
d<   |t          ur||
d<   |t          urt          |          |
d	<   |t          urt          |          |
d
<   |t          urt          |pg           |
d<   |t          urb|
d         
                    |i           }|t          ur||d<   |t          ur||d<   |t          ur||d<   t                      |d<   ||
d         |<   t          |	|
           dS )zBPersist gateway runtime health information for diagnostics/status.r1  r  r   r  r&  r2  r-  r.  r/  r0  r  r   r  r  N)r{   r<  r5  r*  
setdefaultr   _UNSETr   parse_active_agentsr  r`   rB  )r-  r.  r/  r0  r  r  r  r  r  rR   r4  current_recordplatform_payloads                r   write_runtime_statusr    s    $%%Dd##E'C'E'EG&((N{B'''$V,GFO#E*GEN$V,GFO*<8GL(NNGLF""#0 &  !,&&'+,='>'>#$F""#6}#E#E f$$ &*/*?R%@%@!"v";/33HbAA''(6W%V##-7\*&&0=_-)5&)9X&T7#####r   c                 <    t          | pt                                S )aK  Read the persisted gateway runtime health/status information.

    ``path`` is optional so callers that need to inspect a *different*
    profile's state file (e.g. the dashboard enumerating every profile)
    can do so without mutating ``HERMES_HOME`` in-process.  Defaults to
    the active profile's ``gateway_state.json``.
    )r<  r{   rg   s    r   read_runtime_statusr    s     4=#;#=#=>>>r   x   ttl_sc                 z    t          | t                    sdS t          |                     d          pd|          S )a1  Return True when the runtime-status snapshot is older than ``ttl_s``.

    Delegates to the existing :func:`_marker_is_stale` on the record's
    ``updated_at`` timestamp.  A missing or unparseable timestamp is treated as
    stale (the freshness signal is absent, so it cannot vouch for the record).
    Tr2  r^   )r   r;  _marker_is_staler`   )r  r  s     r   runtime_status_is_staler    s<     fd## tFJJ|44:EBBBr   c                     t          |           }|t          |          sdS | pi                     d          }t          |          }|
|||k    rdS dS )a  Return True when the PID recorded in the snapshot is still alive.

    Uses the existing no-kill :func:`_pid_exists` probe and the same
    ``start_time`` PID-reuse guard as :func:`get_runtime_status_running_pid`:
    when both the recorded and live start-times are known they must match, so a
    recycled PID (same number, different process) is not mistaken for the
    original.  Degrades to ``False`` when the record has no usable PID.
    NFr&  T)rL  r  r`   r   )r  r   recorded_startcurrent_starts       r   runtime_status_pid_is_liver  $  sl     6
"
"C
{+c**{ul''55N+C00M"%^++u4r   r   c                 n    	 t          dt          |                     S # t          t          f$ r Y dS w xY w)u  Coerce a persisted ``active_agents`` value to a clamped non-negative int.

    The shared coercion for the in-flight gateway-turn count. Used on the WRITE
    side (``write_runtime_status``) and by both HTTP read surfaces
    (``/api/status`` and ``/health/detailed``) so the count is clamped to a
    single contract — never negative, never raising on a manually-edited or
    otherwise non-numeric value (degrades to ``0``).
    r   )rG   r   rK  rF   )r   s    r   r  r  ;  sD    1c#hhz"   qqs    44runninggateway_runningc                 x    | sdS |t           vrdS 	 t          |          dk    S # t          t          f$ r Y dS w xY w)u  Whether the gateway is actively processing in-flight turns.

    The contract NAS gates lifecycle actions on.  Busy iff the gateway is live
    (``gateway_running``), in the ``running`` state, AND at least one agent is
    mid-turn (``active_agents > 0``).  Degrades to ``False`` whenever liveness
    is unknown, the state is anything but ``running``, or the count is
    absent/unparseable — i.e. a down or file-absent gateway reads "not busy",
    never a spurious "busy".

    NOTE: liveness keys off ``gateway_running`` (a live PID / health probe),
    NEVER ``updated_at`` — a healthy idle gateway never advances that timestamp.
    Fr   )_DRAINABLE_GATEWAY_STATESr   rK  rF   )r  r-  r0  s      r   derive_gateway_busyr  P  sa      u555u=!!A%%z"   uus   $ 99c                 2    t          |           o|t          v S )u  Whether the gateway can accept a begin-drain request right now.

    True iff the gateway is live and in the ``running`` state — i.e. not already
    draining/stopping/stopped and not in a failed-start state.  This is
    independent of ``active_agents``: an idle running gateway is drainable (the
    drain just completes immediately).  Degrades to ``False`` for a down or
    non-running gateway.
    )r   r  )r  r-  s     r   derive_gateway_drainabler  i  s       O]6O%OOr   runtimec                V   | | nt                      }t          |t                    sdS |                    d          dv rdS t	          |          }|t          |          sdS |                    d          }t          |          }|
|||k    rdS t          |||          r|S dS )a`  Return a live gateway PID from the runtime status record, if valid.

    ``get_running_pid()`` is the primary liveness source because it verifies the
    runtime lock and PID file.  Launch-service managers can still leave us with
    a live process and a fresh ``gateway_state.json`` but no ``gateway.pid``; use
    this as a conservative fallback by checking both the persisted state and the
    OS process identity.

    ``expected_home`` scopes the OS-identity check to a specific profile's
    HERMES_HOME.  Pass it when validating *another* profile's state file (the
    dashboard enumerating every profile): a stale record whose PID the OS has
    recycled onto a different profile's live gateway must not be reported
    running for the dead profile.  Omit it (the default) for the active
    profile, where any live gateway command line is acceptable.
    Nr-  >   Nstoppedstartup_failedr&  r   )r  r   r;  r`   rL  r  r   r$  )r  r!  r4  r   r  r  s         r   get_runtime_status_running_pidr  u  s    ( !,gg2E2G2GGgt$$ t{{?##'JJJt
7
#
#C
{+c**{t[[..N+C00M"%^++t'MRRR 
4r   c                  R   	 t                      } t          |           }|Q	 t          |d                   }n# t          t          t
          f$ r d}Y nw xY w||t          j                    k    rdS |                     d           t                       dS # t          $ r Y dS w xY w)ah  Remove the gateway PID file, but only if it belongs to this process.

    During --replace handoffs, the old process's atexit handler can fire AFTER
    the new process has written its own PID file.  Blindly removing the file
    would delete the new process's record, leaving the gateway running with no
    PID file (invisible to ``get_running_pid()``).
    Nr   Tr^  )rs   r<  r   rJ  rK  rF   rK   r(  r`  rP  rO   )rR   r  file_pids      r   remove_pid_filer    s     && ve}--i4       #BIKK(?(?t$$$ """""   s7   B 8 B AB AB 2$B 
B&%B&metadatac                 .   t          | |          }|j                            dd           i t                      | t	          |          |pi t                      d}t          |          }|<|                                r(	 |                    d           n# t          $ r Y nw xY w|r	 t          |d                   }n# t          t          t          f$ r d}Y nw xY w|t          j                    k    r@|                    d          |                    d          k    rt#          ||           d|fS |du }|s}t%          |          sd}njt'          |          }|                    d          |||                    d          k    rd}|sH|                    d          |1t)          |          s"t+          |          }	|	t-          |          sd}|s9|                    d          $|"t)          |          st+          |          }	|	d}|s	 t/          d| d	          }
|
                                rb|
                    d
                                          D ]9}|                    d          r"|                                d         }|dv rd} n:n# t          t8          f$ r Y nw xY w|rv|                    |j        dz             }	 t          j        ||           	 |                    d           n/# t          $ r Y n#w xY w# t@          $ r Y nt          $ r Y nw xY wd|fS 	 t          j!        |t          j"        t          j#        z  t          j$        z            }n!# tJ          $ r dt          |          fcY S w xY w	 t          j&        |dd
          5 }tO          j(        ||           ddd           n# 1 swxY w Y   n7# tR          $ r* 	 |                    d           n# t          $ r Y nw xY w w xY wdS )zAcquire a machine-local lock keyed by scope + identity.

    Used to prevent multiple local gateways from using the same external identity
    at once (e.g. the same Telegram bot token across different HERMES_HOME dirs).
    Tr(   )r   identity_hashr  r2  Nr^  r   r&  r   z/statusr+   r,   zState:r   >   Tr   z.staleFr  )TN)*r   r=   r>   r*  r   r   r<  rA   r`  r   r   rJ  rK  rF   rK   r(  r`   rB  r  r   r
  r   r  r   rB   rC   r   r   r   rv   r  rL   r   r  r  r  r  r  r  r8  rf  rO   )r   r   r  rF  r  rS   existing_pidstaler  r#  _proc_status_line_state	tombstoner  rk  s                   r   acquire_scoped_lockr    sV    %UH55I4$777


$X..N"nn  F y))HI,,..
	---- 	 	 	D	 \#	 x//LL)Z0 	  	  	 LLL	  29;;&&8<<+E+ET`IaIa+a+aY///>!$ 9	|,, 8 7 E ELL..:%1%l)C)CCC E %!l33;}?T7EE @U $9#F#FL#/7QRZ7[7[/ $ % \22>%17EE 2 $9#F#FL#/ $  
'+,J\,J,J,J'K'K'..00 *)5)?)?)?)Q)Q)\)\)^)^ * *#(#3#3H#=#= !*-2[[]]1-=F'-';';04$)E	!*
 $_5    	# "++INX,EFFI
9i000$$$5555   D %        (?"1WY
RY 6 DEE 1 1 1oi0000001Yr3111 	&VIff%%%	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&   	---- 	 	 	D	 :s   B 
B)(B)0C C"!C"B	J) )J=<J= L 6L 
LL
L5)	L54L5=9M7 7NNO 0OO OO OO 
P)P ?P 
P
PPPc                 f   t          | |          }t          |          }|sdS |                    d          t          j                    k    rdS |                    d          t          t          j                              k    rdS 	 |                    d           dS # t          $ r Y dS w xY w)zDRelease a previously-acquired scope lock when owned by this process.Nr   r&  Tr^  )r   r<  r`   rK   r(  r   r`  r   )r   r   rF  rS   s       r   release_scoped_lockr  B  s    $UH55Iy))H ||Ebikk))||L!!%<RY[[%I%IIID)))))   s   
B" "
B0/B0)	owner_pidowner_start_timer  r  c                    t                      }d}|                                r|                    d          D ]}| t          |          }t	          |t
                    s)	 t          |                    d                    }n# t          t          f$ r Y `w xY w|| k    rk||                    d          |k    r	 |
                    d           |dz  }# t          $ r Y w xY w|S )	a&  Remove scoped lock files in the lock directory.

    Called during --replace to clean up stale locks left by stopped/killed
    gateway processes that did not release their locks gracefully. When an
    ``owner_pid`` is provided, only lock records belonging to that gateway
    process are removed. ``owner_start_time`` further narrows the match to
    protect against PID reuse.

    When no owner is provided, preserves the legacy behavior and removes every
    scoped lock file in the directory.

    Returns the number of lock files removed.
    r   z*.lockNr   r&  Tr^  r   )r   rA   globr<  r   r;  r   r`   rK  rF   r`  r   )r  r  lock_dirremoved	lock_filer  
record_pids          r   release_all_scoped_locksr  R  s'   $ HG !x00 	 	I$(33!&$// !$VZZ%6%6!7!7JJ!:.   H**$0

<004DDD  D 1111   Ns$   $"BBBC
C+*C+z.gateway-takeover.json<   z.gateway-planned-stop.jsonr'  c                 P    | pt                      }t          |          t          z  S )zReturn the path to the --replace takeover marker file.

    ``hermes_home`` is supplied only for a verified cross-home handoff.  The
    target process always consumes the marker from its own process-level home.
    )rb   rh   _TAKEOVER_MARKER_FILENAME)r'  rr   s     r   _get_takeover_marker_pathr    s)     4244D!$''*CCCr   c                  2    t                      } | t          z  S )z<Return the path to the intentional gateway stop marker file.)rb   _PLANNED_STOP_MARKER_FILENAMErq   s    r   _get_planned_stop_marker_pathr    s    #%%D///r   
written_atc                     	 t          j        |           }t          j        t          j                  |z
                                  }||k    S # t          t          f$ r Y dS w xY w)NT)r   r   r1   r   r?   total_secondsrK  rF   )r  r  
written_dtages       r   r  r    sj    +J77
|HL))J6EEGGU{z"   tts   AA A$#A$	pid_fieldstart_time_fieldc                ^   t          |           }|sdS 	 t          ||                   }|                    |          }|                    d          pd}nF# t          t          t
          f$ r, 	 |                     d           n# t          $ r Y nw xY wY dS w xY wt          ||          r*	 |                     d           n# t          $ r Y nw xY wdS t                      }|                    d          }	|	(t          |	t                    rt          |	|          sdS n)|                    d          }
|
t          |
|          sdS t          j                    }t          |          }||k    rd}n|	|||k    }nd}	 |                     d           n# t          $ r Y nw xY w|S )NFr  r^   Tr^  target_hermes_homereplacer_hermes_home)r<  r   r`   rJ  rK  rF   r`  r   r  rb   r   rm   rn   rK   r(  r   )rR   r  r  r  r  
target_pidtarget_start_timer  our_hometarget_homereplacer_homeour_pidour_start_timematchess                 r   _consume_pid_marker_for_selfr    s5    T""F u		*++
"JJ'788ZZ--3

i,   	KK4K(((( 	 	 	D	uu 
E** 	KK4K(((( 	 	 	D	u ())H**122K+s++ 	3D4
 4
 	 5	
 

#9::$->8.
 .
$ 5ikkG,W55N W		&>+E#~5t$$$$    NsZ   AA B/BB
BBBBB.C 
CCF 
F*)F*r  r  r   r  r  c          
         	 t          |pt                                }|t          u rt          |           }| |t	          |          t          j                    t	          t          t                                          t                      d}t          t          |          |           dS # t          t          f$ r Y dS w xY w)aB  Record that ``target_pid`` is being replaced by the current process.

    Captures the target's ``start_time`` so that PID reuse after the
    target exits cannot later match the marker. Also records the
    replacer's PID and a UTC timestamp for TTL-based staleness checks.

    A verified scoped-lock handoff supplies ``target_home`` and the already
    validated ``target_start_time`` so the marker is written into the target
    gateway's HERMES_HOME rather than the replacer's.  Same-home callers omit
    both arguments and preserve the historical behavior.

    Returns True on successful write, False on any failure. Historical
    same-home callers may treat the marker as best effort. Cross-home callers
    must fail closed because the target's supervisor could otherwise revive it
    without recognizing the handoff.
    )r   r  r  replacer_pidr  r  TF)rh   rb   r  r   rm   rK   r(  r   rB  r  r   r   )r   r  r  marker_homer  s        r   write_takeover_markerr    s    ,,5355
 
 && 7
 C C$!2"%k"2"2IKK$'&'?'A'ABB% % '..	
 	
 	2;??HHHt_%   uus   B+B/ /CCc                  J    t          t                      ddt                    S )a  Check & unlink the takeover marker if it names the current process.

    Returns True only when a valid (non-stale) marker names this PID +
    start_time. A returning True indicates the current SIGTERM is a
    planned --replace takeover; the caller should exit 0 instead of
    signalling ``_signal_initiated_shutdown``.

    Always unlinks the marker on match (and on detected staleness) so
    subsequent unrelated signals don't re-trigger.
    r   r  r  r  r  )r  r  _TAKEOVER_MARKER_TTL_Sr   r   r    consume_takeover_marker_for_selfr  *  s-     (!##,$	   r   c                 p    	 t          |                               d           dS # t          $ r Y dS w xY w)zDRemove the takeover marker unconditionally. Safe to call repeatedly.Tr^  N)r  r`  r   )r  s    r   clear_takeover_markerr  =  sN    !+..555FFFFF   s   #' 
55c                    t          | t                    rt          |           sdS 	 t          | d                   }n# t          t
          t          f$ r Y dS w xY w|dk    s|t          j                    k    rdS | 	                    d          }t          |t                    rt          |t                    rdS | 	                    d          }t          |t                    r|                                sdS t          |                                                                          sdS t!          |          }t#          |          sdS t%          |          }|||k    rdS t'          |          }|t)          |          sdS t+          |dz            }t          |t                    rt          |          sdS 	 t          |d                   }n# t          t
          t          f$ r Y dS w xY w||k    s|	                    d          |k    rdS |	                    d          }	t          |	t                    rt-          |	|          sdS |||fS )a  Resolve a live scoped-lock owner to a verified gateway identity.

    A machine-global scoped-lock file is only a claim; it is not sufficient
    authority to terminate a process or choose a marker destination.  Require
    the lock record, the target HERMES_HOME's gateway PID record, and the live
    OS process to agree on PID, start-time fingerprint, gateway identity, and
    process home.  Missing legacy metadata fails closed and leaves the normal
    retryable lock-conflict path in charge.
    Nr   r   r&  r'  rp   )r   r;  r  r   rJ  rK  rF   rK   r(  r`   r   rm   rD   r   re   is_absoluterh   r  r   r   r  r<  rn   )
r  r  r  raw_homer  live_start_timer#  
pid_recordpid_record_pidpid_record_homes
             r   $_validated_scoped_lock_gateway_ownerr  E  s    fd## +Ef+M+M tu&&		i,   ttA~~bikk11tzz,//&,, 
;KT0R0R tzz-((Hh$$ HNN,<,< t>>$$&&2244 t(22Ky!! t-i88O/5E"E"Et(33L(O) ) t }!<==Jj$'' /I*/U/U tZ.//i,   tt""jnn\&B&BFV&V&Vt nn]33Oos++ 3D4 4  t&33s!   > AAG G65G6c                 ^    t          |           sdS t          |           }|dS ||k    rdS dS )zBReturn ``same``, ``exited``, or ``unknown`` for a validated owner.exitedNunknownsame)r  r   )r  r  r  s      r   _scoped_lock_owner_stater     sG    y!! x-i88Oy*** x6r   attemptsdelayc                    t          t          d|                    D ]F}t          | |          }|dk    r dS |dk    r dS t          j        t          d|                     Gdt          | |          dk    fS )	zFReturn ``(exited, safe_to_force)`` after bounded identity-aware waits.r   r  )TFr  )FF        Fr  )rangerG   r   timesleep)r  r  r!  r"  _r   s         r    _wait_for_scoped_lock_owner_exitr)    s     3q(##$$ $ $(4DEEH;;I<<
3sE??####*96FGG6QQQr   c                     t           rg S 	 ddl}|                    t          |                                         d          S # t
          $ r" t                              d| d           g cY S w xY w)u  Best-effort snapshot of ``pid``'s live descendants (POSIX only).

    Must be taken while the old gateway is still alive: once the parent
    exits, its children are reparented (to init or a subreaper) and can no
    longer be discovered by a parent walk.  Returns ``[]`` on Windows —
    ``terminate_pid(force=True)`` there already tree-kills via
    ``taskkill /T`` — and on any error (missing psutil, process already
    gone, access denied).  Never raises.
    r   NT)	recursivez-Could not snapshot children of gateway PID %dexc_info)r   r   r   r   childrenrO   rP   rQ   )r   r   s     r   _snapshot_gateway_childrenr/    s      	~~c#hh''0040@@@   ;S4 	 	
 	
 	
 				s   9A )A10A1r:   r   r.  
parent_pidr   c          
         t           s| sdS d}	 ddl}g }| D ]}	 |                                s|                                |j        k    r6|                                |k    r"t                              d|j        |           p|	                                 |
                    |           # |j        $ r Y t          $ r0 t                              dt          |dd          |d           Y w xY w|sdS |                    |t          d	|          
          \  }}t!          |          }|D ]Y}	 |                                 |dz  }# t          $ r0 t                              dt          |dd          |d           Y Vw xY w|rt                              d||           n-# t          $ r  t                              d|d           Y nw xY w|S )a  Best-effort reap of a dead gateway's orphaned descendants (POSIX).

    Mirrors the Windows ``taskkill /T`` tree-kill for the POSIX ``--replace``
    paths: adapter subprocesses that survive their parent keep holding scoped
    token locks and block the replacement gateway.  Call only AFTER the main
    gateway PID is confirmed dead, with a ``children`` snapshot taken via
    :func:`_snapshot_gateway_children` while it was still alive.

    Safety properties:
    - ``psutil.Process.is_running()`` is identity-aware (PID + create time),
      so a recycled child PID is never signalled.
    - A child whose current ppid still equals ``parent_pid`` is skipped: that
      means the parent is in fact alive (caller raced or was mocked) and the
      child is not an orphan.
    - SIGTERM first, bounded wait, SIGKILL only for survivors.
    - Never raises; returns the number of children signalled.
    r   NzCSkipping child PID %d of old gateway %d: parent still appears alivez2Could not terminate child PID %s of old gateway %dr   ?Tr,  r$  r0  r   z3Could not force-kill child PID %s of old gateway %dz@Reaped %d orphaned child process(es) of replaced gateway PID %d.z-Child reap for replaced gateway PID %d failed)r   r   
is_runningr~  r  ppidrP   rQ   r   	terminaterE   r|  rO   r   
wait_procsrG   rM   r   info)	r.  r1  r   reapedr   livechildgonealives	            r   reap_gateway_childrenr>    s   $  ( qF6
 	 	E'')) <<>>V%999::<<:--LL.	"	   !!!E""""'      HE5#..!	        	1''c#w6G6G'HHeT 
	 
	E	

!   IE5#..!	        	KKR  
  
 
 
;ZRV 	 	
 	
 	
 	
 	

 Ms   
F/ B1F/ B1F/ 9B1F/ )B10F/ 1
C6;F/ =6C63F/ 5C66F/ =;F/ 9EF/ 7F
F/ F!F/ /'GG   graceful_attemptsforce_attemptsrA  rB  c                    t          |           }|dS |\  }}}t          |          }t          |||||          }|t          ||           |S )a  Terminate one verified scoped-lock holder for explicit ``--replace``.

    Returns the original owner PID only after that exact PID/start-time identity
    has exited (or ``terminate_pid`` reports it already gone).  Validation or
    marker-write failure returns ``None`` without signalling anything.  This is
    deliberately stricter than the same-home PID-file replacement path: a
    cross-home handoff must place a consumable marker in the target's home or a
    service supervisor could revive the target and start a flap loop.

    On POSIX, after the owner is confirmed dead, its previously snapshotted
    child processes are reaped best-effort (see :func:`reap_gateway_children`)
    so orphaned adapter subprocesses cannot keep holding token locks.
    Nr@  )r1  )r  r/  !_terminate_scoped_lock_owner_oncer>  )	r  rA  rB  ownerr  r  r  owner_childrenreplaceds	            r   take_over_scoped_lock_holderrH    sz    & 188E}t/4,I
 0	::N0+%  H nCCCCOr   c                   t          | ||          sdS 	 t          | |          }|dk    r| t          |           S |dk    r	 t          |           dS 	 t          | d           nC# t          $ r | cY t          |           S t
          t          f$ r Y t          |           dS w xY wt          | ||d          \  }}|r| t          |           S |s	 t          |           dS 	 t          | d	           nC# t          $ r | cY t          |           S t
          t          f$ r Y t          |           dS w xY wt          | ||d
          \  }}|r| nd	 t          |           S # t          |           w xY w)zFMarker-write + bounded identity-aware termination of a verified owner.r	  Nr  r  Fr   g      ?)r!  r"  Tg      ?)r  r   r  r   r  r   r   r)  )	r  r  r  rA  rB  r   r  safe_to_forcer(  s	            r   rD  rD  4  s\    !*   
 t*+(4DEEHN 	k****M F??J 	k*****G	)511111! 	 	 	@ 	k****?  ) 	 	 	< 	k*****?	 !A&	!
 !
 !
  	* 	k****)  	& 	k*****#	)400000! 	 	 	 	k****  ) 	 	 	 	k*****	 5#	
 
 
	 #,yy, 	k****k****sv   E* E* A( 'E* (B(4E* B(E* 'B((E* E* )C; :E* ;D;E* D;(E* :D;;E* *E;c                     	 t          |           }| |t          j                    t                      d}t	          t                      |           dS # t          t          f$ r Y dS w xY w)a&  Record that ``target_pid`` is being stopped intentionally.

    The gateway exits non-zero for unexpected SIGTERM so service managers can
    revive it. Service stop commands send the same SIGTERM, so the CLI writes
    this short-lived marker first to let the target process exit cleanly.
    )r   r  stopper_pidr  TF)r   rK   r(  r   rB  r  r   r   )r   r  r  s      r   write_planned_stop_markerrM  q  s|    3J??$!29;;&..	
 
 	688&AAAt_%   uus   AA A('A(c                  J    t          t                      ddt                    S )zDReturn True when the current process is being intentionally stopped.r   r  r  )r  r  _PLANNED_STOP_MARKER_TTL_Sr   r   r   $consume_planned_stop_marker_for_selfrP    s+    '%'',(	   r   c                  @   t                      } t          |           }|sdS 	 t          |d                   }|                    d          }|                    d          pd}nF# t          t
          t          f$ r, 	 |                     d           n# t          $ r Y nw xY wY dS w xY wt          |t                    r*	 |                     d           n# t          $ r Y nw xY wdS t          j                    }||k    rdS t          |          }||||k    S dS )ue  Return True only when a live planned-stop marker names the current process.

    This is a **non-destructive** probe used by the watcher thread
    (``gateway/run.py:_run_planned_stop_watcher``) to decide whether to
    trigger shutdown. Unlike :func:`consume_planned_stop_marker_for_self`,
    it never unlinks a marker that matches us — the shutdown handler does
    the authoritative consume on its own thread.

    It *does* clean up markers that can never apply to this process:
    malformed markers and markers older than the TTL are unlinked so a
    stale file left behind by a previous gateway instance cannot wedge
    the new one. Markers naming a different PID/start_time are left in
    place (they may still be consumed legitimately by the process they
    name) but report False here.

    Returns False (without raising) on any read/parse error.
    Fr   r  r  r^   Tr^  )r  r<  r   r`   rJ  rK  rF   r`  r   r  rO  rK   r(  r   )rR   r  r   r  r  r  r  s          r    planned_stop_marker_targets_selfrR    sk   $ )**DT""F u
-..
"JJ':;;ZZ--3

i,   	KK4K(((( 	 	 	D	uu 
$>?? 	KK4K(((( 	 	 	D	uikkGWu -W55N$)C N224sH   AA% %B(=BB(
B!B( B!!B('B(C 
C%$C%c                  n    	 t                                          d           dS # t          $ r Y dS w xY w)z/Remove the planned-stop marker unconditionally.Tr^  N)r  r`  r   r   r   r   clear_planned_stop_markerrT    sL    %''..$.?????   s   "& 
44Tr\  c                   | pt                      }t          |          }t          |          }|s'| t                      }||S t	          ||           dS t          |          }t          |          }||fD ]g}t          |          }	|	t          |	          s$|	                    d          }
t          |	          }|
	|||
k    rSt          ||	          r|	c S ht	          ||           | t                      }||S dS )zReturn the PID of a running gateway instance, or ``None``.

    Checks the PID file and verifies the process is actually alive.
    Cleans up stale PID files automatically.
    NrU  r&  )rs   rx   r  r  rb  rE  rH  rL  r  r`   r   r$  )rt   r\  resolved_pid_pathr  lock_activeruntime_pidprimary_recordfallback_recordr  r   r  r  s               r   get_running_pidr\    sU    !3MOO/0ABB01CDDK 8::K&""!"3=QQQQt%&788N/0BCCO!?3  v&&;3 	L11/44%-*CYgHgHg+FC88 	JJJ	 /}MMMM466"4r   )r\  ttl_secondsr]  c                \   |dk    rt          | |          S | pt                      }| du }t          ||          }t          |          t	          |          |f}t          j                    }t          5  t          	                    |          }|#|\  }	}
}||	z
  |k    r|
|k    r|cddd           S ddd           n# 1 swxY w Y   t          | |          }t          ||          }t          5  t          j                    ||ft          |<   ddd           n# 1 swxY w Y   |S )a  Cached read-side wrapper for dashboard/status polling.

    ``get_running_pid()`` probes the runtime lock by briefly opening and locking
    ``gateway.lock``. That is the right authoritative check for control paths,
    but high-frequency read-only HTTP polling can call it hundreds of times per
    minute. Cache for a short window and invalidate on PID/lock/runtime-status
    file changes so status endpoints do not churn file descriptors while still
    noticing gateway start/stop transitions quickly.
    r   rU  N)rW  )
r\  rs   r[  rm   r   r&  	monotonicrN  r   r`   )rt   r\  r]  rW  rW  	signaturekeyr1   cached	cached_atcached_signature
cached_pidr   refreshed_signatures                 r   get_running_pid_cachedrg    s    ax}EEEE 3MOO%-,5  I  !!4#6#68N
OC
.

C	( " "+//446<3I'Y+--2Bi2O2O!" " " " " " " " " " " " " " " " " " " " " " " (-
@
@
@C65   
) 
 
N+
"3'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 Js$   63CC	C6D!!D%(D%c                (    t          | |          duS )z1Check if the gateway daemon is currently running.rU  N)r\  ra  s     r   is_gateway_runningri  6  s     8=AAAMMr   )r#   r$   r7   )r    N)r   r   r8  loggingrK   r   r   r   r)  	threadingr&  r   r   pathlibr   hermes_constantsr   r   typingr   r	   r
   utilsr   r  rr  ru  r  rz   r   r   objectr  rw   r  rq  &_GATEWAY_RUNNING_PID_CACHE_TTL_SECONDSLockrN  r   r;  rY  rm   r   r   r   r   	getLoggerr   rP   r   r"   r[   rb   rh   rn   rs   rx   r{   r   r   r   r   r   r   r   r   r   r   r  r  r  r
  r  r  r  r$  r*  r5  r<  rB  rE  rH  rL  rP  rV  r[  rb  rl  rz  r  r  r  r  r  r  r  r  _RUNTIME_STATUS_STALE_TTL_Sr  r  r  	frozensetr  r  r  r  r  r  r  r  r  r  r  rO  r  r  r  r  r  r  r  r  r   r)  r  r/  r>  rH  rD  rM  rP  rR  rT  r\  rg  ri  r   r   r   <module>rv     s        				       



      ' ' ' ' ' ' ' '       O O O O O O O O , , , , , , , , , , # # # # # #<7MMMMLLL +  lg%	'   # ), &"0)."2"2 ik DsD$!7ueCQTHoW_`cWd?d9e!ef k k k		8	$	$    
   4d 4 4 4 4 ,19LQ9 9 99#(9DI9i9 9 9 9x/$ / / / / 9 9 9 9 9 9
D3J tcz d     t        ) )Xd^ )t ) ) ) );$ ; ; ; ;
2t 2 2 2 22c 2 2 2 2 # / / / / / /d .3   s d t    BE# E# E E E EE Es Et E E E E   #        F( ( ( ( ( (
's 'x} ' ' ' 'TGt Gd
 G G G GT9S4Z 9D 9 9 9 9
FS4Z FD F F F F4S 4T 4 4 4 4
<tCH~ 
<$ 
< 
< 
< 
<
 
(3- 
 
 
 
"c " "$ " " " "R %)	. . .cN.	. D>	.
 
. . . .@4    
d38n 
 
 
 
:$ :8DcN#; : : : :&I4 I$sCx. IT I I I I x~ $    :C C$ C8DQTVYQYNC[ C C C CXd38n5 (3-    + + + +
. .tXc]HSM/Q)R . . . . ! 38_	         .   d     sS sT s s s sn   "d " " " "J    htn     D   8  # !0$ 0$ 0$0$ 0$ 	0$
 0$ 0$ 0$ 0$ 0$ 0$ 
0$ 0$ 0$ 0$f? ?htn ?c3h8P ? ? ? ?  " 
 -C CT#s(^$CC 
C C C CxS#X'? D    .S S    $ &Iyk22 -0AD	   2	P 	Pc 	Pd 	P 	P 	P 	P )-) %)) ) )d38n%) D>) c]	) ) ) )X   2E Es Ec EXd3PS8n=U Eafgkmuvz{~  AD  |D  wE  nF  hF  bG E E E EPs c d    $  $&** * *}* sm* 		* * * *@ 5   <  D D8D> DT D D D D0t 0 0 0 0 S T    J
J J 	J
 J 
J J J J` #'#	) ) )) $) 	)
 
) ) ) )X$    & x~     =4cN=4eCdN#$=4 =4 =4 =4@ s s    RRR 	R
 R 4:R R R R$C D    . PS L L LD L Lu LWZ L L L Ld  	& & &cN& & 	&
 c]& & & &\  :+ :+ :+:+:+ :+
 :+ :+ c]:+ :+ :+ :+z# $    *d    ;$ ; ; ; ;|     $- - - -tn- - c]	- - - -b  $- ?	- - -tn- - 	-
 c]- - - -b  $N N N NtnN N 
	N N N N N Nr   