From 5630c6f561ecc1a828d414a399a7f9e18e64d6bb Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Thu, 24 Sep 2026 21:41:08 -0500 Subject: [PATCH] Count Hermes run as a module from an activated venv as Hermes too With the venv active, a gateway started by hand is `python -m hermes_cli.main gateway run`: the interpreter is bare, the executable resolves outside the runtime, and no runtime path is on the command line, so the removal took it for a stranger and refused over the files it holds rather than closing it. Hermes's own package named with -m is the program. Co-Authored-By: Codex XHigh Co-Authored-By: Claude Fable 5.1 --- bin/omarchy-remove-ai-hermes | 10 +++++++--- test/shell.d/hermes-remove-test.sh | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/bin/omarchy-remove-ai-hermes b/bin/omarchy-remove-ai-hermes index 0237adc8..b8d1f5a9 100755 --- a/bin/omarchy-remove-ai-hermes +++ b/bin/omarchy-remove-ai-hermes @@ -21,7 +21,8 @@ unit_stopped() { # the desktop app and its helpers, a gateway run by hand -- every process # whose program, by its executable or by what it was started as, lives in the # runtime about to be deleted or in the package; for an interpreter that is -# the script it runs, so a gateway started by hand as `python .../hermes` is +# the script it runs, or Hermes's own package named with -m, so a gateway +# started by hand as `python .../hermes` or `python -m hermes_cli.main` is # found too. Judged by the program alone, never by a later argument: an # editor opened on a file in the runtime is the user's. `check` refuses first if anything else has Hermes's files open, by # its executable, its working directory or a descriptor, that editor or a @@ -76,9 +77,12 @@ def processes(): except PermissionError: pass started_as = argv[:1] - if started_as and re.fullmatch(r'(python[0-9.]*|node|bash|sh)', os.path.basename(started_as[0])): + interpreter = bool(started_as) and re.fullmatch(r'(python[0-9.]*|node|bash|sh)', os.path.basename(started_as[0])) + if interpreter: started_as = argv[:2] - ours = any(under(target, runtime) for target in started_as + program) + # Hermes's own package, run as a module from an activated venv. + ours = any(under(target, runtime) for target in started_as + program) or \ + (bool(interpreter) and argv[1:3] and argv[1] == '-m' and argv[2].startswith('hermes_cli')) if ours or any(under(target, roots) for target in touched): try: name = (process / 'comm').read_text().strip() diff --git a/test/shell.d/hermes-remove-test.sh b/test/shell.d/hermes-remove-test.sh index 3c6d7bc7..e948094c 100755 --- a/test/shell.d/hermes-remove-test.sh +++ b/test/shell.d/hermes-remove-test.sh @@ -430,6 +430,25 @@ finally: gateway.wait(timeout=5) print('ok - an interpreter running a runtime script is Hermes and is stopped') +# Run as a module from an activated venv, the interpreter is bare and the +# script is nowhere on the command line; Hermes's own package name is. +home, runtime, env = setup('module') +(runtime / 'hermes_cli').mkdir(parents=True) +(runtime / 'hermes_cli/__init__.py').write_text('') +(runtime / 'hermes_cli/main.py').write_text('import time\ntime.sleep(30)\n') +module = subprocess.Popen([sys.executable, '-m', 'hermes_cli.main', 'gateway', 'run'], cwd=scratch, + env={**os.environ, 'PYTHONPATH': str(runtime)}) +try: + result = remove(env) + assert result.returncode == 0, result + assert 'Stopping Hermes (PIDs: ' + str(module.pid) in result.stdout, result.stdout + assert module.wait(timeout=5) != 0, 'hermes_cli run as a module is stopped' +finally: + if module.poll() is None: + module.terminate() + module.wait(timeout=5) +print('ok - hermes_cli run as a module from a bare interpreter is Hermes and is stopped') + # A program whose executable lives in the runtime is Hermes whatever it was # started as: the Electron helpers name themselves by path, but a copy of the # binary started by a bare name is found through /proc//exe.