-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fix(desktop): cleanup stale packaged backend processes on startup #1479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||||||||||
| from __future__ import annotations | ||||||||||||||
|
|
||||||||||||||
| import os | ||||||||||||||
| import signal | ||||||||||||||
| import socket | ||||||||||||||
| import subprocess | ||||||||||||||
| import sys | ||||||||||||||
|
|
@@ -79,6 +80,89 @@ def _stream_reader(in_stream, out_stream) -> None: | |||||||||||||
| pass | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _pid_exists(pid: int) -> bool: | ||||||||||||||
| """Return True if *pid* exists and is accessible.""" | ||||||||||||||
| try: | ||||||||||||||
| os.kill(pid, 0) | ||||||||||||||
| except ProcessLookupError: | ||||||||||||||
| return False | ||||||||||||||
| except PermissionError: | ||||||||||||||
| return True | ||||||||||||||
| return True | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _cleanup_stale_desktop_backends() -> list[int]: | ||||||||||||||
| """Terminate stale packaged desktop backend processes. | ||||||||||||||
|
|
||||||||||||||
| We only target packaged app backends (inside ``*.app`` bundle) to avoid | ||||||||||||||
| affecting source/development processes like ``python -m copaw app --reload``. | ||||||||||||||
| """ | ||||||||||||||
| if sys.platform == "win32": | ||||||||||||||
| # Packaged desktop backend process pattern below is macOS/Linux-specific. | ||||||||||||||
| return [] | ||||||||||||||
|
|
||||||||||||||
| stale_pids: list[int] = [] | ||||||||||||||
| try: | ||||||||||||||
| ps_out = subprocess.check_output( | ||||||||||||||
| ["ps", "-axo", "pid=,command="], | ||||||||||||||
| text=True, | ||||||||||||||
| ) | ||||||||||||||
| except Exception: | ||||||||||||||
| return [] | ||||||||||||||
|
|
||||||||||||||
| current_pid = os.getpid() | ||||||||||||||
| for raw in ps_out.splitlines(): | ||||||||||||||
| line = raw.strip() | ||||||||||||||
| if not line: | ||||||||||||||
| continue | ||||||||||||||
| parts = line.split(None, 1) | ||||||||||||||
| if len(parts) != 2: | ||||||||||||||
| continue | ||||||||||||||
| pid_str, cmd = parts | ||||||||||||||
| try: | ||||||||||||||
| pid = int(pid_str) | ||||||||||||||
| except ValueError: | ||||||||||||||
| continue | ||||||||||||||
| if pid == current_pid: | ||||||||||||||
| continue | ||||||||||||||
|
|
||||||||||||||
| # Only clean stale desktop-packaged backend processes. | ||||||||||||||
| if ( | ||||||||||||||
| ".app/Contents/Resources/env/bin/python" in cmd | ||||||||||||||
| and "-m uvicorn" in cmd | ||||||||||||||
| and "copaw.app._app:app" in cmd | ||||||||||||||
|
||||||||||||||
| ): | ||||||||||||||
| stale_pids.append(pid) | ||||||||||||||
|
||||||||||||||
|
|
||||||||||||||
| cleaned: list[int] = [] | ||||||||||||||
| for pid in stale_pids: | ||||||||||||||
| try: | ||||||||||||||
| os.kill(pid, signal.SIGTERM) | ||||||||||||||
| cleaned.append(pid) | ||||||||||||||
| except ProcessLookupError: | ||||||||||||||
| continue | ||||||||||||||
| except Exception: | ||||||||||||||
| continue | ||||||||||||||
|
Comment on lines
+142
to
+145
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| if cleaned: | ||||||||||||||
| # Give processes a short grace period for graceful shutdown. | ||||||||||||||
| deadline = time.monotonic() + 2.0 | ||||||||||||||
| while time.monotonic() < deadline: | ||||||||||||||
| if all(not _pid_exists(pid) for pid in cleaned): | ||||||||||||||
| break | ||||||||||||||
| time.sleep(0.1) | ||||||||||||||
|
|
||||||||||||||
| # Force kill any survivors. | ||||||||||||||
| for pid in cleaned: | ||||||||||||||
| if _pid_exists(pid): | ||||||||||||||
| try: | ||||||||||||||
| os.kill(pid, signal.SIGKILL) | ||||||||||||||
| except Exception: | ||||||||||||||
| pass | ||||||||||||||
|
||||||||||||||
| except Exception: | |
| pass | |
| except (ProcessLookupError, PermissionError): | |
| pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
except Exception:is too broad and can hide unexpected errors. It's better to catch specific exceptions thatsubprocess.check_outputis known to raise, such assubprocess.CalledProcessErrorandFileNotFoundError. Also, consider logging the error for easier debugging, which will be helpful if process scanning fails for some reason.