From a64f24ff51a7cbd363293114ba39316a53872be0 Mon Sep 17 00:00:00 2001 From: yousefalshaikh17 <164582835+yousefalshaikh17@users.noreply.github.com> Date: Sat, 26 Apr 2025 08:48:37 +0100 Subject: [PATCH 1/5] Changed process management for linux compatbility is_running() now includes a check to see if the process is a zombie, something exclusively on Linux. close() now waits for the process to be closed and handles timeout. --- process_controller/controller.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/process_controller/controller.py b/process_controller/controller.py index 0085508..a48ff34 100644 --- a/process_controller/controller.py +++ b/process_controller/controller.py @@ -98,7 +98,7 @@ def is_running(self): Returns whether the process is running. """ process = self.get_process() - return process is not None and process.is_running() + return process is not None and process.is_running() and process.status() != psutil.STATUS_ZOMBIE def get_cpu_usage(self, interval=0.1): """ @@ -124,13 +124,18 @@ def close(self): process = self.get_process() if process is not None: try: - process.terminate() - return True + process.terminate() + process.wait(timeout=5) except psutil.NoSuchProcess: return True - except (psutil.AccessDenied, psutil.TimeoutExpired, psutil.ZombieProcess, psutil.Error) as e: + except psutil.TimeoutExpired: + process.kill() + process.wait() + except (psutil.AccessDenied, psutil.ZombieProcess, psutil.Error) as e: print(f"An error occurred: {e}") - return False + return False + + return True def terminate(self): """ @@ -161,8 +166,12 @@ def delayed_termination(): def restart(self): """ - Restarts the process if it is running, terminating and then starting a new instance with the same command line and working directory. + Restarts the process if it is running, terminating and then starting a new instance with the same command line and working directory. Returns the new process or None. """ + # If the process stopped before the restart, cancel restart attempt. + if not self.is_running(): + return None + process = self.get_process() if process: try: From 2b96cde6856a02116811bc0aa475084ba7520885 Mon Sep 17 00:00:00 2001 From: yousefalshaikh17 <164582835+yousefalshaikh17@users.noreply.github.com> Date: Sat, 26 Apr 2025 08:48:57 +0100 Subject: [PATCH 2/5] Changed restart logic to return new processes --- process_controller/controller.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/process_controller/controller.py b/process_controller/controller.py index a48ff34..b5f3657 100644 --- a/process_controller/controller.py +++ b/process_controller/controller.py @@ -178,22 +178,15 @@ def restart(self): # Store cmdline before termination cmdline = process.cmdline() cwd = process.cwd() - - # If the process stopped before the restart, cancel restart attempt. - if not process.is_running(): - return False self.terminate() - - # Wait for thread to die - while self.is_running(): - time.sleep(0.1) # Start thread new_process = psutil.Popen(cmdline, cwd=cwd) self.pid = new_process.pid - self.create_time = new_process.create_time - return True + self.create_time = new_process.create_time() + return new_process except Exception as e: print(f"Error restarting process {self.pid}: {e}") - return False + + return None From 08743686644ab9f904f682d6399a2b77e304cb8c Mon Sep 17 00:00:00 2001 From: yousefalshaikh17 <164582835+yousefalshaikh17@users.noreply.github.com> Date: Sat, 26 Apr 2025 08:51:06 +0100 Subject: [PATCH 3/5] Adjusted tests for cross-compatibility with Linux --- tests/test_process_controller.py | 53 ++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/tests/test_process_controller.py b/tests/test_process_controller.py index ba72788..a80e8db 100644 --- a/tests/test_process_controller.py +++ b/tests/test_process_controller.py @@ -45,43 +45,50 @@ def setUp(self): for process in psutil.process_iter(['cwd']): if process.cwd == data_folder: process.kill() + process.wait() + + self.process = psutil.Popen( + self.cmdline, + cwd=data_folder + ) + self.processes = [self.process] - self.process = subprocess.Popen( - self.cmdline, - cwd=data_folder, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) yield_until_process_running(self.process.pid) - - # Create a ProcessController instance for the new process - process = psutil.Process(self.process.pid) - self.controller = ProcessController.from_process(process) + + self.controller = ProcessController.from_process(self.process) def tearDown(self): """ - Terminate the process after each test. + Terminate the processes after each test. """ - self.controller.terminate() - self.process.terminate() - while self.controller.is_running(): - time.sleep(0.1) + + for process in self.processes: + if process.is_running() and process.status() != psutil.STATUS_ZOMBIE: - + process.terminate() + # Ensure the process exits + try: + process.wait(timeout=5) + except psutil.TimeoutExpired: + process.kill() + process.wait() + + # This is an extra measure to clean up POpen references (Also helps silence early warnings) + process.__exit__(None, None, None) + def test_find_process_1(self): """ Test to verify that the ProcessController can correctly find the running process based on filter parameters. """ filter = { - 'name': 'python.exe', 'cmdline': self.cmdline } process_controllers = ProcessController.find_processes(filter) self.assertTrue( any([process.pid == self.process.pid for process in process_controllers]), - msg="test_process_controller.test_find_process_1: Process was not found with the filter name='python.exe' and cmdline." + msg="test_process_controller.test_find_process_1: Process was not found with the filter name='python' and cmdline." ) def test_find_process_2(self): @@ -165,9 +172,15 @@ def test_restart(self): initial_pid = self.controller.pid initial_create_time = self.controller.create_time + # This is added to allow create_time to differ from the setup process. + time.sleep(0.1) + # Restart the process - self.assertTrue( - self.controller.restart(), + new_process = self.controller.restart() + self.processes.append(new_process) + + self.assertIsNotNone( + new_process, msg="test_process_controller.test_restart: Process could not be restarted." ) From 64ccd6e9009b5d8e8efd64c62eb44e03d97f638f Mon Sep 17 00:00:00 2001 From: yousefalshaikh17 <164582835+yousefalshaikh17@users.noreply.github.com> Date: Sat, 26 Apr 2025 08:52:50 +0100 Subject: [PATCH 4/5] Package is now installed before tests Previously, the tests relied on a local copy of the project source. Now the source is installed as a package. This helped make the code cleaner. --- .github/workflows/run_tests.yml | 4 ++-- tests/test_process_controller.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 362ec79..fe11830 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -10,7 +10,7 @@ on: jobs: test: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Checkout code @@ -24,7 +24,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install . - name: Run unit tests run: | diff --git a/tests/test_process_controller.py b/tests/test_process_controller.py index a80e8db..fec2c61 100644 --- a/tests/test_process_controller.py +++ b/tests/test_process_controller.py @@ -5,7 +5,6 @@ import sys import subprocess -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from process_controller import ProcessController data_folder = os.path.join(os.path.dirname(__file__), 'data') From 5bdcf3cf31a01376cfc0a4f69bb5b02eb5fc25cd Mon Sep 17 00:00:00 2001 From: yousefalshaikh17 <164582835+yousefalshaikh17@users.noreply.github.com> Date: Sat, 26 Apr 2025 08:53:06 +0100 Subject: [PATCH 5/5] Cleaned up unused imports --- process_controller/controller.py | 1 - tests/test_process_controller.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/process_controller/controller.py b/process_controller/controller.py index b5f3657..ce9a195 100644 --- a/process_controller/controller.py +++ b/process_controller/controller.py @@ -1,7 +1,6 @@ import time import psutil import threading -import subprocess class ProcessController(): """ diff --git a/tests/test_process_controller.py b/tests/test_process_controller.py index fec2c61..3504f97 100644 --- a/tests/test_process_controller.py +++ b/tests/test_process_controller.py @@ -2,8 +2,6 @@ import psutil import time import os -import sys -import subprocess from process_controller import ProcessController