Multiprocessing child interception: run the host binary as a headless interpreter for spawn/forkserver/resource-tracker re-execs#9
Conversation
… serious_python_main Python's multiprocessing (spawn/forkserver start methods and the resource tracker) launches helper processes by re-executing sys.executable with a CPython command line. In serious_python-hosted apps sys.executable is the host app binary, so every helper re-launched the host GUI instead of running the multiprocessing protocol: one stray window per worker, tasks that never executed, hung joins, and "resource_tracker: process died unexpectedly, relaunching" loops (flet-dev/flet#4283). Two new stable-ABI exports let the host binary double as a plain Python interpreter for exactly those invocations: - serious_python_is_mp_invocation(argc, argv): matches --multiprocessing-fork anywhere in argv, or a -c payload starting with "from multiprocessing." / "import sys; from multiprocessing." — covering spawn workers, the resource tracker, and the forkserver on CPython 3.12–3.14. Intentionally prefix-based: the exact command lines are CPython implementation details that shift across minor releases, while the helpers stay under multiprocessing.*. - serious_python_main(argc, argv): shared preflight (scrub inherited PYTHONINSPECT, which would hold a real interpreter child open in interactive mode after its -c command; verify PYTHONHOME/PYTHONPATH are present — the parent's serious_python_run setenv's them process-wide, so children inherit the embedded stdlib/site-packages location; register the in-binary dart_bridge module on the inittab so inherited dart_bridge-aware imports fail soft), then delegate to Py_BytesMain for the full standard interpreter lifecycle. Windows additionally gets wide-char variants serious_python_is_mp_invocation_w / serious_python_main_w (→ Py_Main), since wWinMain argv is wchar_t** and decoding through the ANSI code page in the narrow versions would be lossy. Host contract: call the detector first thing in main/wWinMain/ main.swift, before any UI or engine initialization, and exit with serious_python_main's return code on a match. The flet build template runners (macOS/Windows/Linux) do exactly this; resolution is via dlsym/GetProcAddress with graceful fallthrough, so hosts built against older dart_bridge keep launching unchanged. The EXPORT macro on non-Windows now adds __attribute__((used)): on Mach-O the archive is statically linked into the host app and several exports are referenced only via dlsym/Dart FFI — without the no-dead-strip marker, the host link's -dead_strip discarded any export lacking an ordinary C call site. Verified end-to-end on macOS in a flet build app (Process+Queue round-trip, ProcessPoolExecutor with worker reuse, headless children, clean exits); Windows/Linux code paths reviewed but pending runtime verification.
…contract New "Multiprocessing child interception (1.5.0+)" section covering: - why interception exists (multiprocessing helpers re-exec sys.executable, which is the host app binary in serious_python-hosted apps — flet-dev/flet#4283); - the four entry points (narrow + Windows wide-char variants) and which CPython entry point each delegates to (Py_BytesMain / Py_Main); - the host contract: call the detector first thing in main / wWinMain / main.swift, before any UI or engine initialization, and exit with serious_python_main's return code on a match; - what the detector matches and why the match is deliberately prefix-based (the helper command lines are CPython implementation details that shift across minor releases); - how the child finds the embedded stdlib/site-packages (PYTHONHOME / PYTHONPATH inherited from the parent's process-wide setenv); - the Apple `used`-attribute note (statically linked archive whose exports are dlsym-only — dead-strip protection). Also list the new entry points in the serious_python_run.c bullet of "What's in here".
libdart_bridge.so is linked directly into end users' Flutter runner executables (the flet build template links plugin bundled libraries into the main binary), so the glibc of the BUILD runner becomes the minimum glibc of every machine that builds or runs a flet Linux app. Building on ubuntu-24.04 (glibc 2.39) binds C23 symbol versions — e.g. strtoll resolves to __isoc23_strtoll@GLIBC_2.38 — making the artifact unlinkable/unloadable on Ubuntu 22.04 LTS (glibc 2.35). Found the hard way: `flet build linux` on a 22.04 aarch64 host failed its final link against the 24.04-arm-built artifact. Pin both Linux jobs to ubuntu-22.04 / ubuntu-22.04-arm so released artifacts keep a glibc 2.35 floor.
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
|
|
||
| - name: Attach to GitHub Release | ||
| uses: softprops/action-gh-release@v2 | ||
| uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 |
There was a problem hiding this comment.
Pull request overview
This PR adds multiprocessing-child interception support so apps embedding Python via dart_bridge can detect when they’ve been re-exec’d by CPython’s multiprocessing helpers and run as a headless interpreter instead of re-launching the host GUI. It also tightens GitHub Actions security posture and adjusts Linux CI runner selection to keep a lower glibc baseline for published artifacts.
Changes:
- Add new exported entry points to detect multiprocessing helper invocations and delegate to
Py_BytesMain/Py_Mainafter a small child-process preflight. - Document the host-side integration contract and platform-specific notes in the README.
- Pin workflow actions to SHAs, add a zizmor workflow, and pin Linux builds to ubuntu-22.04 runners to preserve a glibc 2.35 floor.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/serious_python_run.c | Adds multiprocessing invocation detection, a child preflight, and interpreter-entry exports (including Windows wide-char variants). |
| README.md | Documents the new multiprocessing interception API and host integration requirements. |
| .github/workflows/zizmor.yml | Adds a zizmor Actions security analysis workflow with least-privilege permissions. |
| .github/workflows/ci.yml | Pins action versions to SHAs, restricts default permissions, and moves Linux builds to ubuntu-22.04 runners for glibc compatibility. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Supersedes the ubuntu-22.04 runner pin: 22.04's glibc 2.35 still binds pthread_create@GLIBC_2.34, while flet's own Linux client matrix builds down to debian:10 / manylinux_2_28 — so a 22.04-built libdart_bridge.so would have been the tightest glibc constraint in the whole stack, breaking `flet build linux` (and its output bundles) on the very distros flet supports. Mirror flet's pattern instead: modern runners (ubuntu-24.04 / ubuntu-24.04-arm) with a debian:10 job container, apt pointed at archive.debian.org (EOL distro), and a modern CMake from Kitware's official binaries (Debian 10 ships 3.13 < our 3.15 minimum; the official tarballs run on old glibc). A new CI step asserts the finished artifact references no glibc symbol newer than 2.28, so a future container bump can't silently raise the floor again.
- Declare `shell: bash` on every run step: inside a job container the
default shell falls back to sh (dash), which rejects
`set -o pipefail` ("Illegal option -o pipefail"). Bare runners
default to bash, which is why this only broke after moving into the
container. Matches what flet's own container jobs do.
- Install `file` in the base-packages step: the artifact staging step
uses it, and unlike the bare runner images the debian:10 base image
doesn't ship it.
Part of flet-dev/flet#4283. Companion PRs: flet-dev/serious-python#228 (consumes these exports and bumps the pin) and flet-dev/flet#6662 (build-template runners that call them).
Problem
Python's
multiprocessing(thespawnandforkserverstart methods, plus the resource tracker) launches helper processes by re-executingsys.executablewith a CPython command line. In apps embedding Python through this library,sys.executableresolves to the host app executable, so every helper re-launched the host GUI instead of running the multiprocessing protocol: one stray window per worker, tasks that never executed, hung joins, andresource_tracker: process died unexpectedly, relaunchingloops. This has been broken since 2024 and is the core of flet-dev/flet#4283. Notably, Python 3.14 madeforkserverthe default start method on Linux, so packaged Linux apps now hit the broken re-exec path even for code that never selects a start method.Solution
Two new stable-ABI exports in
serious_python_run.clet the host binary double as a plain Python interpreter for exactly those helper invocations:serious_python_is_mp_invocation(argc, argv)matches--multiprocessing-forkanywhere in argv, or a-cpayload starting withfrom multiprocessing.orimport sys; from multiprocessing., covering spawn workers, the resource tracker, and the forkserver on CPython 3.12–3.14. The match is deliberately prefix-based because the exact helper command lines are CPython implementation details that shift across minor releases, while the helpers stay undermultiprocessing.*.serious_python_main(argc, argv)runs a shared preflight (scrub inheritedPYTHONINSPECT, which would otherwise hold a real interpreter child open in interactive mode after its-ccommand; verifyPYTHONHOME/PYTHONPATHare present, which the parent'sserious_python_runalready setenv's process-wide so children inherit the embedded stdlib and site-packages; register the in-binarydart_bridgemodule on the inittab so inherited dart_bridge-aware imports fail soft), then delegates toPy_BytesMainfor the full standard interpreter lifecycle.Windows additionally gets wide-char variants
serious_python_is_mp_invocation_w/serious_python_main_w(delegating toPy_Main), sincewWinMainargv iswchar_t**and decoding through the ANSI code page in the narrow versions would be lossy.Host contract (documented in the README): call the detector first thing in
main/wWinMain/main.swift, before any UI or engine initialization, and exit withserious_python_main's return code on a match. The flet build template runners do exactly this, resolving the exports via dlsym/GetProcAddress with graceful fallthrough so hosts built against older dart_bridge keep launching unchanged.The
EXPORTmacro on non-Windows now adds__attribute__((used)): on Mach-O the archive is statically linked into the host app and several exports are referenced only via dlsym/Dart FFI, and without the no-dead-strip marker the host link's-dead_stripdiscarded any export lacking a static call site.CI fix: glibc floor of Linux artifacts
Artifacts are currently built directly on
ubuntu-24.04runners (glibc 2.39), so they bind modern symbol versions — e.g.strtoll→__isoc23_strtoll@GLIBC_2.38. Since the flet build template linkslibdart_bridge.sostraight into the app executable,flet build linuxfails its final link on anything older (reproduced on Ubuntu 22.04 LTS:clang: error: linker command failed). This affects the released 1.4.1 Linux artifacts today, independent of new features — worth re-spinning with the next tag.Fix
Build inside a
debian:10job container on modern runners — the same pattern (and lowest tier) as flet's ownbuild_linuxmatrix (manylinux_2_28). dart-bridge ships one.soper arch, so it must clear the lowest supported tier: a glibc 2.28 floor covers Debian 10/11/12, Ubuntu 20.04/22.04/24.04, and RHEL 8+ with a single artifact.Details (each step is commented in the workflow):
archive.debian.org,Check-Valid-Untiloff; git/ca-certs installed beforeactions/checkout(the action runs inside the container).Verification
Verified end-to-end inside
flet buildapps on macOS (arm64), Windows 11 (arm64 host, x64 app), and Ubuntu 22.04 (aarch64): aProcess+Queueround-trip with a worker defined in the app'smain.py, and aProcessPoolExecutorbenchmark with worker reuse, all completing with headless children, correct results, roughly cpu-count speedups, and clean exits, under both the spawn context and Linux's forkserver default. The interception was also smoke-tested directly by invoking the built app binaries with multiprocessing-shaped command lines.Release notes
Merging this should be followed by tagging 1.5.0 (semver minor: new exports, no breaking changes), which republishes all platform artifacts including the glibc-2.35-floor Linux ones. flet-dev/python-build's
manifest.jsonthen bumpsdart_bridge_versionto 1.5.0, and flet-dev/serious-python#228 picks up the regenerated pin. Note that serious-python 4.3.0's darwin plugin hard-requires these exports at link time (dead-strip keep-alives), so the tag must exist before that releases.