
    Pmj?                       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dl
Z
ddlmZmZ ddlmZ ddlmZmZmZmZ ddlmZ ddlmZ dd	lmZ  ej        e          Zd
ZdZdZdZ dZ!dZ"dZ#dZ$ G d d          Z% G d d          Z&efdJdZ'e e!e"eddKd"Z(dLd$Z)dMdNd'Z*dMdNd(Z+ddddd)dOd0Z,ed1dPd4Z-dQd9Z.ddd:dd;d<dRdDZ/eddddEdSdIZ0dS )Tu  Out-of-loop shutdown and event-loop liveness backstops (#66892, #69089).

When the asyncio loop freezes mid-drain, every asyncio-based recovery path is
structurally unable to fire: the drain deadline, status rewrites, and forensics
all need the same loop that is stuck. launchd/systemd KeepAlive only restarts a
*dead* process, so a wedged-but-alive gateway sits as a zombie until manual
SIGKILL.

This module provides:

1. A plain OS-thread shutdown watchdog armed at ``stop()``. If shutdown has not
   completed within ``restart_drain_timeout + grace``, it dumps all-thread
   stacks via ``faulthandler`` plus a metadata snapshot, then ``os._exit`` so
   the service manager can revive the process.
2. An event-loop heartbeat file at ``<HERMES_HOME>/state/gateway.heartbeat`` so
   external supervision can distinguish "process alive" from "loop frozen"
   (``gateway_state.json`` alone can't — it only rewrites on transitions/turns).
3. A lifetime thread watchdog that can still diagnose and hard-exit when the
   event loop is too frozen to run its own heartbeat or timeout callbacks.
4. A self-rescheduling floor timer that keeps the loop selector's timeout
   finite, giving existing async recovery tasks a chance to resume.
    )annotationsN)datetimetimezone)Path)AnyCallableDictOptional)!GATEWAY_SERVICE_RESTART_EXIT_CODE)get_hermes_home)atomic_json_writeg      N@g      >@g      @g      $@   )statezgateway.heartbeat)logszgateway-shutdown-watchdog.logc                  2    e Zd ZdZddZdd	Zdd
ZddZdS )_LoopFloorTimerHandlezBCancelable owner for the currently scheduled selector floor timer.loopasyncio.AbstractEventLoopintervalfloatc                f    || _         || _        d| _        d | _        |                                  d S )NF)_loop	_interval
_cancelled_timer	_schedule)selfr   r   s      ?/home/thesage/.hermes/hermes-agent/gateway/shutdown_watchdog.py__init__z_LoopFloorTimerHandle.__init__;   s4    
!59    returnNonec                Z    | j                             | j        | j                  | _        d S N)r   
call_laterr   _tickr   r   s    r   r   z_LoopFloorTimerHandle._scheduleB   s#    j++DNDJGGr    c                @    | j         s|                                  d S d S r$   )r   r   r'   s    r   r&   z_LoopFloorTimerHandle._tickE   s,     	NN	 	r    c                X    d| _         | j        | j                                         d S d S )NT)r   r   cancelr'   s    r   r*   z_LoopFloorTimerHandle.cancelI   s3    ;"K      #"r    N)r   r   r   r   r!   r"   )__name__
__module____qualname____doc__r   r   r&   r*    r    r   r   r   8   sn        LL   H H H H   ! ! ! ! ! !r    r   c                  4    e Zd ZdZddZdd	ZdddZddZd
S )_LoopLivenessWatchdogHandlez6Small lifecycle handle for the daemon liveness thread.
stop_eventthreading.Eventthreadthreading.Threadc                "    || _         || _        d S r$   )_stop_event_thread)r   r3   r5   s      r   r   z$_LoopLivenessWatchdogHandle.__init__R   s    %r    r!   r"   c                8    | j                                          d S r$   )r8   setr'   s    r   stopz _LoopLivenessWatchdogHandle.stopV   s    r    NtimeoutOptional[float]c                <    | j                             |           d S )Nr=   )r9   join)r   r=   s     r   rA   z _LoopLivenessWatchdogHandle.joinY   s!    '*****r    boolc                4    | j                                         S r$   )r9   is_aliver'   s    r   rD   z$_LoopLivenessWatchdogHandle.is_alive\   s    |$$&&&r    )r3   r4   r5   r6   r+   r$   )r=   r>   r!   r"   )r!   rB   )r,   r-   r.   r/   r   r<   rA   rD   r0   r    r   r2   r2   O   so        @@      + + + + +' ' ' ' ' 'r    r2   r   r   r   r   r!   c                    	 t          |          }|dk    rt          n# t          t          f$ r
 t          }Y nw xY wt	          | |          S )zAKeep at least one timer pending so selector waits remain bounded.r   )r   
ValueError	TypeError#DEFAULT_LOOP_FLOOR_TIMER_INTERVAL_Sr   )r   r   resolved_intervals      r   _arm_loop_floor_timerrJ   `   sk    
@!(OO!! "z" @ @ @?@ '8999s    ::)probe_intervalprobe_timeoutmax_strikes	exit_coderK   rL   rM   intrN   %Optional[_LoopLivenessWatchdogHandle]c               :   	
 |||
t          j                    	d	fdd 	
fd}t          j        |dd	
          }	 |                                 n-# t          $ r  t
                              dd           Y dS w xY wt          	|          S )uS  Start an out-of-loop watchdog that hard-exits after missed probes.

    The guard is on by default; operators opt out with
    ``gateway.loop_watchdog: false`` in config.yaml (enforced by the caller,
    ``GatewayRunner._start_loop_liveness_guards`` — this module stays
    config-agnostic so bare-loop tests can drive it directly).
    probe_eventr4   r!   Optional[bool]c                   t          j                    z   }	                                 rd S |t          j                    z
  }|dk    r|                                 S |                     t	          |d                    rdS m)NTr   g?r@   )time	monotonicis_setwaitmin)rR   deadline	remainingr3   r=   s      r   _wait_for_probez5start_loop_liveness_watchdog.<locals>._wait_for_probe   s    >##g-	  "" t 4>#3#33IA~~"))+++It(<(<== t	r    r"   c                 &   d}                                svt          j                    }	                     |j                   n9# t
          $ r Y d S t          $ r  t                              dd           Y d S w xY w |          }|d S |rd} 	                                rd S | dz  } | 	k     r	                                rd S 	 t          
                    d|            n# t          $ r Y nw xY w	 t          j        d           n,# t          $ r t                              d	d           Y nw xY w	                                rd S 	 dd
lm}  |d           n# t          $ r Y nw xY wt          j                   d S d S )Nr   r@   z.Failed to schedule gateway loop liveness probeTexc_info   zGateway event loop missed %d consecutive liveness probes; dumping all thread stacks and exiting with code %d so the service supervisor can restart it.all_threadsz&Loop liveness faulthandler dump failedmark_exitedloop_liveness_watchdogreason)rX   	threadingEventcall_soon_threadsafer;   RuntimeError	ExceptionloggerdebugrW   criticalfaulthandlerdump_tracebackgateway.lifecycle_ledgerrd   os_exit)
strikesrR   	respondedrd   r\   rN   r   r   r3   strikes_limits
       r   	_watchdogz/start_loop_liveness_watchdog.<locals>._watchdog   sO   //(/33 6	#/++K
))+/::::       Dt     	 (44I    "" qLG&&  "" 	9        V+===== V V VEPTUUUUUV  "" @@@@@@I.FGGGGG   HYFm6	 6	sM   A
 

B %B ?B C- -
C:9C:>D &D=<D=E+ +
E87E8Tzgateway-loop-liveness-watchdogtargetdaemonnamez.Failed to start gateway loop liveness watchdogr^   N)rR   r4   r!   rS   r+   )rh   ri   Threadstartrl   rm   rn   r2   )r   rK   rL   rM   rN   rx   r5   r\   r   r3   rw   r=   s   `   `  @@@@@r   start_loop_liveness_watchdogr   n   s    HGM""J	 	 	 	 	 	 	8 8 8 8 8 8 8 8 8 8 8t -  F
   EPTUUUtt 'z6:::s   A   &B
	B
r   c                     t           j                            dd                                          } | rt	          |           S t                      S )zHHERMES_HOME for process-level identity files (ignore profile overrides).HERMES_HOME )rs   environgetstripr   r   )vals    r   _process_hermes_homer      sC    
*..
+
+
1
1
3
3C
 Cyyr    homeOptional[Path]c                D    | | nt                      } |j        t           S )z1Return ``<HERMES_HOME>/state/gateway.heartbeat``.)r   joinpath_HEARTBEAT_RELATIVEr   bases     r   get_loop_heartbeat_pathr      s'    #44)=)?)?D4=-..r    c                D    | | nt                      } |j        t           S )zBReturn the faulthandler / metadata dump path for a fired watchdog.)r   r   _WATCHDOG_DUMP_RELATIVEr   s     r   get_shutdown_watchdog_dump_pathr      s'    #44)=)?)?D4=122r    )pid
start_timer   extrar   Optional[int]r   r>   r   Optional[Dict[str, Any]]c                   t          |          }t          | | nt          j                              t	          j        t          j                                                  t          j
                    d}|t          |          |d<   	 ddlm}  |            }|r||d<   n# t          $ r Y nw xY w|r|                    |           	 t!          ||d           n,# t          $ r t"                              dd	
           Y nw xY w|S )u   Atomically rewrite the loop-liveness heartbeat file.

    ``start_time`` is the gateway process start (``time.time()`` epoch seconds)
    so supervisors can detect PID reuse. Best-effort — never raises.
    N)r   
updated_atrV   r   r   )sample_memorymem)indentz&Failed to write gateway loop heartbeatTr^   )r   rO   rs   getpidr   nowr   utc	isoformatrU   rV   r   rr   r   rl   updater   rm   rn   )r   r   r   r   pathpayloadr   r   s           r   write_loop_heartbeatr      sC    #4((D#/33ry{{;;l8<00::<<^%% G
  %j 1 1::::::moo 	! GEN    uN$55555 N N N=MMMMMNKs$   B$ $
B10B1C &DD)grace_sdrain_timeoutr   c                   	 t          t          |           d          }n# t          t          f$ r d}Y nw xY w	 t          t          |          d          }n# t          t          f$ r
 t          }Y nw xY w||z   S )z=Return the wall-clock leash for the shutdown watchdog thread.        )maxr   rG   rF   !DEFAULT_SHUTDOWN_WATCHDOG_GRACE_S)r   r   draingraces       r   resolve_shutdown_watchdog_delayr     s    E-((#..z"   2E'NNC((z" 2 2 2125=s     66A A32A3	dump_pathdelay_ssnapshotr"   c                  	 | j                             dd           n# t          $ r Y dS w xY wdt          j                    |t          j        t          j                  	                                |pi d}	 t          | dd          5 }|                    t          j        |t          	          d
z              |                    d           |                                 	 t!          j        |d           n%# t$          $ r |                    d           Y nw xY w|                    d           |                                 ddd           n# 1 swxY w Y   n# t$          $ r Y nw xY w	 t&          j                            d|ddt          j                     d           t&          j                                         t!          j        d           dS # t$          $ r Y dS w xY w)z:Best-effort faulthandler + metadata dump before hard-exit.T)parentsexist_okNshutdown_watchdog_fired)eventr   r   fired_atr   azutf-8)encoding)default
z(--- faulthandler dump (all threads) ---
)filerb   z%(faulthandler.dump_traceback failed)
z--- end dump ---
z&Gateway shutdown watchdog fired after z.0fzs (pid=z); dumping all thread stacks.
ra   )parentmkdirOSErrorrs   r   r   r   r   r   r   openwritejsondumpsstrflushrp   rq   rl   sysstderr)r   r   r   headerfhs        r   _write_watchdog_dumpr   #  sv   td;;;;    +y{{L..88::N F)S7333 		rHHTZ444t;<<<HH@AAAHHJJJC+FFFFF C C CABBBBBCHH)***HHJJJ		 		 		 		 		 		 		 		 		 		 		 		 		 		 		    

AWD A AIKKA A A	
 	
 	
 	
#555555   sz    
--:E% AE(C?>E?D!E D!!,EE% EE%  E!E% %
E21E26A+G# #
G10G1r`   zgateway-shutdown-watchdog)
done_eventsnapshot_fnrN   r   r|   r   Optional[threading.Event]r   &Optional[Callable[[], Dict[str, Any]]]r|   r   r4   c                  ||nt          j                    	 t          t          |           d          n# t          t
          f$ r
 t          Y nw xY wdk    rS dfd}	 t          j        |d|                                           n,# t          $ r t                              d	d
           Y nw xY wS )a  Arm a daemon-thread hard-exit backstop for a wedged shutdown path.

    If ``done_event`` is set before ``delay_s`` elapses, the thread exits
    quietly (normal / progressing shutdown completed). Otherwise it dumps
    diagnostics and calls ``os._exit(exit_code)``.

    Never raises. Returns the ``done_event`` (creating one when omitted) so
    the caller can disarm on successful completion.
    Nr   r   r!   r"   c                    t          j                    
z   } t          j                    | k     rS| t          j                    z
  }                    t          |d                    rd S t          j                    | k     S                                rd S d }4	              }n(# t
          $ r}dt          |          i}Y d }~nd }~ww xY wnt                      }t          |
|           	 t          
                    d
|           n# t
          $ r Y nw xY wt          j        t          j        fD ]'}	 |                                 # t
          $ r Y $w xY w	 ddlm}m}  |              |             n# t
          $ r Y nw xY w	 ddlm}  |d           n# t
          $ r Y nw xY w	 dd	lm}	  |	d
           n# t
          $ r Y nw xY wt-          j                   d S )N      ?r@   snapshot_error)r   r   uh   Shutdown watchdog fired after %.0fs — forcing process exit (asyncio drain path appears wedged; see %s)r   )remove_pid_filerelease_gateway_runtime_lock)drain_log_queuerc   shutdown_watchdogrf   )rU   rV   rX   rY   rW   rl   reprr   r   rm   ro   r   stdoutr   r   gateway.statusr   r   hermes_loggingr   rr   rd   rs   rt   )rZ   r[   r   excrz   streamr   r   r   rd   delaydoner   rN   r   s             r   rx   z(arm_shutdown_watchdog.<locals>._watchdogl  s    >##e+n)) 4>#3#33IyyY!4!4y55  n)) ;;== 	F-1"9&;== 9 9 9,d3ii89 (39X9Z9ZVUXFFFF	OO>	     	 	 	D	 z3:. 	 	F   	TTTTTTTTO((**** 	 	 	D		666666OC((((( 	 	 	D		<<<<<<K	*=>>>>> 	 	 	D	
sr   
B( (
C2CC5D 
DD=E
EE#F   
FFF$ $
F10F15G	 	
GGTry   zFailed to arm shutdown watchdogr^   r+   )rh   ri   r   r   rG   rF   r   r}   r~   rl   rm   rn   )	r   r   r   rN   r   r|   rx   r   r   s	     ```  @@r   arm_shutdown_watchdogr   Q  s   $ $/::Y_5F5FD2E'NNC((z" 2 2 212 zz: : : : : : : : : :xG	$TBBBHHJJJJ G G G6FFFFFGKs!   < AA-)B &C ?C )
interval_sr   r   should_continuer   r   Optional[Callable[[], bool]]c                4  K   	 t          t          |           d          }n# t          t          f$ r
 t          }Y nw xY wt          ||           	 | |            sdS t          j        |           d{V  | |            sdS t          ||           H)u   Rewrite the loop heartbeat file on a cadence until cancelled / gated off.

    Runs as an asyncio task on the gateway loop — if the loop freezes, this
    task stops and the file mtime/updated_at goes stale for external monitors.
    r   )r   r   TN)r   r   rG   rF   DEFAULT_HEARTBEAT_INTERVAL_Sr   asynciosleep)r   r   r   r   r   s        r   loop_heartbeat_foreverr     s      0uZ((#..z" 0 0 0/0
 JT::::?&/@/@&FmH%%%%%%%%%&/@/@&F
>>>>?s   " ==)r   r   r   r   r!   r   )r   r   rK   r   rL   r   rM   rO   rN   rO   r!   rP   )r!   r   r$   )r   r   r!   r   )
r   r   r   r>   r   r   r   r   r!   r   )r   r   r   r   r!   r   )r   r   r   r   r   r   r!   r"   )r   r   r   r   r   r   rN   rO   r   r   r|   r   r!   r4   )
r   r   r   r>   r   r   r   r   r!   r"   )1r/   
__future__r   r   rp   r   loggingrs   r   rh   rU   r   r   pathlibr   typingr   r   r	   r
   gateway.restartr   hermes_constantsr   utilsr   	getLoggerr,   rm   r   r   rH    DEFAULT_LOOP_WATCHDOG_INTERVAL_SDEFAULT_LOOP_WATCHDOG_TIMEOUT_S!DEFAULT_LOOP_WATCHDOG_MAX_STRIKESr   r   r   r2   rJ   r   r   r   r   r   r   r   r   r   r0   r    r   <module>r      s   . # " " " " "        				 



      ' ' ' ' ' ' ' '       0 0 0 0 0 0 0 0 0 0 0 0 = = = = = = , , , , , , # # # # # #		8	$	$ %) !# &) ##'  "& $% !4 C ! ! ! ! ! ! ! !.' ' ' ' ' ' ' '& :: : : : :" =:86c; c; c; c; c; c;L   / / / / /3 3 3 3 3 "&&*' ' ' ' ' 'Z 7     "+ + + +b -1:> $+[ [ [ [ [ [@ 5"&48? ? ? ? ? ? ? ?r    