Source code for pygacity.util.command
# Author: Cameron F. Abrams, <cfa22@drexel.edu>
"""
Simple command runner
"""
import os
import subprocess
import sys
import logging
logger = logging.getLogger(__name__)
[docs]
class Command:
def __init__(self, command: str, ignore_codes: list[int] = [], timeout: int = 120, env: dict = None, cwd: str = None, **options):
"""
Initializes the Command instance.
Parameters
----------
command : str
the base command to run
ignore_codes : list of int, optional
list of return codes to ignore (default is empty list)
timeout : int, optional
seconds to wait before killing the process (default is 120)
env : dict, optional
extra environment variables to merge with os.environ (default is None)
cwd : str, optional
working directory for the process (default is None, meaning current directory)
options : dict
command-line options as key-value pairs
"""
self.command = command
self.ignore_codes = ignore_codes
self.timeout = timeout
self.env = env
self.cwd = cwd
self.options = options
self.c = f'{self.command} ' + ' '.join([f'-{k} {v}' for k, v in self.options.items()])
[docs]
def run(self):
"""
Runs the command and returns the output and error messages.
Raises
------
subprocess.TimeoutExpired
if the process does not finish within ``self.timeout`` seconds;
the process is killed before the exception propagates
subprocess.SubprocessError
if the process exits with a non-zero return code that is not in
``self.ignore_codes``
"""
merged_env = os.environ.copy()
if self.env:
merged_env.update(self.env)
process = subprocess.Popen(self.c, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=merged_env, cwd=self.cwd)
try:
out, err = process.communicate(timeout=self.timeout)
except subprocess.TimeoutExpired:
if sys.platform == 'win32':
# On Windows, process.kill() only kills the shell (cmd.exe), leaving
# grandchildren (e.g. xelatex) as orphans that hold file locks.
# taskkill /T kills the entire process tree.
subprocess.run(
['taskkill', '/F', '/T', '/PID', str(process.pid)],
capture_output=True,
)
process.kill()
process.communicate() # drain pipes so the child is fully reaped
raise subprocess.TimeoutExpired(
self.c, self.timeout,
output=f'Command "{self.c}" timed out after {self.timeout} s and was killed'
)
if process.returncode != 0 and not process.returncode in self.ignore_codes:
if out.strip():
logger.error(f'stdout from "{self.command}":\n{out}')
if err.strip():
logger.error(f'stderr from "{self.command}":\n{err}')
raise subprocess.SubprocessError(f'Command "{self.c}" failed with returncode {process.returncode}')
return out, err