Route py_func/lua_func subprocess stdio into the parent log
stdout/stderr of the subprocesses were going to DEVNULL — early-startup errors (lua require failures, exceptions before stdio_redir kicks in) were lost. New helper proc_drain.drain_to_log spawns a daemon thread per pipe that print()s each line through stdio_redir, so it reaches the log + live output. Used by py_process and lua_process with [py_func]/[lua_func] prefixes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ from runtime.jrpc import JsonRpcClient
|
||||
from interpreter.utils.paths import subproc_path
|
||||
from runtime.tum_except import ETUMRuntimeError
|
||||
from interpreter.utils import bins
|
||||
from interpreter.utils.proc_drain import drain_to_log
|
||||
|
||||
|
||||
class LuaProcessBase:
|
||||
@@ -93,10 +94,14 @@ class LuaProcessBase:
|
||||
self._process = subprocess.Popen(
|
||||
params, env=env, cwd=func_proc_path,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
restore_signals=False,
|
||||
)
|
||||
# Route subprocess stdout/stderr (lua require failures, syntax
|
||||
# errors, anything written to fd 1/2 before the in-script
|
||||
# remote_print is set up) into the parent's log.
|
||||
drain_to_log(self._process, prefix="[lua_func] ")
|
||||
|
||||
self._rpc = JsonRpcClient(
|
||||
"localhost", self._port, req_handler=self._req_handler
|
||||
|
||||
48
src/testium/interpreter/utils/proc_drain.py
Normal file
48
src/testium/interpreter/utils/proc_drain.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Drain a subprocess stdout/stderr into testium's print pipeline.
|
||||
|
||||
Captured lines go through the parent's stdio_redir, so they reach the
|
||||
test log AND the live output (terminal in batch mode, GUI text panel
|
||||
in -r mode). This is essential for diagnosing early-startup errors
|
||||
of py_func / lua_func subprocesses (missing modules, unhandled
|
||||
exceptions before the in-process redirection kicks in, lua
|
||||
``require`` failures, anything written to fd 1/2 directly).
|
||||
"""
|
||||
import threading
|
||||
|
||||
|
||||
def _drain_pipe(pipe, prefix):
|
||||
try:
|
||||
for raw in iter(pipe.readline, b""):
|
||||
line = raw.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||
if not line:
|
||||
continue
|
||||
if prefix:
|
||||
print(f"{prefix}{line}")
|
||||
else:
|
||||
print(line)
|
||||
finally:
|
||||
try:
|
||||
pipe.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def drain_to_log(process, prefix=""):
|
||||
"""Spawn daemon threads that read ``process.stdout`` and
|
||||
``process.stderr`` line by line and print each line through the
|
||||
parent's stdout (so it reaches the log + live output).
|
||||
|
||||
Each thread exits cleanly when the subprocess closes the
|
||||
corresponding pipe (i.e. when it exits). Daemon flag ensures they
|
||||
do not block testium exit.
|
||||
"""
|
||||
threads = []
|
||||
for pipe in (process.stdout, process.stderr):
|
||||
if pipe is None:
|
||||
continue
|
||||
t = threading.Thread(
|
||||
target=_drain_pipe, args=(pipe, prefix), daemon=True,
|
||||
)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
return threads
|
||||
@@ -7,6 +7,7 @@ import api.testium as tm
|
||||
from runtime.tum_except import ETUMRuntimeError
|
||||
from interpreter.utils.paths import testium_path, subproc_path
|
||||
from interpreter.utils import bins
|
||||
from interpreter.utils.proc_drain import drain_to_log
|
||||
|
||||
|
||||
class PyProcessBase:
|
||||
@@ -77,10 +78,15 @@ class PyProcessBase:
|
||||
self._process = subprocess.Popen(
|
||||
params, env=env, cwd=func_proc_path,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
restore_signals=False,
|
||||
)
|
||||
# Route subprocess stdout/stderr (early-startup errors,
|
||||
# unhandled exceptions, anything written to fd 1/2 before the
|
||||
# in-process JSON-RPC stdio_redir kicks in) into the parent's
|
||||
# log.
|
||||
drain_to_log(self._process, prefix="[py_func] ")
|
||||
|
||||
self._rpc = JsonRpcClient(
|
||||
"localhost", self._port, req_handler=self._req_handler
|
||||
|
||||
Reference in New Issue
Block a user