arrow_backRetour aux issues
nesquena/hermes-webui
#7195
Débutant
Ouvrirarrow_forward
Débutant
Ouvrirarrow_forward
Débutant
Ouvrirarrow_forward
Windows self-restart re-executes sys.argv — infinite recursive pytest spawn + process leak
ecoDébutant
bug
help wanted
investigation
priority
performance
descriptionDescription
# Bug Report: Windows self-restart re-executes `sys.argv` — infinite recursive pytest spawn + process leak
**Repo:** nesquena/hermes-webui
**Component:** `api/updates.py` — `_schedule_restart()` Windows branch
**Severity:** High (process leak / fork-bomb under test; resource exhaustion)
**Environment:** Windows 11, Python 3.11/3.12, WebUI v0.52.113 (HEAD c67fd2dd)
## Summary
On Windows, the WebUI self-restart path (`_schedule_restart()` in `api/updates.py`) re-executes the **current process's `sys.argv`** via `subprocess.Popen([sys.executable] + sys.argv) + os._exit(0)`. When the WebUI process was launched **under pytest** (the `tests/conftest.py` `test_server` fixture Popen's `server.py`), `sys.argv` is the **pytest command line** — so the "restart" re-runs pytest, which runs the same update tests, which hit the restart path again → **infinite recursive pytest spawn**.
Each pytest generation also spawns real `server.py` subprocesses via the conftest `test_server` fixture, which leak because the parent pytest never completes teardown. Observed result: 100+ orphaned Python processes, ~4.4 GB RAM, sustained CPU burn.
## Reproduction
1. Run the WebUI update test suite on Windows:
```
python -m pytest tests/test_updates.py tests/test_update_checker.py tests/test_update_channels.py
```
2. Any test that reaches `apply_update()` → `_schedule_restart()` (e.g. `test_apply_update_*` with `_restart_blocker_snapshot` patched to `restart_blocked: False`) triggers the Windows restart branch.
3. The restart Popen's `[sys.executable] + sys.argv` — which under pytest is the pytest command — and `os._exit(0)` kills the current process.
4. The new pytest process runs the same tests → recursion. Each generation spawns a fresh `server.py` via the conftest fixture. Processes accumulate until manually killed.
## Root cause
`api/updates.py` lines ~1757–1803:
```python
if sys.platform == 'win32':
import subprocess
if getattr(sys, "frozen", False):
args = sys.argv
else:
args = [sys.executable] + sys.argv
...
subprocess.Popen(
args,
...
creationflags=(subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW),
...
)
os._exit(0)
```
The comment documents that `os.execv()` does NOT replace the process on Windows, so the code uses `Popen + os._exit`. But `sys.argv` is only correct when the process was launched as `server.py`. Under pytest (or any wrapper), `sys.argv` is the wrapper's command line — the restart re-runs the wrapper, not the server.
Additionally, the Popen'd child **survives the parent's exit** (documented in the code as intended for the server case), so every restart attempt leaves an orphaned process pair (venv launcher + runtime child) even in the non-pytest case. Each orphan binds its own port and runs a full WebUI loop (gateway watcher polling state.db every 5s, SSE, update checker) — ~12% CPU each.
## Impact
- **Under pytest:** infinite recursive spawn (fork-bomb). Observed: 14+ pytest generations, each spawning server.py pairs every ~2.5s. 105 processes, ~4.4 GB RAM, ~22,000 CPU-seconds before manual cleanup.
- **In production (non-pytest):** every WebUI self-update leaks one orphaned server pair. Over repeated updates (e.g. nightly auto-update), orphans accumulate indefinitely.
## Suggested fix
1. **Restart must re-exec the server, not `sys.argv`.** Resolve the server entry point explicitly (e.g. `[sys.executable, str(REPO_ROOT / 'server.py')]` or the frozen-app argv) instead of `sys.argv`. This makes the restart correct under any launcher (pytest, `python -m`, supervisor).
2. **Kill the old process tree before/after Popen on Windows.** The current code relies on `os._exit(0)` releasing the port, but the Popen'd child is a *new* process that doesn't inherit the port — the old process's port is only released when the old process dies. If `os._exit(0)` is delayed or the child fails to bind, the old process lingers. Use a proper pr
Issues similaires
calkit/calkit
star53
Poids du dépôt moyen
VS Code extension should be robust to YAML parser errors
Seeing this error: ``` Failed to read calkit.yaml: YAMLParseError: A block sequence may not be used as an implicit map…
Python
bug
good first issue
fu351/Doberman-Core
star211
Poids du dépôt léger
dash: a manual Refresh control
The dashboard polls: `refreshStats()` (`src/doberman/dash/app.py:408`) every 5 s and `refreshPending()` (`:546`) every …
Python
enhancement
good first issue
fu351/Doberman-Core
star211
Poids du dépôt léger
dash: "Copy details" button on each pending-approval card
Each pending-approval card in the dashboard (`renderPending`, `src/doberman/dash/app.py:448-544`) shows the risk badge,…
Python
enhancement
good first issue