From 91eb67696a379898d02423c73f8ab8c5ab02a893 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 18 Dec 2025 14:04:34 -0800 Subject: [PATCH 01/14] Remove logging configuration from logcat init script Eliminated the use of the logging module and related configuration from the _logcatInitScript in cpython.dart. This simplifies the script and avoids unnecessary logging setup during initialization. --- src/serious_python_android/lib/src/cpython.dart | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 04f735c0..8bc3267f 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -13,7 +13,7 @@ export 'gen.dart'; CPython? _cpython; String? _logcatForwardingError; const _logcatInitScript = r''' -import sys, logging +import sys # Make this init idempotent across Dart isolate restarts. if not getattr(sys, "__serious_python_logcat_configured__", False): @@ -37,11 +37,6 @@ if not getattr(sys, "__serious_python_logcat_configured__", False): 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) { From 3f6b044a0d9ea26b0682867aa8d65d667dbdb901 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 18 Dec 2025 17:04:18 -0800 Subject: [PATCH 02/14] Refactor Python FFI execution and isolate handling Simplifies sync and async execution paths for running Python programs via FFI. Moves core execution logic to a private function, improves error handling, and ensures proper resource cleanup in async mode. Removes redundant message passing and streamlines isolate communication. --- .../lib/src/cpython.dart | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 8bc3267f..a2e00c28 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -45,22 +45,20 @@ CPython getCPython(String dynamicLibPath) { Future runPythonProgramFFI(bool sync, String dynamicLibPath, String pythonProgramPath, String script) async { - final receivePort = ReceivePort(); if (sync) { - // sync run - return await runPythonProgramInIsolate( - [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]); + // Sync run: do not involve ports (avoids GC/close races). + return _runPythonProgram(dynamicLibPath, pythonProgramPath, script); } else { - var completer = Completer(); - // async run - final isolate = await Isolate.spawn(runPythonProgramInIsolate, - [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]); - receivePort.listen((message) { + // Async run: execute in a separate isolate and await exactly one result. + final receivePort = ReceivePort(); + try { + await Isolate.spawn(runPythonProgramInIsolate, + [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]); + final message = await receivePort.first; + return message is String ? message : message.toString(); + } finally { receivePort.close(); - isolate.kill(); - completer.complete(message); - }); - return completer.future; + } } } @@ -70,6 +68,20 @@ Future runPythonProgramInIsolate(List arguments) async { final pythonProgramPath = arguments[2] as String; final script = arguments[3] as String; + try { + final result = _runPythonProgram(dynamicLibPath, pythonProgramPath, script); + sendPort.send(result); + return result; + } catch (e, st) { + final message = "Dart error running Python: $e\n$st"; + spDebug(message); + sendPort.send(message); + return message; + } +} + +String _runPythonProgram( + String dynamicLibPath, String pythonProgramPath, String script) { var programDirPath = p.dirname(pythonProgramPath); var programModuleName = p.basenameWithoutExtension(pythonProgramPath); @@ -81,7 +93,6 @@ Future runPythonProgramInIsolate(List arguments) async { spDebug("CPython loaded"); if (cpython.Py_IsInitialized() != 0) { spDebug("Python already initialized, skipping execution."); - sendPort.send(""); return ""; } @@ -93,7 +104,6 @@ Future runPythonProgramInIsolate(List arguments) async { final logcatSetupError = _setupLogcatForwarding(cpython); if (logcatSetupError != null) { cpython.Py_Finalize(); - sendPort.send(logcatSetupError); return logcatSetupError; } @@ -119,8 +129,6 @@ Future runPythonProgramInIsolate(List arguments) async { cpython.Py_Finalize(); spDebug("after Py_Finalize()"); - sendPort.send(result); - return result; } From 9327e4d8b07923306ac9de4f777169ffa40fa2a0 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 09:49:30 -0800 Subject: [PATCH 03/14] Serialize Python runs and improve async execution Introduces a queue to serialize Python execution requests, preventing concurrent runs. Refactors async execution to use Isolate.run for better port lifecycle management and adds debug logging for run tracking. --- .../lib/src/cpython.dart | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index a2e00c28..863a14a8 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -12,6 +12,21 @@ export 'gen.dart'; CPython? _cpython; String? _logcatForwardingError; +Future _pythonRunQueue = Future.value(); +var _pythonRunSeq = 0; + +Future _enqueuePythonRun(Future Function() action) { + final completer = Completer(); + _pythonRunQueue = _pythonRunQueue.then((_) async { + try { + completer.complete(await action()); + } catch (e, st) { + completer.completeError(e, st); + } + }); + return completer.future; +} + const _logcatInitScript = r''' import sys @@ -45,21 +60,30 @@ CPython getCPython(String dynamicLibPath) { Future runPythonProgramFFI(bool sync, String dynamicLibPath, String pythonProgramPath, String script) async { - if (sync) { - // Sync run: do not involve ports (avoids GC/close races). - return _runPythonProgram(dynamicLibPath, pythonProgramPath, script); - } else { - // Async run: execute in a separate isolate and await exactly one result. - final receivePort = ReceivePort(); - try { - await Isolate.spawn(runPythonProgramInIsolate, - [receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]); - final message = await receivePort.first; - return message is String ? message : message.toString(); - } finally { - receivePort.close(); + return _enqueuePythonRun(() async { + final runId = ++_pythonRunSeq; + spDebug( + "Python run#$runId start (sync=$sync, script=${script.isNotEmpty}, program=$pythonProgramPath)"); + if (sync) { + // Sync run: do not involve ports (avoids GC/close races). + final result = _runPythonProgram(dynamicLibPath, pythonProgramPath, script); + spDebug("Python run#$runId done (resultLength=${result.length})"); + return result; + } else { + // Async run: use Isolate.run() to avoid manual port lifecycle issues. + try { + final result = await Isolate.run(() => + _runPythonProgram(dynamicLibPath, pythonProgramPath, script)); + spDebug("Python run#$runId done (resultLength=${result.length})"); + return result; + } catch (e, st) { + final message = "Dart error running Python: $e\n$st"; + spDebug(message); + spDebug("Python run#$runId failed"); + return message; + } } - } + }); } Future runPythonProgramInIsolate(List arguments) async { From 633f848c38b8a6a67d5245678155613396e1cf99 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 10:15:52 -0800 Subject: [PATCH 04/14] Improve Python logging and format async code Configures Python logging to use a StreamHandler with a custom formatter and replaces all root handlers. Also reformats async Isolate.run() calls for better readability. --- src/serious_python_android/lib/src/cpython.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 863a14a8..57fae9fe 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -52,6 +52,10 @@ if not getattr(sys, "__serious_python_logcat_configured__", False): 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] '''; CPython getCPython(String dynamicLibPath) { @@ -66,14 +70,15 @@ Future runPythonProgramFFI(bool sync, String dynamicLibPath, "Python run#$runId start (sync=$sync, script=${script.isNotEmpty}, program=$pythonProgramPath)"); if (sync) { // Sync run: do not involve ports (avoids GC/close races). - final result = _runPythonProgram(dynamicLibPath, pythonProgramPath, script); + final result = + _runPythonProgram(dynamicLibPath, pythonProgramPath, script); spDebug("Python run#$runId done (resultLength=${result.length})"); return result; } else { // Async run: use Isolate.run() to avoid manual port lifecycle issues. try { - final result = await Isolate.run(() => - _runPythonProgram(dynamicLibPath, pythonProgramPath, script)); + final result = await Isolate.run( + () => _runPythonProgram(dynamicLibPath, pythonProgramPath, script)); spDebug("Python run#$runId done (resultLength=${result.length})"); return result; } catch (e, st) { From 38e502265a59295cb3e5593db17ea99ad98653d3 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 10:40:26 -0800 Subject: [PATCH 05/14] Add logging import to logcat init script Added the 'logging' module import to the logcat initialization script to ensure logging functionality is available during script execution. --- src/serious_python_android/lib/src/cpython.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 57fae9fe..9fa7ecd4 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -28,7 +28,7 @@ Future _enqueuePythonRun(Future Function() action) { } const _logcatInitScript = r''' -import sys +import logging,sys # Make this init idempotent across Dart isolate restarts. if not getattr(sys, "__serious_python_logcat_configured__", False): From 9e73a210d65abe27a041960c0688f93ac58a982c Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 10:44:55 -0800 Subject: [PATCH 06/14] Set root logger level in embedded Python logging config Adds a line to explicitly set the root logger's level to its effective level in the embedded Python logging configuration. This ensures consistent logging behavior when initializing the logger. --- src/serious_python_android/lib/src/cpython.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 9fa7ecd4..609955cb 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -56,6 +56,7 @@ if not getattr(sys, "__serious_python_logcat_configured__", False): handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) root = logging.getLogger() root.handlers[:] = [handler] + root.setLevel(logging.getLogger().getEffectiveLevel()) '''; CPython getCPython(String dynamicLibPath) { From c47ffd312351abed56f580d66dee324e9e4ae19d Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 10:47:24 -0800 Subject: [PATCH 07/14] Set Python root logger level to DEBUG Changed the root logger's level from its effective level to DEBUG in the embedded Python logging configuration. This ensures all debug messages are captured during execution. --- src/serious_python_android/lib/src/cpython.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 609955cb..1cbf0b2f 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -56,7 +56,7 @@ if not getattr(sys, "__serious_python_logcat_configured__", False): handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) root = logging.getLogger() root.handlers[:] = [handler] - root.setLevel(logging.getLogger().getEffectiveLevel()) + root.setLevel(logging.DEBUG) '''; CPython getCPython(String dynamicLibPath) { From 0f9aa9fdcb05ca88970ad38ec3ad7b09ce87609c Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 11:00:17 -0800 Subject: [PATCH 08/14] Update logcat logger configuration in cpython.dart Switches logging from the root logger to a dedicated 'logcat' logger, disables propagation, and sets the log level to ERROR instead of DEBUG. This change improves log handling and reduces log verbosity. --- src/serious_python_android/lib/src/cpython.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 1cbf0b2f..949b92d3 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -54,9 +54,10 @@ if not getattr(sys, "__serious_python_logcat_configured__", False): 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) + logcat = logging.getLogger("logcat") + logcat.propagate = False + logcat.handlers[:] = [handler] + logcat.setLevel(logging.ERROR) '''; CPython getCPython(String dynamicLibPath) { From ddd850b585b276ffab5ce5f348f58665f4491a9a Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 11:05:25 -0800 Subject: [PATCH 09/14] Remove logcat propagate setting in Python logging config Deleted the line setting 'logcat.propagate = False' from the embedded Python logging configuration. This may allow log messages to propagate to ancestor loggers, aligning with default logging behavior. --- src/serious_python_android/lib/src/cpython.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 949b92d3..2ae8e527 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -55,7 +55,6 @@ if not getattr(sys, "__serious_python_logcat_configured__", False): handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) logcat = logging.getLogger("logcat") - logcat.propagate = False logcat.handlers[:] = [handler] logcat.setLevel(logging.ERROR) '''; From c6433f8fc591aa0a04f74b286c3891562e2d59ca Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 11:10:52 -0800 Subject: [PATCH 10/14] Use root logger for logcat configuration Replaces the use of the 'logcat' logger with the root logger when configuring logging in the embedded Python script. This ensures that all logging output is handled consistently at the ERROR level. --- src/serious_python_android/lib/src/cpython.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 2ae8e527..98b6f89a 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -54,9 +54,9 @@ if not getattr(sys, "__serious_python_logcat_configured__", False): sys.stdout = sys.stderr = _LogcatWriter() handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) - logcat = logging.getLogger("logcat") - logcat.handlers[:] = [handler] - logcat.setLevel(logging.ERROR) + root = logging.getLogger() + root.handlers[:] = [handler] + root.setLevel(logging.ERROR) '''; CPython getCPython(String dynamicLibPath) { From 1634d268a4dd8facbee024c3d0a09f9d279b7137 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 11:34:44 -0800 Subject: [PATCH 11/14] Improve Python error handling and memory management Enhanced getPythonError to handle null pointers, memory allocation failures, and fallback error formatting. Replaced Py_DecRef with malloc.free for native strings and ensured proper reference counting and cleanup to prevent memory leaks. --- .../lib/src/cpython.dart | 82 +++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 98b6f89a..5c9972bb 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -164,13 +164,16 @@ String _runPythonProgram( String getPythonError(CPython cpython) { // get error object - var exPtr = cpython.PyErr_GetRaisedException(); + final exPtr = cpython.PyErr_GetRaisedException(); + if (exPtr == nullptr) { + return "Unknown Python error (no exception set)."; + } // use 'traceback' module to format exception final tracebackModuleNamePtr = "traceback".toNativeUtf8(); var tracebackModulePtr = cpython.PyImport_ImportModule(tracebackModuleNamePtr.cast()); - cpython.Py_DecRef(tracebackModuleNamePtr.cast()); + malloc.free(tracebackModuleNamePtr); if (tracebackModulePtr != nullptr) { //spDebug("Traceback module loaded"); @@ -178,31 +181,100 @@ 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 final pArgs = cpython.PyTuple_New(1); + if (pArgs == nullptr) { + final fallback = cpython.PyObject_Str(exPtr); + if (fallback == nullptr) { + cpython.Py_DecRef(pFormatFunc); + cpython.Py_DecRef(tracebackModulePtr); + cpython.Py_DecRef(exPtr); + return "Failed to allocate args to format Python exception."; + } + final s = cpython + .PyUnicode_AsUTF8(fallback) + .cast() + .toDartString(); + cpython.Py_DecRef(fallback); + cpython.Py_DecRef(pFormatFunc); + cpython.Py_DecRef(tracebackModulePtr); + cpython.Py_DecRef(exPtr); + return s; + } + // Keep a reference for fallback error formatting. + cpython.Py_IncRef(exPtr); cpython.PyTuple_SetItem(pArgs, 0, exPtr); // result is a list var listPtr = cpython.PyObject_CallObject(pFormatFunc, pArgs); + cpython.Py_DecRef(pArgs); + cpython.Py_DecRef(pFormatFunc); + cpython.Py_DecRef(tracebackModulePtr); // get and combine list items var exLines = []; + if (listPtr == nullptr) { + final fallback = cpython.PyObject_Str(exPtr); + if (fallback == nullptr) { + cpython.Py_DecRef(exPtr); + return "Failed to format Python exception."; + } + final s = cpython + .PyUnicode_AsUTF8(fallback) + .cast() + .toDartString(); + cpython.Py_DecRef(fallback); + cpython.Py_DecRef(exPtr); + return s; + } + var listSize = cpython.PyList_Size(listPtr); + if (listSize < 0) { + cpython.Py_DecRef(listPtr); + final fallback = cpython.PyObject_Str(exPtr); + if (fallback == nullptr) { + cpython.Py_DecRef(exPtr); + return "Failed to format Python exception."; + } + final s = cpython + .PyUnicode_AsUTF8(fallback) + .cast() + .toDartString(); + cpython.Py_DecRef(fallback); + cpython.Py_DecRef(exPtr); + return s; + } 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(); + if (itemObjStr == nullptr) { + continue; + } + final cStr = cpython.PyUnicode_AsUTF8(itemObjStr); + if (cStr == nullptr) { + cpython.Py_DecRef(itemObjStr); + continue; + } + var s = cStr.cast().toDartString(); + cpython.Py_DecRef(itemObjStr); exLines.add(s); } + cpython.Py_DecRef(listPtr); + cpython.Py_DecRef(exPtr); return exLines.join(""); } else { + if (pFormatFunc != nullptr) { + cpython.Py_DecRef(pFormatFunc); + } + cpython.Py_DecRef(tracebackModulePtr); + cpython.Py_DecRef(exPtr); return "traceback.format_exception() method not found."; } } else { + cpython.Py_DecRef(exPtr); return "Error loading traceback module."; } } From 4d27e14b45d96ea62dd73bbef669cf0f948af364 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 21:13:34 -0800 Subject: [PATCH 12/14] Refactor Python error formatting and cleanup Simplifies and improves Python exception formatting by extracting logic into helper functions and using more robust error handling. Removes unused run sequence tracking and an obsolete isolate runner. Enhances debug logging and clarifies error messages for better maintainability. --- .../lib/src/cpython.dart | 201 +++++++----------- 1 file changed, 74 insertions(+), 127 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 5c9972bb..2b1890cd 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -13,7 +13,6 @@ export 'gen.dart'; CPython? _cpython; String? _logcatForwardingError; Future _pythonRunQueue = Future.value(); -var _pythonRunSeq = 0; Future _enqueuePythonRun(Future Function() action) { final completer = Completer(); @@ -66,50 +65,32 @@ CPython getCPython(String dynamicLibPath) { Future runPythonProgramFFI(bool sync, String dynamicLibPath, String pythonProgramPath, String script) async { return _enqueuePythonRun(() async { - final runId = ++_pythonRunSeq; spDebug( - "Python run#$runId start (sync=$sync, script=${script.isNotEmpty}, program=$pythonProgramPath)"); + "Python run start (sync=$sync, script=${script.isNotEmpty}, program=$pythonProgramPath)"); if (sync) { // Sync run: do not involve ports (avoids GC/close races). final result = _runPythonProgram(dynamicLibPath, pythonProgramPath, script); - spDebug("Python run#$runId done (resultLength=${result.length})"); + spDebug("Python run done (resultLength=${result.length})"); return result; } else { // Async run: use Isolate.run() to avoid manual port lifecycle issues. try { + spDebug( + "Python async run start (sync=$sync, script=${script.isNotEmpty}, program=$pythonProgramPath)"); final result = await Isolate.run( () => _runPythonProgram(dynamicLibPath, pythonProgramPath, script)); - spDebug("Python run#$runId done (resultLength=${result.length})"); + spDebug("Python run done (resultLength=${result.length})"); return result; } catch (e, st) { final message = "Dart error running Python: $e\n$st"; spDebug(message); - spDebug("Python run#$runId failed"); return message; } } }); } -Future runPythonProgramInIsolate(List arguments) async { - final sendPort = arguments[0] as SendPort; - final dynamicLibPath = arguments[1] as String; - final pythonProgramPath = arguments[2] as String; - final script = arguments[3] as String; - - try { - final result = _runPythonProgram(dynamicLibPath, pythonProgramPath, script); - sendPort.send(result); - return result; - } catch (e, st) { - final message = "Dart error running Python: $e\n$st"; - spDebug(message); - sendPort.send(message); - return message; - } -} - String _runPythonProgram( String dynamicLibPath, String pythonProgramPath, String script) { var programDirPath = p.dirname(pythonProgramPath); @@ -122,7 +103,8 @@ String _runPythonProgram( final cpython = getCPython(dynamicLibPath); spDebug("CPython loaded"); if (cpython.Py_IsInitialized() != 0) { - spDebug("Python already initialized, skipping execution."); + spDebug( + "Python already initialized and another program is running, skipping execution."); return ""; } @@ -163,119 +145,84 @@ String _runPythonProgram( } String getPythonError(CPython cpython) { - // get error object final exPtr = cpython.PyErr_GetRaisedException(); - if (exPtr == nullptr) { - return "Unknown Python error (no exception set)."; + if (exPtr == nullptr) return "Unknown Python error (no exception set)."; + + try { + final formatted = _formatPythonException(cpython, exPtr); + if (formatted != null && formatted.isNotEmpty) return formatted; + + final fallback = _pyObjectToDartString(cpython, exPtr); + return fallback ?? "Unknown Python error (failed to stringify exception)."; + } finally { + cpython.Py_DecRef(exPtr); + // Defensive: formatting can set a new Python error. + cpython.PyErr_Clear(); } +} - // use 'traceback' module to format exception +String? _formatPythonException( + CPython cpython, Pointer exceptionPtr) { + // Uses `traceback.format_exception(exc)` (Python 3.10+ signature). final tracebackModuleNamePtr = "traceback".toNativeUtf8(); - var tracebackModulePtr = + final tracebackModulePtr = cpython.PyImport_ImportModule(tracebackModuleNamePtr.cast()); malloc.free(tracebackModuleNamePtr); + if (tracebackModulePtr == nullptr) return null; - if (tracebackModulePtr != nullptr) { - //spDebug("Traceback module loaded"); - - final formatFuncName = "format_exception".toNativeUtf8(); - final pFormatFunc = cpython.PyObject_GetAttrString( - tracebackModulePtr, formatFuncName.cast()); - malloc.free(formatFuncName); - - if (pFormatFunc != nullptr && cpython.PyCallable_Check(pFormatFunc) != 0) { - // call `traceback.format_exception()` method - final pArgs = cpython.PyTuple_New(1); - if (pArgs == nullptr) { - final fallback = cpython.PyObject_Str(exPtr); - if (fallback == nullptr) { - cpython.Py_DecRef(pFormatFunc); - cpython.Py_DecRef(tracebackModulePtr); - cpython.Py_DecRef(exPtr); - return "Failed to allocate args to format Python exception."; - } - final s = cpython - .PyUnicode_AsUTF8(fallback) - .cast() - .toDartString(); - cpython.Py_DecRef(fallback); - cpython.Py_DecRef(pFormatFunc); - cpython.Py_DecRef(tracebackModulePtr); - cpython.Py_DecRef(exPtr); - return s; - } - // Keep a reference for fallback error formatting. - cpython.Py_IncRef(exPtr); - cpython.PyTuple_SetItem(pArgs, 0, exPtr); - - // result is a list - var listPtr = cpython.PyObject_CallObject(pFormatFunc, pArgs); - cpython.Py_DecRef(pArgs); - cpython.Py_DecRef(pFormatFunc); - cpython.Py_DecRef(tracebackModulePtr); - - // get and combine list items - var exLines = []; - if (listPtr == nullptr) { - final fallback = cpython.PyObject_Str(exPtr); - if (fallback == nullptr) { - cpython.Py_DecRef(exPtr); - return "Failed to format Python exception."; - } - final s = cpython - .PyUnicode_AsUTF8(fallback) - .cast() - .toDartString(); - cpython.Py_DecRef(fallback); - cpython.Py_DecRef(exPtr); - return s; - } + try { + final formatFuncNamePtr = "format_exception".toNativeUtf8(); + final formatFuncPtr = cpython.PyObject_GetAttrString( + tracebackModulePtr, formatFuncNamePtr.cast()); + malloc.free(formatFuncNamePtr); + if (formatFuncPtr == nullptr) return null; - var listSize = cpython.PyList_Size(listPtr); - if (listSize < 0) { - cpython.Py_DecRef(listPtr); - final fallback = cpython.PyObject_Str(exPtr); - if (fallback == nullptr) { - cpython.Py_DecRef(exPtr); - return "Failed to format Python exception."; - } - final s = cpython - .PyUnicode_AsUTF8(fallback) - .cast() - .toDartString(); - cpython.Py_DecRef(fallback); - cpython.Py_DecRef(exPtr); - return s; - } - for (var i = 0; i < listSize; i++) { - var itemObj = cpython.PyList_GetItem(listPtr, i); - var itemObjStr = cpython.PyObject_Str(itemObj); - if (itemObjStr == nullptr) { - continue; - } - final cStr = cpython.PyUnicode_AsUTF8(itemObjStr); - if (cStr == nullptr) { - cpython.Py_DecRef(itemObjStr); - continue; + try { + if (cpython.PyCallable_Check(formatFuncPtr) == 0) return null; + + final listPtr = cpython.PyObject_CallOneArg(formatFuncPtr, exceptionPtr); + if (listPtr == nullptr) return null; + + try { + final listSize = cpython.PyList_Size(listPtr); + if (listSize < 0) return null; + + final buffer = StringBuffer(); + for (var i = 0; i < listSize; i++) { + final itemObj = cpython.PyList_GetItem(listPtr, i); // borrowed ref + if (itemObj == nullptr) continue; + + final line = _pyUnicodeToDartString(cpython, itemObj) ?? + _pyObjectToDartString(cpython, itemObj); + if (line == null) continue; + buffer.write(line); } - var s = cStr.cast().toDartString(); - cpython.Py_DecRef(itemObjStr); - exLines.add(s); - } - cpython.Py_DecRef(listPtr); - cpython.Py_DecRef(exPtr); - return exLines.join(""); - } else { - if (pFormatFunc != nullptr) { - cpython.Py_DecRef(pFormatFunc); + return buffer.toString(); + } finally { + cpython.Py_DecRef(listPtr); } - cpython.Py_DecRef(tracebackModulePtr); - cpython.Py_DecRef(exPtr); - return "traceback.format_exception() method not found."; + } finally { + cpython.Py_DecRef(formatFuncPtr); } - } else { - cpython.Py_DecRef(exPtr); - return "Error loading traceback module."; + } finally { + cpython.Py_DecRef(tracebackModulePtr); + } +} + +String? _pyUnicodeToDartString( + CPython cpython, Pointer unicodeObjPtr) { + final cStr = cpython.PyUnicode_AsUTF8(unicodeObjPtr); + if (cStr == nullptr) return null; + return cStr.cast().toDartString(); +} + +String? _pyObjectToDartString(CPython cpython, Pointer objPtr) { + final strObj = cpython.PyObject_Str(objPtr); + if (strObj == nullptr) return null; + try { + return _pyUnicodeToDartString(cpython, strObj); + } finally { + cpython.Py_DecRef(strObj); } } From f6624038d6bf1aa5366421f0a5db41736ba37750 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 19 Dec 2025 21:17:31 -0800 Subject: [PATCH 13/14] Remove redundant debug log in async Python runner Eliminated an unnecessary debug statement before the async Python execution to reduce log verbosity and improve code clarity. --- src/serious_python_android/lib/src/cpython.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/serious_python_android/lib/src/cpython.dart b/src/serious_python_android/lib/src/cpython.dart index 2b1890cd..d81522c5 100644 --- a/src/serious_python_android/lib/src/cpython.dart +++ b/src/serious_python_android/lib/src/cpython.dart @@ -76,8 +76,6 @@ Future runPythonProgramFFI(bool sync, String dynamicLibPath, } else { // Async run: use Isolate.run() to avoid manual port lifecycle issues. try { - spDebug( - "Python async run start (sync=$sync, script=${script.isNotEmpty}, program=$pythonProgramPath)"); final result = await Isolate.run( () => _runPythonProgram(dynamicLibPath, pythonProgramPath, script)); spDebug("Python run done (resultLength=${result.length})"); From a73b7460bdae94557a5bd53eac3ea0ef96297f98 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sun, 21 Dec 2025 16:35:17 -0800 Subject: [PATCH 14/14] Bump version to 0.9.8 and fix Android logging Update all packages to version 0.9.8 and add changelog entries for fixing logging on Android. Also update build.gradle and podspec files to reflect the new version. --- src/serious_python/CHANGELOG.md | 4 ++++ src/serious_python/pubspec.yaml | 2 +- src/serious_python_android/CHANGELOG.md | 4 ++++ src/serious_python_android/android/build.gradle | 2 +- src/serious_python_android/pubspec.yaml | 2 +- src/serious_python_darwin/CHANGELOG.md | 4 ++++ .../darwin/serious_python_darwin.podspec | 2 +- src/serious_python_darwin/pubspec.yaml | 2 +- src/serious_python_linux/CHANGELOG.md | 4 ++++ src/serious_python_linux/pubspec.yaml | 2 +- src/serious_python_platform_interface/CHANGELOG.md | 4 ++++ src/serious_python_platform_interface/pubspec.yaml | 2 +- src/serious_python_windows/CHANGELOG.md | 4 ++++ src/serious_python_windows/pubspec.yaml | 2 +- 14 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/serious_python/CHANGELOG.md b/src/serious_python/CHANGELOG.md index b7a6e814..09cd00dd 100644 --- a/src/serious_python/CHANGELOG.md +++ b/src/serious_python/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.8 + +* Fix logging on Android. + ## 0.9.7 * Fix app restart on Android 10. diff --git a/src/serious_python/pubspec.yaml b/src/serious_python/pubspec.yaml index 0b95a5e6..04deeb68 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.7 +version: 0.9.8 platforms: ios: diff --git a/src/serious_python_android/CHANGELOG.md b/src/serious_python_android/CHANGELOG.md index 0e9642e6..1f5d9c5d 100644 --- a/src/serious_python_android/CHANGELOG.md +++ b/src/serious_python_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.8 + +* Fix logging on Android. + ## 0.9.7 * Fix app restart on Android 10. diff --git a/src/serious_python_android/android/build.gradle b/src/serious_python_android/android/build.gradle index 940713b1..570e65b6 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.7' +version '0.9.8' def python_version = '3.12' diff --git a/src/serious_python_android/pubspec.yaml b/src/serious_python_android/pubspec.yaml index 33dc3564..cff7d312 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.7 +version: 0.9.8 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 8d5b22d3..198f8877 100644 --- a/src/serious_python_darwin/CHANGELOG.md +++ b/src/serious_python_darwin/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.8 + +* Fix logging on Android. + ## 0.9.7 * Fix app restart on Android 10. diff --git a/src/serious_python_darwin/darwin/serious_python_darwin.podspec b/src/serious_python_darwin/darwin/serious_python_darwin.podspec index 5313acec..85f8ab9a 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.7' + s.version = '0.9.8' 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 fbed2e3c..c7c85435 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.7 +version: 0.9.8 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 a12caaf1..49a447b3 100644 --- a/src/serious_python_linux/CHANGELOG.md +++ b/src/serious_python_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.8 + +* Fix logging on Android. + ## 0.9.7 * Fix app restart on Android 10. diff --git a/src/serious_python_linux/pubspec.yaml b/src/serious_python_linux/pubspec.yaml index 6a8c49a5..3b33cea4 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.7 +version: 0.9.8 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 e4610308..a01d0f60 100644 --- a/src/serious_python_platform_interface/CHANGELOG.md +++ b/src/serious_python_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.8 + +* Fix logging on Android. + ## 0.9.7 * Fix app restart on Android 10. diff --git a/src/serious_python_platform_interface/pubspec.yaml b/src/serious_python_platform_interface/pubspec.yaml index 4405431f..f0203c34 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.7 +version: 0.9.8 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 291b781f..b291d8cb 100644 --- a/src/serious_python_windows/CHANGELOG.md +++ b/src/serious_python_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.8 + +* Fix logging on Android. + ## 0.9.7 * Fix app restart on Android 10. diff --git a/src/serious_python_windows/pubspec.yaml b/src/serious_python_windows/pubspec.yaml index 4ebe443e..ada16c01 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.7 +version: 0.9.8 environment: sdk: '>=3.1.3 <4.0.0'