1 # Copyright 2019 The Pigweed Authors
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 # use this file except in compliance with the License. You may obtain a copy of
7 # https://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations under
14 """Module for running subprocesses from pw and capturing their output."""
21 from typing import IO, Tuple, Union
26 _COLOR = pw_cli.color.colors()
27 _LOG = logging.getLogger(__name__)
29 # Environment variable passed down to subprocesses to indicate that they are
30 # running as a subprocess. Can be imported by other code.
31 PW_SUBPROCESS_ENV = 'PW_SUBPROCESS'
34 class CompletedProcess:
35 """Information about a process executed in run_async."""
36 def __init__(self, process: 'asyncio.subprocess.Process',
37 output: Union[bytes, IO[bytes]]):
38 assert process.returncode is not None
39 self.returncode: int = process.returncode
40 self.pid = process.pid
44 def output(self) -> bytes:
45 # If the output is a file, read it, then close it.
46 if not isinstance(self._output, bytes):
47 with self._output as file:
50 self._output = file.read()
55 async def _run_and_log(program: str, args: Tuple[str, ...], env: dict):
56 process = await asyncio.create_subprocess_exec(
59 stdout=asyncio.subprocess.PIPE,
60 stderr=asyncio.subprocess.STDOUT,
67 line = await process.stdout.readline()
72 _LOG.log(pw_cli.log.LOGLEVEL_STDOUT, '[%s] %s',
73 _COLOR.bold_white(process.pid),
74 line.decode(errors='replace').rstrip())
76 return process, bytes(output)
79 async def run_async(program: str,
81 log_output: bool = False) -> CompletedProcess:
82 """Runs a command, capturing and optionally logging its output.
84 Returns a CompletedProcess with details from the process.
87 _LOG.debug('Running `%s`',
88 ' '.join(shlex.quote(arg) for arg in [program, *args]))
90 env = os.environ.copy()
91 env[PW_SUBPROCESS_ENV] = '1'
92 output: Union[bytes, IO[bytes]]
95 process, output = await _run_and_log(program, args, env)
97 output = tempfile.TemporaryFile()
98 process = await asyncio.create_subprocess_exec(
102 stderr=asyncio.subprocess.STDOUT,
105 if await process.wait() == 0:
106 _LOG.info('%s exited successfully', program)
108 _LOG.error('%s exited with status %d', program, process.returncode)
110 return CompletedProcess(process, output)
113 def run(program: str, *args: str, **kwargs) -> CompletedProcess:
114 """Synchronous wrapper for run_async."""
115 return asyncio.run(run_async(program, *args, **kwargs))