Skip to content

Commit 6d00d09

Browse files
author
Thomas Desveaux
authored
Merge pull request #103 from dontnod/tds/process/custom-output-processing
process: allow to provide a callback to capture_output for custom processing
2 parents 612d75f + 997aaa2 commit 6d00d09

File tree

1 file changed

+68
-13
lines changed

1 file changed

+68
-13
lines changed

nimp/sys/process.py

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,48 @@
2222

2323
'''Process-related system utilities'''
2424

25+
from __future__ import annotations
26+
2527
import ctypes
26-
import logging
2728
import locale
29+
import logging
2830
import os
2931
import os.path
3032
import struct
3133
import subprocess
3234
import threading
3335
import time
36+
from enum import Enum
37+
from typing import TYPE_CHECKING
3438

3539
import nimp.sys.platform
3640

41+
if TYPE_CHECKING:
42+
from typing import Callable
43+
44+
45+
class ProcessOutputStream(Enum):
46+
"""
47+
process output stream identifiers
48+
"""
49+
50+
STDOUT = 1
51+
STDERR = 2
52+
STDDBG = 3
53+
3754

3855
def call(
3956
command,
4057
cwd='.',
4158
heartbeat=0,
4259
stdin=None,
4360
encoding='utf-8',
44-
capture_output=False,
61+
capture_output: bool | Callable[[ProcessOutputStream, str], None] = False,
4562
capture_debug=False,
4663
hide_output=False,
4764
dry_run=False,
4865
timeout=None,
66+
**popen_kwargs,
4967
):
5068
'''Calls a process redirecting its output to nimp's output'''
5169
command = _sanitize_command(command)
@@ -61,6 +79,16 @@ def call(
6179
else:
6280
debug_pipe = None
6381

82+
for reserved_popen_kwargs in [
83+
'stdout',
84+
'stderr',
85+
'stdin',
86+
'bufsize',
87+
]:
88+
if reserved_popen_kwargs in popen_kwargs:
89+
value = popen_kwargs.pop(reserved_popen_kwargs)
90+
logging.debug("Ignoring reserved subprocess.Popen kwarg '%s=%s'", reserved_popen_kwargs, value)
91+
6492
# The bufsize = -1 is important; if we don’t bufferise the output, we’re
6593
# going to make the callee lag a lot. In Python 3.3.1 this is now the
6694
# default behaviour, but it used to default to 0.
@@ -82,9 +110,26 @@ def call(
82110
debug_pipe.start()
83111

84112
# FIXME: put all this in a class instead!
85-
all_pipes = [process.stdout, process.stderr, debug_pipe.output if debug_pipe else None]
86-
87-
all_captures = [[] if capture_output else None, [] if capture_output else None, None]
113+
all_pipes = {
114+
ProcessOutputStream.STDOUT: process.stdout,
115+
ProcessOutputStream.STDERR: process.stderr,
116+
ProcessOutputStream.STDDBG: debug_pipe.output if debug_pipe else None,
117+
}
118+
119+
all_captures = {
120+
ProcessOutputStream.STDOUT: [],
121+
ProcessOutputStream.STDERR: [],
122+
}
123+
124+
def _capture_output_default(stream: ProcessOutputStream, value: str):
125+
if (capture_array := all_captures.get(stream)) is not None:
126+
capture_array.append(value)
127+
128+
capture_processor = None
129+
if capture_output is True:
130+
capture_processor = _capture_output_default
131+
elif callable(capture_output):
132+
capture_processor = capture_output
88133

89134
debug_info = [False]
90135

@@ -100,9 +145,8 @@ def _input_worker(in_pipe, data):
100145
in_pipe.write(data)
101146
in_pipe.close()
102147

103-
def _output_worker(index, decoding_format):
148+
def _output_worker(index: ProcessOutputStream, decoding_format):
104149
in_pipe = all_pipes[index]
105-
capture_array = all_captures[index]
106150
if in_pipe is None:
107151
return
108152
force_ascii = locale.getpreferredencoding().lower() != 'utf-8'
@@ -125,15 +169,15 @@ def _output_worker(index, decoding_format):
125169
except UnicodeError:
126170
pass
127171

128-
if capture_array is not None:
129-
capture_array.append(line)
172+
if capture_processor is not None:
173+
capture_processor(line)
130174

131175
# Stop reading data from stdout if data has arrived on OutputDebugString
132176
if index == 2:
133177
debug_info[0] = True
134178
elif index == 0 and debug_info[0]:
135179
logging.info('Stopping stdout monitoring (OutputDebugString is active)')
136-
all_pipes[0].close()
180+
all_pipes[ProcessOutputStream.STDOUT].close()
137181
return
138182

139183
if not hide_output:
@@ -144,7 +188,14 @@ def _output_worker(index, decoding_format):
144188
time.sleep(0.010)
145189

146190
# Default threads
147-
all_workers = [threading.Thread(target=_output_worker, args=(i, encoding)) for i in range(3)]
191+
all_workers = [
192+
threading.Thread(target=_output_worker, args=(i, encoding))
193+
for i in [
194+
ProcessOutputStream.STDOUT,
195+
ProcessOutputStream.STDERR,
196+
ProcessOutputStream.STDDBG,
197+
]
198+
]
148199

149200
# Thread to feed stdin data if necessary
150201
if stdin is not None:
@@ -172,8 +223,12 @@ def _output_worker(index, decoding_format):
172223
if not hide_output:
173224
logging.info('Finished with exit code %d (0x%08x)', exit_code, exit_code)
174225

175-
if capture_output:
176-
return exit_code, ''.join(all_captures[0]), ''.join(all_captures[1])
226+
if capture_output is True:
227+
return (
228+
exit_code,
229+
''.join(all_captures[ProcessOutputStream.STDOUT]),
230+
''.join(all_captures[ProcessOutputStream.STDERR]),
231+
)
177232
return exit_code
178233

179234

0 commit comments

Comments
 (0)