
    `gj*                       d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlmZmZ 	 ddlZe	j                   j#                  d       ZddgZd	Zd
ZdZddZ G d de      Z G d d      Zy# e$ r dZdZY /w xY w)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  c                    	 t        |       }|t        k  rt        S ||kD  r|S |S # t        t        t        f$ r	 t        cY S w xY w)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``.
    )int	TypeError
ValueErrorOverflowError_MIN_DIMENSION)valuemaximumns      H/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/pty_bridge.py_clamp_dimensionr   ?   sO    J 	>7{H z=1 s   % AAc                      e Zd ZdZy)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y)r   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.
    c                B    || _         |j                  | _        d| _        y NF)_procfd_fd_closed)selfprocs     r   __init__zPtyBridge.__init__d   s    
r   c                     t        t              S )z.True if a PTY can be spawned on this platform.)bool_PTY_AVAILABLE)clss    r   is_availablezPtyBridge.is_availablek   s     N##r   NP      )cwdenvcolsrowsc                  t         sFt        j                  j                  d      rt	        d      t
        t	        d      t	        d      |t        j                  j                         n|j                         }|j                  d      sd|d<   t
        j                  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.zgThe `ptyprocess` package is missing. Install with: pip install ptyprocess (or pip install -e '.[pty]').z!Pseudo-terminals are unavailable.TERMzxterm-256color)r-   r.   
dimensions)r(   sysplatform
startswithr   
ptyprocessosenvironcopyget
PtyProcessspawnlist)r)   argvr-   r.   r/   r0   	spawn_envr$   s           r   r=   zPtyBridge.spawnp   s      ||&&u-)B  !)4 
 &&IJJ +.+RZZ__&388:	}}V$ 0If$$**Jd|	 + 
 4yr   c                @    t        | j                  j                        S N)r   r   pidr#   s    r   rC   zPtyBridge.pid   s    4::>>""r   c                    | j                   ry	 t        | j                  j                               S # t        $ r Y yw xY wr   )r"   r'   r   isalive	ExceptionrD   s    r   is_alivezPtyBridge.is_alive   s;    <<	

**,-- 		s   "2 	>>c                n   | j                   ry	 t        j                  | j                  gg g |      \  }}}|sy	 t        j                  | j                  d      }|sy|S # t        t        f$ r Y yw xY w# t        $ r7}|j                  t        j                  t        j                  hv rY d}~y d}~ww xY w)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   r8   readerrnoEIOEBADF)r#   timeoutreadable_dataexcs         r   rL   zPtyBridge.read   s     <<	#]]DHH:r2wGNHa 	77488U+D  $ 		  	yyUYY44		s.   'A  A4 A10A14	B4=,B/.B//B4c                @   | j                   s|syt        |      }|r0	 t        j                  | j                  |      }|dk  ry||d }|r/yy# t
        $ rF}|j                  t        j                  t        j                  t        j                  hv rY d}~y d}~ww xY w)z;Write raw bytes to the PTY master (i.e. the child's stdin).Nr   )
r"   
memoryviewr8   writer!   rK   rM   rN   rO   EPIPE)r#   rS   viewr   rT   s        r   rW   zPtyBridge.write   s    <<t$HHTXXt,
 Av8D   99EKK EEs    A 	B;BBBc                   | j                   ryt        |t              }t        |t              }t	        j
                  d||dd      }	 t        j                  | j                  t        j                  |       y# t        $ r Y y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
TIOCSWINSZrK   )r#   r/   r0   winsizes       r   resizezPtyBridge.resize   sm     <<i0i0++fdD!Q7	KK'"4"4g> 		s   /A8 8	BBc                H   | j                   ryd| _         	 t        j                  | j                  j                        }t        j                  t        j                  t        j                  fD ]  }| j                  j                         s n	 |t        j                  ||       n| j                  j                  |       t        j                         dz   }| j                  j                         st        j                         |k  st        j                  d       | j                  j                         st        j                         |k  rH 	 | j                  j!                  d       y# t
        $ r d}Y Cw xY w# t
        $ r Y w xY w# t
        $ r Y y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"   r8   getpgidr   rC   rG   signalSIGHUPSIGTERMSIGKILLrF   killpgkilltime	monotonicsleepclose)r#   pgidsigdeadlines       r   rr   zPtyBridge.close   sC    <<	::djjnn-D MM6>>6>>B 	!C::%%'#IIdC(JJOOC( ~~'#-H**$$&4>>+;h+F

4  **$$&4>>+;h+F	!	JJ4(-  	D	    		s5   )E4 4FF 4FF	FF	F! F!c                    | S rB   r   rD   s    r   	__enter__zPtyBridge.__enter__  s    r   c                $    | j                          y rB   )rr   )r#   _excs     r   __exit__zPtyBridge.__exit__  s    

r   )r$   z'ptyprocess.PtyProcess')returnr'   )r?   zSequence[str]r-   zOptional[str]r.   zOptional[dict]r/   r   r0   r   r{   'PtyBridge')r{   r   )g?)rP   floatr{   zOptional[bytes])rS   bytesr{   None)r/   r   r0   r   r{   r   )r{   r   )r{   r|   )r   r   r   r   r%   classmethodr*   r=   propertyrC   rH   rL   rW   re   rr   rw   rz   r   r   r   r   r   Y   s     $ $ 
 ""** 	*
 * * * 
* *X # #<"2$Nr   )r   r   r   r   r{   r   )r   
__future__r   rM   r`   r8   rJ   ri   r^   r4   rb   ro   typingr   r   r7   r5   r6   r(   ImportError__all__r   r\   r]   r   RuntimeErrorr   r   r   r   r   <module>r      s   8 #   	    
   %0077N -
. 		", E EY  JNs    A: :	BB