fix: harden restart_claude_desktop across platforms (#33)
- Linux (and any non-desktop platform): refuse instead of 'pkill claude', which substring-matched running Claude Code CLI sessions and relaunched the CLI, not a desktop app. New restart_supported() gates the button. - macOS: wait (<=5s) for the old instance to exit before 'open -a Claude' so the relaunch can't re-activate the dying process. Runs off the UI thread via a RestartWorker. - Windows: verify the Start-menu shortcut exists BEFORE taskkill, so an MSIX/Store install is never killed without a relaunch path. Closes #33 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+49
-19
@@ -1837,8 +1837,41 @@ def _run_quiet(cmd: list[str]) -> None:
|
||||
subprocess.run(cmd, capture_output=True)
|
||||
|
||||
|
||||
def restart_supported() -> bool:
|
||||
"""
|
||||
True only where restarting Claude Desktop makes sense (macOS, Windows).
|
||||
There is no official Claude Desktop for Linux, and the obvious binary
|
||||
name there ("claude") is the Claude Code CLI — killing or spawning it
|
||||
would be actively harmful. GUI callers gate the Restart button on this.
|
||||
"""
|
||||
return sys.platform == "darwin" or sys.platform.startswith("win")
|
||||
|
||||
|
||||
_MACOS_QUIT_WAIT_S = 5.0
|
||||
|
||||
|
||||
def _macos_claude_running() -> bool:
|
||||
try:
|
||||
return subprocess.run(["pgrep", "-x", "Claude"], capture_output=True).returncode == 0
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _restart_claude_desktop_macos() -> RestartResult:
|
||||
_run_quiet(["pkill", "-x", "Claude"])
|
||||
# Wait for the old instance to actually exit: `open -a` against a dying
|
||||
# process can merely re-activate it, and the config is only re-read on a
|
||||
# true relaunch. Blocks up to _MACOS_QUIT_WAIT_S — callers run this off
|
||||
# the UI thread (see RestartWorker in bcc.py).
|
||||
deadline = time.monotonic() + _MACOS_QUIT_WAIT_S
|
||||
while _macos_claude_running():
|
||||
if time.monotonic() > deadline:
|
||||
return RestartResult(
|
||||
False,
|
||||
f"Claude Desktop didn't quit within {_MACOS_QUIT_WAIT_S:.0f}s — "
|
||||
"quit it manually, then reopen it.",
|
||||
)
|
||||
time.sleep(0.15)
|
||||
try:
|
||||
result = subprocess.run(["open", "-a", "Claude"], capture_output=True, text=True)
|
||||
except OSError as e:
|
||||
@@ -1855,8 +1888,18 @@ def _claude_windows_start_menu_shortcut() -> Path:
|
||||
|
||||
|
||||
def _restart_claude_desktop_windows() -> RestartResult:
|
||||
_run_quiet(["taskkill", "/IM", "Claude.exe", "/F"])
|
||||
shortcut = _claude_windows_start_menu_shortcut()
|
||||
if not shortcut.is_file():
|
||||
# Checked BEFORE killing: an MSIX/Store install has no Start-menu .lnk
|
||||
# at this path, and killing without a relaunch path would leave the
|
||||
# user with no running Claude at all.
|
||||
return RestartResult(
|
||||
False,
|
||||
f"Claude's Start-menu shortcut wasn't found ({shortcut}). "
|
||||
"If Claude Desktop is installed from the Microsoft Store, "
|
||||
"quit and reopen it manually.",
|
||||
)
|
||||
_run_quiet(["taskkill", "/IM", "Claude.exe", "/F"])
|
||||
try:
|
||||
# `cmd /c start "" <target>` launches detached, the same as double-clicking
|
||||
# the Start-menu shortcut, and returns immediately.
|
||||
@@ -1873,32 +1916,19 @@ def _restart_claude_desktop_windows() -> RestartResult:
|
||||
return RestartResult(True, "Claude Desktop restarted.")
|
||||
|
||||
|
||||
def _restart_claude_desktop_linux() -> RestartResult:
|
||||
_run_quiet(["pkill", "claude"])
|
||||
try:
|
||||
# The Linux launcher is the app itself (no "open"-style helper), so it
|
||||
# has to be started detached rather than waited on.
|
||||
subprocess.Popen(
|
||||
["claude"],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
except OSError as e:
|
||||
return RestartResult(False, f"Couldn't launch Claude Desktop: {e}")
|
||||
return RestartResult(True, "Claude Desktop restarted.")
|
||||
|
||||
|
||||
def restart_claude_desktop() -> RestartResult:
|
||||
"""
|
||||
Kill and relaunch the Claude Desktop app so a freshly saved config takes
|
||||
effect. The app not currently running is NOT a failure -- pkill/taskkill
|
||||
exiting non-zero just means "nothing to kill", and we go straight to
|
||||
relaunching. Only a failed relaunch is reported as success=False.
|
||||
|
||||
macOS blocks for up to _MACOS_QUIT_WAIT_S while the old instance exits —
|
||||
run off the UI thread. Unsupported platforms (see restart_supported())
|
||||
refuse without touching any process.
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
return _restart_claude_desktop_macos()
|
||||
if sys.platform.startswith("win"):
|
||||
return _restart_claude_desktop_windows()
|
||||
return _restart_claude_desktop_linux()
|
||||
return RestartResult(False, "Restarting Claude Desktop isn't supported on this platform.")
|
||||
|
||||
Reference in New Issue
Block a user