mirror of
https://codeberg.org/privacy1st/subprocess-util
synced 2024-12-22 22:06:05 +01:00
init
This commit is contained in:
commit
e658a70cc9
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/.idea
|
125
subprocess_util.py
Normal file
125
subprocess_util.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
from queue import Queue
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
from typing import AnyStr, IO, Callable
|
||||||
|
|
||||||
|
|
||||||
|
class _Assert:
|
||||||
|
@staticmethod
|
||||||
|
def true(a):
|
||||||
|
if not a:
|
||||||
|
raise ValueError(f'Expected a to be true: {a}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def equal(a, b):
|
||||||
|
if a != b:
|
||||||
|
raise ValueError(f'Expected a and b to be equal: {a}, {b}')
|
||||||
|
|
||||||
|
|
||||||
|
def _read_output(pipe: IO[AnyStr], queue_put: Queue.put, list_append: list.append, prefix: str = ''):
|
||||||
|
line: str
|
||||||
|
for line in pipe:
|
||||||
|
func: Callable[[str], None]
|
||||||
|
for func in (queue_put, list_append):
|
||||||
|
func(f'{prefix}{line}')
|
||||||
|
|
||||||
|
# TODO: Has this any effect?
|
||||||
|
# pipe.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _write_output(queue_get: Queue.get):
|
||||||
|
# Take items out of queue until taken item is None.
|
||||||
|
for line in iter(queue_get, None):
|
||||||
|
sys.stdout.write(line)
|
||||||
|
|
||||||
|
|
||||||
|
# Goal: We want to **capture** and **print** stdout/stderr while running the command.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# https://docs.python.org/3/library/subprocess.html#using-the-subprocess-module
|
||||||
|
#
|
||||||
|
# The recommended approach to invoking subprocesses is to use the run() function for all use cases it can handle.
|
||||||
|
# For more advanced use cases, the underlying Popen interface can be used directly.
|
||||||
|
#
|
||||||
|
# subprocess.run():
|
||||||
|
#
|
||||||
|
# - Run the command described by args. Wait for command to complete,
|
||||||
|
# then return a CompletedProcess instance.
|
||||||
|
#
|
||||||
|
# - capture_output: If capture_output is true, stdout and stderr will be captured.
|
||||||
|
# When used, the internal Popen object is automatically created with stdout=PIPE and stderr=PIPE.
|
||||||
|
# The stdout and stderr arguments may not be supplied at the same time as capture_output.
|
||||||
|
#
|
||||||
|
# - Conclusion: One cannot print and capture stdout/stderr at the same time.
|
||||||
|
# - If we need to stream output as it appears in real time, we can use Popen instead.
|
||||||
|
# (https://csatlas.com/python-subprocess-run-stdout-stderr/)
|
||||||
|
#
|
||||||
|
# subprocess.Popen():
|
||||||
|
#
|
||||||
|
# - args: Should be a sequence of program arguments. By default, the program to execute is the first item in args.
|
||||||
|
# - Warning: For maximum reliability, use a fully qualified path for the executable.
|
||||||
|
# To search for an unqualified name on PATH, use shutil.which().
|
||||||
|
# - stdin, stdout, stderr:
|
||||||
|
# Valid values are PIPE, DEVNULL, an existing file descriptor (a positive integer),
|
||||||
|
# an existing file object with a valid file descriptor, and None.
|
||||||
|
# PIPE indicates that a new pipe to the child should be created.
|
||||||
|
#
|
||||||
|
# subprocess.PIPE: Most useful with Popen.communicate().
|
||||||
|
#
|
||||||
|
# Popen.communicate():
|
||||||
|
#
|
||||||
|
# - Read data from stdout and stderr, until end-of-file is reached.
|
||||||
|
# - Note: The data read is buffered in memory, so do not use this method if the data size is large or unlimited.
|
||||||
|
def execute_print_capture(command: list[str], encoding='UTF-8') -> [int, list[str], list[str]]:
|
||||||
|
"""
|
||||||
|
Executes the given command.
|
||||||
|
|
||||||
|
Stdout and stderr are printed in real time.
|
||||||
|
|
||||||
|
:param command: Command to execute, e.g. ['ls', '-la', '/home']
|
||||||
|
:param encoding:
|
||||||
|
:return: (returncode, stdout, stderr)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# INSPIRATION OF THIS IMPLEMENTATION:
|
||||||
|
# https://www.itcodar.com/python/subprocess-popen-cloning-stdout-and-stderr-both-to-terminal-and-variables.html
|
||||||
|
# ALTERNATIVE IMPLEMENTATION:
|
||||||
|
# https://stackoverflow.com/a/26611142
|
||||||
|
|
||||||
|
_Assert.equal(sys.stdout.encoding.upper(), encoding)
|
||||||
|
|
||||||
|
process = subprocess.Popen(
|
||||||
|
command,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
close_fds=True,
|
||||||
|
bufsize=1,
|
||||||
|
text=True,
|
||||||
|
encoding=encoding,
|
||||||
|
)
|
||||||
|
|
||||||
|
q = Queue()
|
||||||
|
out: list[str] = []
|
||||||
|
err: list[str] = []
|
||||||
|
|
||||||
|
t_out = threading.Thread(
|
||||||
|
target=_read_output, args=(process.stdout, q.put, out.append, "[STDOUT] "))
|
||||||
|
t_err = threading.Thread(
|
||||||
|
target=_read_output, args=(process.stderr, q.put, err.append, "[STDERR] "))
|
||||||
|
t_write = threading.Thread(
|
||||||
|
target=_write_output, args=(q.get,))
|
||||||
|
|
||||||
|
for t in (t_out, t_err, t_write):
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
returncode = process.wait()
|
||||||
|
|
||||||
|
for t in (t_out, t_err):
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
# This ends iter(queue_get, None), see above.
|
||||||
|
q.put(None)
|
||||||
|
t_write.join()
|
||||||
|
|
||||||
|
return returncode, out, err
|
Loading…
Reference in New Issue
Block a user