From 8d84d6f40c18a5a94a1b1d83834e6fc0b5ec6bdf Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 10 Dec 2025 13:03:17 -0800 Subject: [PATCH 01/31] Make setenv calls awaitable in Python init Refactored the setenv function to return a Future and updated all calls to setenv to use await. This ensures environment variables are set in the correct order before proceeding. --- .../lib/serious_python_android.dart | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/serious_python_android/lib/serious_python_android.dart b/src/serious_python_android/lib/serious_python_android.dart index a3e84dcf..5c4c7067 100644 --- a/src/serious_python_android/lib/serious_python_android.dart +++ b/src/serious_python_android/lib/serious_python_android.dart @@ -33,10 +33,9 @@ class SeriousPythonAndroid extends SeriousPythonPlatform { List? modulePaths, Map? environmentVariables, bool? sync}) async { - Future setenv(String key, String value) async { - await methodChannel.invokeMethod( - 'setEnvironmentVariable', {'name': key, 'value': value}); - } + Future setenv(String key, String value) => + methodChannel.invokeMethod( + 'setEnvironmentVariable', {'name': key, 'value': value}); // load libpyjni.so to get JNI reference try { @@ -78,18 +77,18 @@ class SeriousPythonAndroid extends SeriousPythonPlatform { moduleSearchPaths.add(sitePackagesPath); } - setenv("PYTHONINSPECT", "1"); - setenv("PYTHONDONTWRITEBYTECODE", "1"); - setenv("PYTHONNOUSERSITE", "1"); - setenv("PYTHONUNBUFFERED", "1"); - setenv("LC_CTYPE", "UTF-8"); - setenv("PYTHONHOME", pythonLibPath); - setenv("PYTHONPATH", moduleSearchPaths.join(":")); + await setenv("PYTHONINSPECT", "1"); + await setenv("PYTHONDONTWRITEBYTECODE", "1"); + await setenv("PYTHONNOUSERSITE", "1"); + await setenv("PYTHONUNBUFFERED", "1"); + await setenv("LC_CTYPE", "UTF-8"); + await setenv("PYTHONHOME", pythonLibPath); + await setenv("PYTHONPATH", moduleSearchPaths.join(":")); // set environment variables if (environmentVariables != null) { for (var v in environmentVariables.entries) { - setenv(v.key, v.value); + await setenv(v.key, v.value); } } From 3f4adc08b82147a46452b19d2a29d6af715a58a0 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 10 Dec 2025 17:30:57 -0800 Subject: [PATCH 02/31] Ensure single CPython interpreter per process Introduces a guard to prevent repeated initialization and finalization of the CPython interpreter, addressing crashes on second launch with CPython 3.12. Also ensures the current native thread is registered with the GIL when running Python code, improving stability when re-entering Python from new Dart isolates or threads. --- .../lib/src/cpython.dart | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 441ad98e..6e18e542 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -11,6 +11,9 @@ import 'gen.dart'; export 'gen.dart'; CPython? _cpython; +// Keep a single interpreter per process; repeated init/finalize of CPython 3.12 +// from an embedder is fragile and was crashing on second launch. +bool _pythonInitialized = false; CPython getCPython(String dynamicLibPath) { return _cpython ??= _cpython = CPython(DynamicLibrary.open(dynamicLibPath)); @@ -51,33 +54,43 @@ Future runPythonProgramInIsolate(List arguments) async { debugPrint("programModuleName: $programModuleName"); final cpython = getCPython(dynamicLibPath); - cpython.Py_Initialize(); - debugPrint("after Py_Initialize()"); + if (!_pythonInitialized) { + cpython.Py_Initialize(); + _pythonInitialized = true; + debugPrint("after Py_Initialize()"); + } else { + debugPrint("Python already initialized; reusing interpreter"); + } var result = ""; - - if (script != "") { - // run script - final scriptPtr = script.toNativeUtf8(); - int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); - debugPrint("PyRun_SimpleString for script result: $sr"); - malloc.free(scriptPtr); - if (sr != 0) { - result = getPythonError(cpython); - } - } else { - // run program - final moduleNamePtr = programModuleName.toNativeUtf8(); - var modulePtr = cpython.PyImport_ImportModule(moduleNamePtr.cast()); - if (modulePtr == nullptr) { - result = getPythonError(cpython); + // Ensure the current native thread is registered with the GIL; this is + // required if we re-enter Python from a new Dart isolate/thread. + final gilState = cpython.PyGILState_Ensure(); + + try { + if (script != "") { + // run script + final scriptPtr = script.toNativeUtf8(); + int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); + debugPrint("PyRun_SimpleString for script result: $sr"); + malloc.free(scriptPtr); + if (sr != 0) { + result = getPythonError(cpython); + } + } else { + // run program + final moduleNamePtr = programModuleName.toNativeUtf8(); + var modulePtr = + cpython.PyImport_ImportModule(moduleNamePtr.cast()); + if (modulePtr == nullptr) { + result = getPythonError(cpython); + } + malloc.free(moduleNamePtr); } - malloc.free(moduleNamePtr); + } finally { + cpython.PyGILState_Release(gilState); } - cpython.Py_Finalize(); - debugPrint("after Py_Finalize()"); - sendPort.send(result); return result; From f31ee2b2dec396553cdaafc8cd49274c9064263f Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 10 Dec 2025 17:36:38 -0800 Subject: [PATCH 03/31] Remove 'Warning:' prefix from debug message Updated the debugPrint statement to remove the 'Warning:' prefix when unable to load libpyjni.so. This change streamlines log output for consistency. --- src/serious_python_android/lib/serious_python_android.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serious_python_android/lib/serious_python_android.dart b/src/serious_python_android/lib/serious_python_android.dart index 5c4c7067..0ec59840 100644 --- a/src/serious_python_android/lib/serious_python_android.dart +++ b/src/serious_python_android/lib/serious_python_android.dart @@ -43,7 +43,7 @@ class SeriousPythonAndroid extends SeriousPythonPlatform { .invokeMethod('loadLibrary', {'libname': 'pyjni'}); await setenv("FLET_JNI_READY", "1"); } catch (e) { - debugPrint("Warning: Unable to load libpyjni.so library: $e"); + debugPrint("Unable to load libpyjni.so library: $e"); } // unpack python bundle From e3e46f92ef8c4155acf851240e3c84d139388c7d Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 10 Dec 2025 23:04:43 -0800 Subject: [PATCH 04/31] Revert "Ensure single CPython interpreter per process" This reverts commit 3f4adc08b82147a46452b19d2a29d6af715a58a0. --- .../lib/src/cpython.dart | 57 +++++++------------ 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 6e18e542..441ad98e 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -11,9 +11,6 @@ import 'gen.dart'; export 'gen.dart'; CPython? _cpython; -// Keep a single interpreter per process; repeated init/finalize of CPython 3.12 -// from an embedder is fragile and was crashing on second launch. -bool _pythonInitialized = false; CPython getCPython(String dynamicLibPath) { return _cpython ??= _cpython = CPython(DynamicLibrary.open(dynamicLibPath)); @@ -54,43 +51,33 @@ Future runPythonProgramInIsolate(List arguments) async { debugPrint("programModuleName: $programModuleName"); final cpython = getCPython(dynamicLibPath); - if (!_pythonInitialized) { - cpython.Py_Initialize(); - _pythonInitialized = true; - debugPrint("after Py_Initialize()"); - } else { - debugPrint("Python already initialized; reusing interpreter"); - } + cpython.Py_Initialize(); + debugPrint("after Py_Initialize()"); var result = ""; - // Ensure the current native thread is registered with the GIL; this is - // required if we re-enter Python from a new Dart isolate/thread. - final gilState = cpython.PyGILState_Ensure(); - - try { - if (script != "") { - // run script - final scriptPtr = script.toNativeUtf8(); - int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); - debugPrint("PyRun_SimpleString for script result: $sr"); - malloc.free(scriptPtr); - if (sr != 0) { - result = getPythonError(cpython); - } - } else { - // run program - final moduleNamePtr = programModuleName.toNativeUtf8(); - var modulePtr = - cpython.PyImport_ImportModule(moduleNamePtr.cast()); - if (modulePtr == nullptr) { - result = getPythonError(cpython); - } - malloc.free(moduleNamePtr); + + if (script != "") { + // run script + final scriptPtr = script.toNativeUtf8(); + int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); + debugPrint("PyRun_SimpleString for script result: $sr"); + malloc.free(scriptPtr); + if (sr != 0) { + result = getPythonError(cpython); + } + } else { + // run program + final moduleNamePtr = programModuleName.toNativeUtf8(); + var modulePtr = cpython.PyImport_ImportModule(moduleNamePtr.cast()); + if (modulePtr == nullptr) { + result = getPythonError(cpython); } - } finally { - cpython.PyGILState_Release(gilState); + malloc.free(moduleNamePtr); } + cpython.Py_Finalize(); + debugPrint("after Py_Finalize()"); + sendPort.send(result); return result; From 15be21d0dedc2944357a57a84cd74ec80bc7e4f5 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 11:25:31 -0800 Subject: [PATCH 05/31] Refactor debug logging and Python runtime init Replaces direct debugPrint calls with a private _debug function for consistent logging. Adds a check to only initialize the Python runtime if it is not already active, and removes unconditional Py_Finalize call to avoid finalizing an already active runtime. --- .../lib/src/cpython.dart | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 441ad98e..9099bfbb 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -16,6 +16,10 @@ CPython getCPython(String dynamicLibPath) { return _cpython ??= _cpython = CPython(DynamicLibrary.open(dynamicLibPath)); } +void _debug(String message) { + debugPrint("[serious_python] $message"); +} + Future runPythonProgramFFI(bool sync, String dynamicLibPath, String pythonProgramPath, String script) async { final receivePort = ReceivePort(); @@ -46,13 +50,16 @@ Future runPythonProgramInIsolate(List arguments) async { var programDirPath = p.dirname(pythonProgramPath); var programModuleName = p.basenameWithoutExtension(pythonProgramPath); - debugPrint("dynamicLibPath: $dynamicLibPath"); - debugPrint("programDirPath: $programDirPath"); - debugPrint("programModuleName: $programModuleName"); + _debug("dynamicLibPath: $dynamicLibPath"); + _debug("programDirPath: $programDirPath"); + _debug("programModuleName: $programModuleName"); final cpython = getCPython(dynamicLibPath); - cpython.Py_Initialize(); - debugPrint("after Py_Initialize()"); + if (cpython.Py_IsInitialized() == 0) { + // Initialize the runtime only if it is not already active. + cpython.Py_Initialize(); + _debug("after Py_Initialize()"); + } var result = ""; @@ -60,7 +67,7 @@ Future runPythonProgramInIsolate(List arguments) async { // run script final scriptPtr = script.toNativeUtf8(); int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); - debugPrint("PyRun_SimpleString for script result: $sr"); + _debug("PyRun_SimpleString for script result: $sr"); malloc.free(scriptPtr); if (sr != 0) { result = getPythonError(cpython); @@ -75,9 +82,6 @@ Future runPythonProgramInIsolate(List arguments) async { malloc.free(moduleNamePtr); } - cpython.Py_Finalize(); - debugPrint("after Py_Finalize()"); - sendPort.send(result); return result; @@ -94,7 +98,7 @@ String getPythonError(CPython cpython) { cpython.Py_DecRef(tracebackModuleNamePtr.cast()); if (tracebackModulePtr != nullptr) { - //debugPrint("Traceback module loaded"); + //_debug("Traceback module loaded"); final formatFuncName = "format_exception".toNativeUtf8(); final pFormatFunc = cpython.PyObject_GetAttrString( From 5f6c02992e7d166b6bd9a9de2012dfad1269ed90 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 12:04:04 -0800 Subject: [PATCH 06/31] Add logcat forwarding for Python stdout/stderr Introduces a Python initialization script that redirects stdout and stderr to Android logcat using a custom writer and logging handler. Adds a Dart function to inject this script into the Python interpreter, ensuring Python logs are visible in Android logcat. Also adds debug messages to trace script execution. --- .../lib/src/cpython.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 9099bfbb..58b309a4 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -11,6 +11,32 @@ import 'gen.dart'; export 'gen.dart'; CPython? _cpython; +const _logcatInitScript = r''' +import sys, logging +from ctypes import cdll +liblog = cdll.LoadLibrary("liblog.so") +ANDROID_LOG_INFO = 4 + +def _log_to_logcat(msg, level=ANDROID_LOG_INFO): + if not msg: + return + if isinstance(msg, bytes): + msg = msg.decode("utf-8", errors="replace") + liblog.__android_log_write(level, b"serious_python", msg.encode("utf-8")) + +class _LogcatWriter: + def write(self, msg): + _log_to_logcat(msg.strip()) + def flush(self): + pass + +sys.stdout = sys.stderr = _LogcatWriter() +handler = logging.StreamHandler(sys.stderr) +handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) +root = logging.getLogger() +root.handlers[:] = [handler] +root.setLevel(logging.DEBUG) +'''; CPython getCPython(String dynamicLibPath) { return _cpython ??= _cpython = CPython(DynamicLibrary.open(dynamicLibPath)); @@ -61,10 +87,17 @@ Future runPythonProgramInIsolate(List arguments) async { _debug("after Py_Initialize()"); } + final logcatSetupError = _setupLogcatForwarding(cpython); + if (logcatSetupError != null) { + sendPort.send(logcatSetupError); + return logcatSetupError; + } + var result = ""; if (script != "") { // run script + _debug("Running script: $script"); final scriptPtr = script.toNativeUtf8(); int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); _debug("PyRun_SimpleString for script result: $sr"); @@ -74,6 +107,7 @@ Future runPythonProgramInIsolate(List arguments) async { } } else { // run program + _debug("Running program module: $programModuleName"); final moduleNamePtr = programModuleName.toNativeUtf8(); var modulePtr = cpython.PyImport_ImportModule(moduleNamePtr.cast()); if (modulePtr == nullptr) { @@ -82,6 +116,8 @@ Future runPythonProgramInIsolate(List arguments) async { malloc.free(moduleNamePtr); } + _debug("Python program finished"); + sendPort.send(result); return result; @@ -131,3 +167,17 @@ String getPythonError(CPython cpython) { return "Error loading traceback module."; } } + +String? _setupLogcatForwarding(CPython cpython) { + _debug("Setting up logcat forwarding"); + final setupPtr = _logcatInitScript.toNativeUtf8(); + final result = cpython.PyRun_SimpleString(setupPtr.cast()); + malloc.free(setupPtr); + + if (result != 0) { + return getPythonError(cpython); + } + + _debug("logcat forwarding configured"); + return null; +} From 8633a0101f1c1b2b80b9a427c755eb5ecaa38b5f Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 12:10:47 -0800 Subject: [PATCH 07/31] Ensure GIL is held for all Python C API calls Introduced a _withGIL helper to wrap all calls to the Python C API with GIL acquisition and release. This change improves thread safety and correctness when interacting with the Python interpreter from Dart, especially in isolate contexts. --- .../lib/src/cpython.dart | 78 ++++++++++++------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 58b309a4..f4204a38 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -46,6 +46,15 @@ void _debug(String message) { debugPrint("[serious_python] $message"); } +T _withGIL(CPython cpython, T Function() action) { + final gil = cpython.PyGILState_Ensure(); + try { + return action(); + } finally { + cpython.PyGILState_Release(gil); + } +} + Future runPythonProgramFFI(bool sync, String dynamicLibPath, String pythonProgramPath, String script) async { final receivePort = ReceivePort(); @@ -87,7 +96,7 @@ Future runPythonProgramInIsolate(List arguments) async { _debug("after Py_Initialize()"); } - final logcatSetupError = _setupLogcatForwarding(cpython); + final logcatSetupError = _withGIL(cpython, () => _setupLogcatForwarding(cpython)); if (logcatSetupError != null) { sendPort.send(logcatSetupError); return logcatSetupError; @@ -97,23 +106,31 @@ Future runPythonProgramInIsolate(List arguments) async { if (script != "") { // run script - _debug("Running script: $script"); - final scriptPtr = script.toNativeUtf8(); - int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); - _debug("PyRun_SimpleString for script result: $sr"); - malloc.free(scriptPtr); - if (sr != 0) { - result = getPythonError(cpython); - } + result = _withGIL(cpython, () { + _debug("Running script: $script"); + final scriptPtr = script.toNativeUtf8(); + int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); + _debug("PyRun_SimpleString for script result: $sr"); + malloc.free(scriptPtr); + if (sr != 0) { + return getPythonError(cpython); + } + return ""; + }); } else { // run program - _debug("Running program module: $programModuleName"); - final moduleNamePtr = programModuleName.toNativeUtf8(); - var modulePtr = cpython.PyImport_ImportModule(moduleNamePtr.cast()); - if (modulePtr == nullptr) { - result = getPythonError(cpython); - } - malloc.free(moduleNamePtr); + result = _withGIL(cpython, () { + _debug("Running program module: $programModuleName"); + final moduleNamePtr = programModuleName.toNativeUtf8(); + var modulePtr = cpython.PyImport_ImportModule(moduleNamePtr.cast()); + if (modulePtr == nullptr) { + final error = getPythonError(cpython); + malloc.free(moduleNamePtr); + return error; + } + malloc.free(moduleNamePtr); + return ""; + }); } _debug("Python program finished"); @@ -125,38 +142,41 @@ Future runPythonProgramInIsolate(List arguments) async { String getPythonError(CPython cpython) { // get error object - var exPtr = cpython.PyErr_GetRaisedException(); + var exPtr = _withGIL(cpython, () => cpython.PyErr_GetRaisedException()); // use 'traceback' module to format exception final tracebackModuleNamePtr = "traceback".toNativeUtf8(); - var tracebackModulePtr = - cpython.PyImport_ImportModule(tracebackModuleNamePtr.cast()); + var tracebackModulePtr = _withGIL(cpython, + () => cpython.PyImport_ImportModule(tracebackModuleNamePtr.cast())); cpython.Py_DecRef(tracebackModuleNamePtr.cast()); if (tracebackModulePtr != nullptr) { //_debug("Traceback module loaded"); final formatFuncName = "format_exception".toNativeUtf8(); - final pFormatFunc = cpython.PyObject_GetAttrString( - tracebackModulePtr, formatFuncName.cast()); + final pFormatFunc = _withGIL( + cpython, + () => cpython.PyObject_GetAttrString( + tracebackModulePtr, formatFuncName.cast())); cpython.Py_DecRef(tracebackModuleNamePtr.cast()); if (pFormatFunc != nullptr && cpython.PyCallable_Check(pFormatFunc) != 0) { // call `traceback.format_exception()` method - final pArgs = cpython.PyTuple_New(1); - cpython.PyTuple_SetItem(pArgs, 0, exPtr); + final pArgs = _withGIL(cpython, () => cpython.PyTuple_New(1)); + _withGIL(cpython, () => cpython.PyTuple_SetItem(pArgs, 0, exPtr)); // result is a list - var listPtr = cpython.PyObject_CallObject(pFormatFunc, pArgs); + var listPtr = + _withGIL(cpython, () => cpython.PyObject_CallObject(pFormatFunc, pArgs)); // get and combine list items var exLines = []; - var listSize = cpython.PyList_Size(listPtr); + var listSize = _withGIL(cpython, () => cpython.PyList_Size(listPtr)); for (var i = 0; i < listSize; i++) { - var itemObj = cpython.PyList_GetItem(listPtr, i); - var itemObjStr = cpython.PyObject_Str(itemObj); - var s = - cpython.PyUnicode_AsUTF8(itemObjStr).cast().toDartString(); + var itemObj = _withGIL(cpython, () => cpython.PyList_GetItem(listPtr, i)); + var itemObjStr = _withGIL(cpython, () => cpython.PyObject_Str(itemObj)); + var s = _withGIL(cpython, + () => cpython.PyUnicode_AsUTF8(itemObjStr).cast().toDartString()); exLines.add(s); } return exLines.join(""); From a272d792b270276929d76cae914f689f1c833483 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 12:24:13 -0800 Subject: [PATCH 08/31] Refactor GIL usage in Python execution and error handling Moved _withGIL calls to wrap larger code blocks instead of individual function calls in runPythonProgramInIsolate and getPythonError. This simplifies the code and reduces redundant GIL management, improving readability and maintainability. --- .../lib/src/cpython.dart | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index f4204a38..ac3b5bb7 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -96,17 +96,14 @@ Future runPythonProgramInIsolate(List arguments) async { _debug("after Py_Initialize()"); } - final logcatSetupError = _withGIL(cpython, () => _setupLogcatForwarding(cpython)); - if (logcatSetupError != null) { - sendPort.send(logcatSetupError); - return logcatSetupError; - } - - var result = ""; + final result = _withGIL(cpython, () { + final logcatSetupError = _setupLogcatForwarding(cpython); + if (logcatSetupError != null) { + return logcatSetupError; + } - if (script != "") { - // run script - result = _withGIL(cpython, () { + if (script != "") { + // run script _debug("Running script: $script"); final scriptPtr = script.toNativeUtf8(); int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); @@ -115,11 +112,8 @@ Future runPythonProgramInIsolate(List arguments) async { if (sr != 0) { return getPythonError(cpython); } - return ""; - }); - } else { - // run program - result = _withGIL(cpython, () { + } else { + // run program _debug("Running program module: $programModuleName"); final moduleNamePtr = programModuleName.toNativeUtf8(); var modulePtr = cpython.PyImport_ImportModule(moduleNamePtr.cast()); @@ -129,9 +123,10 @@ Future runPythonProgramInIsolate(List arguments) async { return error; } malloc.free(moduleNamePtr); - return ""; - }); - } + } + + return ""; + }); _debug("Python program finished"); @@ -142,41 +137,38 @@ Future runPythonProgramInIsolate(List arguments) async { String getPythonError(CPython cpython) { // get error object - var exPtr = _withGIL(cpython, () => cpython.PyErr_GetRaisedException()); + var exPtr = cpython.PyErr_GetRaisedException(); // use 'traceback' module to format exception final tracebackModuleNamePtr = "traceback".toNativeUtf8(); - var tracebackModulePtr = _withGIL(cpython, - () => cpython.PyImport_ImportModule(tracebackModuleNamePtr.cast())); + var tracebackModulePtr = + cpython.PyImport_ImportModule(tracebackModuleNamePtr.cast()); cpython.Py_DecRef(tracebackModuleNamePtr.cast()); if (tracebackModulePtr != nullptr) { //_debug("Traceback module loaded"); final formatFuncName = "format_exception".toNativeUtf8(); - final pFormatFunc = _withGIL( - cpython, - () => cpython.PyObject_GetAttrString( - tracebackModulePtr, formatFuncName.cast())); + final pFormatFunc = + cpython.PyObject_GetAttrString(tracebackModulePtr, formatFuncName.cast()); cpython.Py_DecRef(tracebackModuleNamePtr.cast()); if (pFormatFunc != nullptr && cpython.PyCallable_Check(pFormatFunc) != 0) { // call `traceback.format_exception()` method - final pArgs = _withGIL(cpython, () => cpython.PyTuple_New(1)); - _withGIL(cpython, () => cpython.PyTuple_SetItem(pArgs, 0, exPtr)); + final pArgs = cpython.PyTuple_New(1); + cpython.PyTuple_SetItem(pArgs, 0, exPtr); // result is a list - var listPtr = - _withGIL(cpython, () => cpython.PyObject_CallObject(pFormatFunc, pArgs)); + var listPtr = cpython.PyObject_CallObject(pFormatFunc, pArgs); // get and combine list items var exLines = []; - var listSize = _withGIL(cpython, () => cpython.PyList_Size(listPtr)); + var listSize = cpython.PyList_Size(listPtr); for (var i = 0; i < listSize; i++) { - var itemObj = _withGIL(cpython, () => cpython.PyList_GetItem(listPtr, i)); - var itemObjStr = _withGIL(cpython, () => cpython.PyObject_Str(itemObj)); - var s = _withGIL(cpython, - () => cpython.PyUnicode_AsUTF8(itemObjStr).cast().toDartString()); + var itemObj = cpython.PyList_GetItem(listPtr, i); + var itemObjStr = cpython.PyObject_Str(itemObj); + var s = + cpython.PyUnicode_AsUTF8(itemObjStr).cast().toDartString(); exLines.add(s); } return exLines.join(""); From 78c3e0f54fef5b16b153efa4b84330287f84af73 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 12:50:44 -0800 Subject: [PATCH 09/31] Add AppLifecycleListener to finalize Python on detach Replaces flutter/foundation.dart with flutter/widgets.dart and adds an AppLifecycleListener to call Py_FinalizeEx when the app detaches. This ensures proper cleanup of the Python interpreter when the app lifecycle ends. --- src/serious_python_android/lib/src/cpython.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index ac3b5bb7..40878f1f 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -3,7 +3,7 @@ import 'dart:ffi'; import 'dart:isolate'; import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:path/path.dart' as p; import 'gen.dart'; @@ -96,6 +96,16 @@ Future runPythonProgramInIsolate(List arguments) async { _debug("after Py_Initialize()"); } + AppLifecycleListener(onDetach: () { + _debug("AppLifecycleListener: onDetach"); + _withGIL(cpython, () { + if (cpython.Py_IsInitialized() != 0) { + cpython.Py_FinalizeEx(); + _debug("after Py_FinalizeEx()"); + } + }); + }); + final result = _withGIL(cpython, () { final logcatSetupError = _setupLogcatForwarding(cpython); if (logcatSetupError != null) { @@ -149,8 +159,8 @@ String getPythonError(CPython cpython) { //_debug("Traceback module loaded"); final formatFuncName = "format_exception".toNativeUtf8(); - final pFormatFunc = - cpython.PyObject_GetAttrString(tracebackModulePtr, formatFuncName.cast()); + final pFormatFunc = cpython.PyObject_GetAttrString( + tracebackModulePtr, formatFuncName.cast()); cpython.Py_DecRef(tracebackModuleNamePtr.cast()); if (pFormatFunc != nullptr && cpython.PyCallable_Check(pFormatFunc) != 0) { From 6823613c243db0f59f4f0cc2a3d2290340cf6bc9 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 13:24:39 -0800 Subject: [PATCH 10/31] Move Py_FinalizeEx call to end of isolate function Removed AppLifecycleListener-based finalization and now explicitly call cpython.Py_FinalizeEx() at the end of runPythonProgramInIsolate. This ensures Python finalization occurs after the program finishes, improving resource management. --- src/serious_python_android/lib/src/cpython.dart | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 40878f1f..213f9912 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -96,16 +96,6 @@ Future runPythonProgramInIsolate(List arguments) async { _debug("after Py_Initialize()"); } - AppLifecycleListener(onDetach: () { - _debug("AppLifecycleListener: onDetach"); - _withGIL(cpython, () { - if (cpython.Py_IsInitialized() != 0) { - cpython.Py_FinalizeEx(); - _debug("after Py_FinalizeEx()"); - } - }); - }); - final result = _withGIL(cpython, () { final logcatSetupError = _setupLogcatForwarding(cpython); if (logcatSetupError != null) { @@ -140,6 +130,9 @@ Future runPythonProgramInIsolate(List arguments) async { _debug("Python program finished"); + cpython.Py_FinalizeEx(); + _debug("after Py_FinalizeEx()"); + sendPort.send(result); return result; From cc1a2379f089d9d63eb7686985086e5f3cdc2147 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 13:29:55 -0800 Subject: [PATCH 11/31] Comment out Py_FinalizeEx call in runPythonProgramInIsolate The calls to cpython.Py_FinalizeEx() and its debug log have been commented out in runPythonProgramInIsolate. This may be to prevent issues related to finalizing the Python interpreter in this context. --- src/serious_python_android/lib/src/cpython.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 213f9912..234a25de 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -130,8 +130,8 @@ Future runPythonProgramInIsolate(List arguments) async { _debug("Python program finished"); - cpython.Py_FinalizeEx(); - _debug("after Py_FinalizeEx()"); +// cpython.Py_FinalizeEx(); +// _debug("after Py_FinalizeEx()"); sendPort.send(result); From a0371f95a7a0d79daea8bfe0d904ba6e710961fa Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 13:39:44 -0800 Subject: [PATCH 12/31] Ensure Python interpreter finalization after execution Refactored runPythonProgramInIsolate to always finalize the Python interpreter in a finally block, ensuring proper cleanup and GIL management after script or module execution. --- .../lib/src/cpython.dart | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 234a25de..7b13ced3 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -96,42 +96,52 @@ Future runPythonProgramInIsolate(List arguments) async { _debug("after Py_Initialize()"); } - final result = _withGIL(cpython, () { - final logcatSetupError = _setupLogcatForwarding(cpython); - if (logcatSetupError != null) { - return logcatSetupError; - } - - if (script != "") { - // run script - _debug("Running script: $script"); - final scriptPtr = script.toNativeUtf8(); - int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); - _debug("PyRun_SimpleString for script result: $sr"); - malloc.free(scriptPtr); - if (sr != 0) { - return getPythonError(cpython); + String result = ""; + try { + result = _withGIL(cpython, () { + final logcatSetupError = _setupLogcatForwarding(cpython); + if (logcatSetupError != null) { + return logcatSetupError; } - } else { - // run program - _debug("Running program module: $programModuleName"); - final moduleNamePtr = programModuleName.toNativeUtf8(); - var modulePtr = cpython.PyImport_ImportModule(moduleNamePtr.cast()); - if (modulePtr == nullptr) { - final error = getPythonError(cpython); + + if (script != "") { + // run script + _debug("Running script: $script"); + final scriptPtr = script.toNativeUtf8(); + int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); + _debug("PyRun_SimpleString for script result: $sr"); + malloc.free(scriptPtr); + if (sr != 0) { + return getPythonError(cpython); + } + } else { + // run program + _debug("Running program module: $programModuleName"); + final moduleNamePtr = programModuleName.toNativeUtf8(); + var modulePtr = + cpython.PyImport_ImportModule(moduleNamePtr.cast()); + if (modulePtr == nullptr) { + final error = getPythonError(cpython); + malloc.free(moduleNamePtr); + return error; + } malloc.free(moduleNamePtr); - return error; } - malloc.free(moduleNamePtr); - } - return ""; - }); + _debug("Python program finished"); - _debug("Python program finished"); - -// cpython.Py_FinalizeEx(); -// _debug("after Py_FinalizeEx()"); + return ""; + }); + } finally { + // Always finalize interpreter so the next run starts clean and can obtain the GIL. + _withGIL(cpython, () { + if (cpython.Py_IsInitialized() != 0) { + cpython.Py_FinalizeEx(); + _debug("after Py_FinalizeEx()"); + } + }); + _cpython = null; + } sendPort.send(result); From 5763e5b8a30e51af6471b68c76ce7535a6a55a23 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 13:46:48 -0800 Subject: [PATCH 13/31] Add safe interpreter finalization for CPython Introduced the _finalizeInterpreter function to safely finalize the CPython interpreter without releasing the GIL after Py_FinalizeEx, preventing fatal errors. Updated runPythonProgramInIsolate to use this new function for proper cleanup. --- .../lib/src/cpython.dart | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 7b13ced3..bdaf3ec8 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -55,6 +55,21 @@ T _withGIL(CPython cpython, T Function() action) { } } +/// Finalize interpreter safely without releasing GIL afterwards (Py_FinalizeEx +/// tears down the current thread state, so releasing would fatal). +void _finalizeInterpreter(CPython cpython) { + if (cpython.Py_IsInitialized() == 0) { + return; + } + cpython.PyGILState_Ensure(); + try { + cpython.Py_FinalizeEx(); + _debug("after Py_FinalizeEx()"); + } finally { + // Do NOT call PyGILState_Release after finalize; the thread state is gone. + } +} + Future runPythonProgramFFI(bool sync, String dynamicLibPath, String pythonProgramPath, String script) async { final receivePort = ReceivePort(); @@ -134,12 +149,7 @@ Future runPythonProgramInIsolate(List arguments) async { }); } finally { // Always finalize interpreter so the next run starts clean and can obtain the GIL. - _withGIL(cpython, () { - if (cpython.Py_IsInitialized() != 0) { - cpython.Py_FinalizeEx(); - _debug("after Py_FinalizeEx()"); - } - }); + _finalizeInterpreter(cpython); _cpython = null; } From 0fdfe4033c6fe9f266300de04ceed46ee4a5ad03 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 13:53:07 -0800 Subject: [PATCH 14/31] Fix memory deallocation for native UTF-8 strings Replaced incorrect use of Py_DecRef with malloc.free for freeing native UTF-8 strings allocated with toNativeUtf8. This ensures proper memory management and prevents potential memory leaks. --- src/serious_python_android/lib/src/cpython.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index bdaf3ec8..64d30e61 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -166,7 +166,7 @@ String getPythonError(CPython cpython) { final tracebackModuleNamePtr = "traceback".toNativeUtf8(); var tracebackModulePtr = cpython.PyImport_ImportModule(tracebackModuleNamePtr.cast()); - cpython.Py_DecRef(tracebackModuleNamePtr.cast()); + malloc.free(tracebackModuleNamePtr); if (tracebackModulePtr != nullptr) { //_debug("Traceback module loaded"); @@ -174,7 +174,7 @@ String getPythonError(CPython cpython) { final formatFuncName = "format_exception".toNativeUtf8(); final pFormatFunc = cpython.PyObject_GetAttrString( tracebackModulePtr, formatFuncName.cast()); - cpython.Py_DecRef(tracebackModuleNamePtr.cast()); + malloc.free(formatFuncName); if (pFormatFunc != nullptr && cpython.PyCallable_Check(pFormatFunc) != 0) { // call `traceback.format_exception()` method From 919926d77351bab3d7bfecf5216691a9330e01ef Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 14:02:39 -0800 Subject: [PATCH 15/31] Update interpreter finalization logic for Android Refactored _finalizeInterpreter to remove unnecessary GIL release after Py_FinalizeEx. Interpreter is no longer finalized in runPythonProgramInIsolate to prevent crashes when re-initializing extension modules on Android. --- src/serious_python_android/lib/src/cpython.dart | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 64d30e61..62ef771a 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -61,13 +61,11 @@ void _finalizeInterpreter(CPython cpython) { if (cpython.Py_IsInitialized() == 0) { return; } + // Acquire the GIL but avoid releasing it after finalize; Py_FinalizeEx will + // tear down the current thread state. cpython.PyGILState_Ensure(); - try { - cpython.Py_FinalizeEx(); - _debug("after Py_FinalizeEx()"); - } finally { - // Do NOT call PyGILState_Release after finalize; the thread state is gone. - } + cpython.Py_FinalizeEx(); + _debug("after Py_FinalizeEx()"); } Future runPythonProgramFFI(bool sync, String dynamicLibPath, @@ -148,9 +146,8 @@ Future runPythonProgramInIsolate(List arguments) async { return ""; }); } finally { - // Always finalize interpreter so the next run starts clean and can obtain the GIL. - _finalizeInterpreter(cpython); - _cpython = null; + // Keep interpreter alive between runs; finalizing caused crashes when + // re-initializing extension modules (e.g. _ctypes) on Android. } sendPort.send(result); From aa32d6ba566226f2d8d79c79e808c280220b0e90 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 14:20:55 -0800 Subject: [PATCH 16/31] Comment out GIL management in _withGIL function The GIL acquisition and release logic in _withGIL has been commented out, and the function now directly executes the action. Interpreter finalization code in runPythonProgramInIsolate remains commented out, with updated comments explaining the rationale. --- .../lib/src/cpython.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 62ef771a..b71ea985 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -47,12 +47,13 @@ void _debug(String message) { } T _withGIL(CPython cpython, T Function() action) { - final gil = cpython.PyGILState_Ensure(); - try { - return action(); - } finally { - cpython.PyGILState_Release(gil); - } +// final gil = cpython.PyGILState_Ensure(); +// try { +// return action(); +// } finally { +// cpython.PyGILState_Release(gil); +// } + return action(); } /// Finalize interpreter safely without releasing GIL afterwards (Py_FinalizeEx @@ -146,8 +147,9 @@ Future runPythonProgramInIsolate(List arguments) async { return ""; }); } finally { - // Keep interpreter alive between runs; finalizing caused crashes when - // re-initializing extension modules (e.g. _ctypes) on Android. + // Finalize interpreter so subsequent runs start clean and GIL is free. + // _finalizeInterpreter(cpython); + // _cpython = null; } sendPort.send(result); From 524ce7386d8335f86527384897dde920fe953e28 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 14:26:54 -0800 Subject: [PATCH 17/31] Enable GIL management and interpreter finalization Uncommented code to properly acquire and release the Python GIL in _withGIL, and to finalize the interpreter after running a Python program in an isolate. This ensures thread safety and clean interpreter state between runs. --- src/serious_python_android/lib/src/cpython.dart | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index b71ea985..a97f6acf 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -47,13 +47,12 @@ void _debug(String message) { } T _withGIL(CPython cpython, T Function() action) { -// final gil = cpython.PyGILState_Ensure(); -// try { -// return action(); -// } finally { -// cpython.PyGILState_Release(gil); -// } - return action(); + final gil = cpython.PyGILState_Ensure(); + try { + return action(); + } finally { + cpython.PyGILState_Release(gil); + } } /// Finalize interpreter safely without releasing GIL afterwards (Py_FinalizeEx @@ -148,8 +147,8 @@ Future runPythonProgramInIsolate(List arguments) async { }); } finally { // Finalize interpreter so subsequent runs start clean and GIL is free. - // _finalizeInterpreter(cpython); - // _cpython = null; + _finalizeInterpreter(cpython); + _cpython = null; } sendPort.send(result); From 4a7f724ae38219a25389fdb199d0f2ba9b18090a Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 14:38:17 -0800 Subject: [PATCH 18/31] Improve CPython isolate handling and logcat setup Ensures logcat forwarding is idempotent across Dart isolate restarts and avoids finalizing the CPython interpreter between runs to prevent native crashes. Also refines isolate management to prevent killing isolates that may leave the interpreter in a bad state, and improves error handling for logcat forwarding setup. --- .../lib/src/cpython.dart | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index a97f6acf..882c92e1 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -11,8 +11,15 @@ import 'gen.dart'; export 'gen.dart'; CPython? _cpython; +String? _logcatForwardingError; const _logcatInitScript = r''' import sys, logging + +# Make this init idempotent across Dart isolate restarts. +if getattr(sys, "__serious_python_logcat_configured__", False): + raise SystemExit +sys.__serious_python_logcat_configured__ = True + from ctypes import cdll liblog = cdll.LoadLibrary("liblog.so") ANDROID_LOG_INFO = 4 @@ -73,19 +80,23 @@ Future runPythonProgramFFI(bool sync, String dynamicLibPath, final receivePort = ReceivePort(); if (sync) { // sync run - return await runPythonProgramInIsolate( - [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]); + try { + return await runPythonProgramInIsolate( + [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]); + } finally { + receivePort.close(); + } } else { - var completer = Completer(); // async run - final isolate = await Isolate.spawn(runPythonProgramInIsolate, + // + // IMPORTANT: do not `isolate.kill()` here. Killing the isolate can abort the + // underlying OS thread while it still interacts with CPython, leaving the + // interpreter/GIL in a bad state for subsequent runs. + await Isolate.spawn(runPythonProgramInIsolate, [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]); - receivePort.listen((message) { - receivePort.close(); - isolate.kill(); - completer.complete(message); - }); - return completer.future; + final message = await receivePort.first; + receivePort.close(); + return message as String; } } @@ -146,9 +157,9 @@ Future runPythonProgramInIsolate(List arguments) async { return ""; }); } finally { - // Finalize interpreter so subsequent runs start clean and GIL is free. - _finalizeInterpreter(cpython); - _cpython = null; + // Keep interpreter alive between runs. Finalizing + re-initializing the + // interpreter is not reliably supported and has caused native crashes + // (e.g. during _ctypes re-import) on Android. } sendPort.send(result); @@ -208,9 +219,11 @@ String? _setupLogcatForwarding(CPython cpython) { malloc.free(setupPtr); if (result != 0) { - return getPythonError(cpython); + _logcatForwardingError = getPythonError(cpython); + return _logcatForwardingError; } _debug("logcat forwarding configured"); + _logcatForwardingError = null; return null; } From 2776a74a037ce9e7a0f97a051d5f057a47bf242c Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 16:10:34 -0800 Subject: [PATCH 19/31] Refactor Python execution to use sub-interpreters Each Python invocation now runs in a fresh sub-interpreter to reduce cross-run leakage and prevent GIL deadlocks. The logcat initialization script is now idempotent and only runs once per process. The unused _finalizeInterpreter function was removed. --- .../lib/src/cpython.dart | 132 +++++++++--------- 1 file changed, 67 insertions(+), 65 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 882c92e1..94c7521c 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -16,33 +16,32 @@ const _logcatInitScript = r''' import sys, logging # Make this init idempotent across Dart isolate restarts. -if getattr(sys, "__serious_python_logcat_configured__", False): - raise SystemExit -sys.__serious_python_logcat_configured__ = True - -from ctypes import cdll -liblog = cdll.LoadLibrary("liblog.so") -ANDROID_LOG_INFO = 4 - -def _log_to_logcat(msg, level=ANDROID_LOG_INFO): - if not msg: - return - if isinstance(msg, bytes): - msg = msg.decode("utf-8", errors="replace") - liblog.__android_log_write(level, b"serious_python", msg.encode("utf-8")) - -class _LogcatWriter: - def write(self, msg): - _log_to_logcat(msg.strip()) - def flush(self): - pass - -sys.stdout = sys.stderr = _LogcatWriter() -handler = logging.StreamHandler(sys.stderr) -handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) -root = logging.getLogger() -root.handlers[:] = [handler] -root.setLevel(logging.DEBUG) +if not getattr(sys, "__serious_python_logcat_configured__", False): + sys.__serious_python_logcat_configured__ = True + + from ctypes import cdll + liblog = cdll.LoadLibrary("liblog.so") + ANDROID_LOG_INFO = 4 + + def _log_to_logcat(msg, level=ANDROID_LOG_INFO): + if not msg: + return + if isinstance(msg, bytes): + msg = msg.decode("utf-8", errors="replace") + liblog.__android_log_write(level, b"serious_python", msg.encode("utf-8")) + + class _LogcatWriter: + def write(self, msg): + _log_to_logcat(msg.strip()) + def flush(self): + pass + + sys.stdout = sys.stderr = _LogcatWriter() + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) + root = logging.getLogger() + root.handlers[:] = [handler] + root.setLevel(logging.DEBUG) '''; CPython getCPython(String dynamicLibPath) { @@ -62,19 +61,6 @@ T _withGIL(CPython cpython, T Function() action) { } } -/// Finalize interpreter safely without releasing GIL afterwards (Py_FinalizeEx -/// tears down the current thread state, so releasing would fatal). -void _finalizeInterpreter(CPython cpython) { - if (cpython.Py_IsInitialized() == 0) { - return; - } - // Acquire the GIL but avoid releasing it after finalize; Py_FinalizeEx will - // tear down the current thread state. - cpython.PyGILState_Ensure(); - cpython.Py_FinalizeEx(); - _debug("after Py_FinalizeEx()"); -} - Future runPythonProgramFFI(bool sync, String dynamicLibPath, String pythonProgramPath, String script) async { final receivePort = ReceivePort(); @@ -123,38 +109,54 @@ Future runPythonProgramInIsolate(List arguments) async { String result = ""; try { result = _withGIL(cpython, () { - final logcatSetupError = _setupLogcatForwarding(cpython); - if (logcatSetupError != null) { - return logcatSetupError; + // Run each invocation in a fresh sub-interpreter to reduce cross-run + // leakage (event loops/background threads/modules) that can otherwise + // lead to GIL deadlocks on the next app start. + final mainThreadState = cpython.PyThreadState_Get(); + final subThreadState = cpython.Py_NewInterpreter(); + if (subThreadState == nullptr) { + return "Py_NewInterpreter() failed."; } - if (script != "") { - // run script - _debug("Running script: $script"); - final scriptPtr = script.toNativeUtf8(); - int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); - _debug("PyRun_SimpleString for script result: $sr"); - malloc.free(scriptPtr); - if (sr != 0) { - return getPythonError(cpython); + try { + final logcatSetupError = _setupLogcatForwarding(cpython); + if (logcatSetupError != null) { + return logcatSetupError; } - } else { - // run program - _debug("Running program module: $programModuleName"); - final moduleNamePtr = programModuleName.toNativeUtf8(); - var modulePtr = - cpython.PyImport_ImportModule(moduleNamePtr.cast()); - if (modulePtr == nullptr) { - final error = getPythonError(cpython); + + if (script != "") { + // run script + _debug("Running script: $script"); + final scriptPtr = script.toNativeUtf8(); + int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); + _debug("PyRun_SimpleString for script result: $sr"); + malloc.free(scriptPtr); + if (sr != 0) { + return getPythonError(cpython); + } + } else { + // run program + _debug("Running program module: $programModuleName"); + final moduleNamePtr = programModuleName.toNativeUtf8(); + var modulePtr = + cpython.PyImport_ImportModule(moduleNamePtr.cast()); + if (modulePtr == nullptr) { + final error = getPythonError(cpython); + malloc.free(moduleNamePtr); + return error; + } malloc.free(moduleNamePtr); - return error; } - malloc.free(moduleNamePtr); - } - _debug("Python program finished"); + _debug("Python program finished"); - return ""; + return ""; + } finally { + cpython.Py_EndInterpreter(subThreadState); + if (mainThreadState != nullptr) { + cpython.PyThreadState_Swap(mainThreadState); + } + } }); } finally { // Keep interpreter alive between runs. Finalizing + re-initializing the From 09b66cc28acaf6154fa60de6c960328d892d84c9 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 16:21:55 -0800 Subject: [PATCH 20/31] Add process termination support for Android plugin Introduces a 'terminate' method to the Android plugin and Dart interface, allowing the app to kill its process for a clean restart. Also disables GIL management in _withGIL due to potential issues with embedded CPython state after Dart isolate restarts. --- .../flet/serious_python_android/AndroidPlugin.java | 10 ++++++++++ .../lib/serious_python_android.dart | 8 ++++++++ src/serious_python_android/lib/src/cpython.dart | 13 +++++++------ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java b/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java index dcced8db..7c11f359 100644 --- a/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java +++ b/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java @@ -5,6 +5,9 @@ import androidx.annotation.NonNull; import android.system.Os; import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -80,6 +83,13 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { } catch (Exception e) { result.error("Error", e.getMessage(), null); } + } else if (call.method.equals("terminate")) { + // Terminate the process shortly after responding to Dart. + result.success(null); + new Handler(Looper.getMainLooper()).postDelayed( + () -> Process.killProcess(Process.myPid()), + 100 + ); } else { result.notImplemented(); } diff --git a/src/serious_python_android/lib/serious_python_android.dart b/src/serious_python_android/lib/serious_python_android.dart index 0ec59840..a2d67c5e 100644 --- a/src/serious_python_android/lib/serious_python_android.dart +++ b/src/serious_python_android/lib/serious_python_android.dart @@ -95,4 +95,12 @@ class SeriousPythonAndroid extends SeriousPythonPlatform { return runPythonProgramFFI( sync ?? false, "libpython3.12.so", appPath, script ?? ""); } + + @override + void terminate() { + // CPython is embedded in-process; after Flutter engine/Dart isolate restarts, + // native CPython state (including the GIL) can be left in a bad state. + // Killing the process is the most reliable way to guarantee a clean start. + methodChannel.invokeMethod('terminate'); + } } diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 94c7521c..49b47cf5 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -53,12 +53,13 @@ void _debug(String message) { } T _withGIL(CPython cpython, T Function() action) { - final gil = cpython.PyGILState_Ensure(); - try { - return action(); - } finally { - cpython.PyGILState_Release(gil); - } +// final gil = cpython.PyGILState_Ensure(); +// try { +// return action(); +// } finally { +// cpython.PyGILState_Release(gil); +// } + return action(); } Future runPythonProgramFFI(bool sync, String dynamicLibPath, From ccf5ffd763059c414a05686e4c767488082eb635 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 12 Dec 2025 16:25:38 -0800 Subject: [PATCH 21/31] Enable GIL management in _withGIL function Uncommented and restored the logic to acquire and release the Python GIL in the _withGIL function, ensuring thread safety when interacting with the Python interpreter. --- src/serious_python_android/lib/src/cpython.dart | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 49b47cf5..94c7521c 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -53,13 +53,12 @@ void _debug(String message) { } T _withGIL(CPython cpython, T Function() action) { -// final gil = cpython.PyGILState_Ensure(); -// try { -// return action(); -// } finally { -// cpython.PyGILState_Release(gil); -// } - return action(); + final gil = cpython.PyGILState_Ensure(); + try { + return action(); + } finally { + cpython.PyGILState_Release(gil); + } } Future runPythonProgramFFI(bool sync, String dynamicLibPath, From 60f675670d5053c08b44f755a9e31e68fd10df0e Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 15 Dec 2025 14:08:02 -0800 Subject: [PATCH 22/31] Refactor CPython FFI execution and error handling Simplifies the CPython FFI execution flow by removing sub-interpreter usage and the custom GIL management helper. Updates error handling, logging, and resource management to use debugPrint and proper finalization. Also improves asynchronous execution and logcat forwarding setup. --- .../lib/src/cpython.dart | 151 +++++++----------- 1 file changed, 55 insertions(+), 96 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 94c7521c..62cfd7ba 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -3,7 +3,7 @@ import 'dart:ffi'; import 'dart:isolate'; import 'package:ffi/ffi.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; import 'gen.dart'; @@ -48,41 +48,24 @@ CPython getCPython(String dynamicLibPath) { return _cpython ??= _cpython = CPython(DynamicLibrary.open(dynamicLibPath)); } -void _debug(String message) { - debugPrint("[serious_python] $message"); -} - -T _withGIL(CPython cpython, T Function() action) { - final gil = cpython.PyGILState_Ensure(); - try { - return action(); - } finally { - cpython.PyGILState_Release(gil); - } -} - Future runPythonProgramFFI(bool sync, String dynamicLibPath, String pythonProgramPath, String script) async { final receivePort = ReceivePort(); if (sync) { // sync run - try { - return await runPythonProgramInIsolate( - [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]); - } finally { - receivePort.close(); - } + return await runPythonProgramInIsolate( + [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]); } else { + var completer = Completer(); // async run - // - // IMPORTANT: do not `isolate.kill()` here. Killing the isolate can abort the - // underlying OS thread while it still interacts with CPython, leaving the - // interpreter/GIL in a bad state for subsequent runs. - await Isolate.spawn(runPythonProgramInIsolate, + final isolate = await Isolate.spawn(runPythonProgramInIsolate, [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]); - final message = await receivePort.first; - receivePort.close(); - return message as String; + receivePort.listen((message) { + receivePort.close(); + isolate.kill(); + completer.complete(message); + }); + return completer.future; } } @@ -95,75 +78,50 @@ Future runPythonProgramInIsolate(List arguments) async { var programDirPath = p.dirname(pythonProgramPath); var programModuleName = p.basenameWithoutExtension(pythonProgramPath); - _debug("dynamicLibPath: $dynamicLibPath"); - _debug("programDirPath: $programDirPath"); - _debug("programModuleName: $programModuleName"); + debugPrint("dynamicLibPath: $dynamicLibPath"); + debugPrint("programDirPath: $programDirPath"); + debugPrint("programModuleName: $programModuleName"); final cpython = getCPython(dynamicLibPath); - if (cpython.Py_IsInitialized() == 0) { - // Initialize the runtime only if it is not already active. - cpython.Py_Initialize(); - _debug("after Py_Initialize()"); + if (cpython.Py_IsInitialized() != 0) { + sendPort.send(""); + return ""; } - String result = ""; - try { - result = _withGIL(cpython, () { - // Run each invocation in a fresh sub-interpreter to reduce cross-run - // leakage (event loops/background threads/modules) that can otherwise - // lead to GIL deadlocks on the next app start. - final mainThreadState = cpython.PyThreadState_Get(); - final subThreadState = cpython.Py_NewInterpreter(); - if (subThreadState == nullptr) { - return "Py_NewInterpreter() failed."; - } + cpython.Py_Initialize(); + debugPrint("after Py_Initialize()"); - try { - final logcatSetupError = _setupLogcatForwarding(cpython); - if (logcatSetupError != null) { - return logcatSetupError; - } - - if (script != "") { - // run script - _debug("Running script: $script"); - final scriptPtr = script.toNativeUtf8(); - int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); - _debug("PyRun_SimpleString for script result: $sr"); - malloc.free(scriptPtr); - if (sr != 0) { - return getPythonError(cpython); - } - } else { - // run program - _debug("Running program module: $programModuleName"); - final moduleNamePtr = programModuleName.toNativeUtf8(); - var modulePtr = - cpython.PyImport_ImportModule(moduleNamePtr.cast()); - if (modulePtr == nullptr) { - final error = getPythonError(cpython); - malloc.free(moduleNamePtr); - return error; - } - malloc.free(moduleNamePtr); - } - - _debug("Python program finished"); - - return ""; - } finally { - cpython.Py_EndInterpreter(subThreadState); - if (mainThreadState != nullptr) { - cpython.PyThreadState_Swap(mainThreadState); - } - } - }); - } finally { - // Keep interpreter alive between runs. Finalizing + re-initializing the - // interpreter is not reliably supported and has caused native crashes - // (e.g. during _ctypes re-import) on Android. + var result = ""; + + final logcatSetupError = _setupLogcatForwarding(cpython); + if (logcatSetupError != null) { + cpython.Py_Finalize(); + sendPort.send(logcatSetupError); + return logcatSetupError; } + if (script != "") { + // run script + final scriptPtr = script.toNativeUtf8(); + int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); + debugPrint("PyRun_SimpleString for script result: $sr"); + malloc.free(scriptPtr); + if (sr != 0) { + result = getPythonError(cpython); + } + } else { + // run program + final moduleNamePtr = programModuleName.toNativeUtf8(); + var modulePtr = cpython.PyImport_ImportModule(moduleNamePtr.cast()); + if (modulePtr == nullptr) { + result = getPythonError(cpython); + } + malloc.free(moduleNamePtr); + } + + cpython.Py_Finalize(); + debugPrint("after Py_Finalize()"); + sendPort.send(result); return result; @@ -177,15 +135,15 @@ String getPythonError(CPython cpython) { final tracebackModuleNamePtr = "traceback".toNativeUtf8(); var tracebackModulePtr = cpython.PyImport_ImportModule(tracebackModuleNamePtr.cast()); - malloc.free(tracebackModuleNamePtr); + cpython.Py_DecRef(tracebackModuleNamePtr.cast()); if (tracebackModulePtr != nullptr) { - //_debug("Traceback module loaded"); + //debugPrint("Traceback module loaded"); final formatFuncName = "format_exception".toNativeUtf8(); final pFormatFunc = cpython.PyObject_GetAttrString( tracebackModulePtr, formatFuncName.cast()); - malloc.free(formatFuncName); + cpython.Py_DecRef(tracebackModuleNamePtr.cast()); if (pFormatFunc != nullptr && cpython.PyCallable_Check(pFormatFunc) != 0) { // call `traceback.format_exception()` method @@ -215,7 +173,10 @@ String getPythonError(CPython cpython) { } String? _setupLogcatForwarding(CPython cpython) { - _debug("Setting up logcat forwarding"); + if (_logcatForwardingError != null) { + return _logcatForwardingError; + } + final setupPtr = _logcatInitScript.toNativeUtf8(); final result = cpython.PyRun_SimpleString(setupPtr.cast()); malloc.free(setupPtr); @@ -225,7 +186,5 @@ String? _setupLogcatForwarding(CPython cpython) { return _logcatForwardingError; } - _debug("logcat forwarding configured"); - _logcatForwardingError = null; return null; } From 88a064c90dbfacc1c7b8c8ef0ea16426397e69b6 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 15 Dec 2025 14:23:13 -0800 Subject: [PATCH 23/31] Replace debugPrint with spDebug and add log utility Introduced a new spDebug function in src/log.dart to standardize debug logging with a '[serious_python]' prefix. Updated all debugPrint calls in serious_python_android.dart and cpython.dart to use spDebug for consistent log output. --- .../lib/serious_python_android.dart | 9 +++++---- src/serious_python_android/lib/src/cpython.dart | 16 ++++++++-------- src/serious_python_android/lib/src/log.dart | 10 ++++++++++ 3 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 src/serious_python_android/lib/src/log.dart diff --git a/src/serious_python_android/lib/serious_python_android.dart b/src/serious_python_android/lib/serious_python_android.dart index a2d67c5e..b5f36cf8 100644 --- a/src/serious_python_android/lib/serious_python_android.dart +++ b/src/serious_python_android/lib/serious_python_android.dart @@ -8,6 +8,7 @@ import 'package:path/path.dart' as p; import 'package:serious_python_platform_interface/serious_python_platform_interface.dart'; import 'src/cpython.dart'; +import 'src/log.dart'; /// An implementation of [SeriousPythonPlatform] that uses method channels. class SeriousPythonAndroid extends SeriousPythonPlatform { @@ -43,13 +44,13 @@ class SeriousPythonAndroid extends SeriousPythonPlatform { .invokeMethod('loadLibrary', {'libname': 'pyjni'}); await setenv("FLET_JNI_READY", "1"); } catch (e) { - debugPrint("Unable to load libpyjni.so library: $e"); + spDebug("Unable to load libpyjni.so library: $e"); } // unpack python bundle final nativeLibraryDir = await methodChannel.invokeMethod('getNativeLibraryDir'); - debugPrint("getNativeLibraryDir: $nativeLibraryDir"); + spDebug("getNativeLibraryDir: $nativeLibraryDir"); var bundlePath = "$nativeLibraryDir/libpythonbundle.so"; var sitePackagesZipPath = "$nativeLibraryDir/libpythonsitepackages.so"; @@ -59,7 +60,7 @@ class SeriousPythonAndroid extends SeriousPythonPlatform { } var pythonLibPath = await extractFileZip(bundlePath, targetPath: "python_bundle"); - debugPrint("pythonLibPath: $pythonLibPath"); + spDebug("pythonLibPath: $pythonLibPath"); var programDirPath = p.dirname(appPath); @@ -73,7 +74,7 @@ class SeriousPythonAndroid extends SeriousPythonPlatform { if (await File(sitePackagesZipPath).exists()) { var sitePackagesPath = await extractFileZip(sitePackagesZipPath, targetPath: "python_site_packages"); - debugPrint("sitePackagesPath: $sitePackagesPath"); + spDebug("sitePackagesPath: $sitePackagesPath"); moduleSearchPaths.add(sitePackagesPath); } diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 62cfd7ba..90fe6ce6 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -3,10 +3,10 @@ import 'dart:ffi'; import 'dart:isolate'; import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; import 'gen.dart'; +import 'log.dart'; export 'gen.dart'; @@ -78,9 +78,9 @@ Future runPythonProgramInIsolate(List arguments) async { var programDirPath = p.dirname(pythonProgramPath); var programModuleName = p.basenameWithoutExtension(pythonProgramPath); - debugPrint("dynamicLibPath: $dynamicLibPath"); - debugPrint("programDirPath: $programDirPath"); - debugPrint("programModuleName: $programModuleName"); + spDebug("dynamicLibPath: $dynamicLibPath"); + spDebug("programDirPath: $programDirPath"); + spDebug("programModuleName: $programModuleName"); final cpython = getCPython(dynamicLibPath); if (cpython.Py_IsInitialized() != 0) { @@ -89,7 +89,7 @@ Future runPythonProgramInIsolate(List arguments) async { } cpython.Py_Initialize(); - debugPrint("after Py_Initialize()"); + spDebug("after Py_Initialize()"); var result = ""; @@ -104,7 +104,7 @@ Future runPythonProgramInIsolate(List arguments) async { // run script final scriptPtr = script.toNativeUtf8(); int sr = cpython.PyRun_SimpleString(scriptPtr.cast()); - debugPrint("PyRun_SimpleString for script result: $sr"); + spDebug("PyRun_SimpleString for script result: $sr"); malloc.free(scriptPtr); if (sr != 0) { result = getPythonError(cpython); @@ -120,7 +120,7 @@ Future runPythonProgramInIsolate(List arguments) async { } cpython.Py_Finalize(); - debugPrint("after Py_Finalize()"); + spDebug("after Py_Finalize()"); sendPort.send(result); @@ -138,7 +138,7 @@ String getPythonError(CPython cpython) { cpython.Py_DecRef(tracebackModuleNamePtr.cast()); if (tracebackModulePtr != nullptr) { - //debugPrint("Traceback module loaded"); + //spDebug("Traceback module loaded"); final formatFuncName = "format_exception".toNativeUtf8(); final pFormatFunc = cpython.PyObject_GetAttrString( diff --git a/src/serious_python_android/lib/src/log.dart b/src/serious_python_android/lib/src/log.dart new file mode 100644 index 00000000..2cfa01ec --- /dev/null +++ b/src/serious_python_android/lib/src/log.dart @@ -0,0 +1,10 @@ +import 'package:flutter/foundation.dart'; + +void spDebug(String message) { + if (message.startsWith('[serious_python]')) { + debugPrint(message); + } else { + debugPrint('[serious_python] $message'); + } +} + From de953434743e0137ef30af8922b52927b9740c50 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 15 Dec 2025 14:45:28 -0800 Subject: [PATCH 24/31] Add debug logs for CPython loading and initialization Added debug statements to log when CPython is loaded and when Python is already initialized, aiding in troubleshooting and execution flow visibility. --- src/serious_python_android/lib/src/cpython.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 90fe6ce6..04f735c0 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -83,7 +83,9 @@ Future runPythonProgramInIsolate(List arguments) async { spDebug("programModuleName: $programModuleName"); final cpython = getCPython(dynamicLibPath); + spDebug("CPython loaded"); if (cpython.Py_IsInitialized() != 0) { + spDebug("Python already initialized, skipping execution."); sendPort.send(""); return ""; } From 67000682bac2d1ddf43b81dcdbf989521098d344 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 15 Dec 2025 17:38:59 -0800 Subject: [PATCH 25/31] Add debug print for missing asset hash file Adds a debugPrint statement to log when the asset hash file is not found during asset extraction, aiding in debugging missing hash issues. --- src/serious_python_platform_interface/lib/src/utils.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/serious_python_platform_interface/lib/src/utils.dart b/src/serious_python_platform_interface/lib/src/utils.dart index a45e7f34..ec6bcc81 100644 --- a/src/serious_python_platform_interface/lib/src/utils.dart +++ b/src/serious_python_platform_interface/lib/src/utils.dart @@ -29,7 +29,9 @@ Future extractAssetOrFile(String path, try { assetHash = (await rootBundle.loadString("$path.hash")).trim(); // ignore: empty_catches - } catch (e) {} + } catch (e) { + debugPrint("No asset hash file found for $path.hash: $e"); + } if (await hashFile.exists()) { destHash = (await hashFile.readAsString()).trim(); } From 9b6f2c123bb47012f249806d457c4f1eaa537594 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 15 Dec 2025 17:46:20 -0800 Subject: [PATCH 26/31] Add debug prints for asset hash extraction and writing Added debugPrint statements to log asset hash extraction, hash file writing, and conditional hash file operations in extractAssetOrFile. This improves traceability and debugging of asset handling. --- src/serious_python_platform_interface/lib/src/utils.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/serious_python_platform_interface/lib/src/utils.dart b/src/serious_python_platform_interface/lib/src/utils.dart index ec6bcc81..d5fbadd3 100644 --- a/src/serious_python_platform_interface/lib/src/utils.dart +++ b/src/serious_python_platform_interface/lib/src/utils.dart @@ -28,6 +28,7 @@ Future extractAssetOrFile(String path, // read asset hash from asset try { assetHash = (await rootBundle.loadString("$path.hash")).trim(); + debugPrint("Asset hash for $path: $assetHash"); // ignore: empty_catches } catch (e) { debugPrint("No asset hash file found for $path.hash: $e"); @@ -74,7 +75,11 @@ Future extractAssetOrFile(String path, debugPrint("Finished unpacking application archive in ${stopwatch.elapsed}"); if (checkHash) { + debugPrint("Writing hash file: ${hashFile.path}, hash: $assetHash"); await hashFile.writeAsString(assetHash); + debugPrint("Hash file written."); + } else { + debugPrint("Hash check not requested, skipping hash file write."); } return destDir.path; From 3ee0cc6cf0527041f8d40aec1dfb4fbbd70d5fdc Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 15 Dec 2025 17:53:35 -0800 Subject: [PATCH 27/31] Refactor asset hash reading in extractAssetOrFile Moved asset hash reading outside of the checkHash block to ensure it is always attempted. Removed redundant debug prints and simplified hash file writing logic. --- .../lib/src/utils.dart | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/serious_python_platform_interface/lib/src/utils.dart b/src/serious_python_platform_interface/lib/src/utils.dart index d5fbadd3..b84bd3a8 100644 --- a/src/serious_python_platform_interface/lib/src/utils.dart +++ b/src/serious_python_platform_interface/lib/src/utils.dart @@ -15,6 +15,12 @@ Future extractAssetOrFile(String path, Directory(p.join(supportDir.path, "flet", targetPath ?? p.dirname(path))); String assetHash = ""; + // read asset hash from asset + try { + assetHash = (await rootBundle.loadString("$path.hash")).trim(); + // ignore: empty_catches + } catch (e) {} + String destHash = ""; var hashFile = File(p.join(destDir.path, ".hash")); @@ -25,14 +31,6 @@ Future extractAssetOrFile(String path, await destDir.delete(recursive: true); } else { if (checkHash) { - // read asset hash from asset - try { - assetHash = (await rootBundle.loadString("$path.hash")).trim(); - debugPrint("Asset hash for $path: $assetHash"); - // ignore: empty_catches - } catch (e) { - debugPrint("No asset hash file found for $path.hash: $e"); - } if (await hashFile.exists()) { destHash = (await hashFile.readAsString()).trim(); } @@ -77,9 +75,6 @@ Future extractAssetOrFile(String path, if (checkHash) { debugPrint("Writing hash file: ${hashFile.path}, hash: $assetHash"); await hashFile.writeAsString(assetHash); - debugPrint("Hash file written."); - } else { - debugPrint("Hash check not requested, skipping hash file write."); } return destDir.path; From 656063fead40129df2221f46051f9ccc38dc82a3 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 15 Dec 2025 18:13:37 -0800 Subject: [PATCH 28/31] Remove terminate method from AndroidPlugin Eliminated the handling of the 'terminate' method call, including the delayed process termination logic, from the AndroidPlugin class. --- .../com/flet/serious_python_android/AndroidPlugin.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java b/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java index 7c11f359..57b76e1a 100644 --- a/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java +++ b/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java @@ -83,13 +83,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { } catch (Exception e) { result.error("Error", e.getMessage(), null); } - } else if (call.method.equals("terminate")) { - // Terminate the process shortly after responding to Dart. - result.success(null); - new Handler(Looper.getMainLooper()).postDelayed( - () -> Process.killProcess(Process.myPid()), - 100 - ); } else { result.notImplemented(); } From b69fc2dd3e960d15d3d4e688e13fcebc12490e83 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 15 Dec 2025 18:13:58 -0800 Subject: [PATCH 29/31] Remove unused imports from AndroidPlugin.java Deleted unused imports for Handler, Looper, and Process to clean up the code and improve maintainability. --- .../java/com/flet/serious_python_android/AndroidPlugin.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java b/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java index 57b76e1a..dcced8db 100644 --- a/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java +++ b/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java @@ -5,9 +5,6 @@ import androidx.annotation.NonNull; import android.system.Os; import android.content.Intent; -import android.os.Handler; -import android.os.Looper; -import android.os.Process; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; From 16e983a8664d78aaa4aada6640016cf3d06b94ee Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 16 Dec 2025 12:08:16 -0800 Subject: [PATCH 30/31] Remove terminate method from SeriousPythonAndroid The terminate() override and its related comments were removed from SeriousPythonAndroid. This simplifies the class and removes the process termination logic previously handled via methodChannel. --- .../lib/serious_python_android.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/serious_python_android/lib/serious_python_android.dart b/src/serious_python_android/lib/serious_python_android.dart index b5f36cf8..53d6089b 100644 --- a/src/serious_python_android/lib/serious_python_android.dart +++ b/src/serious_python_android/lib/serious_python_android.dart @@ -96,12 +96,4 @@ class SeriousPythonAndroid extends SeriousPythonPlatform { return runPythonProgramFFI( sync ?? false, "libpython3.12.so", appPath, script ?? ""); } - - @override - void terminate() { - // CPython is embedded in-process; after Flutter engine/Dart isolate restarts, - // native CPython state (including the GIL) can be left in a bad state. - // Killing the process is the most reliable way to guarantee a clean start. - methodChannel.invokeMethod('terminate'); - } } From df1397f4cc2a8de86e36e486db42c7fddc7040ac Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 16 Dec 2025 14:39:50 -0800 Subject: [PATCH 31/31] Bump version to 0.9.7 and update changelogs Release 0.9.7 for all serious_python packages. This update fixes app restart on Android 10 and redirects Python output to logcat. All relevant pubspec, build, and podspec files have been updated to reflect the new version. --- src/serious_python/CHANGELOG.md | 5 +++++ src/serious_python/pubspec.yaml | 2 +- src/serious_python_android/CHANGELOG.md | 5 +++++ src/serious_python_android/android/build.gradle | 2 +- src/serious_python_android/pubspec.yaml | 2 +- src/serious_python_darwin/CHANGELOG.md | 5 +++++ .../darwin/serious_python_darwin.podspec | 2 +- src/serious_python_darwin/pubspec.yaml | 2 +- src/serious_python_linux/CHANGELOG.md | 5 +++++ src/serious_python_linux/pubspec.yaml | 2 +- src/serious_python_platform_interface/CHANGELOG.md | 5 +++++ src/serious_python_platform_interface/pubspec.yaml | 2 +- src/serious_python_windows/CHANGELOG.md | 5 +++++ src/serious_python_windows/pubspec.yaml | 2 +- 14 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/serious_python/CHANGELOG.md b/src/serious_python/CHANGELOG.md index e0941792..b7a6e814 100644 --- a/src/serious_python/CHANGELOG.md +++ b/src/serious_python/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.7 + +* Fix app restart on Android 10. +* Redirect Python output to logcat. + ## 0.9.6 * Make zipDirectory call asynchronous. diff --git a/src/serious_python/pubspec.yaml b/src/serious_python/pubspec.yaml index 9b5bc1d5..0b95a5e6 100644 --- a/src/serious_python/pubspec.yaml +++ b/src/serious_python/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python description: A cross-platform plugin for adding embedded Python runtime to your Flutter apps. homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 0.9.6 +version: 0.9.7 platforms: ios: diff --git a/src/serious_python_android/CHANGELOG.md b/src/serious_python_android/CHANGELOG.md index 5d4c08f1..0e9642e6 100644 --- a/src/serious_python_android/CHANGELOG.md +++ b/src/serious_python_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.7 + +* Fix app restart on Android 10. +* Redirect Python output to logcat. + ## 0.9.6 * Make zipDirectory call asynchronous. diff --git a/src/serious_python_android/android/build.gradle b/src/serious_python_android/android/build.gradle index d4bc782d..940713b1 100644 --- a/src/serious_python_android/android/build.gradle +++ b/src/serious_python_android/android/build.gradle @@ -1,5 +1,5 @@ group 'com.flet.serious_python_android' -version '0.9.6' +version '0.9.7' def python_version = '3.12' diff --git a/src/serious_python_android/pubspec.yaml b/src/serious_python_android/pubspec.yaml index d0eae473..33dc3564 100644 --- a/src/serious_python_android/pubspec.yaml +++ b/src/serious_python_android/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python_android description: Android implementation of the serious_python plugin homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 0.9.6 +version: 0.9.7 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/src/serious_python_darwin/CHANGELOG.md b/src/serious_python_darwin/CHANGELOG.md index ef480644..8d5b22d3 100644 --- a/src/serious_python_darwin/CHANGELOG.md +++ b/src/serious_python_darwin/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.7 + +* Fix app restart on Android 10. +* Redirect Python output to logcat. + ## 0.9.6 * Make zipDirectory call asynchronous. diff --git a/src/serious_python_darwin/darwin/serious_python_darwin.podspec b/src/serious_python_darwin/darwin/serious_python_darwin.podspec index 96771d89..5313acec 100644 --- a/src/serious_python_darwin/darwin/serious_python_darwin.podspec +++ b/src/serious_python_darwin/darwin/serious_python_darwin.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'serious_python_darwin' - s.version = '0.9.6' + s.version = '0.9.7' s.summary = 'A cross-platform plugin for adding embedded Python runtime to your Flutter apps.' s.description = <<-DESC A cross-platform plugin for adding embedded Python runtime to your Flutter apps. diff --git a/src/serious_python_darwin/pubspec.yaml b/src/serious_python_darwin/pubspec.yaml index a7527666..fbed2e3c 100644 --- a/src/serious_python_darwin/pubspec.yaml +++ b/src/serious_python_darwin/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python_darwin description: iOS and macOS implementations of the serious_python plugin homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 0.9.6 +version: 0.9.7 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/src/serious_python_linux/CHANGELOG.md b/src/serious_python_linux/CHANGELOG.md index 49f50392..a12caaf1 100644 --- a/src/serious_python_linux/CHANGELOG.md +++ b/src/serious_python_linux/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.7 + +* Fix app restart on Android 10. +* Redirect Python output to logcat. + ## 0.9.6 * Make zipDirectory call asynchronous. diff --git a/src/serious_python_linux/pubspec.yaml b/src/serious_python_linux/pubspec.yaml index 7b9a2aa6..6a8c49a5 100644 --- a/src/serious_python_linux/pubspec.yaml +++ b/src/serious_python_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python_linux description: Linux implementations of the serious_python plugin homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 0.9.6 +version: 0.9.7 environment: sdk: '>=3.1.3 <4.0.0' diff --git a/src/serious_python_platform_interface/CHANGELOG.md b/src/serious_python_platform_interface/CHANGELOG.md index 545a5e03..e4610308 100644 --- a/src/serious_python_platform_interface/CHANGELOG.md +++ b/src/serious_python_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.7 + +* Fix app restart on Android 10. +* Redirect Python output to logcat. + ## 0.9.6 * Make zipDirectory call asynchronous. diff --git a/src/serious_python_platform_interface/pubspec.yaml b/src/serious_python_platform_interface/pubspec.yaml index e8e59615..4405431f 100644 --- a/src/serious_python_platform_interface/pubspec.yaml +++ b/src/serious_python_platform_interface/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python_platform_interface description: A common platform interface for the serious_python plugin. homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 0.9.6 +version: 0.9.7 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/src/serious_python_windows/CHANGELOG.md b/src/serious_python_windows/CHANGELOG.md index 55054601..291b781f 100644 --- a/src/serious_python_windows/CHANGELOG.md +++ b/src/serious_python_windows/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.7 + +* Fix app restart on Android 10. +* Redirect Python output to logcat. + ## 0.9.6 * Make zipDirectory call asynchronous. diff --git a/src/serious_python_windows/pubspec.yaml b/src/serious_python_windows/pubspec.yaml index 87798abd..4ebe443e 100644 --- a/src/serious_python_windows/pubspec.yaml +++ b/src/serious_python_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python_windows description: Windows implementations of the serious_python plugin homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 0.9.6 +version: 0.9.7 environment: sdk: '>=3.1.3 <4.0.0'