Skip to content

Commit 4263351

Browse files
authored
feat(subprocess-error): enhance error handling/message for subprocess (#977)
1 parent 499c7d1 commit 4263351

File tree

2 files changed

+45
-8
lines changed

2 files changed

+45
-8
lines changed

python/cocoindex/subprocess_exec.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,16 @@ def _watch() -> None:
152152

153153

154154
def _subprocess_init(user_apps: list[str], parent_pid: int) -> None:
155+
import signal
156+
import faulthandler
157+
158+
faulthandler.enable()
159+
# Ignore SIGINT in the subprocess on best-effort basis.
160+
try:
161+
signal.signal(signal.SIGINT, signal.SIG_IGN)
162+
except Exception:
163+
pass
164+
155165
_start_parent_watchdog(parent_pid)
156166

157167
# In case any user app is already in this subprocess, e.g. the subprocess is forked, we need to avoid loading it again.
@@ -193,10 +203,15 @@ class _ExecutorEntry:
193203

194204
def _call_method(method: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
195205
"""Run an awaitable/coroutine to completion synchronously, otherwise return as-is."""
196-
if asyncio.iscoroutinefunction(method):
197-
return asyncio.run(method(*args, **kwargs))
198-
else:
199-
return method(*args, **kwargs)
206+
try:
207+
if asyncio.iscoroutinefunction(method):
208+
return asyncio.run(method(*args, **kwargs))
209+
else:
210+
return method(*args, **kwargs)
211+
except Exception as e:
212+
raise RuntimeError(
213+
f"Error calling method `{method.__name__}` from subprocess"
214+
) from e
200215

201216

202217
def _get_or_create_entry(key_bytes: bytes) -> _ExecutorEntry:

src/py/mod.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::server::{self, ServerSettings};
1010
use crate::settings::Settings;
1111
use crate::setup::{self};
1212
use pyo3::IntoPyObjectExt;
13+
use pyo3::types::{PyDict, PyModule, PyString};
1314
use pyo3::{exceptions::PyException, prelude::*};
1415
use pyo3_async_runtimes::tokio::future_into_py;
1516
use std::fmt::Write;
@@ -37,10 +38,31 @@ impl<T> ToResultWithPyTrace<T> for Result<T, PyErr> {
3738
match self {
3839
Ok(value) => Ok(value),
3940
Err(err) => {
40-
let mut err_str = format!("Error calling Python function: {err}");
41-
if let Some(tb) = err.traceback(py) {
42-
write!(&mut err_str, "\n{}", tb.format()?)?;
43-
}
41+
// Attempt to render a full Python-style traceback including cause/context chain
42+
let full_trace: PyResult<String> = (|| {
43+
let exc = err.value(py);
44+
let traceback = PyModule::import(py, "traceback")?;
45+
let tbe_class = traceback.getattr("TracebackException")?;
46+
let tbe = tbe_class.call_method1("from_exception", (exc,))?;
47+
let kwargs = PyDict::new(py);
48+
kwargs.set_item("chain", true)?;
49+
let lines = tbe.call_method("format", (), Some(&kwargs))?;
50+
let joined = PyString::new(py, "").call_method1("join", (lines,))?;
51+
joined.extract::<String>()
52+
})();
53+
54+
let err_str = match full_trace {
55+
Ok(trace) => format!("Error calling Python function:\n{trace}"),
56+
Err(_) => {
57+
// Fallback: include the PyErr display and available traceback formatting
58+
let mut s = format!("Error calling Python function: {err}");
59+
if let Some(tb) = err.traceback(py) {
60+
write!(&mut s, "\n{}", tb.format()?).ok();
61+
}
62+
s
63+
}
64+
};
65+
4466
Err(anyhow::anyhow!(err_str))
4567
}
4668
}

0 commit comments

Comments
 (0)