diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 88e3b194d..2efe2bc13 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -55,12 +55,6 @@ jobs: run: | ${{ matrix.platform.python_exec }} -m mypy python - name: Python tests - if: ${{ !startsWith(matrix.platform.runner, 'windows') }} - run: | - ${{ matrix.platform.python_exec }} -m pytest --capture=no python/cocoindex/tests - - name: Python tests (Windows cmd) - if: ${{ startsWith(matrix.platform.runner, 'windows') }} - shell: cmd # Use `cmd` to run test for Windows, as PowerShell doesn't detect exit code by `os._exit(0)` correctly. run: | ${{ matrix.platform.python_exec }} -m pytest --capture=no python/cocoindex/tests diff --git a/pyproject.toml b/pyproject.toml index 9bf787734..a3d27d77c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "python-dotenv>=1.1.0", "watchfiles>=1.1.0", "numpy>=1.23.2", + "psutil>=7.0.0", ] license = "Apache-2.0" license-files = ["THIRD_PARTY_NOTICES.html"] diff --git a/python/cocoindex/subprocess_exec.py b/python/cocoindex/subprocess_exec.py index 11cadf7eb..c60adfd2d 100644 --- a/python/cocoindex/subprocess_exec.py +++ b/python/cocoindex/subprocess_exec.py @@ -134,18 +134,27 @@ def _start_parent_watchdog( This runs in a background daemon thread so it never blocks pool work. """ + import psutil # type: ignore + + if parent_pid is None: + parent_pid = os.getppid() + + try: + p = psutil.Process(parent_pid) + # Cache create_time to defeat PID reuse. + created = p.create_time() + except psutil.Error: + # Parent already gone or not accessible + os._exit(1) + def _watch() -> None: while True: - # If PPID changed (parent died and we were reparented), exit. - if os.getppid() != parent_pid: - os._exit(1) - - # Best-effort liveness probe in case PPID was reused. try: - os.kill(parent_pid, 0) - except OSError: + # is_running() + same create_time => same process and still alive + if not (p.is_running() and p.create_time() == created): + os._exit(1) + except psutil.NoSuchProcess: os._exit(1) - time.sleep(interval_seconds) threading.Thread(target=_watch, name="parent-watchdog", daemon=True).start() diff --git a/python/cocoindex/tests/conftest.py b/python/cocoindex/tests/conftest.py deleted file mode 100644 index 109898e07..000000000 --- a/python/cocoindex/tests/conftest.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest -import typing -import os -import signal -import sys - - -@pytest.fixture(scope="session", autouse=True) -def _cocoindex_windows_env_fixture( - request: pytest.FixtureRequest, -) -> typing.Generator[None, None, None]: - """Shutdown the subprocess pool at exit on Windows.""" - - yield - - if not sys.platform.startswith("win"): - return - - try: - import cocoindex.subprocess_exec - - original_sigint_handler = signal.getsignal(signal.SIGINT) - try: - signal.signal(signal.SIGINT, signal.SIG_IGN) - cocoindex.subprocess_exec.shutdown_pool_at_exit() - - # If any test failed, let pytest exit normally with nonzero code - if request.session.testsfailed == 0: - os._exit(0) # immediate success exit (skips atexit/teardown) - - finally: - try: - signal.signal(signal.SIGINT, original_sigint_handler) - except ValueError: # noqa: BLE001 - pass - - except (ImportError, AttributeError): # noqa: BLE001 - pass