mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-07 23:42:21 +02:00
fix(browser): stop treating a missing cmdline as proof the daemon exited
_terminate_owned_daemon() read /proc/<pid>/cmdline and, on FileNotFoundError, unlinked the pid file on the stated assumption that "the daemon may have exited". Off Linux that file is always missing, so the branch always fired: the pid file of a live daemon was deleted and the daemon itself never killed. _owned_daemon_exists() swallowed the same error and therefore always returned False, which is precisely the state its own docstring warns about, since a close against an unrecognised session can bootstrap a fresh daemon and wait on its browser indefinitely. Demonstrated on macOS before the change: a pid file holding a live pid is removed by _terminate_owned_daemon() and _owned_daemon_exists() reports False. After it, the file survives and the daemon is reported present. _process_command_line() now returns None for "this host cannot tell" and _process_is_alive() answers the separate question of whether the pid exists. Without procfs we decline to kill a process we cannot confirm is ours, and we only forget a pid file once the pid is genuinely gone. Linux behaviour is unchanged: the command-line identity check still gates both paths.
This commit is contained in:
@@ -125,6 +125,37 @@ def _browser_pid_file_candidates(
|
||||
# host that has no procfs, and on one that does.
|
||||
_PROC_ROOT = Path("/proc")
|
||||
|
||||
|
||||
def _process_command_line(pid: int) -> str | None:
|
||||
"""Command line of a running process, or ``None`` when it cannot be read.
|
||||
|
||||
``None`` means "this host cannot tell", not "the process is gone". Off
|
||||
Linux there is no procfs to read a command line from, so callers must not
|
||||
treat it as proof that the process exited.
|
||||
"""
|
||||
|
||||
try:
|
||||
return (_PROC_ROOT / str(pid) / "cmdline").read_bytes().replace(
|
||||
b"\0", b" "
|
||||
).decode("utf-8", errors="replace")
|
||||
except (OSError, UnicodeError):
|
||||
return None
|
||||
|
||||
|
||||
def _process_is_alive(pid: int) -> bool:
|
||||
"""Whether a pid currently exists. Signal 0 checks without delivering."""
|
||||
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
# Alive, owned by somebody else.
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
_SCHOLARLY_METADATA_CUE_RE = re.compile(
|
||||
r"\b(?:accept(?:ed|ance)?|publish(?:ed|ing|cation)?|venue|conference|"
|
||||
r"journal|proceedings|doi)\b",
|
||||
@@ -2359,16 +2390,18 @@ class PrivateBrowserTool:
|
||||
for pid_file in pid_files:
|
||||
try:
|
||||
pid = int(pid_file.read_text().strip())
|
||||
command_line = (Path("/proc") / str(pid) / "cmdline").read_bytes().replace(
|
||||
b"\0", b" "
|
||||
).decode("utf-8", errors="replace")
|
||||
except FileNotFoundError:
|
||||
# The daemon may have exited between writing its pid file and
|
||||
# this cleanup pass. The exact file is still ours to remove.
|
||||
with contextlib.suppress(FileNotFoundError, PermissionError, OSError):
|
||||
pid_file.unlink()
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
except (OSError, UnicodeError, ValueError):
|
||||
command_line = _process_command_line(pid)
|
||||
if command_line is None:
|
||||
# Either the daemon exited between writing its pid file and
|
||||
# this pass, or this host has no procfs to ask. Only the first
|
||||
# justifies forgetting the pid file. Without procfs we cannot
|
||||
# confirm the process is ours, so we neither kill it nor drop
|
||||
# the record that would let a later pass find it.
|
||||
if not _process_is_alive(pid):
|
||||
with contextlib.suppress(FileNotFoundError, PermissionError, OSError):
|
||||
pid_file.unlink()
|
||||
continue
|
||||
if "agent-browser" in command_line:
|
||||
with contextlib.suppress(ProcessLookupError, PermissionError, OSError):
|
||||
@@ -2395,10 +2428,17 @@ class PrivateBrowserTool:
|
||||
for pid_file in _browser_pid_file_candidates(runtime_dir, namespace, session_id):
|
||||
try:
|
||||
pid = int(pid_file.read_text().strip())
|
||||
command_line = (Path("/proc") / str(pid) / "cmdline").read_bytes().replace(
|
||||
b"\0", b" "
|
||||
).decode("utf-8", errors="replace")
|
||||
except (FileNotFoundError, OSError, UnicodeError, ValueError):
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
command_line = _process_command_line(pid)
|
||||
if command_line is None:
|
||||
# Without procfs we can only tell that something with this pid
|
||||
# is alive, not that it is agent-browser. The pid file is our
|
||||
# own namespaced one, so treat a live pid as a match: answering
|
||||
# "no daemon" here is what lets `close` bootstrap a fresh one
|
||||
# and wait on its browser forever.
|
||||
if _process_is_alive(pid):
|
||||
return True
|
||||
continue
|
||||
if "agent-browser" in command_line:
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user