From 8c2db5c554c2960b04e5a4a06d25c96bcfa9a7e8 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Fri, 31 Jul 2026 20:45:55 +0500 Subject: [PATCH] fix: handle TimeoutExpired and OSError in dispatch_command() subprocess.run() with timeout parameter can raise TimeoutExpired if the timeout elapses. Also catch OSError for cases where the binary is not found or not executable (TOCTOU race between shutil.which and run). --- src/specify_cli/integrations/base.py | 37 +++++++++++++------ .../integrations/copilot/__init__.py | 37 +++++++++++++------ 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index cca4f13976..c3043aaec6 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -407,18 +407,31 @@ def dispatch_command( "stderr": "", } - result = subprocess.run( - exec_args, - capture_output=True, - text=True, - cwd=cwd, - timeout=timeout, - ) - return { - "exit_code": result.returncode, - "stdout": result.stdout, - "stderr": result.stderr, - } + try: + result = subprocess.run( + exec_args, + capture_output=True, + text=True, + cwd=cwd, + timeout=timeout, + ) + return { + "exit_code": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + } + except subprocess.TimeoutExpired: + return { + "exit_code": 124, + "stdout": "", + "stderr": f"Command timed out after {timeout}s", + } + except OSError as exc: + return { + "exit_code": 1, + "stdout": "", + "stderr": f"Failed to execute command: {exc}", + } # -- Primitives — building blocks for setup() ------------------------- diff --git a/src/specify_cli/integrations/copilot/__init__.py b/src/specify_cli/integrations/copilot/__init__.py index e6f86e8991..e78c2ffd7e 100644 --- a/src/specify_cli/integrations/copilot/__init__.py +++ b/src/specify_cli/integrations/copilot/__init__.py @@ -320,18 +320,31 @@ def dispatch_command( "stderr": "", } - result = subprocess.run( - cli_args, - capture_output=True, - text=True, - cwd=cwd, - timeout=timeout, - ) - return { - "exit_code": result.returncode, - "stdout": result.stdout, - "stderr": result.stderr, - } + try: + result = subprocess.run( + cli_args, + capture_output=True, + text=True, + cwd=cwd, + timeout=timeout, + ) + return { + "exit_code": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + } + except subprocess.TimeoutExpired: + return { + "exit_code": 124, + "stdout": "", + "stderr": f"Command timed out after {timeout}s", + } + except OSError as exc: + return { + "exit_code": 1, + "stdout": "", + "stderr": f"Failed to execute command: {exc}", + } def command_filename(self, template_name: str) -> str: """Copilot commands use ``.agent.md`` extension."""