
    |+j*                        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ZddlmZmZ 	 ddlZe	j                            d           Zn# e$ r dZdZY nw xY wddgZd	Zd
ZdZddZ G d de          Z G d d          ZdS )u  PTY bridge for `hermes dashboard` chat tab.

Wraps a child process behind a pseudo-terminal so its ANSI output can be
streamed to a browser-side terminal emulator (xterm.js) and typed
keystrokes can be fed back in.  The only caller today is the
``/api/pty`` WebSocket endpoint in ``hermes_cli.web_server``.

Design constraints:

* **POSIX-only.**  This module depends on ``fcntl``, ``termios``, and
  ``ptyprocess``, none of which exist on native Windows Python.  Native
  Windows ConPTY is a different API (Windows 10 build 17763+) and would
  need a separate Windows implementation (``pywinpty``) — that's tracked
  as a future enhancement.  On native Windows, importing this module
  raises :class:`ImportError` and the dashboard's ``/chat`` tab shows a
  WSL-recommended banner instead of crashing.  Every other feature in the
  dashboard (sessions, jobs, metrics, config editor) works natively.
* **Zero Node dependency on the server side.**  We use :mod:`ptyprocess`,
  which is a pure-Python wrapper around the OS calls.  The browser talks
  to the same ``hermes --tui`` binary it would launch from the CLI, so
  every TUI feature (slash popover, model picker, tool rows, markdown,
  skin engine, clarify/sudo/approval prompts) ships automatically.
* **Byte-safe I/O.**  Reads and writes go through the PTY master fd
  directly — we avoid :class:`ptyprocess.PtyProcessUnicode` because
  streaming ANSI is inherently byte-oriented and UTF-8 boundaries may land
  mid-read.
    )annotationsN)OptionalSequencewinF	PtyBridgePtyUnavailableError   i  i  valueintmaximumreturnc                    	 t          |           }n$# t          t          t          f$ r
 t          cY S w xY w|t          k     rt          S ||k    r|S |S )zClamp a reported terminal dimension into ``[_MIN_DIMENSION, maximum]``.

    Non-integer / non-finite values fall back to ``_MIN_DIMENSION`` so a bad
    probe can never reach ``struct.pack`` and raise ``struct.error``.
    )r   	TypeError
ValueErrorOverflowError_MIN_DIMENSION)r
   r   ns      4/usr/local/lib/hermes-agent/hermes_cli/pty_bridge.py_clamp_dimensionr   ?   sf    JJz=1   >7{{Hs    33c                      e Zd ZdZdS )r   zRaised when a PTY cannot be created on this platform.

    Today this means native Windows (no ConPTY bindings) or a dev
    environment missing the ``ptyprocess`` dependency.  The dashboard
    surfaces the message to the user as a chat-tab banner.
    N)__name__
__module____qualname____doc__     r   r   r   P   s           r   c                      e Zd ZdZd&dZed'd            Zeddd	d
dd(d            Zed)d            Z	d'dZ
d*d+dZd,d!Zd-d"Zd.d#Zd/d$Zd.d%ZdS )0r   u  Thin wrapper around ``ptyprocess.PtyProcess`` for byte streaming.

    Not thread-safe.  A single bridge is owned by the WebSocket handler
    that spawned it; the reader runs in an executor thread while writes
    happen on the event-loop thread.  Both sides are OK because the
    kernel PTY is the actual synchronization point — we never call
    :mod:`ptyprocess` methods concurrently, we only call ``os.read`` and
    ``os.write`` on the master fd, which is safe.
    proc'ptyprocess.PtyProcess'c                :    || _         |j        | _        d| _        d S NF)_procfd_fd_closed)selfr   s     r   __init__zPtyBridge.__init__d   s    
r   r   boolc                *    t          t                    S )z.True if a PTY can be spawned on this platform.)r(   _PTY_AVAILABLE)clss    r   is_availablezPtyBridge.is_availablek   s     N###r   NP      )cwdenvcolsrowsargvSequence[str]r/   Optional[str]r0   Optional[dict]r1   r   r2   'PtyBridge'c                  t           sSt          j                            d          rt	          d          t
          t	          d          t	          d          |t          j                                        n|                                }|	                    d          sd|d<   t
          j
                            t          |          ||||f          } | |          S )	a  Spawn ``argv`` behind a new PTY and return a bridge.

        Raises :class:`PtyUnavailableError` if the platform can't host a
        PTY.  Raises :class:`FileNotFoundError` or :class:`OSError` for
        ordinary exec failures (missing binary, bad cwd, etc.).
        r   z^Pseudo-terminals are unavailable on this platform. Hermes Agent supports Windows only via WSL.NzgThe `ptyprocess` package is missing. Install with: pip install ptyprocess (or pip install -e '.[pty]').z!Pseudo-terminals are unavailable.TERMzxterm-256color)r/   r0   
dimensions)r*   sysplatform
startswithr   
ptyprocessosenvironcopyget
PtyProcessspawnlist)r+   r3   r/   r0   r1   r2   	spawn_envr   s           r   rD   zPtyBridge.spawnp   s       	K|&&u-- )B   !)4  
 &&IJJJ +.+RZ__&&&388::	}}V$$ 	1 0If$**JJd|	 + 
 
 s4yyr   c                4    t          | j        j                  S N)r   r"   pidr&   s    r   rI   zPtyBridge.pid   s    4:>"""r   c                    | j         rdS 	 t          | j                                                  S # t          $ r Y dS w xY wr!   )r%   r(   r"   isalive	ExceptionrJ   s    r   is_alivezPtyBridge.is_alive   sT    < 	5	
**,,--- 	 	 	55	s   %1 
??皙?timeoutfloatOptional[bytes]c                F   | j         rdS 	 t          j        | j        gg g |          \  }}}n# t          t          f$ r Y dS w xY w|sdS 	 t          j        | j        d          }n8# t          $ r+}|j        t          j        t          j	        hv rY d}~dS  d}~ww xY w|sdS |S )u  Read up to 64 KiB of raw bytes from the PTY master.

        Returns:
            * bytes — zero or more bytes of child output
            * empty bytes (``b""``) — no data available within ``timeout``
            * None — child has exited and the master fd is at EOF

        Never blocks longer than ``timeout`` seconds.  Safe to call after
        :meth:`close`; returns ``None`` in that case.
        Nr   i   )
r%   selectr$   OSErrorr   r?   readerrnoEIOEBADF)r&   rP   readable_dataexcs         r   rV   zPtyBridge.read   s     < 	4	#]DH:r2wGGNHa$ 	 	 	44	 	3	748U++DD 	 	 	yUY444ttttt		
  	4s-   !- AA
A% %
B/BBBr\   bytesNonec                (   | j         s|sdS t          |          }|ru	 t          j        | j        |          }nC# t
          $ r6}|j        t          j        t          j        t          j	        hv rY d}~dS  d}~ww xY w|dk    rdS ||d         }|sdS dS )z;Write raw bytes to the PTY master (i.e. the child's stdin).Nr   )
r%   
memoryviewr?   writer$   rU   rW   rX   rY   EPIPE)r&   r\   viewr   r]   s        r   rb   zPtyBridge.write   s    < 	t 	F$ 		HTXt,,   9EK EEEFFFFF Avv8D  		 		 		 		 		s   9 
A9*A43A44A9c                
   | j         rdS t          |t                    }t          |t                    }t	          j        d||dd          }	 t          j        | j        t          j
        |           dS # t          $ r Y dS w xY w)u  Forward a terminal resize to the child via ``TIOCSWINSZ``.

        Dimensions are clamped to a sane range first.  Some hosts report
        garbage window sizes — the motivating case is WSL2, where xterm.js
        in the dashboard ``/chat`` tab can pick up ``columns=131072,
        rows=1`` from a broken winsize probe.  ``struct winsize`` packs each
        field as an unsigned short (max 65535), so an unclamped 131072 would
        raise ``struct.error`` (not ``OSError``) and break the resize path,
        leaving the TUI laid out for a one-row / absurdly-wide screen —
        which is what shows up as blank / disappearing text.
        NHHHHr   )r%   r   	_MAX_COLS	_MAX_ROWSstructpackfcntlioctlr$   termios
TIOCSWINSZrU   )r&   r1   r2   winsizes       r   resizezPtyBridge.resize   s     < 	Fi00i00+fdD!Q77	K'"4g>>>>> 	 	 	DD	s   %A4 4
BBc                   | j         rdS d| _         	 t          j        | j        j                  }n# t
          $ r d}Y nw xY wt          j        t          j        t          j	        fD ]}| j        
                                s n	 |t          j        ||           n| j                            |           n# t
          $ r Y nw xY wt          j                    dz   }| j        
                                r[t          j                    |k     rDt          j        d           | j        
                                rt          j                    |k     D	 | j                            d           dS # t
          $ r Y dS w xY w)u   Terminate the child (SIGTERM → 0.5s grace → SIGKILL) and close fds.

        Idempotent.  Reaping the child is important so we don't leak
        zombies across the lifetime of the dashboard process.
        NTg      ?g{Gz?)force)r%   r?   getpgidr"   rI   rM   signalSIGHUPSIGTERMSIGKILLrL   killpgkilltime	monotonicsleepclose)r&   pgidsigdeadlines       r   r}   zPtyBridge.close   s    < 	F	:djn--DD 	 	 	DDD	 M6>6>B 	! 	!C:%%'' #IdC((((JOOC(((   ~''#-H*$$&& !4>+;+;h+F+F
4    *$$&& !4>+;+;h+F+F	J4((((( 	 	 	DD	s2   1 A A 2B77
CCE0 0
E>=E>c                    | S rH   r   rJ   s    r   	__enter__zPtyBridge.__enter__  s    r   c                .    |                                   d S rH   )r}   )r&   _excs     r   __exit__zPtyBridge.__exit__  s    

r   )r   r   )r   r(   )r3   r4   r/   r5   r0   r6   r1   r   r2   r   r   r7   )r   r   )rO   )rP   rQ   r   rR   )r\   r^   r   r_   )r1   r   r2   r   r   r_   )r   r_   )r   r7   )r   r   r   r   r'   classmethodr,   rD   propertyrI   rN   rV   rb   rp   r}   r   r   r   r   r   r   r   Y   s=            $ $ $ [$ 
 ""* * * * * [*X # # # X#       <   "   2$ $ $ $N        r   )r
   r   r   r   r   r   )r   
__future__r   rW   rk   r?   rT   rt   ri   r;   rm   rz   typingr   r   r>   r<   r=   r*   ImportError__all__r   rg   rh   r   RuntimeErrorr   r   r   r   r   <module>r      sx   8 # " " " " "   				    



   % % % % % % % %00777NN   JNNN
 -
. 		   "    ,   E E E E E E E E E Es   A 	A"!A"