mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 14:42:22 +02:00
Compare commits
3 Commits
api--set-A
...
remote_deb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
264ad31f50 | ||
|
|
cced288ddb | ||
|
|
4aa323bc20 |
3
Makefile
3
Makefile
@@ -118,6 +118,9 @@ run-worker: ## Run the main authentik worker process
|
||||
run-worker-watch: ## Run the authentik worker, with auto reloading
|
||||
watchexec --on-busy-update=restart --stop-signal=SIGINT --exts py,rs --no-meta --notify -- $(UV) run ak worker
|
||||
|
||||
debug-attach: ## Attach pdb to a running authentik Python worker (PEP 768). PID=<pid> to pick; SUDO=1 on macOS.
|
||||
$(UV) run python scripts/debug_attach.py
|
||||
|
||||
core-i18n-extract:
|
||||
$(UV) run ak makemessages \
|
||||
--add-location file \
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import faulthandler
|
||||
import os
|
||||
import random
|
||||
import signal
|
||||
@@ -76,6 +77,12 @@ def main(worker_id: int, socket_path: str):
|
||||
signal.signal(signal.SIGINT, immediate_shutdown)
|
||||
signal.signal(signal.SIGQUIT, immediate_shutdown)
|
||||
signal.signal(signal.SIGTERM, graceful_shutdown)
|
||||
# SIGUSR1 dumps every thread's traceback to stderr. Without this, the default
|
||||
# action is "terminate", which kills the worker (and trips the Rust supervisor).
|
||||
# Side-benefit: signal delivery wakes the eval loop, so `pdb -p` can attach to
|
||||
# an otherwise-idle worker parked in a C-level syscall.
|
||||
faulthandler.enable()
|
||||
faulthandler.register(signal.SIGUSR1)
|
||||
|
||||
random.seed()
|
||||
|
||||
@@ -97,7 +104,11 @@ def main(worker_id: int, socket_path: str):
|
||||
# Notify rust process that we are ready
|
||||
os.kill(os.getppid(), signal.SIGUSR2)
|
||||
|
||||
shutdown.wait()
|
||||
# Poll instead of waiting indefinitely so the main thread's eval loop ticks
|
||||
# periodically — PEP 768's debugger pending hook is serviced on the main
|
||||
# thread, and a permanent Event.wait() never returns to bytecode execution.
|
||||
while not shutdown.wait(timeout=1.0):
|
||||
pass
|
||||
|
||||
logger.info("Shutting down worker...")
|
||||
|
||||
|
||||
91
scripts/debug_attach.py
Normal file
91
scripts/debug_attach.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Attach pdb to a running authentik Python worker via PEP 768."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess # nosec B404 — needed to launch ps and pdb
|
||||
import sys
|
||||
|
||||
PS_BIN = shutil.which("ps") or "/bin/ps"
|
||||
|
||||
|
||||
def list_python_procs() -> list[tuple[int, int, str]]:
|
||||
# argv is fully controlled, no shell, ps path resolved at startup.
|
||||
out = subprocess.check_output([PS_BIN, "-eo", "pid,ppid,command"], text=True) # nosec B603
|
||||
procs: list[tuple[int, int, str]] = []
|
||||
for line in out.splitlines()[1:]:
|
||||
try:
|
||||
pid_s, ppid_s, cmd = line.split(None, 2)
|
||||
except ValueError:
|
||||
continue
|
||||
if not pid_s.isdigit() or not ppid_s.isdigit():
|
||||
continue
|
||||
procs.append((int(pid_s), int(ppid_s), cmd))
|
||||
return procs
|
||||
|
||||
|
||||
def find_targets() -> list[tuple[int, str]]:
|
||||
# Match any authentik Python process: dev_server / runserver via manage.py,
|
||||
# gunicorn, dramatiq, or the worker_process supervisor. Go/Rust supervisors
|
||||
# and the `uv run` / shell wrappers don't match these patterns.
|
||||
needles = (
|
||||
"manage.py",
|
||||
"manage dev_server",
|
||||
"manage runserver",
|
||||
"gunicorn",
|
||||
"dramatiq",
|
||||
"lifecycle.worker_process",
|
||||
"lifecycle/worker_process",
|
||||
)
|
||||
matches = [(p, pp, c) for p, pp, c in list_python_procs() if any(n in c for n in needles)]
|
||||
matched_pids = {p for p, _, _ in matches}
|
||||
parents_of_matches = {pp for _, pp, _ in matches if pp in matched_pids}
|
||||
# A leaf is a match that isn't itself the parent of another match — this
|
||||
# picks the dev_server reloader child or the gunicorn worker, and still
|
||||
# includes single-process workers (which trivially have no child match).
|
||||
leaves = [(p, c) for p, _, c in matches if p not in parents_of_matches]
|
||||
return leaves
|
||||
|
||||
|
||||
def attach(pid: int) -> int:
|
||||
use_sudo = os.environ.get("SUDO") == "1"
|
||||
cmd = [sys.executable, "-m", "pdb", "-p", str(pid)]
|
||||
if use_sudo:
|
||||
cmd = ["sudo", "-E", *cmd]
|
||||
print(f"attaching pdb to pid {pid} (Ctrl-D or `quit` to detach)", file=sys.stderr)
|
||||
# cmd is built from sys.executable plus a digits-only PID.
|
||||
rc = subprocess.call(cmd) # nosec B603
|
||||
if rc != 0 and not use_sudo and sys.platform == "darwin":
|
||||
print(
|
||||
"\nattach failed. On macOS task_for_pid is restricted; "
|
||||
f"retry with: SUDO=1 make debug-attach PID={pid}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return rc
|
||||
|
||||
|
||||
def main() -> int:
|
||||
env_pid = os.environ.get("PID")
|
||||
if env_pid:
|
||||
if not env_pid.isdigit():
|
||||
print(f"PID={env_pid!r} is not numeric", file=sys.stderr)
|
||||
return 2
|
||||
return attach(int(env_pid))
|
||||
|
||||
targets = find_targets()
|
||||
if not targets:
|
||||
print(
|
||||
"no gunicorn/dramatiq Python workers found — is `make run-server` "
|
||||
"or `make run-worker` running?",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if len(targets) > 1:
|
||||
print("multiple worker candidates — pick one with PID=<pid>:", file=sys.stderr)
|
||||
for pid, cmd in targets:
|
||||
print(f" {pid}\t{cmd[:120]}", file=sys.stderr)
|
||||
return 1
|
||||
return attach(targets[0][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user