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/process_controller/controller.py b/process_controller/controller.py index 0085508..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(): """ @@ -98,7 +97,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 +123,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,30 +165,27 @@ 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: # 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 diff --git a/tests/test_process_controller.py b/tests/test_process_controller.py index ba72788..3504f97 100644 --- a/tests/test_process_controller.py +++ b/tests/test_process_controller.py @@ -2,10 +2,7 @@ import psutil import time import os -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') @@ -45,43 +42,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 +169,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." )