From 300cc8ad105a4e1a17e1e0d5586ceeb68b380bca Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Fri, 8 Aug 2025 12:50:28 +0100 Subject: [PATCH 01/10] Add utility to kill old (except newest) processes --- kill.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100755 kill.py diff --git a/kill.py b/kill.py new file mode 100755 index 0000000..23c3e3f --- /dev/null +++ b/kill.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +# Deskflow -- mouse and keyboard sharing utility +# SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception +# Copyright (C) 2025 Symless Ltd. + +# A utility to kill processes by name. Using Python avoids writing platform-specific shell scripts. +# +# One function is to kill all but the newest instance of a process, which is useful when developing +# Deskflow to avoid conflicts with multiple instances of the same process. + + +import argparse +import psutil +import sys + + +def main(): + parser = argparse.ArgumentParser( + description="A cross-platform kill utility tuned for Deskflow development." + ) + parser.add_argument("names", nargs="+", help="Process names to target") + parser.add_argument( + "--keep-newest", + action="store_true", + help="Keep the newest instance of the process (default: kill all)", + ) + args = parser.parse_args() + kill_all(args.names, args.keep_newest) + + +def kill_all(names, keep_newest=False): + killed = 0 + for raw in names: + name = raw.lower() + matches = [] + + if not matches: + continue + + if keep_newest: + # Sort newest first, keep the first, kill the rest + matches.sort(key=lambda p: p.info.get("create_time", 0), reverse=True) + to_keep = matches[0] + to_kill = matches[1:] + print( + f"Keeping newest {raw} (PID {to_keep.pid}), killing {len(to_kill)} others" + ) + else: + to_kill = matches + print(f"Killing all {raw} processes ({len(to_kill)} found)") + + for proc in to_kill: + try: + print(f"Terminating PID {proc.pid}") + proc.terminate() + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + _, alive = psutil.wait_procs(to_kill, timeout=2) + for proc in alive: + try: + print(f"Force killing PID {proc.pid}") + proc.kill() + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + killed += 1 + + print(f"Processes killed: {killed}") + + +if __name__ == "__main__": + main() From 412fee83a85321e4c79c802aad1dc0ada6bcee68 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Fri, 8 Aug 2025 13:44:24 +0100 Subject: [PATCH 02/10] Refactor kill utility to improve process termination logic and enhance readability --- kill.py | 94 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/kill.py b/kill.py index 23c3e3f..fbf0840 100755 --- a/kill.py +++ b/kill.py @@ -7,7 +7,9 @@ # A utility to kill processes by name. Using Python avoids writing platform-specific shell scripts. # # One function is to kill all but the newest instance of a process, which is useful when developing -# Deskflow to avoid conflicts with multiple instances of the same process. +# Deskflow to avoid conflicts with multiple instances of the same process. This will be less useful +# when we eventually dedupe the core processes, but even after then this will help with bugs where +# multiple instances of the same process are created. import argparse @@ -29,43 +31,63 @@ def main(): kill_all(args.names, args.keep_newest) +def get_kill_lists(name, matches, keep_newest): + if not keep_newest: + print(f"Killing all {name} processes ({len(matches)} found)") + return matches + + # Sort newest first, keep the first, kill the rest + matches.sort(key=lambda p: p.info.get("create_time", 0), reverse=True) + to_keep = matches[0] + if len(matches) == 1: + print(f"Keeping only {name} (PID {to_keep.pid}), nothing to kill") + return [] + + to_kill = matches[1:] + print(f"Keeping newest {name} (PID {to_keep.pid}), killing {len(to_kill)}") + + return to_kill + + +def kill(raw_name, keep_newest): + name = raw_name.lower() + matches = [] + for proc in psutil.process_iter(attrs=["pid", "name", "create_time"]): + try: + if name == proc.info["name"].lower(): + matches.append(proc) + except (psutil.NoSuchProcess, psutil.AccessDenied): + print(f"Process {proc.pid} no longer exists or access denied") + return False + + if not matches: + print(f"No processes found for '{raw_name}'") + return False + + to_kill = get_kill_lists(name, matches, keep_newest) + + for proc in to_kill: + try: + print(f"Terminating PID {proc.pid}") + proc.terminate() + except (psutil.NoSuchProcess, psutil.AccessDenied): + return False + + _, alive = psutil.wait_procs(to_kill, timeout=2) + for proc in alive: + try: + print(f"Force killing PID {proc.pid}") + proc.kill() + except (psutil.NoSuchProcess, psutil.AccessDenied): + return False + + return True + + def kill_all(names, keep_newest=False): killed = 0 - for raw in names: - name = raw.lower() - matches = [] - - if not matches: - continue - - if keep_newest: - # Sort newest first, keep the first, kill the rest - matches.sort(key=lambda p: p.info.get("create_time", 0), reverse=True) - to_keep = matches[0] - to_kill = matches[1:] - print( - f"Keeping newest {raw} (PID {to_keep.pid}), killing {len(to_kill)} others" - ) - else: - to_kill = matches - print(f"Killing all {raw} processes ({len(to_kill)} found)") - - for proc in to_kill: - try: - print(f"Terminating PID {proc.pid}") - proc.terminate() - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - - _, alive = psutil.wait_procs(to_kill, timeout=2) - for proc in alive: - try: - print(f"Force killing PID {proc.pid}") - proc.kill() - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - - killed += 1 + for name in names: + killed += 1 if kill(name, keep_newest) else 0 print(f"Processes killed: {killed}") From d33fbee1bcaa902a66f4f3278afa8fde4f8f7e0d Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Fri, 8 Aug 2025 13:48:31 +0100 Subject: [PATCH 03/10] Fix kill function to return the number of processes killed instead of a static True --- kill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kill.py b/kill.py index fbf0840..6585f36 100755 --- a/kill.py +++ b/kill.py @@ -81,7 +81,7 @@ def kill(raw_name, keep_newest): except (psutil.NoSuchProcess, psutil.AccessDenied): return False - return True + return len(to_kill) > 0 def kill_all(names, keep_newest=False): From bba6722e20e8bbbca5373646b001744bb89ba856 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Fri, 8 Aug 2025 14:19:50 +0100 Subject: [PATCH 04/10] Fix process name handling for Windows by appending '.exe' when necessary --- kill.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/kill.py b/kill.py index 6585f36..3816681 100755 --- a/kill.py +++ b/kill.py @@ -49,8 +49,16 @@ def get_kill_lists(name, matches, keep_newest): return to_kill -def kill(raw_name, keep_newest): +def get_process_name(raw_name): name = raw_name.lower() + if sys.platform == "win32" and not name.endswith(".exe"): + return f"{name}.exe" + + return name + + +def kill(raw_name, keep_newest): + name = get_process_name(raw_name) matches = [] for proc in psutil.process_iter(attrs=["pid", "name", "create_time"]): try: From dc6272cc204e6d78a8f160f8348b0f3b2898b33b Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Tue, 12 Aug 2025 14:18:17 +0100 Subject: [PATCH 05/10] Enhance kill utility with watch mode and verbose logging options --- kill.py | 53 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/kill.py b/kill.py index 3816681..88c236d 100755 --- a/kill.py +++ b/kill.py @@ -15,6 +15,7 @@ import argparse import psutil import sys +import time def main(): @@ -27,11 +28,32 @@ def main(): action="store_true", help="Keep the newest instance of the process (default: kill all)", ) + parser.add_argument( + "--watch", + action="store_true", + help="Watches and keeps looking for processes to kill" + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Enable verbose logging output" + ) args = parser.parse_args() - kill_all(args.names, args.keep_newest) + try: + if args.watch: + print("Watching for processes to kill. Press Ctrl+C to exit.") + + while args.watch: + kill_all(args.names, args.keep_newest, args.verbose) + + if args.watch: + time.sleep(1) + except KeyboardInterrupt: + print("Exiting...") + sys.exit(0) -def get_kill_lists(name, matches, keep_newest): +def get_kill_lists(name, matches, keep_newest, verbose_logs): if not keep_newest: print(f"Killing all {name} processes ({len(matches)} found)") return matches @@ -40,11 +62,13 @@ def get_kill_lists(name, matches, keep_newest): matches.sort(key=lambda p: p.info.get("create_time", 0), reverse=True) to_keep = matches[0] if len(matches) == 1: - print(f"Keeping only {name} (PID {to_keep.pid}), nothing to kill") + if verbose_logs: + print(f"Keeping only {name} (PID {to_keep.pid}), nothing to kill") return [] to_kill = matches[1:] - print(f"Keeping newest {name} (PID {to_keep.pid}), killing {len(to_kill)}") + if verbose_logs: + print(f"Keeping newest {name} (PID {to_keep.pid}), killing {len(to_kill)}") return to_kill @@ -57,7 +81,7 @@ def get_process_name(raw_name): return name -def kill(raw_name, keep_newest): +def kill(raw_name, keep_newest, verbose_logs): name = get_process_name(raw_name) matches = [] for proc in psutil.process_iter(attrs=["pid", "name", "create_time"]): @@ -69,14 +93,15 @@ def kill(raw_name, keep_newest): return False if not matches: - print(f"No processes found for '{raw_name}'") + if verbose_logs: + print(f"No processes found for '{raw_name}'") return False - to_kill = get_kill_lists(name, matches, keep_newest) + to_kill = get_kill_lists(name, matches, keep_newest, verbose_logs) for proc in to_kill: try: - print(f"Terminating PID {proc.pid}") + print(f"Terminating PID {proc.pid} ({proc.info['name']})") proc.terminate() except (psutil.NoSuchProcess, psutil.AccessDenied): return False @@ -84,7 +109,7 @@ def kill(raw_name, keep_newest): _, alive = psutil.wait_procs(to_kill, timeout=2) for proc in alive: try: - print(f"Force killing PID {proc.pid}") + print(f"Force killing PID {proc.pid} ({proc.info['name']})") proc.kill() except (psutil.NoSuchProcess, psutil.AccessDenied): return False @@ -92,12 +117,16 @@ def kill(raw_name, keep_newest): return len(to_kill) > 0 -def kill_all(names, keep_newest=False): +def kill_all(names, keep_newest, verbose_logs): killed = 0 for name in names: - killed += 1 if kill(name, keep_newest) else 0 + killed += 1 if kill(name, keep_newest, verbose_logs) else 0 - print(f"Processes killed: {killed}") + if killed == 0: + if verbose_logs: + print("No processes killed") + else: + print(f"Processes killed: {killed}") if __name__ == "__main__": From 164856a391cdba0a022268acda0f340012adc0e7 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Tue, 12 Aug 2025 14:25:39 +0100 Subject: [PATCH 06/10] Improve exit message formatting in main function --- kill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kill.py b/kill.py index 88c236d..cf86429 100755 --- a/kill.py +++ b/kill.py @@ -49,7 +49,7 @@ def main(): if args.watch: time.sleep(1) except KeyboardInterrupt: - print("Exiting...") + print("\nExiting...") sys.exit(0) From 5e809378c963cf872101a2620ce44848730960ec Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 13 Aug 2025 12:42:57 +0100 Subject: [PATCH 07/10] Fix copyright notice formatting and improve argument help text in kill utility --- kill.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/kill.py b/kill.py index cf86429..c54ad72 100755 --- a/kill.py +++ b/kill.py @@ -2,7 +2,7 @@ # Deskflow -- mouse and keyboard sharing utility # SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception -# Copyright (C) 2025 Symless Ltd. +# SPDX-FileCopyrightText: 2025 Symless Ltd. # A utility to kill processes by name. Using Python avoids writing platform-specific shell scripts. # @@ -31,12 +31,10 @@ def main(): parser.add_argument( "--watch", action="store_true", - help="Watches and keeps looking for processes to kill" + help="Watches and keeps looking for processes to kill", ) parser.add_argument( - "--verbose", - action="store_true", - help="Enable verbose logging output" + "--verbose", action="store_true", help="Enable verbose logging output" ) args = parser.parse_args() try: From 0fcd2b8b4ab90e81c3a1c9be7956db969716b45c Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 13 Aug 2025 12:46:12 +0100 Subject: [PATCH 08/10] Add timestamp to logging --- kill.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/kill.py b/kill.py index c54ad72..778e353 100755 --- a/kill.py +++ b/kill.py @@ -18,6 +18,9 @@ import time +def log(message): + print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {message}") + def main(): parser = argparse.ArgumentParser( description="A cross-platform kill utility tuned for Deskflow development." @@ -39,7 +42,7 @@ def main(): args = parser.parse_args() try: if args.watch: - print("Watching for processes to kill. Press Ctrl+C to exit.") + log("Watching for processes to kill. Press Ctrl+C to exit.") while args.watch: kill_all(args.names, args.keep_newest, args.verbose) @@ -47,13 +50,13 @@ def main(): if args.watch: time.sleep(1) except KeyboardInterrupt: - print("\nExiting...") + log("\nExiting...") sys.exit(0) def get_kill_lists(name, matches, keep_newest, verbose_logs): if not keep_newest: - print(f"Killing all {name} processes ({len(matches)} found)") + log(f"Killing all {name} processes ({len(matches)} found)") return matches # Sort newest first, keep the first, kill the rest @@ -61,12 +64,12 @@ def get_kill_lists(name, matches, keep_newest, verbose_logs): to_keep = matches[0] if len(matches) == 1: if verbose_logs: - print(f"Keeping only {name} (PID {to_keep.pid}), nothing to kill") + log(f"Keeping only {name} (PID {to_keep.pid}), nothing to kill") return [] to_kill = matches[1:] if verbose_logs: - print(f"Keeping newest {name} (PID {to_keep.pid}), killing {len(to_kill)}") + log(f"Keeping newest {name} (PID {to_keep.pid}), killing {len(to_kill)}") return to_kill @@ -87,19 +90,19 @@ def kill(raw_name, keep_newest, verbose_logs): if name == proc.info["name"].lower(): matches.append(proc) except (psutil.NoSuchProcess, psutil.AccessDenied): - print(f"Process {proc.pid} no longer exists or access denied") + log(f"Process {proc.pid} no longer exists or access denied") return False if not matches: if verbose_logs: - print(f"No processes found for '{raw_name}'") + log(f"No processes found for '{raw_name}'") return False to_kill = get_kill_lists(name, matches, keep_newest, verbose_logs) for proc in to_kill: try: - print(f"Terminating PID {proc.pid} ({proc.info['name']})") + log(f"Terminating PID {proc.pid} ({proc.info['name']})") proc.terminate() except (psutil.NoSuchProcess, psutil.AccessDenied): return False @@ -107,7 +110,7 @@ def kill(raw_name, keep_newest, verbose_logs): _, alive = psutil.wait_procs(to_kill, timeout=2) for proc in alive: try: - print(f"Force killing PID {proc.pid} ({proc.info['name']})") + log(f"Force killing PID {proc.pid} ({proc.info['name']})") proc.kill() except (psutil.NoSuchProcess, psutil.AccessDenied): return False @@ -122,9 +125,9 @@ def kill_all(names, keep_newest, verbose_logs): if killed == 0: if verbose_logs: - print("No processes killed") + log("No processes killed") else: - print(f"Processes killed: {killed}") + log(f"Processes killed: {killed}") if __name__ == "__main__": From a67f47d13cfe4f7256097bc834bf7e70269ce53a Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 20 Aug 2025 11:21:39 +0100 Subject: [PATCH 09/10] Add mode for killing orphaned Core --- kill.py | 129 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 91 insertions(+), 38 deletions(-) diff --git a/kill.py b/kill.py index 778e353..dca4fdf 100755 --- a/kill.py +++ b/kill.py @@ -16,20 +16,30 @@ import psutil import sys import time +import enum -def log(message): - print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {message}") +class KillMode(enum.Enum): + KEEP_NEWEST = "keep-newest" + KILL_ORPHANS = "kill-orphans" + def main(): parser = argparse.ArgumentParser( description="A cross-platform kill utility tuned for Deskflow development." ) - parser.add_argument("names", nargs="+", help="Process names to target") parser.add_argument( - "--keep-newest", - action="store_true", - help="Keep the newest instance of the process (default: kill all)", + "mode", + type=killmode_type, + choices=list(KillMode), + help=( + "Kill mode:\n" + " keep-newest: Keep the newest instance of matching processes.\n" + " kill-orphans: Kill orphaned processes (e.g. Core that has no GUI)." + ), + ) + parser.add_argument( + "--names", nargs="+", required=True, help="Process names to kill" ) parser.add_argument( "--watch", @@ -40,12 +50,25 @@ def main(): "--verbose", action="store_true", help="Enable verbose logging output" ) args = parser.parse_args() - try: - if args.watch: - log("Watching for processes to kill. Press Ctrl+C to exit.") + if args.verbose: + log(f"Starting kill utility with mode: {args.mode.value}") + log(f"Processes to kill: {', '.join(args.names)}") + + if args.watch: + log("Watching for processes to kill. Press Ctrl+C to exit.") + + try: while args.watch: - kill_all(args.names, args.keep_newest, args.verbose) + killed = 0 + for name in args.names: + killed += 1 if kill(name, args.mode, args.verbose) else 0 + + if killed == 0: + if args.verbose: + log("No processes killed") + else: + log(f"Processes killed: {killed}") if args.watch: time.sleep(1) @@ -54,22 +77,64 @@ def main(): sys.exit(0) -def get_kill_lists(name, matches, keep_newest, verbose_logs): - if not keep_newest: - log(f"Killing all {name} processes ({len(matches)} found)") - return matches +def log(message): + print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {message}") + + +def killmode_type(value): + try: + return KillMode(value) + except ValueError: + raise argparse.ArgumentTypeError(f"Invalid mode: {value}") + + +def get_kill_lists(name, matches, mode, verbose): # Sort newest first, keep the first, kill the rest - matches.sort(key=lambda p: p.info.get("create_time", 0), reverse=True) - to_keep = matches[0] - if len(matches) == 1: - if verbose_logs: - log(f"Keeping only {name} (PID {to_keep.pid}), nothing to kill") - return [] + if mode == KillMode.KEEP_NEWEST: + if verbose: + log("Looking for processes, keeping newest instances") + + matches.sort(key=lambda p: p.info.get("create_time", 0), reverse=True) + to_keep = matches[0] + if len(matches) == 1: + if verbose: + log(f"Keeping only {name} (PID {to_keep.pid}), nothing to kill") + return [] + + to_kill = matches[1:] + if verbose: + log(f"Keeping newest {name} (PID {to_keep.pid}), killing {len(to_kill)}") + + # Kill processes that have no parent or are orphaned + elif mode == KillMode.KILL_ORPHANS: + if verbose: + log("Looking for orphaned processes to kill") + + to_kill = [] + for proc in matches: + if verbose: + log(f"Checking process {proc.pid} ({proc.info['name']})") + log( + f"Parent PID: {proc.ppid()} ({proc.parent().name() if proc.parent() else 'No parent'})" + ) + + try: + # When the GUI is killed but the Core stays alive, it becomes owned by systemd. + parent_systemd = proc.parent() and proc.parent().name() == "systemd" + if parent_systemd or proc.ppid() == 0: + to_kill.append(proc) + except (psutil.NoSuchProcess, psutil.AccessDenied): + log(f"Process {proc.pid} no longer exists or access denied") + continue + + if not to_kill: + if verbose: + log(f"No orphaned processes found for '{name}'") + return [] - to_kill = matches[1:] - if verbose_logs: - log(f"Keeping newest {name} (PID {to_keep.pid}), killing {len(to_kill)}") + else: + raise ValueError(f"Unknown kill mode: {mode}") return to_kill @@ -82,7 +147,7 @@ def get_process_name(raw_name): return name -def kill(raw_name, keep_newest, verbose_logs): +def kill(raw_name, mode, verbose): name = get_process_name(raw_name) matches = [] for proc in psutil.process_iter(attrs=["pid", "name", "create_time"]): @@ -94,11 +159,11 @@ def kill(raw_name, keep_newest, verbose_logs): return False if not matches: - if verbose_logs: + if verbose: log(f"No processes found for '{raw_name}'") return False - to_kill = get_kill_lists(name, matches, keep_newest, verbose_logs) + to_kill = get_kill_lists(name, matches, mode, verbose) for proc in to_kill: try: @@ -118,17 +183,5 @@ def kill(raw_name, keep_newest, verbose_logs): return len(to_kill) > 0 -def kill_all(names, keep_newest, verbose_logs): - killed = 0 - for name in names: - killed += 1 if kill(name, keep_newest, verbose_logs) else 0 - - if killed == 0: - if verbose_logs: - log("No processes killed") - else: - log(f"Processes killed: {killed}") - - if __name__ == "__main__": main() From b61b6b641285708ae01ea3e710d614e4f82ef3c5 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 20 Aug 2025 11:54:22 +0100 Subject: [PATCH 10/10] Use flags rather than mode for orphan/keep newest logic --- kill.py | 70 +++++++++++++++++++++------------------------------------ 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/kill.py b/kill.py index dca4fdf..95a9dab 100755 --- a/kill.py +++ b/kill.py @@ -19,24 +19,19 @@ import enum -class KillMode(enum.Enum): - KEEP_NEWEST = "keep-newest" - KILL_ORPHANS = "kill-orphans" - - def main(): parser = argparse.ArgumentParser( description="A cross-platform kill utility tuned for Deskflow development." ) parser.add_argument( - "mode", - type=killmode_type, - choices=list(KillMode), - help=( - "Kill mode:\n" - " keep-newest: Keep the newest instance of matching processes.\n" - " kill-orphans: Kill orphaned processes (e.g. Core that has no GUI)." - ), + "--orphans", + action="store_true", + help="Kill orphaned processes (e.g. Core that has no GUI)." + ) + parser.add_argument( + "--keep-newest", + action="store_true", + help="Keep only the newest instance of matching processes." ) parser.add_argument( "--names", nargs="+", required=True, help="Process names to kill" @@ -52,7 +47,7 @@ def main(): args = parser.parse_args() if args.verbose: - log(f"Starting kill utility with mode: {args.mode.value}") + log(f"Starting with options: orphans={args.orphans}, keep-newest={args.keep_newest}") log(f"Processes to kill: {', '.join(args.names)}") if args.watch: @@ -62,7 +57,7 @@ def main(): while args.watch: killed = 0 for name in args.names: - killed += 1 if kill(name, args.mode, args.verbose) else 0 + killed += 1 if kill(name, args.orphans, args.keep_newest, args.verbose) else 0 if killed == 0: if args.verbose: @@ -80,18 +75,11 @@ def main(): def log(message): print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {message}") - -def killmode_type(value): - try: - return KillMode(value) - except ValueError: - raise argparse.ArgumentTypeError(f"Invalid mode: {value}") - - -def get_kill_lists(name, matches, mode, verbose): +def get_kill_lists(name, matches, orphans, keep_newest, verbose): + to_kill = [] # Sort newest first, keep the first, kill the rest - if mode == KillMode.KEEP_NEWEST: + if keep_newest: if verbose: log("Looking for processes, keeping newest instances") @@ -100,27 +88,27 @@ def get_kill_lists(name, matches, mode, verbose): if len(matches) == 1: if verbose: log(f"Keeping only {name} (PID {to_keep.pid}), nothing to kill") - return [] - - to_kill = matches[1:] - if verbose: - log(f"Keeping newest {name} (PID {to_keep.pid}), killing {len(to_kill)}") + else: + to_kill = matches[1:] + if verbose: + log(f"Keeping newest {name} (PID {to_keep.pid}), killing {len(to_kill)}") + else: + to_kill = matches.copy() # Kill processes that have no parent or are orphaned - elif mode == KillMode.KILL_ORPHANS: + if orphans: if verbose: log("Looking for orphaned processes to kill") - to_kill = [] for proc in matches: if verbose: + parent_name = proc.parent().name() if proc.parent() else 'No parent' log(f"Checking process {proc.pid} ({proc.info['name']})") - log( - f"Parent PID: {proc.ppid()} ({proc.parent().name() if proc.parent() else 'No parent'})" - ) + log(f"Parent PID: {proc.ppid()} ({parent_name})") try: # When the GUI is killed but the Core stays alive, it becomes owned by systemd. + # Gotcha: On macOS this doesn't work, since PIDs are reused by app bundles. parent_systemd = proc.parent() and proc.parent().name() == "systemd" if parent_systemd or proc.ppid() == 0: to_kill.append(proc) @@ -128,14 +116,6 @@ def get_kill_lists(name, matches, mode, verbose): log(f"Process {proc.pid} no longer exists or access denied") continue - if not to_kill: - if verbose: - log(f"No orphaned processes found for '{name}'") - return [] - - else: - raise ValueError(f"Unknown kill mode: {mode}") - return to_kill @@ -147,7 +127,7 @@ def get_process_name(raw_name): return name -def kill(raw_name, mode, verbose): +def kill(raw_name, orphans, keep_newest, verbose): name = get_process_name(raw_name) matches = [] for proc in psutil.process_iter(attrs=["pid", "name", "create_time"]): @@ -163,7 +143,7 @@ def kill(raw_name, mode, verbose): log(f"No processes found for '{raw_name}'") return False - to_kill = get_kill_lists(name, matches, mode, verbose) + to_kill = get_kill_lists(name, matches, orphans, keep_newest, verbose) for proc in to_kill: try: