2025-12-22 04:02:44 -05:00
|
|
|
#!/usr/bin/env python3
|
2026-03-11 23:15:07 -04:00
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
2025-12-22 04:02:44 -05:00
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
import argparse
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
import psutil
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
WRAPPER_SCRIPT_NAMES = ["./odoo_bin.sh", "./run.sh"]
|
2025-12-22 04:02:44 -05:00
|
|
|
|
2026-03-09 16:18:14 -04:00
|
|
|
# Processes that must never be killed — killing these
|
|
|
|
|
# can crash the desktop session or the system.
|
|
|
|
|
PROTECTED_NAMES = {
|
|
|
|
|
"systemd",
|
|
|
|
|
"init",
|
|
|
|
|
"gnome-session",
|
|
|
|
|
"gnome-session-binary",
|
|
|
|
|
"gnome-shell",
|
|
|
|
|
"gdm",
|
|
|
|
|
"gdm3",
|
|
|
|
|
"gdm-session-worker",
|
|
|
|
|
"lightdm",
|
|
|
|
|
"sddm",
|
|
|
|
|
"Xorg",
|
|
|
|
|
"Xwayland",
|
|
|
|
|
"plasmashell",
|
|
|
|
|
"kwin_wayland",
|
|
|
|
|
"kwin_x11",
|
|
|
|
|
"loginctl",
|
|
|
|
|
"login",
|
|
|
|
|
"sshd",
|
|
|
|
|
"tmux: server",
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 04:02:44 -05:00
|
|
|
|
|
|
|
|
def proc_desc(p: psutil.Process) -> str:
|
|
|
|
|
try:
|
|
|
|
|
cmd = " ".join(p.cmdline()) if p.cmdline() else p.name()
|
|
|
|
|
return f"pid={p.pid} user={p.username()} name={p.name()} cmd={cmd}"
|
|
|
|
|
except psutil.Error:
|
|
|
|
|
return f"pid={p.pid}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_ancestry(pid: int):
|
|
|
|
|
"""
|
|
|
|
|
Returns list of psutil.Process from child -> parent -> ... until PID 1 or missing.
|
|
|
|
|
"""
|
|
|
|
|
chain = []
|
|
|
|
|
try:
|
|
|
|
|
p = psutil.Process(pid)
|
|
|
|
|
except psutil.NoSuchProcess:
|
|
|
|
|
return chain
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
chain.append(p)
|
|
|
|
|
try:
|
|
|
|
|
ppid = p.ppid()
|
|
|
|
|
except psutil.Error:
|
|
|
|
|
break
|
|
|
|
|
if ppid <= 0 or ppid == p.pid:
|
|
|
|
|
break
|
|
|
|
|
try:
|
|
|
|
|
p = psutil.Process(ppid)
|
|
|
|
|
except psutil.NoSuchProcess:
|
|
|
|
|
break
|
|
|
|
|
if p.pid == 1:
|
|
|
|
|
chain.append(p)
|
|
|
|
|
break
|
|
|
|
|
return chain
|
|
|
|
|
|
|
|
|
|
|
2026-03-10 03:06:07 -04:00
|
|
|
def choose_target(chain, parent_depth):
|
2025-12-22 04:02:44 -05:00
|
|
|
"""
|
|
|
|
|
Decide which process to kill.
|
|
|
|
|
Default: kill the highest ancestor before PID 1 (so not systemd).
|
|
|
|
|
"""
|
|
|
|
|
if not chain:
|
|
|
|
|
return None
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
ancestors = []
|
|
|
|
|
for proc in chain:
|
|
|
|
|
ancestors.append(proc)
|
|
|
|
|
for wrapper in WRAPPER_SCRIPT_NAMES:
|
|
|
|
|
if wrapper in proc.cmdline():
|
|
|
|
|
return proc, ancestors
|
2026-03-10 03:06:07 -04:00
|
|
|
return chain[parent_depth - 1], [chain[parent_depth - 1]]
|
2025-12-22 04:02:44 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def kill_process(p: psutil.Process, force: bool):
|
|
|
|
|
if force:
|
|
|
|
|
p.kill()
|
|
|
|
|
else:
|
|
|
|
|
p.terminate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def kill_tree(root: psutil.Process, force: bool):
|
|
|
|
|
"""
|
|
|
|
|
Kill root and all its children (best-effort).
|
|
|
|
|
"""
|
|
|
|
|
children = root.children(recursive=True)
|
|
|
|
|
# terminate children first
|
|
|
|
|
for ch in children:
|
|
|
|
|
try:
|
|
|
|
|
kill_process(ch, force=force)
|
|
|
|
|
except psutil.Error:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
kill_process(root, force=force)
|
|
|
|
|
except psutil.Error:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# wait a bit
|
|
|
|
|
gone, alive = psutil.wait_procs(children + [root], timeout=3)
|
|
|
|
|
return alive
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_listeners(port: int):
|
|
|
|
|
pids = set()
|
|
|
|
|
for c in psutil.net_connections(kind="tcp"):
|
|
|
|
|
if (
|
|
|
|
|
c.laddr
|
|
|
|
|
and c.laddr.port == port
|
|
|
|
|
and c.status == psutil.CONN_LISTEN
|
|
|
|
|
and c.pid
|
|
|
|
|
):
|
|
|
|
|
pids.add(c.pid)
|
|
|
|
|
return sorted(pids)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
ap = argparse.ArgumentParser(
|
|
|
|
|
description="Free a TCP port by killing the parent (or process tree) of the listener."
|
|
|
|
|
)
|
|
|
|
|
ap.add_argument("port", type=int, help="TCP port (1-65535)")
|
|
|
|
|
ap.add_argument(
|
|
|
|
|
"-n",
|
|
|
|
|
"--dry-run",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Show what would be killed, do nothing",
|
|
|
|
|
)
|
|
|
|
|
ap.add_argument(
|
|
|
|
|
"-f",
|
|
|
|
|
"--force",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Use SIGKILL instead of SIGTERM",
|
|
|
|
|
)
|
|
|
|
|
ap.add_argument(
|
|
|
|
|
"--kill-tree",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Kill target and all its children recursively",
|
|
|
|
|
)
|
|
|
|
|
ap.add_argument(
|
|
|
|
|
"--nb_parent",
|
2026-03-10 03:06:07 -04:00
|
|
|
dest="parent_depth",
|
2025-12-22 04:02:44 -05:00
|
|
|
type=int,
|
|
|
|
|
default=1,
|
|
|
|
|
help="Kill parent too",
|
|
|
|
|
)
|
2026-03-09 16:18:14 -04:00
|
|
|
ap.add_argument(
|
|
|
|
|
"--automatic",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Ignore ask confirmation before killing",
|
|
|
|
|
)
|
2025-12-22 04:02:44 -05:00
|
|
|
args = ap.parse_args()
|
|
|
|
|
|
|
|
|
|
if not (1 <= args.port <= 65535):
|
2026-03-10 03:45:11 -04:00
|
|
|
print("Invalid port (1-65535).", file=sys.stderr)
|
2025-12-22 04:02:44 -05:00
|
|
|
return 2
|
|
|
|
|
|
|
|
|
|
pids = find_listeners(args.port)
|
|
|
|
|
if not pids:
|
2026-03-10 03:45:11 -04:00
|
|
|
print(f"No process listening on port {args.port}.")
|
2025-12-22 04:02:44 -05:00
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
for pid in pids:
|
|
|
|
|
chain = get_ancestry(pid)
|
|
|
|
|
if not chain:
|
|
|
|
|
continue
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
target, target_chain = choose_target(chain, args.parent_depth)
|
2025-12-22 04:02:44 -05:00
|
|
|
|
|
|
|
|
print(f"\nListener PID {pid} ancestry:")
|
|
|
|
|
for i, p in enumerate(chain):
|
2026-03-09 16:18:14 -04:00
|
|
|
prefix = " " + ("-> " if p == target else " ")
|
|
|
|
|
print(f"[{i}]" + prefix + proc_desc(p))
|
2025-12-22 04:02:44 -05:00
|
|
|
|
|
|
|
|
if target is None:
|
|
|
|
|
continue
|
|
|
|
|
|
2026-03-09 16:18:14 -04:00
|
|
|
if target.pid == 1:
|
2025-12-22 04:02:44 -05:00
|
|
|
print(
|
2026-03-10 03:45:11 -04:00
|
|
|
"Refused: target is PID 1 (systemd)."
|
|
|
|
|
" Use systemctl to stop the service"
|
|
|
|
|
" instead.",
|
2026-03-09 16:18:14 -04:00
|
|
|
)
|
|
|
|
|
continue
|
|
|
|
|
if target.name() in PROTECTED_NAMES:
|
|
|
|
|
print(
|
2026-03-10 03:45:11 -04:00
|
|
|
f"Refused: process '{target.name()}' is protected and dangerous to kill.",
|
2025-12-22 04:02:44 -05:00
|
|
|
)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
action = "KILL" if args.force else "TERM"
|
2026-03-09 16:18:14 -04:00
|
|
|
|
|
|
|
|
if not args.automatic:
|
|
|
|
|
has_response = False
|
|
|
|
|
ignore_kill = False
|
|
|
|
|
while not has_response and not ignore_kill:
|
|
|
|
|
confirm = (
|
|
|
|
|
input(
|
2026-03-10 03:45:11 -04:00
|
|
|
f"Kill process at index {args.parent_depth} (enter) or enter "
|
|
|
|
|
f"index [0 to {len(chain) - 1}] of "
|
|
|
|
|
f"process to kill, (c/C) to cancel: \n"
|
2026-03-09 16:18:14 -04:00
|
|
|
)
|
|
|
|
|
.strip()
|
|
|
|
|
.lower()
|
|
|
|
|
)
|
|
|
|
|
if not confirm:
|
|
|
|
|
has_response = True
|
|
|
|
|
continue
|
|
|
|
|
if confirm == "c":
|
|
|
|
|
ignore_kill = True
|
|
|
|
|
print("Cancel")
|
|
|
|
|
continue
|
|
|
|
|
if confirm.isdigit():
|
|
|
|
|
has_response = True
|
|
|
|
|
target = chain[int(confirm)]
|
|
|
|
|
if ignore_kill:
|
|
|
|
|
continue
|
|
|
|
|
|
2025-12-22 04:02:44 -05:00
|
|
|
if args.kill_tree:
|
|
|
|
|
print(f"Target: pid={target.pid} ({action}) + children (tree)")
|
|
|
|
|
if not args.dry_run:
|
|
|
|
|
alive = kill_tree(target, force=args.force)
|
|
|
|
|
if alive:
|
|
|
|
|
print(
|
2026-03-10 03:45:11 -04:00
|
|
|
"Still alive:",
|
2025-12-22 04:02:44 -05:00
|
|
|
", ".join(str(p.pid) for p in alive),
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
print(f"Target: pid={target.pid} ({action})")
|
|
|
|
|
if not args.dry_run:
|
|
|
|
|
try:
|
|
|
|
|
kill_process(target, force=args.force)
|
|
|
|
|
except psutil.AccessDenied as e:
|
|
|
|
|
print(
|
2026-03-10 03:45:11 -04:00
|
|
|
f"AccessDenied: {e} (run the script with sudo).",
|
2025-12-22 04:02:44 -05:00
|
|
|
file=sys.stderr,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|