azure-devops/azext_devops/dev/common/external_tool.py (63 lines of code) (raw):

# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import os import signal import subprocess import sys import humanfriendly from knack.log import get_logger from knack.util import CLIError logger = get_logger(__name__) class ExternalToolInvoker: _proc = None _terminating = False def __init__(self): signal.signal(signal.SIGINT, self._sigint_handler) self._args = None def start(self, command_args, env): if self._proc is not None: raise RuntimeError("Attempted to invoke already-running external tool") logger.debug("Running external command: %s", ' '.join(command_args)) DEVNULL = open(os.devnull, 'w') # Note: subprocess.DEVNULL not available on python 2.7 self._args = command_args self._proc = subprocess.Popen( command_args, shell=False, stdin=DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) def wait(self): if self._proc is None: return None # Ensure process completed, and emit error if returncode is non-zero (including any remaining stderr) self._proc.wait() if self._proc.returncode != 0 and not self._terminating: stderr = self._proc.stderr.read().decode('utf-8', 'ignore').strip() if stderr != "": stderr = "\n{}".format(stderr) raise CLIError( "Process {proc} with PID {pid} exited with return code {code}{err}" .format(proc=self._args, pid=self._proc.pid, code=self._proc.returncode, err=stderr) ) return self._proc def _sigint_handler(self): self._terminating = True if self._proc: # Would be better to try sending SIGINT first, # but that's hard to support on multiple platforms (esp Windows) logger.debug("Killing process %s", self._proc.pid) self._proc.kill() class ProgressReportingExternalToolInvoker(ExternalToolInvoker): _spinner = None def run(self, command_args, env, initial_progress_text, stderr_handler): with humanfriendly.Spinner( # pylint: disable=no-member label=initial_progress_text, total=100, stream=sys.stderr) as self._spinner: self._spinner.step() # Start the process, process stderr for progress reporting, check the process result self.start(command_args, env) try: for bline in iter(self._proc.stderr.readline, b''): line = bline.decode('utf-8', 'ignore').strip() stderr_handler(line, self._update_progress) return self.wait() except IOError as ex: if not self._terminating: raise ex def _update_progress(self, progress_text, percentage): if self._spinner: self._spinner.step(label=progress_text, progress=percentage)