fix(desktop): detach the Windows packaged launch and swallow launcher Ctrl-C
Two launcher defects in cmd_gui's packaged launch path: - On Windows the packaged Desktop was spawned with a console-inheriting subprocess.run, so closing the launching shell sent CTRL_CLOSE_EVENT down the process group and killed the app with it, while Electron/Node stdout flooded (and, under cp936, mojibaked) the parent terminal. Spawn it detached instead — CREATE_NEW_PROCESS_GROUP|DETACHED_PROCESS (+ CREATE_BREAKAWAY_FROM_JOB with a no-breakaway fallback), DEVNULL stdio — and exit 0 immediately, like the bundled launcher and gateway_windows._spawn_detached. macOS/Linux keep the foreground run. - On the foreground platforms, Ctrl-C in the attached terminal raised a raw KeyboardInterrupt traceback out of the CLI instead of a clean close. Catch it and exit 0 with a short message. Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com> Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
This commit is contained in:
@@ -1549,10 +1549,60 @@ def cmd_gui(args: argparse.Namespace):
|
||||
if deferred_entry is not None:
|
||||
env = deferred_entry.child_env(env)
|
||||
pass_fds = deferred_entry.pass_fds
|
||||
with desktop_console_output(source_mode=source_mode) as streams:
|
||||
launch_result = subprocess.run(
|
||||
launch_command, cwd=desktop_dir, env=env, check=False, pass_fds=pass_fds, **streams
|
||||
if not source_mode and sys.platform == "win32":
|
||||
# Windows: detach the packaged Desktop from the parent console + process
|
||||
# group, then return immediately (#58275). A console-inheriting
|
||||
# subprocess.run dies with the launching shell (CTRL_CLOSE_EVENT fans
|
||||
# out to the process group) and floods the parent terminal — under
|
||||
# cp936, mojibake — with Electron/Node stdout. Mirrors the bundled
|
||||
# launcher (_launch_bundled_desktop) and gateway_windows._spawn_detached.
|
||||
# macOS/Linux keep the foreground run below: those launches are
|
||||
# expected to stay attached to the terminal, and the desktop_console
|
||||
# drain is a Windows-only concern.
|
||||
from hermes_cli._subprocess_compat import (
|
||||
windows_detach_flags,
|
||||
windows_detach_flags_without_breakaway,
|
||||
)
|
||||
|
||||
popen_kwargs = dict(
|
||||
cwd=desktop_dir,
|
||||
env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
close_fds=True,
|
||||
)
|
||||
try:
|
||||
subprocess.Popen(launch_command, creationflags=windows_detach_flags(), **popen_kwargs)
|
||||
except OSError as exc:
|
||||
# Only recover from a denied job breakaway (the parent's job object
|
||||
# lacks JOB_OBJECT_LIMIT_BREAKAWAY_OK), which surfaces as
|
||||
# ERROR_ACCESS_DENIED (winerror == 5). Re-raise every other spawn
|
||||
# failure (bad argv/env, missing exe) so it stays a clear, single
|
||||
# error instead of being masked by a doomed second attempt.
|
||||
if getattr(exc, "winerror", None) != 5:
|
||||
raise
|
||||
subprocess.Popen(
|
||||
launch_command,
|
||||
creationflags=windows_detach_flags_without_breakaway(),
|
||||
**popen_kwargs,
|
||||
)
|
||||
if deferred_entry is not None:
|
||||
deferred_entry.finish()
|
||||
desktop_launch_notice("✓ Hermes Desktop launched in a detached window; you can close this shell.")
|
||||
sys.exit(0)
|
||||
with desktop_console_output(source_mode=source_mode) as streams:
|
||||
try:
|
||||
launch_result = subprocess.run(
|
||||
launch_command, cwd=desktop_dir, env=env, check=False, pass_fds=pass_fds, **streams
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
# Ctrl-C in the terminal the launcher is attached to is the user
|
||||
# closing the Desktop, not a launcher crash. Exit cleanly instead
|
||||
# of dumping a KeyboardInterrupt traceback from subprocess.run
|
||||
# (#59848).
|
||||
print("\n✓ Hermes Desktop closed.")
|
||||
sys.exit(0)
|
||||
if deferred_entry is not None:
|
||||
deferred_entry.finish()
|
||||
sys.exit(launch_result.returncode)
|
||||
|
||||
Reference in New Issue
Block a user