@@ -15,7 +15,6 @@ CPython? _cpython;
1515// "main thread" identity across invocations.
1616Isolate ? _pythonIsolate;
1717SendPort ? _pythonSendPort;
18- bool _signalPatched = false ;
1918const _pythonRunTimeout = Duration (seconds: 30 );
2019
2120void _pyLog (String msg) {
@@ -122,37 +121,33 @@ Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
122121 cpython.Py_Initialize ();
123122 _pyLog ("after Py_Initialize()" );
124123 } else {
125- // Interpreter was left running (likely from a previous app session). Tear
126- // it down and start fresh to avoid reusing a wedged event loop.
127- _pyLog ("Python already initialized; finalizing stale interpreter" );
128- cpython.PyGILState_Ensure (); // ensure we own the GIL before finalizing
129- cpython.Py_FinalizeEx ();
130- _pyLog ("after Py_FinalizeEx(), reinitializing" );
131- cpython.Py_Initialize ();
132- _pyLog ("after Py_Initialize() (fresh)" );
124+ _pyLog ("Python already initialized; reusing runtime" );
133125 }
134126
135127 var result = "" ;
136128
137- // Ensure the calling thread owns the GIL; this is safe across isolates .
129+ // Use a fresh sub-interpreter per run to avoid wedged state .
138130 final gilState = cpython.PyGILState_Ensure ();
139131 _pyLog ("PyGILState_Ensure -> $gilState " );
132+ final mainState = cpython.PyThreadState_Get ();
133+ _pyLog ("main thread state: 0x${mainState .address .toRadixString (16 )}" );
134+ final subState = cpython.Py_NewInterpreter ();
135+ if (subState == nullptr) {
136+ _pyLog ("Py_NewInterpreter returned null" );
137+ cpython.PyGILState_Release (gilState);
138+ sendPort.send ("Py_NewInterpreter failed" );
139+ return "Py_NewInterpreter failed" ;
140+ }
141+ _pyLog ("created sub-interpreter: 0x${subState .address .toRadixString (16 )}" );
140142
141- // Patch signal handling if we're not on the interpreter's main thread; some
142- // libraries (e.g., asyncio) call signal.signal which would otherwise raise.
143- if (! _signalPatched) {
144- const patch = """
143+ // Within the sub-interpreter: patch signals and reset asyncio loop.
144+ const patchSignals = """
145145import threading, signal
146146if threading.current_thread() is not threading.main_thread():
147147 signal.signal = lambda *args, **kwargs: None
148148""" ;
149- _pyLog ("patching signal.signal for non-main thread use" );
150- cpython.PyRun_SimpleString (patch.toNativeUtf8 ().cast ());
151- _signalPatched = true ;
152- }
149+ cpython.PyRun_SimpleString (patchSignals.toNativeUtf8 ().cast ());
153150
154- // Reset/replace asyncio loop between runs to avoid reusing a running loop from
155- // a previous app session that could block new runs.
156151 const loopReset = """
157152import asyncio
158153try:
@@ -161,10 +156,9 @@ try:
161156 loop.stop()
162157 loop.close()
163158 asyncio.set_event_loop(asyncio.new_event_loop())
164- except Exception as e :
159+ except Exception:
165160 pass
166161""" ;
167- _pyLog ("resetting asyncio loop" );
168162 cpython.PyRun_SimpleString (loopReset.toNativeUtf8 ().cast ());
169163
170164 try {
@@ -192,7 +186,9 @@ except Exception as e:
192186 malloc.free (moduleNamePtr);
193187 }
194188 } finally {
195- // Drop the GIL again so subsequent runs/threads can re-acquire it cleanly.
189+ _pyLog ("ending sub-interpreter 0x${subState .address .toRadixString (16 )}" );
190+ cpython.Py_EndInterpreter (subState);
191+ cpython.PyThreadState_Swap (mainState);
196192 _pyLog ("PyGILState_Release($gilState )" );
197193 cpython.PyGILState_Release (gilState);
198194 }
0 commit comments