From 7bf1eb8b0ee2d376a53576177518dc2fe340d273 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Tue, 4 Aug 2026 00:56:34 +0500 Subject: [PATCH] fix: add timeout to run_command() to prevent indefinite hangs Add a timeout parameter (default 120s) to subprocess.run() calls in run_command() utility. Previously, if a spawned process hung, the CLI would block forever with no way to recover. Also removes redundant hasattr(e, 'stderr') check on CalledProcessError which always has a stderr attribute when capture_output=True. Assisted-by: GitHub Copilot (model: mimo-v2-free, supervised) --- src/specify_cli/_utils.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/specify_cli/_utils.py b/src/specify_cli/_utils.py index 85b659d67b..314a5eb35b 100644 --- a/src/specify_cli/_utils.py +++ b/src/specify_cli/_utils.py @@ -82,6 +82,7 @@ def run_command( cmd: list[str], check_return: bool = True, capture: bool = False, + timeout: int = 120, ) -> str | None: """Run a command without invoking a shell and optionally capture output. @@ -89,19 +90,30 @@ def run_command( argv ``list[str]``. There is deliberately no ``shell`` parameter: the argv-list contract makes shell interpolation impossible by construction, so the shell-injection surface cannot be re-enabled at a call site. + + Args: + cmd: Command and arguments as a list (argv-style). + check_return: If True, raise on non-zero exit codes. + capture: If True, capture and return stdout. + timeout: Maximum seconds to wait for the process (default 120). """ try: if capture: - result = subprocess.run(cmd, check=check_return, capture_output=True, text=True) + result = subprocess.run( + cmd, check=check_return, capture_output=True, text=True, timeout=timeout, + ) return result.stdout.strip() else: - subprocess.run(cmd, check=check_return) + subprocess.run(cmd, check=check_return, timeout=timeout) return None + except subprocess.TimeoutExpired: + console.print(f"[red]Command timed out after {timeout}s:[/red] {' '.join(cmd)}") + raise except subprocess.CalledProcessError as e: if check_return: console.print(f"[red]Error running command:[/red] {' '.join(cmd)}") console.print(f"[red]Exit code:[/red] {e.returncode}") - if hasattr(e, 'stderr') and e.stderr: + if e.stderr: console.print(f"[red]Error output:[/red] {e.stderr}") raise return None