
    .cjB                        d Z ddlmZ ddlZddlZddlZddlmZ g dZej	        dk    Z
d dZdZdZdZdZd!dZd!dZd!dZd"dZd#dZd$dZd%dZdS )&u  Windows subprocess compatibility helpers.

Hermes is developed on Linux / macOS and tested natively on Windows too.
Several common subprocess patterns break silently-or-loudly on Windows:

* ``["npm", "install", ...]`` — on Windows ``npm`` is ``npm.cmd``, a batch
  shim.  ``subprocess.Popen(["npm", ...])`` fails with WinError 193
  ("not a valid Win32 application") because CreateProcessW can't run a
  ``.cmd`` file without ``shell=True`` or PATHEXT resolution.

* ``start_new_session=True`` — on POSIX, this maps to ``os.setsid()`` and
  actually detaches the child.  On Windows it's silently ignored; the
  Windows equivalent is the ``CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW``
  creationflags bundle, which Python only applies when you pass it
  explicitly.

* Console-window flashes — every ``subprocess.Popen`` of a ``.exe`` on
  Windows spawns a cmd window briefly unless ``CREATE_NO_WINDOW`` is
  passed.  Cosmetic but jarring for background daemons.

This module centralizes the platform-branching logic so the rest of the
codebase doesn't sprinkle ``if sys.platform == "win32":`` everywhere.

**All helpers are no-ops on non-Windows** — calling them in Linux/macOS
code paths is safe by design.  That's the "do no damage on POSIX"
guarantee.
    )annotationsN)Sequence)
IS_WINDOWSresolve_node_commandsuppress_platform_ver_consolewindows_detach_flags&windows_detach_flags_without_breakawaywindows_hide_flagswindows_detach_popen_kwargsbounded_git_probewin32namestrargvSequence[str]return	list[str]c                B    t          j        |           }|r|g|S | g|S )u  Resolve a Node-ecosystem command name to an absolute-path argv.

    On Windows, commands like ``npm``, ``npx``, ``yarn``, ``pnpm``,
    ``playwright``, ``prettier`` ship as ``.cmd`` files (batch shims).
    ``subprocess.Popen(["npm", "install"])`` fails with WinError 193
    because CreateProcessW doesn't execute batch files directly.

    ``shutil.which(name)`` *does* resolve ``.cmd`` via PATHEXT and returns
    the fully-qualified path — which CreateProcessW accepts because the
    extension tells Windows to route through ``cmd.exe /c``.

    On POSIX ``shutil.which`` also returns a fully-qualified path when
    found.  That's a small change from bare-name resolution (the OS does
    its own PATH search) but functionally identical and has the side
    benefit of making the argv reproducible in logs.

    Behavior when the command is not on PATH:
    - On Windows: return the bare name — caller can still try with
      ``shell=True`` as a last resort, OR the subsequent Popen will
      raise FileNotFoundError with a readable error we want to surface.
    - On POSIX: same.  Bare ``npm`` on a Linux box without npm installed
      fails the same way it did before this function existed.

    Args:
        name: The command name to resolve (``npm``, ``npx``, ``node`` …).
        argv: The remaining arguments.  Must NOT include ``name`` itself —
            this function builds the full argv list.

    Returns:
        A list suitable for passing to subprocess.Popen/run/call.
    )shutilwhich)r   r   resolveds      ?/home/ice/.hermes/hermes-agent/hermes_cli/_subprocess_compat.pyr   r   8   s6    @ |D!!H ! 4  =4=    i      i   i   intc                 B    t           sdS t          t          z  t          z  S )uC  Return Win32 creationflags that detach a child from the parent
    console and process group without leaving it console-less.  0 on
    non-Windows.

    Pair with ``start_new_session=False`` (default) when calling
    subprocess.Popen — on POSIX use ``start_new_session=True`` instead,
    which maps to ``os.setsid()`` in the child.

    Rationale:
    - ``CREATE_NEW_PROCESS_GROUP`` — child has its own process group so
      Ctrl+C in the parent console doesn't propagate.
    - ``CREATE_NO_WINDOW`` — the child gets its own fresh console that is
      never shown.  This both detaches it from the parent's console
      lifetime (closing the launching terminal doesn't CTRL_CLOSE it) AND
      gives every console-subsystem descendant (git, gh, cmd, node, …) a
      console to inherit, so they don't allocate visible flashing ones.
      This deliberately replaces the old ``DETACHED_PROCESS`` approach:
      MSDN specifies CREATE_NO_WINDOW is *ignored* when combined with
      DETACHED_PROCESS, and a truly console-less daemon re-creates the
      per-descendant console-flash bug (#54220/#56747) at every spawn —
      see the note on ``_DETACHED_PROCESS`` above.
    - ``CREATE_BREAKAWAY_FROM_JOB`` — escape any job object the parent is
      in.  Electron (Desktop app) and Tauri (bootstrap installer) wrap
      their children in job objects; without breakaway, those children
      die when the parent process exits even though they have their own
      console.  This was the missing flag that made the post-update
      gateway respawn watcher silently die alongside the Tauri updater
      after the Electron Desktop's update flow finished.

    If a process is in a job that disallows breakaway (rare —
    JOB_OBJECT_LIMIT_BREAKAWAY_OK isn't set), CreateProcess returns
    ERROR_ACCESS_DENIED.  Python surfaces that as ``PermissionError``
    on the ``subprocess.Popen`` call.  Callers in this codebase already
    wrap detached spawns in ``try/except OSError`` and fall back to a
    cmd.exe wrapper, so the breakaway-denied case degrades gracefully
    rather than crashing.
    r   )r   _CREATE_NEW_PROCESS_GROUP_CREATE_NO_WINDOW_CREATE_BREAKAWAY_FROM_JOB r   r   r   r      s*    L  q!
	
$	%r   c                 2    t           sdS t          t          z  S )u  Same as :func:`windows_detach_flags` minus ``CREATE_BREAKAWAY_FROM_JOB``.

    The docstring on :func:`windows_detach_flags` notes that a process in
    a job which disallows breakaway (no ``JOB_OBJECT_LIMIT_BREAKAWAY_OK``)
    will see ``ERROR_ACCESS_DENIED`` from CreateProcess, surfacing as
    ``OSError`` (``PermissionError``) on the ``subprocess.Popen`` call.
    Callers that want to recover — by retrying without the breakaway
    bit — can pair the two helpers symbolically rather than coding the
    ``& ~0x01000000`` magic at every site:

    .. code-block:: python

        try:
            subprocess.Popen(argv, creationflags=windows_detach_flags(), …)
        except OSError:
            subprocess.Popen(
                argv,
                creationflags=windows_detach_flags_without_breakaway(),
                …,
            )

    See ``gateway_windows.py::_spawn_detached`` for the canonical
    implementation of this pattern.  Returns 0 on non-Windows.
    r   )r   r   r   r    r   r   r	   r	      s    2  q$'888r   c                 "    t           sdS t          S )u  Return Win32 creationflags that merely hide the child's console
    window without detaching the child.  0 on non-Windows.

    Use for short-lived console apps spawned as part of a larger
    operation (``taskkill``, ``where``, version probes) where we want no
    flash but also want to collect stdout/exit code synchronously.

    The difference from :func:`windows_detach_flags`: no
    ``CREATE_NEW_PROCESS_GROUP`` / ``CREATE_BREAKAWAY_FROM_JOB`` — the
    child stays in the parent's process group and job so Ctrl+C and job
    teardown propagate normally, as a short-lived helper wants.  Stdio
    handles are inherited either way, so ``capture_output=True`` works
    with both bundles.
    r   )r   r   r    r   r   r
   r
      s      qr   Nonec                     t           sdS 	 ddl} t          | d          r	 	 dd}|| _        dS dS # t          $ r Y dS w xY w)u  Stub out ``platform._syscmd_ver`` on Windows so it can never flash a
    console window.  No-op on non-Windows.

    CPython's ``platform.win32_ver()`` — reached by ``platform.uname()``,
    ``platform.version()``, and ``platform.platform()`` — unconditionally
    shells out ``cmd /c ver`` via ``subprocess.check_output(..., shell=True)``
    with no ``CREATE_NO_WINDOW``.  From a windowless parent (the pythonw
    gateway and every kanban worker it spawns) that allocates a fresh
    *visible* console: one flashing ``cmd`` window per process, triggered by
    any dependency that merely touches ``platform.uname()`` at import time.

    With ``_syscmd_ver`` stubbed to return its inputs, ``win32_ver()`` hits
    the documented ``ValueError`` fallback and reads the version from
    ``sys.getwindowsversion().platform_version`` — same information, queried
    in-process, no subprocess, no window.  Verified equivalent on
    CPython 3.11 (``platform()`` → ``Windows-10-10.0.xxxxx-SP0`` either way).

    Call early, before heavyweight imports — the flash typically happens
    during a dependency's import, not from Hermes' own code.
    Nr   _syscmd_ver r   win16dosc                    | ||fS )Nr    )systemreleaseversionsupported_platformss       r   _quiet_syscmd_verz8suppress_platform_ver_console.<locals>._quiet_syscmd_ver  s    w//r   )r&   r&   r&   r'   )r   platformhasattrr%   	Exception)r0   r/   s     r   r   r      s    *  8]++ 	5AC6O0 0 0 0 $5H   	5 	5    s   !0 
>>dictc                 8    t           rdt                      iS ddiS )u  Return a dict of Popen kwargs that detach a child on Windows and
    fall back to the POSIX equivalent (``start_new_session=True``) on
    Linux/macOS.

    Usage pattern:

    .. code-block:: python

        subprocess.Popen(
            argv,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            stdin=subprocess.DEVNULL,
            close_fds=True,
            **windows_detach_popen_kwargs(),
        )

    This replaces the unsafe-on-Windows pattern:

    .. code-block:: python

        subprocess.Popen(..., start_new_session=True)

    which silently fails to detach on Windows (the flag is accepted but
    has no effect — the child stays attached to the parent's console
    and dies when the console closes).
    creationflagsstart_new_sessionT)r   r   r    r   r   r   r     s)    8  9!5!7!788&&r   proc'subprocess.Popen'c           
     @   	 |                                   n# t          $ r Y nw xY wt          rp	 t          j        ddddt          | j                  gt          j        t          j        t          j        ddt                                 dS # t          $ r Y dS w xY wdS )	u  Best-effort terminate *proc* and, on Windows, its descendants.

    ``proc.kill()`` alone only terminates the PATH-resolved ``git`` launcher; a
    suspended descendant ``git.exe`` can survive holding duplicates of the
    captured pipe handles, which keeps the pipes from reaching EOF and leaks two
    reader threads + the process per fired timeout. ``taskkill /T /F`` takes the
    whole tree down so the bounded drain that follows can actually reach EOF.

    All failures are swallowed — this is cleanup on an already-failing path, and
    the caller's contract is to fail open. ``kill()`` can raise (access denied,
    already reaped); an unhandled raise here would escape the caller's ``except``
    handler and break that contract. The ``taskkill`` spawn itself cannot
    re-enter the deadlock class it fixes: it captures no pipes (DEVNULL), so its
    own timeout cleanup has no reader threads to join.
    taskkillz/Tz/Fz/PID   F)stdoutstderrstdintimeoutcheckr5   N)
killOSErrorr   
subprocessrunr   pidDEVNULLr
   r2   )r7   s    r   _kill_git_process_treerG   1  s     		    	NT4TX?!)!) (022       	 	 	DD	 s    
$$AB 
BBr?   floatc          
        t           rdt                      ini }	 t          j        t	          |           ft          j        t          j        t          j        dddd|}n# t          $ r Y dS w xY w	 |                    |          \  }}nH# t          $ r; t          |           	 |                    d           n# t          $ r Y nw xY wY dS w xY w|j
        d	k    r|                                ndS )
u  Run a short, throwaway ``git`` probe and return stripped stdout, or ``""``
    on ANY failure (nonzero exit, timeout, spawn error, decode error).

    This is the shared, deadlock-safe replacement for
    ``subprocess.run(["git", ...], timeout=...)`` at fail-open probe call sites
    (``tui_gateway.git_probe.run_git``, ``agent.coding_context._git``).

    Why not ``subprocess.run``: on Windows, ``run()``'s post-timeout cleanup
    calls an *unbounded* ``communicate()`` after killing git. Killing the
    PATH-resolved launcher can leave a suspended descendant ``git.exe`` holding
    duplicates of the captured stdout/stderr handles, so the pipes never reach
    EOF and the reader-thread join blocks forever. On the Desktop agent-build
    path (``_start_agent_build → _session_info → branch() → run_git``) that turned
    an optional branch label into ``agent initialization timed out``
    (issues #68609 / #66037).

    The bounded flow: an explicit ``communicate(timeout)``, then on any failure a
    tree-kill (see :func:`_kill_git_process_tree`) plus a bounded 1s post-kill
    drain; if the pipes are still held after that, they're abandoned (the orphaned
    reader threads are daemonic and cost nothing).

    The normal-path spawn contract mirrors the previous ``run`` call byte-for-byte:
    PIPE/PIPE/DEVNULL, ``text`` with UTF-8 ``errors="replace"`` decoding, and the
    hidden-window ``creationflags`` on Windows only.
    r5   Tzutf-8replace)r<   r=   r>   textencodingerrorsr&   )r?      r   )r   r
   rC   PopenlistPIPErF   r2   communicaterG   
returncodestrip)r   r?   _popen_kwargsr7   r<   _s         r   r   r   T  sF   4 @JQ_&8&:&:;;rMJJ	
??$	
 	
 	
 	
    rr$$W$55	 	 	 	 	t$$$	Q'''' 	 	 	D	rr	 "_116<<>>>r9sH   AA   
A.-A.2B C&B=<C=
C
C	C

CC)r   r   r   r   r   r   )r   r   )r   r#   )r   r3   )r7   r8   r   r#   )r   r   r?   rH   r   r   )__doc__
__future__r   r   rC   systypingr   __all__r0   r   r   r   _DETACHED_PROCESSr   r   r   r	   r
   r   r   rG   r   r    r   r   <module>r]      s?   8 # " " " " "      



      	 	 	 \W$
# # # #\ ' "    ( , , , ,^9 9 9 9<   (" " " "J' ' ' 'L       F4: 4: 4: 4: 4: 4:r   