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 interpreter.utils.paths import subproc_path
|
||||||
from runtime.tum_except import ETUMRuntimeError
|
from runtime.tum_except import ETUMRuntimeError
|
||||||
from interpreter.utils import bins
|
from interpreter.utils import bins
|
||||||
|
from interpreter.utils.proc_drain import drain_to_log
|
||||||
|
|
||||||
|
|
||||||
class LuaProcessBase:
|
class LuaProcessBase:
|
||||||
@@ -93,10 +94,14 @@ class LuaProcessBase:
|
|||||||
self._process = subprocess.Popen(
|
self._process = subprocess.Popen(
|
||||||
params, env=env, cwd=func_proc_path,
|
params, env=env, cwd=func_proc_path,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.PIPE,
|
||||||
restore_signals=False,
|
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(
|
self._rpc = JsonRpcClient(
|
||||||
"localhost", self._port, req_handler=self._req_handler
|
"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 runtime.tum_except import ETUMRuntimeError
|
||||||
from interpreter.utils.paths import testium_path, subproc_path
|
from interpreter.utils.paths import testium_path, subproc_path
|
||||||
from interpreter.utils import bins
|
from interpreter.utils import bins
|
||||||
|
from interpreter.utils.proc_drain import drain_to_log
|
||||||
|
|
||||||
|
|
||||||
class PyProcessBase:
|
class PyProcessBase:
|
||||||
@@ -77,10 +78,15 @@ class PyProcessBase:
|
|||||||
self._process = subprocess.Popen(
|
self._process = subprocess.Popen(
|
||||||
params, env=env, cwd=func_proc_path,
|
params, env=env, cwd=func_proc_path,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.PIPE,
|
||||||
restore_signals=False,
|
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(
|
self._rpc = JsonRpcClient(
|
||||||
"localhost", self._port, req_handler=self._req_handler
|
"localhost", self._port, req_handler=self._req_handler
|
||||||
|
|||||||
Reference in New Issue
Block a user