125 lines
3.5 KiB
Plaintext
125 lines
3.5 KiB
Plaintext
|
|
#!/usr/bin/env python3
|
||
|
|
# tractor: structured concurrent "actors".
|
||
|
|
# Copyright 2018-eternity Tyler Goodlet.
|
||
|
|
#
|
||
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||
|
|
'''
|
||
|
|
`tractor-reap` — SC-polite zombie-subactor reaper.
|
||
|
|
|
||
|
|
Finds `tractor` subactor processes left alive after a
|
||
|
|
`pytest` (or any tractor-app) run that failed to fully
|
||
|
|
cancel its actor tree, then sends SIGINT with a bounded
|
||
|
|
grace window before escalating to SIGKILL.
|
||
|
|
|
||
|
|
Detection modes (auto-selected):
|
||
|
|
|
||
|
|
--parent <pid> : descendant-mode — kill procs whose
|
||
|
|
PPid == <pid>. Use when a parent
|
||
|
|
is still alive and you want to
|
||
|
|
scope the sweep precisely (e.g.
|
||
|
|
CI wrapper calling in from outside
|
||
|
|
pytest).
|
||
|
|
|
||
|
|
(default) : orphan-mode — kill procs with
|
||
|
|
PPid==1 (init-reparented) whose
|
||
|
|
cwd matches the repo root AND
|
||
|
|
whose cmdline contains `python`.
|
||
|
|
The cwd filter is what prevents
|
||
|
|
sweeping unrelated init-children.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
|
||
|
|
# after a pytest run crashed/was Ctrl+C'd
|
||
|
|
scripts/tractor-reap
|
||
|
|
|
||
|
|
# from inside a still-live supervisor
|
||
|
|
scripts/tractor-reap --parent 12345
|
||
|
|
|
||
|
|
# dry-run: list what would be reaped, don't signal
|
||
|
|
scripts/tractor-reap -n
|
||
|
|
|
||
|
|
'''
|
||
|
|
import argparse
|
||
|
|
import pathlib
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
|
||
|
|
|
||
|
|
def _repo_root() -> pathlib.Path:
|
||
|
|
'''
|
||
|
|
Use `git rev-parse --show-toplevel` when available;
|
||
|
|
fall back to the repo this script lives in.
|
||
|
|
|
||
|
|
'''
|
||
|
|
try:
|
||
|
|
out: str = subprocess.check_output(
|
||
|
|
['git', 'rev-parse', '--show-toplevel'],
|
||
|
|
stderr=subprocess.DEVNULL,
|
||
|
|
text=True,
|
||
|
|
).strip()
|
||
|
|
return pathlib.Path(out)
|
||
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||
|
|
return pathlib.Path(__file__).resolve().parent.parent
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
parser = argparse.ArgumentParser(
|
||
|
|
prog='tractor-reap',
|
||
|
|
description=__doc__,
|
||
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
'--parent', '-p',
|
||
|
|
type=int,
|
||
|
|
default=None,
|
||
|
|
help='descendant-mode: reap procs with PPid==<pid>',
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
'--grace', '-g',
|
||
|
|
type=float,
|
||
|
|
default=3.0,
|
||
|
|
help='SIGINT grace window in seconds (default 3.0)',
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
'--dry-run', '-n',
|
||
|
|
action='store_true',
|
||
|
|
help='list matched pids but do not signal',
|
||
|
|
)
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
# import lazily so `--help` doesn't require the tractor
|
||
|
|
# package to be importable (e.g. when running from a
|
||
|
|
# shell not inside a venv).
|
||
|
|
repo = _repo_root()
|
||
|
|
sys.path.insert(0, str(repo))
|
||
|
|
from tractor._testing._reap import (
|
||
|
|
find_descendants,
|
||
|
|
find_orphans,
|
||
|
|
reap,
|
||
|
|
)
|
||
|
|
|
||
|
|
if args.parent is not None:
|
||
|
|
pids: list[int] = find_descendants(args.parent)
|
||
|
|
mode: str = f'descendants of PPid={args.parent}'
|
||
|
|
else:
|
||
|
|
pids = find_orphans(repo)
|
||
|
|
mode = f'orphans (PPid=1, cwd={repo})'
|
||
|
|
|
||
|
|
if not pids:
|
||
|
|
print(f'[tractor-reap] no {mode} to reap')
|
||
|
|
return 0
|
||
|
|
|
||
|
|
if args.dry_run:
|
||
|
|
print(f'[tractor-reap] dry-run — {mode}:\n {pids}')
|
||
|
|
return 0
|
||
|
|
|
||
|
|
signalled, survivors = reap(pids, grace=args.grace)
|
||
|
|
# exit 0 if everyone exited cleanly, else 1 to signal
|
||
|
|
# escalation happened — makes the command useful in
|
||
|
|
# CI health-checks and `||`-chaining.
|
||
|
|
return 0 if not survivors else 1
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
raise SystemExit(main())
|