Skip to content

Commit 1e8c078

Browse files
committed
Add timeout and restart logic for Python isolate
Introduces a 30-second timeout for running Python programs in the isolate. If a timeout or error occurs, the Python interpreter isolate is restarted to ensure reliability. Refactors isolate management into helper functions for clarity and robustness.
1 parent 6bcd134 commit 1e8c078

File tree

1 file changed

+48
-16
lines changed

1 file changed

+48
-16
lines changed

src/serious_python_android/lib/src/cpython.dart

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ CPython? _cpython;
1616
Isolate? _pythonIsolate;
1717
SendPort? _pythonSendPort;
1818
bool _signalPatched = false;
19+
const _pythonRunTimeout = Duration(seconds: 30);
1920

2021
void _pyLog(String msg) {
2122
debugPrint("[PY] ${DateTime.now().toIso8601String()} $msg");
@@ -35,20 +36,27 @@ Future<String> runPythonProgramFFI(bool sync, String dynamicLibPath,
3536
}
3637

3738
// 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();
44-
}
39+
await _ensurePythonIsolate();
4540

4641
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;
42+
_pythonSendPort!.send(
43+
[response.sendPort, dynamicLibPath, pythonProgramPath, script]);
44+
try {
45+
final result = await response.first
46+
.timeout(_pythonRunTimeout, onTimeout: () => "<timeout>");
47+
response.close();
48+
if (result == "<timeout>") {
49+
_pyLog("Python run timed out; restarting interpreter isolate");
50+
_restartPythonIsolate();
51+
return "Python run timed out";
52+
}
53+
return result as String;
54+
} catch (e) {
55+
response.close();
56+
_pyLog("Python run failed: $e");
57+
_restartPythonIsolate();
58+
rethrow;
59+
}
5260
}
5361

5462
// Long-lived isolate entry point to serialize all Python runs on one thread.
@@ -61,13 +69,37 @@ void _pythonIsolateMain(SendPort readyPort) {
6169
final dynamicLibPath = args[1] as String;
6270
final pythonProgramPath = args[2] as String;
6371
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);
72+
try {
73+
final result = await runPythonProgramInIsolate(
74+
[sendPort, dynamicLibPath, pythonProgramPath, script]);
75+
// runPythonProgramInIsolate already sends the result, but return anyway.
76+
sendPort.send(result);
77+
} catch (e) {
78+
sendPort.send("Python isolate error: $e");
79+
}
6880
});
6981
}
7082

83+
Future<void> _ensurePythonIsolate() async {
84+
if (_pythonIsolate != null && _pythonSendPort != null) {
85+
return;
86+
}
87+
_pyLog("spawning Python isolate");
88+
final readyPort = ReceivePort();
89+
_pythonIsolate = await Isolate.spawn(_pythonIsolateMain, readyPort.sendPort);
90+
_pythonSendPort = await readyPort.first as SendPort;
91+
readyPort.close();
92+
_signalPatched = false;
93+
}
94+
95+
void _restartPythonIsolate() {
96+
_pyLog("restarting Python isolate");
97+
_pythonIsolate?.kill(priority: Isolate.immediate);
98+
_pythonIsolate = null;
99+
_pythonSendPort = null;
100+
_signalPatched = false;
101+
}
102+
71103
Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
72104
final sendPort = arguments[0] as SendPort;
73105
final dynamicLibPath = arguments[1] as String;

0 commit comments

Comments
 (0)