Skip to content

Commit eb60c82

Browse files
FeodorFitsnerCreeper19472
authored andcommitted
Fixed app restart on Android 10, Python output redirect to logcat (flet-dev#187)
* Make setenv calls awaitable in Python init Refactored the setenv function to return a Future<void> and updated all calls to setenv to use await. This ensures environment variables are set in the correct order before proceeding. * 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. * 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. * Revert "Ensure single CPython interpreter per process" This reverts commit 3f4adc0. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * Remove terminate method from AndroidPlugin Eliminated the handling of the 'terminate' method call, including the delayed process termination logic, from the AndroidPlugin class. * Remove unused imports from AndroidPlugin.java Deleted unused imports for Handler, Looper, and Process to clean up the code and improve maintainability. * 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. * 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.
1 parent 927624c commit eb60c82

File tree

18 files changed

+131
-25
lines changed

18 files changed

+131
-25
lines changed

src/serious_python/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.9.7
2+
3+
* Fix app restart on Android 10.
4+
* Redirect Python output to logcat.
5+
16
## 0.9.6
27

38
* Make zipDirectory call asynchronous.

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/cfms-dev/serious-python
5-
version: 0.9.6
5+
version: 0.9.7
66

77
platforms:
88
ios:

src/serious_python_android/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.9.7
2+
3+
* Fix app restart on Android 10.
4+
* Redirect Python output to logcat.
5+
16
## 0.9.6
27

38
* Make zipDirectory call asynchronous.

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.6'
2+
version '0.9.7'
33

44
def python_version = '3.14.2'
55

src/serious_python_android/lib/serious_python_android.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:path/path.dart' as p;
88
import 'package:serious_python_platform_interface/serious_python_platform_interface.dart';
99

1010
import 'src/cpython.dart';
11+
import 'src/log.dart';
1112

1213
/// An implementation of [SeriousPythonPlatform] that uses method channels.
1314
class SeriousPythonAndroid extends SeriousPythonPlatform {
@@ -43,13 +44,13 @@ class SeriousPythonAndroid extends SeriousPythonPlatform {
4344
.invokeMethod<String>('loadLibrary', {'libname': 'pyjni'});
4445
await setenv("FLET_JNI_READY", "1");
4546
} catch (e) {
46-
debugPrint("Unable to load libpyjni.so library: $e");
47+
spDebug("Unable to load libpyjni.so library: $e");
4748
}
4849

4950
// unpack python bundle
5051
final nativeLibraryDir =
5152
await methodChannel.invokeMethod<String>('getNativeLibraryDir');
52-
debugPrint("getNativeLibraryDir: $nativeLibraryDir");
53+
spDebug("getNativeLibraryDir: $nativeLibraryDir");
5354

5455
var bundlePath = "$nativeLibraryDir/libpythonbundle.so";
5556
var sitePackagesZipPath = "$nativeLibraryDir/libpythonsitepackages.so";
@@ -59,7 +60,7 @@ class SeriousPythonAndroid extends SeriousPythonPlatform {
5960
}
6061
var pythonLibPath =
6162
await extractFileZip(bundlePath, targetPath: "python_bundle");
62-
debugPrint("pythonLibPath: $pythonLibPath");
63+
spDebug("pythonLibPath: $pythonLibPath");
6364

6465
var programDirPath = p.dirname(appPath);
6566

@@ -73,7 +74,7 @@ class SeriousPythonAndroid extends SeriousPythonPlatform {
7374
if (await File(sitePackagesZipPath).exists()) {
7475
var sitePackagesPath = await extractFileZip(sitePackagesZipPath,
7576
targetPath: "python_site_packages");
76-
debugPrint("sitePackagesPath: $sitePackagesPath");
77+
spDebug("sitePackagesPath: $sitePackagesPath");
7778
moduleSearchPaths.add(sitePackagesPath);
7879
}
7980

src/serious_python_android/lib/src/cpython.dart

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,46 @@ import 'dart:ffi';
33
import 'dart:isolate';
44

55
import 'package:ffi/ffi.dart';
6-
import 'package:flutter/foundation.dart';
76
import 'package:path/path.dart' as p;
87

98
import 'gen.dart';
9+
import 'log.dart';
1010

1111
export 'gen.dart';
1212

1313
CPython? _cpython;
14+
String? _logcatForwardingError;
15+
const _logcatInitScript = r'''
16+
import sys, logging
17+
18+
# Make this init idempotent across Dart isolate restarts.
19+
if not getattr(sys, "__serious_python_logcat_configured__", False):
20+
sys.__serious_python_logcat_configured__ = True
21+
22+
from ctypes import cdll
23+
liblog = cdll.LoadLibrary("liblog.so")
24+
ANDROID_LOG_INFO = 4
25+
26+
def _log_to_logcat(msg, level=ANDROID_LOG_INFO):
27+
if not msg:
28+
return
29+
if isinstance(msg, bytes):
30+
msg = msg.decode("utf-8", errors="replace")
31+
liblog.__android_log_write(level, b"serious_python", msg.encode("utf-8"))
32+
33+
class _LogcatWriter:
34+
def write(self, msg):
35+
_log_to_logcat(msg.strip())
36+
def flush(self):
37+
pass
38+
39+
sys.stdout = sys.stderr = _LogcatWriter()
40+
handler = logging.StreamHandler(sys.stderr)
41+
handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
42+
root = logging.getLogger()
43+
root.handlers[:] = [handler]
44+
root.setLevel(logging.DEBUG)
45+
''';
1446

1547
CPython getCPython(String dynamicLibPath) {
1648
return _cpython ??= _cpython = CPython(DynamicLibrary.open(dynamicLibPath));
@@ -46,21 +78,35 @@ Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
4678
var programDirPath = p.dirname(pythonProgramPath);
4779
var programModuleName = p.basenameWithoutExtension(pythonProgramPath);
4880

49-
debugPrint("dynamicLibPath: $dynamicLibPath");
50-
debugPrint("programDirPath: $programDirPath");
51-
debugPrint("programModuleName: $programModuleName");
81+
spDebug("dynamicLibPath: $dynamicLibPath");
82+
spDebug("programDirPath: $programDirPath");
83+
spDebug("programModuleName: $programModuleName");
5284

5385
final cpython = getCPython(dynamicLibPath);
86+
spDebug("CPython loaded");
87+
if (cpython.Py_IsInitialized() != 0) {
88+
spDebug("Python already initialized, skipping execution.");
89+
sendPort.send("");
90+
return "";
91+
}
92+
5493
cpython.Py_Initialize();
55-
debugPrint("after Py_Initialize()");
94+
spDebug("after Py_Initialize()");
5695

5796
var result = "";
5897

98+
final logcatSetupError = _setupLogcatForwarding(cpython);
99+
if (logcatSetupError != null) {
100+
cpython.Py_Finalize();
101+
sendPort.send(logcatSetupError);
102+
return logcatSetupError;
103+
}
104+
59105
if (script != "") {
60106
// run script
61107
final scriptPtr = script.toNativeUtf8();
62108
int sr = cpython.PyRun_SimpleString(scriptPtr.cast<Char>());
63-
debugPrint("PyRun_SimpleString for script result: $sr");
109+
spDebug("PyRun_SimpleString for script result: $sr");
64110
malloc.free(scriptPtr);
65111
if (sr != 0) {
66112
result = getPythonError(cpython);
@@ -76,7 +122,7 @@ Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
76122
}
77123

78124
cpython.Py_Finalize();
79-
debugPrint("after Py_Finalize()");
125+
spDebug("after Py_Finalize()");
80126

81127
sendPort.send(result);
82128

@@ -94,7 +140,7 @@ String getPythonError(CPython cpython) {
94140
cpython.Py_DecRef(tracebackModuleNamePtr.cast());
95141

96142
if (tracebackModulePtr != nullptr) {
97-
//debugPrint("Traceback module loaded");
143+
//spDebug("Traceback module loaded");
98144

99145
final formatFuncName = "format_exception".toNativeUtf8();
100146
final pFormatFunc = cpython.PyObject_GetAttrString(
@@ -127,3 +173,20 @@ String getPythonError(CPython cpython) {
127173
return "Error loading traceback module.";
128174
}
129175
}
176+
177+
String? _setupLogcatForwarding(CPython cpython) {
178+
if (_logcatForwardingError != null) {
179+
return _logcatForwardingError;
180+
}
181+
182+
final setupPtr = _logcatInitScript.toNativeUtf8();
183+
final result = cpython.PyRun_SimpleString(setupPtr.cast<Char>());
184+
malloc.free(setupPtr);
185+
186+
if (result != 0) {
187+
_logcatForwardingError = getPythonError(cpython);
188+
return _logcatForwardingError;
189+
}
190+
191+
return null;
192+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import 'package:flutter/foundation.dart';
2+
3+
void spDebug(String message) {
4+
if (message.startsWith('[serious_python]')) {
5+
debugPrint(message);
6+
} else {
7+
debugPrint('[serious_python] $message');
8+
}
9+
}
10+

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.6
5+
version: 0.9.7
66

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

src/serious_python_darwin/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.9.7
2+
3+
* Fix app restart on Android 10.
4+
* Redirect Python output to logcat.
5+
16
## 0.9.6
27

38
* Make zipDirectory call asynchronous.

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.6'
7+
s.version = '0.9.7'
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.

0 commit comments

Comments
 (0)