@@ -11,6 +11,11 @@ import 'gen.dart';
1111export 'gen.dart' ;
1212
1313CPython ? _cpython;
14+ // Keep a single, long-lived isolate/thread for running Python to preserve the
15+ // "main thread" identity across invocations.
16+ Isolate ? _pythonIsolate;
17+ SendPort ? _pythonSendPort;
18+ bool _signalPatched = false ;
1419
1520void _pyLog (String msg) {
1621 debugPrint ("[PY] ${DateTime .now ().toIso8601String ()} $msg " );
@@ -22,23 +27,45 @@ CPython getCPython(String dynamicLibPath) {
2227
2328Future <String > runPythonProgramFFI (bool sync , String dynamicLibPath,
2429 String pythonProgramPath, String script) async {
25- final receivePort = ReceivePort ();
2630 if (sync ) {
27- // sync run
31+ // sync run on current isolate/thread
32+ final rp = ReceivePort ();
2833 return await runPythonProgramInIsolate (
29- [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]);
30- } else {
31- var completer = Completer <String >();
32- // async run
33- final isolate = await Isolate .spawn (runPythonProgramInIsolate,
34- [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]);
35- receivePort.listen ((message) {
36- receivePort.close ();
37- isolate.kill ();
38- completer.complete (message);
39- });
40- return completer.future;
34+ [rp.sendPort, dynamicLibPath, pythonProgramPath, script]);
35+ }
36+
37+ // Ensure a single long-lived isolate/thread to keep Python "main thread" consistent.
38+ if (_pythonIsolate == null || _pythonSendPort == null ) {
39+ final readyPort = ReceivePort ();
40+ _pythonIsolate =
41+ await Isolate .spawn (_pythonIsolateMain, readyPort.sendPort);
42+ _pythonSendPort = await readyPort.first as SendPort ;
43+ readyPort.close ();
4144 }
45+
46+ final response = ReceivePort ();
47+ _pythonSendPort!
48+ .send ([response.sendPort, dynamicLibPath, pythonProgramPath, script]);
49+ final result = await response.first as String ;
50+ response.close ();
51+ return result;
52+ }
53+
54+ // Long-lived isolate entry point to serialize all Python runs on one thread.
55+ void _pythonIsolateMain (SendPort readyPort) {
56+ final commandPort = ReceivePort ();
57+ readyPort.send (commandPort.sendPort);
58+ commandPort.listen ((message) async {
59+ final args = message as List <Object >;
60+ final sendPort = args[0 ] as SendPort ;
61+ final dynamicLibPath = args[1 ] as String ;
62+ final pythonProgramPath = args[2 ] as String ;
63+ final script = args[3 ] as String ;
64+ final result = await runPythonProgramInIsolate (
65+ [sendPort, dynamicLibPath, pythonProgramPath, script]);
66+ // runPythonProgramInIsolate already sends the result, but return anyway.
67+ sendPort.send (result);
68+ });
4269}
4370
4471Future <String > runPythonProgramInIsolate (List <Object > arguments) async {
@@ -72,6 +99,19 @@ Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
7299 final gilState = cpython.PyGILState_Ensure ();
73100 _pyLog ("PyGILState_Ensure -> $gilState " );
74101
102+ // Patch signal handling if we're not on the interpreter's main thread; some
103+ // libraries (e.g., asyncio) call signal.signal which would otherwise raise.
104+ if (! _signalPatched) {
105+ const patch = """
106+ import threading, signal
107+ if threading.current_thread() is not threading.main_thread():
108+ signal.signal = lambda *args, **kwargs: None
109+ """ ;
110+ _pyLog ("patching signal.signal for non-main thread use" );
111+ cpython.PyRun_SimpleString (patch.toNativeUtf8 ().cast ());
112+ _signalPatched = true ;
113+ }
114+
75115 try {
76116 if (script != "" ) {
77117 // run script
0 commit comments