[IMP] script kill_process_by_port: script to kill odoo execution with TODO
- implement todo process kill by process, read config port
This commit is contained in:
parent
2805f94477
commit
9122244aee
3 changed files with 227 additions and 0 deletions
|
|
@ -29,6 +29,7 @@ python-dateutil
|
||||||
unidecode
|
unidecode
|
||||||
sshconf
|
sshconf
|
||||||
cloudflare
|
cloudflare
|
||||||
|
psutil
|
||||||
|
|
||||||
# Force upgrade package to fix Ubuntu 25.04
|
# Force upgrade package to fix Ubuntu 25.04
|
||||||
virtualenv==20.30.0
|
virtualenv==20.30.0
|
||||||
|
|
|
||||||
193
script/process/kill_process_by_port.py
Executable file
193
script/process/kill_process_by_port.py
Executable file
|
|
@ -0,0 +1,193 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
STOP_PARENT_KILL = ["./odoo_bin.sh", "./run.sh"]
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def choose_target(chain, nb_parent):
|
||||||
|
"""
|
||||||
|
Decide which process to kill.
|
||||||
|
Default: kill the highest ancestor before PID 1 (so not systemd).
|
||||||
|
"""
|
||||||
|
if not chain:
|
||||||
|
return None
|
||||||
|
|
||||||
|
lst_chain_parent = []
|
||||||
|
for pid in chain:
|
||||||
|
lst_chain_parent.append(pid)
|
||||||
|
for str_to_stop in STOP_PARENT_KILL:
|
||||||
|
if str_to_stop in pid.cmdline():
|
||||||
|
return pid, lst_chain_parent
|
||||||
|
return chain[nb_parent - 1], [chain[nb_parent - 1]]
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Kill parent too",
|
||||||
|
)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if not (1 <= args.port <= 65535):
|
||||||
|
print("Port invalide (1-65535).", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
pids = find_listeners(args.port)
|
||||||
|
if not pids:
|
||||||
|
print(f"Aucun process en LISTEN sur {args.port}.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
for pid in pids:
|
||||||
|
chain = get_ancestry(pid)
|
||||||
|
if not chain:
|
||||||
|
continue
|
||||||
|
|
||||||
|
target, lst_target = choose_target(chain, args.nb_parent)
|
||||||
|
|
||||||
|
print(f"\nListener PID {pid} ancestry:")
|
||||||
|
for i, p in enumerate(chain):
|
||||||
|
prefix = " " + ("-> " if p == target else " ")
|
||||||
|
print(prefix + proc_desc(p))
|
||||||
|
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if target.pid == 1 and not args.allow_pid1:
|
||||||
|
print(
|
||||||
|
"Refus: la cible est PID 1 (systemd). Utilise plutôt systemctl pour arrêter le service.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
action = "KILL" if args.force else "TERM"
|
||||||
|
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(
|
||||||
|
"Toujours vivants:",
|
||||||
|
", ".join(str(p.pid) for p in alive),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
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(
|
||||||
|
f"AccessDenied: {e} (lance le script avec sudo).",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
|
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
import configparser
|
||||||
import datetime
|
import datetime
|
||||||
import getpass
|
import getpass
|
||||||
import json
|
import json
|
||||||
|
|
@ -220,6 +221,7 @@ class TODO:
|
||||||
[4] Code - Outil pour développeur
|
[4] Code - Outil pour développeur
|
||||||
[5] Doc - Recherche de documentation
|
[5] Doc - Recherche de documentation
|
||||||
[6] Database - Outils sur les bases de données
|
[6] Database - Outils sur les bases de données
|
||||||
|
[7] Process - Outils sur les exécutions
|
||||||
[0] Retour
|
[0] Retour
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -251,6 +253,10 @@ class TODO:
|
||||||
status = self.prompt_execute_database()
|
status = self.prompt_execute_database()
|
||||||
if status is not False:
|
if status is not False:
|
||||||
return
|
return
|
||||||
|
elif status == "7":
|
||||||
|
status = self.prompt_execute_process()
|
||||||
|
if status is not False:
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
print("Commande non trouvée 🤖!")
|
print("Commande non trouvée 🤖!")
|
||||||
|
|
||||||
|
|
@ -681,6 +687,23 @@ class TODO:
|
||||||
else:
|
else:
|
||||||
print("Commande non trouvée 🤖!")
|
print("Commande non trouvée 🤖!")
|
||||||
|
|
||||||
|
def prompt_execute_process(self):
|
||||||
|
print("🤖 Manipuler les processus d'exécution!")
|
||||||
|
lst_instance = [
|
||||||
|
{"prompt_description": "Kill process from actual port"},
|
||||||
|
]
|
||||||
|
help_info = self.fill_help_info(lst_instance)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
status = click.prompt(help_info)
|
||||||
|
print()
|
||||||
|
if status == "0":
|
||||||
|
return False
|
||||||
|
elif status == "1":
|
||||||
|
self.process_kill_from_port()
|
||||||
|
else:
|
||||||
|
print("Commande non trouvée 🤖!")
|
||||||
|
|
||||||
def get_odoo_version(self):
|
def get_odoo_version(self):
|
||||||
with open(VERSION_DATA_FILE) as txt:
|
with open(VERSION_DATA_FILE) as txt:
|
||||||
data_version = json.load(txt)
|
data_version = json.load(txt)
|
||||||
|
|
@ -1056,6 +1079,16 @@ class TODO:
|
||||||
source_erplibre=False,
|
source_erplibre=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def process_kill_from_port(self):
|
||||||
|
cfg = configparser.ConfigParser()
|
||||||
|
cfg.read("./config.conf")
|
||||||
|
http_port = cfg.getint("options", "http_port")
|
||||||
|
|
||||||
|
status = self.executer_commande_live(
|
||||||
|
f"./script/process/kill_process_by_port.py {http_port} --kill-tree --nb_parent 2",
|
||||||
|
source_erplibre=False,
|
||||||
|
)
|
||||||
|
|
||||||
def download_database_backup_cli(self, show_remote_list=True):
|
def download_database_backup_cli(self, show_remote_list=True):
|
||||||
database_domain = input("Domain Odoo (ex. https://mondomain.com) : ")
|
database_domain = input("Domain Odoo (ex. https://mondomain.com) : ")
|
||||||
if show_remote_list:
|
if show_remote_list:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue