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 04f735c..8bc3267 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 8bc3267..a2e00c2 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 a2e00c2..863a14a 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 863a14a..57fae9f 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 57fae9f..9fa7ecd 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 9fa7ecd..609955c 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 609955c..1cbf0b2 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 1cbf0b2..949b92d 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 949b92d..2ae8e52 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 2ae8e52..98b6f89 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 98b6f89..5c9972b 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 5c9972b..2b1890c 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 2b1890c..d81522c 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 b7a6e81..09cd00d 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 0b95a5e..04deeb6 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 0e9642e..1f5d9c5 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 940713b..570e65b 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 33dc356..cff7d31 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 8d5b22d..198f887 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 5313ace..85f8ab9 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 fbed2e3..c7c8543 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 a12caaf..49a447b 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 6a8c49a..3b33cea 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 e461030..a01d0f6 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 4405431..f0203c3 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 291b781..b291d8c 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 4ebe443..ada16c0 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'