Add per-actor `setproctitle` via `devx._proctitle`
New `tractor.devx._proctitle` mod sets each
sub-actor's `argv[0]` (and kernel `comm`) to
`tractor[<aid.reprol()>]` — e.g.
`tractor[doggy@1027301b]` — so `ps`/`top`/`htop`
and `acli.pytree`/reaper tooling can identify
actors at a glance without parsing full cmdlines.
Deats,
- `set_actor_proctitle()` wraps the `setproctitle`
pkg with `ImportError` guard; optional at runtime
but listed in `pyproject.toml` so default installs
benefit.
- called early in `_child._actor_child_main()` after
`Actor` construction, before `_trio_main()` entry.
- tests in `tests/devx/test_proctitle.py`: format
unit test, `/proc/{cmdline,comm}` integration
test, negative detection test.
Resolves #457
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
(cherry picked from commit d60245777e)
trionics_start_or_cancel
parent
6b14c3dbe7
commit
338f0a1463
|
|
@ -43,15 +43,20 @@ dependencies = [
|
||||||
"tricycle>=0.4.1,<0.5",
|
"tricycle>=0.4.1,<0.5",
|
||||||
"wrapt>=1.16.0,<2",
|
"wrapt>=1.16.0,<2",
|
||||||
"colorlog>=6.8.2,<7",
|
"colorlog>=6.8.2,<7",
|
||||||
|
|
||||||
# built-in multi-actor `pdb` REPL
|
# built-in multi-actor `pdb` REPL
|
||||||
"pdbp>=1.8.2,<2", # windows only (from `pdbp`)
|
"pdbp>=1.8.2,<2", # windows only (from `pdbp`)
|
||||||
|
|
||||||
# typed IPC msging
|
# typed IPC msging
|
||||||
"msgspec>=0.20.0",
|
"msgspec>=0.20.0",
|
||||||
"bidict>=0.23.1",
|
"bidict>=0.23.1",
|
||||||
"multiaddr>=0.2.0",
|
"multiaddr>=0.2.0",
|
||||||
"platformdirs>=4.4.0",
|
"platformdirs>=4.4.0",
|
||||||
|
# per-actor `argv[0]` proc-title for OS-level diag tools
|
||||||
|
# (`ps`, `top`, `psutil`-backed tooling like `acli.pytree`).
|
||||||
|
# Optional at runtime — guarded by `try/except ImportError` in
|
||||||
|
# `tractor.devx._proctitle` — but listed here so default
|
||||||
|
# installs benefit from it. See tracking issue for follow-ups
|
||||||
|
# (e.g. richer formats, per-backend overrides).
|
||||||
|
"setproctitle>=1.3,<2",
|
||||||
]
|
]
|
||||||
|
|
||||||
# ------ project ------
|
# ------ project ------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
'''
|
||||||
|
Tests for `tractor.devx._proctitle` (per-actor `setproctitle`)
|
||||||
|
and the intrinsic-signal sub-actor detection in
|
||||||
|
`tractor._testing._reap`.
|
||||||
|
|
||||||
|
The proctitle is set in `tractor._child._actor_child_main()`
|
||||||
|
after `Actor` construction, so any spawned sub-actor process
|
||||||
|
should:
|
||||||
|
|
||||||
|
- have `argv[0]` (== `/proc/<pid>/cmdline`) start with
|
||||||
|
`tractor[<aid.reprol()>]`
|
||||||
|
- have `/proc/<pid>/comm` start with `tractor[` (kernel
|
||||||
|
truncates to ~15 bytes)
|
||||||
|
- be detected as a tractor sub-actor by
|
||||||
|
`_is_tractor_subactor(pid)` via the cmdline marker.
|
||||||
|
|
||||||
|
`set_actor_proctitle()` itself is also unit-tested in-process
|
||||||
|
to verify the format string.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
import platform
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
import pytest
|
||||||
|
import trio
|
||||||
|
import tractor
|
||||||
|
|
||||||
|
from tractor.runtime._runtime import Actor
|
||||||
|
from tractor.devx._proctitle import set_actor_proctitle
|
||||||
|
from tractor._testing._reap import (
|
||||||
|
_is_tractor_subactor,
|
||||||
|
_read_cmdline,
|
||||||
|
_read_comm,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_non_linux: bool = platform.system() != 'Linux'
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_actor_proctitle_format():
|
||||||
|
'''
|
||||||
|
`set_actor_proctitle()` returns the canonical
|
||||||
|
`tractor[<aid.reprol()>]` form and actually mutates
|
||||||
|
the running proc's title.
|
||||||
|
|
||||||
|
'''
|
||||||
|
pytest.importorskip(
|
||||||
|
'setproctitle',
|
||||||
|
reason='`setproctitle` is an optional runtime dep',
|
||||||
|
)
|
||||||
|
import setproctitle
|
||||||
|
|
||||||
|
# save + restore so we don't pollute pytest's own title
|
||||||
|
saved: str = setproctitle.getproctitle()
|
||||||
|
try:
|
||||||
|
actor = Actor(
|
||||||
|
name='unit_test_actor',
|
||||||
|
uuid='1027301b-a0e3-430e-8806-a5279f21abe6',
|
||||||
|
)
|
||||||
|
title: str = set_actor_proctitle(actor)
|
||||||
|
|
||||||
|
# canonical wrapping: `tractor[<aid.reprol()>]`. We
|
||||||
|
# compare against the runtime-computed `reprol()`
|
||||||
|
# rather than a hard-coded value so the test stays
|
||||||
|
# decoupled from `Aid.reprol()`'s internal format
|
||||||
|
# (currently `<name>@<pid>`, but could evolve).
|
||||||
|
expected: str = f'tractor[{actor.aid.reprol()}]'
|
||||||
|
assert title == expected
|
||||||
|
# sanity: the actor's name must be in the title
|
||||||
|
# somewhere (so a future `reprol()` change that
|
||||||
|
# drops the name is also caught).
|
||||||
|
assert 'unit_test_actor' in title
|
||||||
|
|
||||||
|
# actually set on the running proc
|
||||||
|
assert setproctitle.getproctitle() == title
|
||||||
|
|
||||||
|
finally:
|
||||||
|
setproctitle.setproctitle(saved)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
_non_linux,
|
||||||
|
reason=(
|
||||||
|
'detection helpers read `/proc/<pid>/{cmdline,comm}` '
|
||||||
|
'which is Linux-specific'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_subactor_proctitle_visible_via_proc():
|
||||||
|
'''
|
||||||
|
Spawn a sub-actor and verify its proc-title is visible
|
||||||
|
via both `/proc/<pid>/cmdline` AND `/proc/<pid>/comm`,
|
||||||
|
AND that `_is_tractor_subactor()` correctly identifies
|
||||||
|
it.
|
||||||
|
|
||||||
|
'''
|
||||||
|
pytest.importorskip('setproctitle')
|
||||||
|
|
||||||
|
async def main() -> dict:
|
||||||
|
async with tractor.open_nursery() as an:
|
||||||
|
portal = await an.start_actor('proctitle_boi')
|
||||||
|
# let the child finish setproctitle in
|
||||||
|
# `_actor_child_main`
|
||||||
|
await trio.sleep(0.3)
|
||||||
|
|
||||||
|
# the sub-actor's pid is on the portal's chan
|
||||||
|
# repr; psutil-walk `me.children()` is simpler.
|
||||||
|
me = psutil.Process()
|
||||||
|
sub_pids: list[int] = [
|
||||||
|
p.pid for p in me.children(recursive=True)
|
||||||
|
]
|
||||||
|
assert sub_pids, (
|
||||||
|
'expected at least one spawned sub-actor pid'
|
||||||
|
)
|
||||||
|
|
||||||
|
results: dict = {}
|
||||||
|
for pid in sub_pids:
|
||||||
|
results[pid] = {
|
||||||
|
'cmdline': _read_cmdline(pid),
|
||||||
|
'comm': _read_comm(pid),
|
||||||
|
'is_tractor': _is_tractor_subactor(pid),
|
||||||
|
}
|
||||||
|
|
||||||
|
await portal.cancel_actor()
|
||||||
|
return results
|
||||||
|
|
||||||
|
found: dict = trio.run(main)
|
||||||
|
|
||||||
|
# at least one of the spawned procs should match the
|
||||||
|
# `proctitle_boi` actor we started; assert the proc-
|
||||||
|
# title shape on it specifically.
|
||||||
|
matched: list[tuple[int, dict]] = [
|
||||||
|
(pid, info)
|
||||||
|
for pid, info in found.items()
|
||||||
|
if 'proctitle_boi' in info['cmdline']
|
||||||
|
]
|
||||||
|
assert matched, (
|
||||||
|
f'no sub-actor pid had a `proctitle_boi` cmdline; '
|
||||||
|
f'all={found}'
|
||||||
|
)
|
||||||
|
|
||||||
|
pid, info = matched[0]
|
||||||
|
# canonical proctitle prefix in cmdline (full form)
|
||||||
|
assert info['cmdline'].startswith('tractor[proctitle_boi@'), (
|
||||||
|
f'cmdline missing `tractor[proctitle_boi@…]` prefix: '
|
||||||
|
f'{info["cmdline"]!r}'
|
||||||
|
)
|
||||||
|
# comm is kernel-truncated to ~15 bytes — just check the
|
||||||
|
# `tractor[` prefix made it.
|
||||||
|
assert info['comm'].startswith('tractor['), (
|
||||||
|
f'comm missing `tractor[` prefix: {info["comm"]!r}'
|
||||||
|
)
|
||||||
|
# intrinsic-signal detector should match.
|
||||||
|
assert info['is_tractor'] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
_non_linux,
|
||||||
|
reason='reads /proc/<pid>/{cmdline,comm}',
|
||||||
|
)
|
||||||
|
def test_is_tractor_subactor_negative():
|
||||||
|
'''
|
||||||
|
`_is_tractor_subactor()` returns False for non-tractor
|
||||||
|
procs (e.g. the pytest test-runner pid itself, which
|
||||||
|
is `python -m pytest …` — no `tractor[` proctitle, no
|
||||||
|
`tractor._child` cmdline).
|
||||||
|
|
||||||
|
'''
|
||||||
|
import os
|
||||||
|
assert _is_tractor_subactor(os.getpid()) is False
|
||||||
|
|
@ -77,6 +77,35 @@ def _actor_child_main(
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
spawn_method=spawn_method,
|
spawn_method=spawn_method,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# XXX, set a stable OS-level proc-title BEFORE entering
|
||||||
|
# the trio runtime so `ps`/`top`/`acli.pytree` and
|
||||||
|
# orphan-reapers can identify this actor for its full
|
||||||
|
# lifetime — e.g.
|
||||||
|
# `tractor[doggy@1027301b]`
|
||||||
|
# vs. the default uninformative
|
||||||
|
# `python -m tractor._child --uid (...)`
|
||||||
|
#
|
||||||
|
# `setproctitle` mutates `argv[0]` (visible in
|
||||||
|
# `/proc/<pid>/cmdline`) AND the kernel `comm`
|
||||||
|
# (visible in `/proc/<pid>/comm`, kernel-truncated to
|
||||||
|
# ~15 bytes, but preserved through zombie state). Both
|
||||||
|
# surfaces are enough for `_testing._reap` /
|
||||||
|
# `acli.reap` orphan- and zombie-detection to identify
|
||||||
|
# tractor sub-actors via intrinsic signals — no cwd,
|
||||||
|
# venv path, or env-var coincidence-of-implementation
|
||||||
|
# matching needed.
|
||||||
|
#
|
||||||
|
# NB: an earlier draft also wrote `TRACTOR_AID` to
|
||||||
|
# `os.environ` here for `pgrep --env`-style discovery,
|
||||||
|
# but Linux snapshots `/proc/<pid>/environ` at exec/fork
|
||||||
|
# time, so post-fork runtime mutations don't propagate
|
||||||
|
# to the kernel-visible env. The proc-title path
|
||||||
|
# provides equivalent ergonomics
|
||||||
|
# (`pgrep -f 'tractor\['`) without that gotcha.
|
||||||
|
from .devx._proctitle import set_actor_proctitle
|
||||||
|
set_actor_proctitle(subactor)
|
||||||
|
|
||||||
_trio_main(
|
_trio_main(
|
||||||
subactor,
|
subactor,
|
||||||
parent_addr=parent_addr,
|
parent_addr=parent_addr,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
# tractor: structured concurrent "actors".
|
||||||
|
# Copyright 2018-eternity Tyler Goodlet.
|
||||||
|
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
'''
|
||||||
|
Per-actor proc-title via `py-setproctitle`.
|
||||||
|
|
||||||
|
Sets a stable, OS-level identifier for each `tractor` actor
|
||||||
|
process so diag tools (`ps`, `top`, `htop`, `psutil`) and our
|
||||||
|
own `acli.pytree`/`acli.hung_dump` can show "which actor is
|
||||||
|
which" at a glance without needing to read full
|
||||||
|
`/proc/<pid>/cmdline`.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
``tractor[<aid.reprol()>]`` e.g. ``tractor[doggy@1027301b]``
|
||||||
|
|
||||||
|
Uses the canonical `Aid.reprol()` form
|
||||||
|
(``<name>@<uuid_short>``) so the proc-title matches the
|
||||||
|
identifier shape used in tractor's logs, the `TRACTOR_AID`
|
||||||
|
env-var, and orphan-reaper scans — one identity across
|
||||||
|
all surfaces.
|
||||||
|
|
||||||
|
Optional dep: silently no-op when `setproctitle` is missing.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from tractor.runtime._runtime import Actor
|
||||||
|
|
||||||
|
|
||||||
|
# `setproctitle` is an optional dep — tractor's runtime path
|
||||||
|
# treats this as best-effort diag, so missing import is a
|
||||||
|
# no-op rather than a hard error.
|
||||||
|
try:
|
||||||
|
import setproctitle as _stp
|
||||||
|
except ImportError:
|
||||||
|
_stp = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_actor_proctitle(actor: 'Actor') -> str | None:
|
||||||
|
'''
|
||||||
|
Set the calling process's proc-title to identify it as a
|
||||||
|
tractor sub-actor.
|
||||||
|
|
||||||
|
Returns the title string set, or `None` if `setproctitle`
|
||||||
|
isn't available.
|
||||||
|
|
||||||
|
Should be called early in the actor's process lifetime
|
||||||
|
(after `Actor` construction, before `_trio_main`) so the
|
||||||
|
new title is visible to OS-level tooling for the entire
|
||||||
|
runtime.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if _stp is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
title: str = f'tractor[{actor.aid.reprol()}]'
|
||||||
|
_stp.setproctitle(title)
|
||||||
|
return title
|
||||||
Loading…
Reference in New Issue