From 90a7d68d4a09c245e53055607229770c1ffa0e0c Mon Sep 17 00:00:00 2001 From: maziggy Date: Tue, 7 Jul 2026 07:50:08 +0200 Subject: [PATCH] fix(windows): bundle vcruntime140_1.dll so greenlet loads on fresh Win10 (#2474) A clean Windows 10 install crashed on startup: init_db() -> SQLAlchemy async engine -> greenlet failed with "DLL load failed while importing _greenlet: The specified module could not be found", so uvicorn never bound :8000 and the dashboard refused all connections while the NSSM service still showed running. greenlet's _greenlet.pyd is C++ and needs vcruntime140_1.dll, which the python.org embeddable distribution does not ship (it includes only vcruntime140.dll, enough for the pure-C python313.dll). Machines with the VC++ 2015-2022 redistributable already installed have the DLL in System32, which masked the bug in testing. Stage vcruntime140_1.dll and msvcp140.dll next to python.exe at build time, from a vendored copy or the runner's System32, failing loudly if absent. The Inno Setup [Files] step already copies staging\python\* recursively. --- CHANGELOG.md | 1 + installers/windows/build.py | 42 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a370f14..afe0c2367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file. ## [0.2.5b2] - Unreleased ### Fixed +- **Windows installer: fresh install fails to start — "connection refused", nothing listening on port 8000 (#2474, reporter @fangme)** — On a clean Windows 10 machine, installing the `.exe` left the Bambuddy service showing "running" in services.msc but the dashboard refused every connection (localhost, IP, and hostname), and `netstat -ano` showed nothing on :8000. **Root cause — bottom of a 100k-line `service-stderr.log` traceback:** `init_db()` → SQLAlchemy async engine → `greenlet_spawn` → `ValueError: the greenlet library is required to use this function. DLL load failed while importing _greenlet: The specified module could not be found.` The 100k repeated `merged_lifespan` frames above it are just FastAPI's nested-lifespan stack unwinding — noise. The real failure: greenlet's `_greenlet.pyd` couldn't load, SQLAlchemy's async engine couldn't start, the FastAPI lifespan raised, and uvicorn **never bound the port** — so the supervised process stays up (NSSM sees it running) while the app itself has crashed on startup. **Why greenlet specifically:** the Win32 error 126 ("The specified module could not be found") on a `.pyd` that pip installed successfully means the module is present but a *dependency DLL* is missing. greenlet's extension is **C++** and needs `vcruntime140_1.dll` (table-based exception handling); the python.org **embeddable** distribution the installer bundles ships `vcruntime140.dll` but **not** `vcruntime140_1.dll`. `python313.dll` is pure C and only needs the former, which is exactly why python.exe starts and runs all the way to `init_db` before greenlet is the first thing to need the missing C++ runtime. On machines that already have the VC++ 2015-2022 redistributable installed (the maintainer's box, CI runners) `vcruntime140_1.dll` is in System32 and everything works — masking the bug until a truly fresh Win10 box hit it. **Fix:** the installer build now ships the C++ runtime app-locally — `vcruntime140_1.dll` and `msvcp140.dll` are staged next to `python.exe` (where `vcruntime140.dll` already lives), sourced from a vendored copy if present or the build runner's System32 otherwise, failing the build loudly if neither has them. `installers/windows/build.py` only; the Inno Setup `[Files]` step already copies `staging\python\*` recursively so the new DLLs are packaged automatically. **Workaround for the current build:** install the "Microsoft Visual C++ 2015-2022 Redistributable (x64)" from Microsoft, then restart the Bambuddy service — that puts `vcruntime140_1.dll` in System32 where the embedded Python finds it. - **Virtual Printer "bind interface" dropdown is empty on macOS** — Adding a Virtual Printer on macOS showed no interfaces to bind to. `get_network_interfaces()` only routed Windows to the cross-platform psutil path; macOS fell into the Linux branch, which uses the Linux-only `SIOCGIFADDR`/`SIOCGIFNETMASK` ioctls (`0x8915`/`0x891B`). macOS/BSD have `fcntl` but different ioctl numbers and sockaddr layout, so every per-interface ioctl raised `OSError` and the function silently returned an empty list (and `get_all_interface_ips()`, which has no `ip` binary to fall back to on macOS, inherited the empty result). Interface enumeration now routes **all** non-Linux platforms (macOS, BSD, Windows) through psutil, which returns each interface's name + IPv4 + netmask and filters loopback/link-local/down adapters while keeping real LAN and VPN (utun/Tailscale) interfaces bindable. Linux keeps its existing ioctl path unchanged. - **Native install script fails on macOS with Homebrew/venv permission errors** — On macOS the installer mixed root-only steps (defaulting to `/opt/bambuddy`, which nudged users into `sudo ./install.sh`) with steps that must **not** run as root: `brew install` hard-refuses to run as root (aborting the script mid-way), and any venv / `node_modules` created by root can't be managed by the launchd agent (which runs as the user), producing permission errors on `pip`/`npm`. The macOS path is now fully rootless: the script refuses to run under `sudo` on macOS with an actionable message, defaults the install directory to `~/bambuddy` (user-owned, no `/opt` write), and the download/venv/frontend/env/directory steps no longer shell out to `sudo` on macOS. A custom `--path` under a root-owned parent still works — the script elevates only to create+chown that one directory to the user, then continues rootless. Linux behaviour (service user + systemd) is unchanged. - **External camera "connection lost" when the snapshot URL serves a non-JPEG image (#1902)** — An external camera configured in HTTP-snapshot mode failed to load in Bambuddy with a repeating "connection lost", even though the camera URL rendered fine when opened directly in a browser. The log showed `Snapshot does not appear to be JPEG` on every polled frame followed by the stream ending. Root cause: `_capture_snapshot` returned the fetched bytes even when they weren't JPEG, and the MJPEG stream wraps every part with a hard-coded `Content-Type: image/jpeg` boundary — so a camera serving PNG/WebP/BMP stills (common on IP cameras and reverse-proxied snapshot endpoints) sent the browser a non-JPEG payload labelled as JPEG, which the browser rejected, tearing down the whole `multipart/x-mixed-replace` stream. `_capture_snapshot` now transcodes non-JPEG stills to JPEG (via OpenCV, already a dependency) before streaming; genuine JPEG snapshots keep their byte-for-byte fast path, and truly undecodable responses (HTML error pages, auth redirects) fall back to the previous raw-return behaviour with a clearer one-off warning instead of a per-frame log flood. Fixes browser playback and keeps the JPEG-only downstream (plate detection, Obico, finish photo) working for these cameras. diff --git a/installers/windows/build.py b/installers/windows/build.py index 0167b9aea..fb91c2538 100644 --- a/installers/windows/build.py +++ b/installers/windows/build.py @@ -61,6 +61,19 @@ FFMPEG_URL = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffm # get-pip.py for bootstrapping pip into the embedded distribution GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" +# C++ runtime DLLs the embeddable distribution does NOT ship. The python.org +# embeddable zip includes vcruntime140.dll but not vcruntime140_1.dll or +# msvcp140.dll. python313.dll is pure C and only needs vcruntime140.dll, so +# python.exe starts fine — but greenlet's _greenlet.pyd is C++ and needs +# vcruntime140_1.dll (table-based exception handling). On a fresh Windows box +# that never had the VC++ 2015-2022 redistributable installed, loading greenlet +# fails with "DLL load failed ... The specified module could not be found", +# SQLAlchemy's async engine can't start, init_db() raises, and the app never +# binds its port — the service shows "running" but the dashboard refuses the +# connection (issue #2474). These runtime DLLs are redistributable, so we ship +# them app-locally next to python.exe (where vcruntime140.dll already lives). +VCRUNTIME_DLLS = ("vcruntime140_1.dll", "msvcp140.dll") + def log(msg: str) -> None: print(f"[build] {msg}", flush=True) @@ -143,6 +156,34 @@ def stage_embedded_python() -> Path: return target +def stage_vcruntime(python_dir: Path) -> None: + """Ship the C++ runtime DLLs the embeddable distribution omits. + + Placed next to python.exe so the extension-module loader (which searches the + interpreter's own directory) finds them without a redistributable install on + the target machine. Prefers vendored copies under installers/windows/vendor/ + for reproducibility; falls back to the build runner's System32, where the + redistributable runtime lives. Fails loudly if neither source has them, so a + misconfigured build machine is caught here instead of by end users. + """ + vendor = INSTALLER_DIR / "vendor" + system32 = Path(os.environ.get("SYSTEMROOT", r"C:\Windows")) / "System32" + for dll in VCRUNTIME_DLLS: + dst = python_dir / dll + if dst.exists(): + log(f"{dll} already present in embedded Python") + continue + src = vendor / dll if (vendor / dll).exists() else system32 / dll + if not src.exists(): + raise RuntimeError( + f"required C++ runtime DLL not found: looked in {vendor} and {system32} " + f"for {dll}. Install the Microsoft Visual C++ 2015-2022 Redistributable " + f"(x64) on the build machine, or vendor {dll} under installers/windows/vendor/." + ) + log(f"staging {dll} from {src}") + shutil.copy(src, dst) + + def install_requirements(python_dir: Path) -> None: """Install Bambuddy's requirements.txt into the embedded Python.""" py = python_dir / "python.exe" @@ -370,6 +411,7 @@ def main() -> int: STAGING.mkdir(parents=True, exist_ok=True) python_dir = stage_embedded_python() + stage_vcruntime(python_dir) if not args.skip_pip: install_requirements(python_dir)