Terminate built apps without running process teardown - #6830
Open
FeodorFitsner wants to merge 6 commits into
Open
Terminate built apps without running process teardown#6830FeodorFitsner wants to merge 6 commits into
FeodorFitsner wants to merge 6 commits into
Conversation
A built app whose Python code was still running when the process ended could
crash with EXC_BAD_ACCESS, reported by the OS as an application crash even
though the app had finished its work.
Python runs on its own thread alongside Flutter, and a normal process exit
runs __cxa_finalize (DLL_PROCESS_DETACH on Windows), destroying the C++
statics inside every loaded C extension module while that thread is still
executing inside one of them. The reported case died in matplotlib's ft2font
looking up a pybind11 type-caster map that had just been destructed; numpy,
Pillow and Flutter's own Skia statics are torn down by the same pass.
Both exit paths were affected:
* Window close on desktop. The macOS and Linux runners now _exit, and the
Windows runner uses TerminateProcess, since _exit and ExitProcess both
still run DLL_PROCESS_DETACH and would not help.
* sys.exit on every native platform, which is the worse of the two: Flet's
patched sys.exit posts the exit code to Dart and returns rather than
raising SystemExit, so the interpreter is fully alive and running on into
Py_Finalize when Dart tears the process down. This now routes through
DartBridge.hardExit, falling back to exit() against an older bridge.
Exit codes are preserved. The trade-off is documented as a contract: atexit
handlers, __del__ finalizers and unflushed buffered writes are not guaranteed
to run on exit.
Bumps serious_python to 4.7.0 and re-pins python-build to 20260908, keeping
PYTHON_BUILD_RELEASE_DATE in sync with serious_python's pythonReleaseDate as
that pin requires. No CPython or Pyodide versions change.
Verified on macOS against the published packages: baseline 2 crashes in 6
window closes, 0 in 10 after; sys.exit(3) preserved across 5 runs with no
crashes; SharedPreferences writes survive both exit paths.
Deploying flet-website-v2 with
|
| Latest commit: |
d2b9f18
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://aa20e5ba.flet-website-v2.pages.dev |
| Branch Preview URL: | https://hard-exit.flet-website-v2.pages.dev |
The termination section told readers to persist state before exiting but only showed the sys.exit path, which is the one the app controls. On desktop the user closes the window and the OS initiates termination, so without a hook none of their code runs at all. Adds the prevent_close + WindowEventType.CLOSE handler as the counterpart, and notes that the handler holds up the app's exit so it should stay quick. Verified against a built macOS app with the hard exit in place: the handler runs on every close, since prevent_close intercepts well before applicationWillTerminate.
…only assets
Asset syncing walked source trees with a bare rglob filtered only by
extension, so an example app that had been built locally leaked its build
output into the docs site: a single `flet build ios` under
sdk/python/examples leaves ~1.4 GB in build/, and 192 of its PNGs matched the
filter, including Swift Package Manager checkouts.
Those checkouts ship read-only (0444), and copy2 preserves the source mode,
so the copies landed read-only too. That made the sync succeed once and fail
on every later run, because copyfile opens the destination for writing:
PermissionError: [Errno 13] Permission denied:
.../rive_animations/build/flutter/build/ios/SourcePackages/checkouts/
DKCamera/DKCamera/DKCameraResource.bundle/Images/camera_cancel.png
Prunes generated directories (build, .dart_tool, node_modules, .venv, .git,
__pycache__) and unlinks the destination before copying so a read-only file
is replaced rather than reopened.
Locally this removed 227 build-artifact files from website/static/docs/
examples and took it from 171M to 84M, with `yarn build` then repeatable.
The tests parametrized over (3, 11, 0) and (3, 12, 0) and patched sys.version_info to steer rmtree's branch, which picks shutil.rmtree's callback kwarg: onexc on 3.12+, onerror below it. Patching only steers the branch; the shutil.rmtree underneath is still the running interpreter's, and it only accepts onexc from 3.12 on. So the 3.12 parameter failed on 3.10 and 3.11 with TypeError: rmtree() got an unexpected keyword argument 'onexc' which was a limitation of the test setup, not a defect in rmtree, whose version guard is correct. Both tests assert interpreter-agnostic behaviour: that a failing deletion propagates PermissionError, and that ignore_errors swallows it. Neither needs a spoofed version, so the parametrization and the patch are dropped and each test runs against the real interpreter. Branch coverage is unchanged in aggregate, since CI spans 3.10 through 3.14: onerror is exercised on 3.10 and 3.11, onexc on 3.12+, each on an interpreter that can actually run it.
The PWA manifest still described Flet as "the fastest way to build Flutter apps in Python". It was the last place in the repo using that wording, and it leads with Flutter rather than what Flet offers. Replaced with the tagline the site and README already use.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A built app whose Python code was still running when the process ended could crash with
EXC_BAD_ACCESS, reported by the OS as an application crash even though the app had finished its work.Python runs on its own thread alongside Flutter. A normal process exit runs
__cxa_finalize(DLL_PROCESS_DETACHon Windows), destroying the C++ statics inside every loaded C extension module while that thread is still executing inside one of them:Nothing is matplotlib-specific. A second capture of the same crash caught thread 0 inside
SkTypefaceCache::~SkTypefaceCache(), Flutter's own Skia static, on the same pass;numpyandPillowhave statics torn down by it too.Both exit paths were affected:
sys.exit()(every native platform), which is the worse of the two. Flet's patchedsys.exit(flet_exitinpython.dart) posts the exit code to Dart and returns rather than raisingSystemExit, so the interpreter is fully alive, running on intosp_run_target's return andPy_Finalize(), when Dart'sexit()tears the process down. Two concurrent teardowns.Change
The desktop runners hard-exit:
_exiton macOS and Linux,TerminateProcesson Windows. Windows needs the different primitive because_exit()andExitProcessboth still runDLL_PROCESS_DETACH, where the CRT'sDllMainruns each DLL's static destructors, which is the thing being avoided.native_runtime.dartroutes thesys.exitpath throughDartBridge.hardExit, falling back toexit()against a pre-1.9.0 bridge. Exit codes are preserved:terminate:always exits 0 and carries no status, so the macOS hook's_exit(0)is not overriding a requested code; a Python-requested exit does not pass through it.Bumps
serious_pythonto 4.7.0 and re-pinsPYTHON_BUILD_RELEASE_DATEto 20260908, keeping it in sync withserious_python'spythonReleaseDateas that pin's own comment requires. No CPython or Pyodide versions change.Documented contract
This is a deliberate trade, so it is written down rather than left implicit, in a new How a built app terminates section with pointers from the macOS, Linux and Windows pages:
Verification
macOS, against the published
serious_python4.7.0 / python-build 20260908 /dart_bridge1.9.0 (cleanflet cleanrebuild resolving from pub.dev, no path or git overrides), using the matplotlib playground app whose render loop keeps the interpreter thread hot:sys.exit(3), afterSharedPreferencesdurability, window closeSharedPreferencesdurability,sys.exitexit()At the observed baseline rate, twelve clean closes by chance is roughly 0.8%.
The repro only fires with the window frontmost: unfocused, Flutter throttles rendering and the interpreter sits in
asyncio.sleeprather than inside a C call, which is why an initial unactivated run showed zero crashes before the fix as well.Not verified
Linux and Windows. Both are implemented from the same analysis but were not run; I had no host for either. The Windows branch deserves a real check before it is relied on, since it is the one platform where the obvious primitive (
_exit) is the wrong answer: confirmExitProcessstill crashes andTerminateProcessdoes not.A narrower fallback than it looks. The soft symbol lookup covers an older native
libdart_bridgeat runtime. It does not cover an olderserious_pythonDart package, whereDartBridge.hardExitdoes not exist and the template fails to compile.Chain
Summary by Sourcery
Make built applications terminate without running unsafe process teardown while documenting the resulting shutdown guarantees and updating supporting runtime dependencies.
Bug Fixes:
sys.exit().Enhancements:
Build:
serious_python4.7.0 and pin the Python build release to 20260908.Documentation:
Tests:
Chores: