mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-09 17:20:20 +00:00
fix(bootstrap): suppress console window on Windows foreground restart
The foreground restart path in bootstrap.py (used by supervisor auto-restart and Task Scheduler) spawned server.py with only CREATE_NEW_PROCESS_GROUP, which still creates a visible console window on Windows. Apply the same robust pattern already used in api/updates._schedule_restart: - Prefer pythonw.exe (windowless subsystem) over python.exe - Use DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW flags - Redirect stdin/stdout/stderr to DEVNULL - Set close_fds=True Without this fix, every supervisor-triggered restart (crash recovery, Task Scheduler autostart) flashes an empty terminal window that users may mistakenly kill, taking down the WebUI process.
This commit is contained in:
+24
-3
@@ -1174,12 +1174,33 @@ def _schedule_restart(delay: float = 2.0) -> None:
|
||||
args = sys.argv
|
||||
else:
|
||||
args = [sys.executable] + sys.argv
|
||||
# Start new process detached, redirect all stdio to
|
||||
# avoid broken-pipe errors when the parent exits.
|
||||
# Prefer pythonw.exe over python.exe so the restarted
|
||||
# server does not create a visible console window.
|
||||
# sys.executable may point at python.exe (console
|
||||
# subsystem); substitute pythonw.exe if it exists
|
||||
# next to python.exe.
|
||||
_exe = sys.executable
|
||||
if _exe.lower().endswith('python.exe'):
|
||||
_w_exe = _exe[:-4] + 'w.exe' # python.exe -> pythonw.exe
|
||||
if os.path.isfile(_w_exe):
|
||||
if getattr(sys, "frozen", False):
|
||||
args = sys.argv
|
||||
else:
|
||||
args = [_w_exe] + sys.argv
|
||||
# Start new process fully detached with NO console
|
||||
# window. DETACHED_PROCESS alone is not sufficient
|
||||
# on modern Windows — without CREATE_NO_WINDOW a
|
||||
# python.exe (console-subsystem) child still flashes
|
||||
# an empty terminal window, which the user then
|
||||
# manually kills (taking the WebUI with it).
|
||||
subprocess.Popen(
|
||||
args,
|
||||
cwd=os.getcwd(),
|
||||
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
creationflags=(
|
||||
subprocess.DETACHED_PROCESS
|
||||
| subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.CREATE_NO_WINDOW
|
||||
),
|
||||
close_fds=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
|
||||
+24
-10
@@ -542,16 +542,30 @@ def main() -> int:
|
||||
# spawns a new process instead of replacing (Python calls CreateProcess),
|
||||
# orphaning it from any supervisor. Use Popen + exit there instead.
|
||||
if sys.platform == "win32":
|
||||
# CREATE_NEW_PROCESS_GROUP only exists in the subprocess module on
|
||||
# Windows; resolve it defensively (0 = no extra flags) so this line
|
||||
# can't AttributeError if reached on a non-Windows interpreter
|
||||
# (e.g. a win32-simulating test) — mirrors the getattr() guard used
|
||||
# for SO_EXCLUSIVEADDRUSE.
|
||||
_CREATE_NEW_PROCESS_GROUP = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
||||
subprocess.Popen([python_exe, server_path],
|
||||
cwd=server_cwd,
|
||||
env=os.environ.copy(),
|
||||
creationflags=_CREATE_NEW_PROCESS_GROUP)
|
||||
# Mirror the robust pattern from api/updates._schedule_restart:
|
||||
# 1. Prefer pythonw.exe (windowless subsystem) over python.exe
|
||||
# so the restarted server never creates a visible console window.
|
||||
# 2. DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
|
||||
# suppresses the brief console flash even when python.exe is used.
|
||||
_exe = str(python_exe)
|
||||
if _exe.lower().endswith("python.exe"):
|
||||
_w = _exe[:-4] + "w.exe" # python.exe -> pythonw.exe
|
||||
if os.path.isfile(_w):
|
||||
_exe = _w
|
||||
_flags = 0
|
||||
for _attr in ("DETACHED_PROCESS", "CREATE_NEW_PROCESS_GROUP",
|
||||
"CREATE_NO_WINDOW"):
|
||||
_flags |= getattr(subprocess, _attr, 0)
|
||||
subprocess.Popen(
|
||||
[_exe, str(server_path)],
|
||||
cwd=str(server_cwd),
|
||||
env=os.environ.copy(),
|
||||
creationflags=_flags,
|
||||
close_fds=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
sys.exit(0)
|
||||
os.execv(python_exe, [python_exe, server_path])
|
||||
# Unreachable — execv either replaces the process or raises.
|
||||
|
||||
Reference in New Issue
Block a user