some new features

This commit is contained in:
ilgazca
2025-07-30 18:53:50 +03:00
parent 8019bd3b7c
commit 079804a0fc
2118 changed files with 297840 additions and 502 deletions

View File

@ -0,0 +1,94 @@
"""
Run commands and handle returncodes
"""
import os
import subprocess
import sys
from typing import Callable, List, Optional, TextIO
from .filesystem import pushd
from .logging import get_logger
def do_command(
cmd: List[str],
cwd: Optional[str] = None,
*,
fd_out: Optional[TextIO] = sys.stdout,
pbar: Optional[Callable[[str], None]] = None,
) -> None:
"""
Run command as subprocess, polls process output pipes and
either streams outputs to supplied output stream or sends
each line (stripped) to the supplied progress bar callback hook.
Raises ``RuntimeError`` on non-zero return code or execption ``OSError``.
:param cmd: command and args.
:param cwd: directory in which to run command, if unspecified,
run command in the current working directory.
:param fd_out: when supplied, streams to this output stream,
else writes to sys.stdout.
:param pbar: optional callback hook to tqdm, which takes
single ``str`` arguent, see:
https://github.com/tqdm/tqdm#hooks-and-callbacks.
"""
get_logger().debug('cmd: %s\ncwd: %s', ' '.join(cmd), cwd)
try:
# NB: Using this rather than cwd arg to Popen due to windows behavior
with pushd(cwd if cwd is not None else '.'):
# TODO: replace with subprocess.run in later Python versions?
proc = subprocess.Popen(
cmd,
bufsize=1,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # avoid buffer overflow
env=os.environ,
universal_newlines=True,
)
while proc.poll() is None:
if proc.stdout is not None:
line = proc.stdout.readline()
if fd_out is not None:
fd_out.write(line)
if pbar is not None:
pbar(line.strip())
stdout, _ = proc.communicate()
if stdout:
if len(stdout) > 0:
if fd_out is not None:
fd_out.write(stdout)
if pbar is not None:
pbar(stdout.strip())
if proc.returncode != 0: # throw RuntimeError + msg
serror = ''
try:
serror = os.strerror(proc.returncode)
except (ArithmeticError, ValueError):
pass
msg = 'Command {}\n\t{} {}'.format(
cmd, returncode_msg(proc.returncode), serror
)
raise RuntimeError(msg)
except OSError as e:
msg = 'Command: {}\nfailed with error {}\n'.format(cmd, str(e))
raise RuntimeError(msg) from e
def returncode_msg(retcode: int) -> str:
"""interpret retcode"""
if retcode < 0:
sig = -1 * retcode
return f'terminated by signal {sig}'
if retcode <= 125:
return 'error during processing'
if retcode == 126: # shouldn't happen
return ''
if retcode == 127:
return 'program not found'
sig = retcode - 128
return f'terminated by signal {sig}'