diff --git a/poetry.lock b/poetry.lock index f8aaf15..985cc2e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4330,6 +4330,18 @@ files = [ {file = "types_pynput-1.8.1.20260408.tar.gz", hash = "sha256:25ac9ab16c2cd23d9a8eea80dc46a1a733291fdaa0b6cbd24c21b0f28944b73e"}, ] +[[package]] +name = "types-pywin32" +version = "312.0.0.20260609" +description = "Typing stubs for pywin32" +optional = false +python-versions = ">=3.10" +groups = ["lint"] +files = [ + {file = "types_pywin32-312.0.0.20260609-py3-none-any.whl", hash = "sha256:713096e5de8202e6a46392e7731653aa99e458f76cebfea0b15d80901f3bb5e1"}, + {file = "types_pywin32-312.0.0.20260609.tar.gz", hash = "sha256:3b6ec4ebf8607ef97563e907e381d8dc40260c3c54ff8819dcea696bd67ca13f"}, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -4345,4 +4357,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.14" -content-hash = "1d970670845a18862e13496d6edf5f48f498231ea4e6996cabf8e71699fb0e64" +content-hash = "67327e6ff548952ed61968278ffcda85e878159486f2575d9efc8e52dfaf8f74" diff --git a/pyproject.toml b/pyproject.toml index c538a95..8c4cd35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,8 @@ format = [ lint = [ "mypy (>=1.20.1,<2.0.0)", "types-pynput (>=1.8.1.20260408,<2.0.0.0)", - "types-psutil (>=7.2.2.20260408,<8.0.0.0)" + "types-psutil (>=7.2.2.20260408,<8.0.0.0)", + "types-pywin32 (>=312.0.0.20260609,<313.0.0.0)" ] diff --git a/src/visiongui/driver/close.py b/src/visiongui/driver/close.py index e15336f..6b89127 100644 --- a/src/visiongui/driver/close.py +++ b/src/visiongui/driver/close.py @@ -1,6 +1,7 @@ import logging import psutil +import win32process from visiongui.driver.DesktopDriverInterface import ( DesktopDriverInterface, @@ -19,7 +20,8 @@ def close( window = driver.window pid = None if window is not None: - pid = window.getPID() + # Obtain the process ID directly from the window handle via the Win32 API. In some cases, window.getPID() returned invalid process IDs (e.g. negative or nonexistent values), causing psutil.Process(pid) to fail even though the window was still present. GetWindowThreadProcessId() reliably returns the process that owns the window. + _, pid = win32process.GetWindowThreadProcessId(window._hWnd) logger.debug(f"Forcefully killing process owning the window: {pid}") proc = psutil.Process(pid) diff --git a/tests/driver/test_close.py b/tests/driver/test_close.py index f462996..38818d2 100644 --- a/tests/driver/test_close.py +++ b/tests/driver/test_close.py @@ -99,3 +99,29 @@ def test_forced_process_termination(self, stubborn_app_path: Path) -> None: assert self.desktop_driver.process is None assert self.desktop_driver.window is None + + def test_close_uses_window_handle_pid_instead_of_window_getPID( + self, + dummy_app_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + self.desktop_driver.launch_process(cmd=["python", str(dummy_app_path)]) + + self.desktop_driver.window = self.desktop_driver.find_window( + title=re.compile(f"^{re.escape(self.test_case_name)}$"), + timeout=5, + ) + + assert self.desktop_driver.process is not None + assert self.desktop_driver.window is not None + + monkeypatch.setattr( + self.desktop_driver.window, + "getPID", + lambda: -1103552400, + ) + + self.desktop_driver.close(OS_PROCESS_KILL_TIMEOUT=OS_PROCESS_KILL_TIMEOUT) + + assert self.desktop_driver.process is None + assert self.desktop_driver.window is None