Skip to content

Commit 68b5e84

Browse files
Fix logging for Android (#188)
* 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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.
1 parent f3dd026 commit 68b5e84

File tree

15 files changed

+148
-74
lines changed

15 files changed

+148
-74
lines changed

src/serious_python/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.8
2+
3+
* Fix logging on Android.
4+
15
## 0.9.7
26

37
* Fix app restart on Android 10.

src/serious_python/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: serious_python
22
description: A cross-platform plugin for adding embedded Python runtime to your Flutter apps.
33
homepage: https://flet.dev
44
repository: https://github.com/flet-dev/serious-python
5-
version: 0.9.7
5+
version: 0.9.8
66

77
platforms:
88
ios:

src/serious_python_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.8
2+
3+
* Fix logging on Android.
4+
15
## 0.9.7
26

37
* Fix app restart on Android 10.

src/serious_python_android/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
group 'com.flet.serious_python_android'
2-
version '0.9.7'
2+
version '0.9.8'
33

44
def python_version = '3.12'
55

src/serious_python_android/lib/src/cpython.dart

Lines changed: 116 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,22 @@ export 'gen.dart';
1212

1313
CPython? _cpython;
1414
String? _logcatForwardingError;
15+
Future<void> _pythonRunQueue = Future<void>.value();
16+
17+
Future<T> _enqueuePythonRun<T>(Future<T> Function() action) {
18+
final completer = Completer<T>();
19+
_pythonRunQueue = _pythonRunQueue.then((_) async {
20+
try {
21+
completer.complete(await action());
22+
} catch (e, st) {
23+
completer.completeError(e, st);
24+
}
25+
});
26+
return completer.future;
27+
}
28+
1529
const _logcatInitScript = r'''
16-
import sys, logging
30+
import logging,sys
1731
1832
# Make this init idempotent across Dart isolate restarts.
1933
if not getattr(sys, "__serious_python_logcat_configured__", False):
@@ -41,7 +55,7 @@ if not getattr(sys, "__serious_python_logcat_configured__", False):
4155
handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
4256
root = logging.getLogger()
4357
root.handlers[:] = [handler]
44-
root.setLevel(logging.DEBUG)
58+
root.setLevel(logging.ERROR)
4559
''';
4660

4761
CPython getCPython(String dynamicLibPath) {
@@ -50,31 +64,33 @@ CPython getCPython(String dynamicLibPath) {
5064

5165
Future<String> runPythonProgramFFI(bool sync, String dynamicLibPath,
5266
String pythonProgramPath, String script) async {
53-
final receivePort = ReceivePort();
54-
if (sync) {
55-
// sync run
56-
return await runPythonProgramInIsolate(
57-
[receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]);
58-
} else {
59-
var completer = Completer<String>();
60-
// async run
61-
final isolate = await Isolate.spawn(runPythonProgramInIsolate,
62-
[receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]);
63-
receivePort.listen((message) {
64-
receivePort.close();
65-
isolate.kill();
66-
completer.complete(message);
67-
});
68-
return completer.future;
69-
}
67+
return _enqueuePythonRun(() async {
68+
spDebug(
69+
"Python run start (sync=$sync, script=${script.isNotEmpty}, program=$pythonProgramPath)");
70+
if (sync) {
71+
// Sync run: do not involve ports (avoids GC/close races).
72+
final result =
73+
_runPythonProgram(dynamicLibPath, pythonProgramPath, script);
74+
spDebug("Python run done (resultLength=${result.length})");
75+
return result;
76+
} else {
77+
// Async run: use Isolate.run() to avoid manual port lifecycle issues.
78+
try {
79+
final result = await Isolate.run(
80+
() => _runPythonProgram(dynamicLibPath, pythonProgramPath, script));
81+
spDebug("Python run done (resultLength=${result.length})");
82+
return result;
83+
} catch (e, st) {
84+
final message = "Dart error running Python: $e\n$st";
85+
spDebug(message);
86+
return message;
87+
}
88+
}
89+
});
7090
}
7191

72-
Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
73-
final sendPort = arguments[0] as SendPort;
74-
final dynamicLibPath = arguments[1] as String;
75-
final pythonProgramPath = arguments[2] as String;
76-
final script = arguments[3] as String;
77-
92+
String _runPythonProgram(
93+
String dynamicLibPath, String pythonProgramPath, String script) {
7894
var programDirPath = p.dirname(pythonProgramPath);
7995
var programModuleName = p.basenameWithoutExtension(pythonProgramPath);
8096

@@ -85,8 +101,8 @@ Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
85101
final cpython = getCPython(dynamicLibPath);
86102
spDebug("CPython loaded");
87103
if (cpython.Py_IsInitialized() != 0) {
88-
spDebug("Python already initialized, skipping execution.");
89-
sendPort.send("");
104+
spDebug(
105+
"Python already initialized and another program is running, skipping execution.");
90106
return "";
91107
}
92108

@@ -98,7 +114,6 @@ Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
98114
final logcatSetupError = _setupLogcatForwarding(cpython);
99115
if (logcatSetupError != null) {
100116
cpython.Py_Finalize();
101-
sendPort.send(logcatSetupError);
102117
return logcatSetupError;
103118
}
104119

@@ -124,53 +139,88 @@ Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
124139
cpython.Py_Finalize();
125140
spDebug("after Py_Finalize()");
126141

127-
sendPort.send(result);
128-
129142
return result;
130143
}
131144

132145
String getPythonError(CPython cpython) {
133-
// get error object
134-
var exPtr = cpython.PyErr_GetRaisedException();
146+
final exPtr = cpython.PyErr_GetRaisedException();
147+
if (exPtr == nullptr) return "Unknown Python error (no exception set).";
148+
149+
try {
150+
final formatted = _formatPythonException(cpython, exPtr);
151+
if (formatted != null && formatted.isNotEmpty) return formatted;
152+
153+
final fallback = _pyObjectToDartString(cpython, exPtr);
154+
return fallback ?? "Unknown Python error (failed to stringify exception).";
155+
} finally {
156+
cpython.Py_DecRef(exPtr);
157+
// Defensive: formatting can set a new Python error.
158+
cpython.PyErr_Clear();
159+
}
160+
}
135161

136-
// use 'traceback' module to format exception
162+
String? _formatPythonException(
163+
CPython cpython, Pointer<PyObject> exceptionPtr) {
164+
// Uses `traceback.format_exception(exc)` (Python 3.10+ signature).
137165
final tracebackModuleNamePtr = "traceback".toNativeUtf8();
138-
var tracebackModulePtr =
166+
final tracebackModulePtr =
139167
cpython.PyImport_ImportModule(tracebackModuleNamePtr.cast<Char>());
140-
cpython.Py_DecRef(tracebackModuleNamePtr.cast());
141-
142-
if (tracebackModulePtr != nullptr) {
143-
//spDebug("Traceback module loaded");
144-
145-
final formatFuncName = "format_exception".toNativeUtf8();
146-
final pFormatFunc = cpython.PyObject_GetAttrString(
147-
tracebackModulePtr, formatFuncName.cast());
148-
cpython.Py_DecRef(tracebackModuleNamePtr.cast());
149-
150-
if (pFormatFunc != nullptr && cpython.PyCallable_Check(pFormatFunc) != 0) {
151-
// call `traceback.format_exception()` method
152-
final pArgs = cpython.PyTuple_New(1);
153-
cpython.PyTuple_SetItem(pArgs, 0, exPtr);
154-
155-
// result is a list
156-
var listPtr = cpython.PyObject_CallObject(pFormatFunc, pArgs);
157-
158-
// get and combine list items
159-
var exLines = [];
160-
var listSize = cpython.PyList_Size(listPtr);
161-
for (var i = 0; i < listSize; i++) {
162-
var itemObj = cpython.PyList_GetItem(listPtr, i);
163-
var itemObjStr = cpython.PyObject_Str(itemObj);
164-
var s =
165-
cpython.PyUnicode_AsUTF8(itemObjStr).cast<Utf8>().toDartString();
166-
exLines.add(s);
168+
malloc.free(tracebackModuleNamePtr);
169+
if (tracebackModulePtr == nullptr) return null;
170+
171+
try {
172+
final formatFuncNamePtr = "format_exception".toNativeUtf8();
173+
final formatFuncPtr = cpython.PyObject_GetAttrString(
174+
tracebackModulePtr, formatFuncNamePtr.cast());
175+
malloc.free(formatFuncNamePtr);
176+
if (formatFuncPtr == nullptr) return null;
177+
178+
try {
179+
if (cpython.PyCallable_Check(formatFuncPtr) == 0) return null;
180+
181+
final listPtr = cpython.PyObject_CallOneArg(formatFuncPtr, exceptionPtr);
182+
if (listPtr == nullptr) return null;
183+
184+
try {
185+
final listSize = cpython.PyList_Size(listPtr);
186+
if (listSize < 0) return null;
187+
188+
final buffer = StringBuffer();
189+
for (var i = 0; i < listSize; i++) {
190+
final itemObj = cpython.PyList_GetItem(listPtr, i); // borrowed ref
191+
if (itemObj == nullptr) continue;
192+
193+
final line = _pyUnicodeToDartString(cpython, itemObj) ??
194+
_pyObjectToDartString(cpython, itemObj);
195+
if (line == null) continue;
196+
buffer.write(line);
197+
}
198+
return buffer.toString();
199+
} finally {
200+
cpython.Py_DecRef(listPtr);
167201
}
168-
return exLines.join("");
169-
} else {
170-
return "traceback.format_exception() method not found.";
202+
} finally {
203+
cpython.Py_DecRef(formatFuncPtr);
171204
}
172-
} else {
173-
return "Error loading traceback module.";
205+
} finally {
206+
cpython.Py_DecRef(tracebackModulePtr);
207+
}
208+
}
209+
210+
String? _pyUnicodeToDartString(
211+
CPython cpython, Pointer<PyObject> unicodeObjPtr) {
212+
final cStr = cpython.PyUnicode_AsUTF8(unicodeObjPtr);
213+
if (cStr == nullptr) return null;
214+
return cStr.cast<Utf8>().toDartString();
215+
}
216+
217+
String? _pyObjectToDartString(CPython cpython, Pointer<PyObject> objPtr) {
218+
final strObj = cpython.PyObject_Str(objPtr);
219+
if (strObj == nullptr) return null;
220+
try {
221+
return _pyUnicodeToDartString(cpython, strObj);
222+
} finally {
223+
cpython.Py_DecRef(strObj);
174224
}
175225
}
176226

src/serious_python_android/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: serious_python_android
22
description: Android implementation of the serious_python plugin
33
homepage: https://flet.dev
44
repository: https://github.com/flet-dev/serious-python
5-
version: 0.9.7
5+
version: 0.9.8
66

77
environment:
88
sdk: ">=3.0.0 <4.0.0"

src/serious_python_darwin/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.8
2+
3+
* Fix logging on Android.
4+
15
## 0.9.7
26

37
* Fix app restart on Android 10.

src/serious_python_darwin/darwin/serious_python_darwin.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55
Pod::Spec.new do |s|
66
s.name = 'serious_python_darwin'
7-
s.version = '0.9.7'
7+
s.version = '0.9.8'
88
s.summary = 'A cross-platform plugin for adding embedded Python runtime to your Flutter apps.'
99
s.description = <<-DESC
1010
A cross-platform plugin for adding embedded Python runtime to your Flutter apps.

src/serious_python_darwin/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: serious_python_darwin
22
description: iOS and macOS implementations of the serious_python plugin
33
homepage: https://flet.dev
44
repository: https://github.com/flet-dev/serious-python
5-
version: 0.9.7
5+
version: 0.9.8
66

77
environment:
88
sdk: ">=3.0.0 <4.0.0"

src/serious_python_linux/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.8
2+
3+
* Fix logging on Android.
4+
15
## 0.9.7
26

37
* Fix app restart on Android 10.

0 commit comments

Comments
 (0)