Troubleshooting¶
build:backend fails with ModuleNotFoundError: No module named 'fastapi'¶
PyInstaller is running against the wrong Python environment — almost
certainly anaconda's. Make sure backend-api/venv/ exists and was created
with:
cd backend-api
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt pyinstaller
scripts/build-backend.mjs auto-prefers backend-api/venv/bin/python when
present. If you see the warning "using bare pyinstaller from PATH" in
the build output, the venv wasn't found.
build:backend fails with BUNDLE IMPORT SELF-TEST FAILED¶
The bundle built, but the packaged binary can't import a module the app needs at runtime. The failing module is named in the output:
[selftest] FAIL sklearn.mixture: ModuleNotFoundError: No module named 'sklearn.mixture'
[selftest] 13/14 imports OK
PyInstaller only bundles what its static analysis can see, so a
function-local import inside a rarely-exercised endpoint gets dropped
silently. Fix it by declaring the module in scripts/build-backend.mjs:
hiddenImports— pure-Python modules with no data files.collectAll— packages with native extensions, data files, or submodules PyInstaller doesn't trace. Note that--collect-allgathers a package's own files but not its external dependencies; those need their ownhiddenImportsentries.
Then re-run npm run build:backend. Don't ship a bundle that fails this
check — it builds and launches fine, and only dies when a user reaches the
affected feature.
This check exists because that exact bug shipped: sklearn.mixture was
missing from the bundle, so wood/leaf segmentation raised
ModuleNotFoundError in the packaged app while dev and pytest — which run
against backend-api/venv, where every module is present — passed.
build:backend fails with No such file or directory: …/venv/bin/python3¶
The venv has stale shebangs (usually from being copied or its parent directory being renamed). Delete and recreate:
cd backend-api && rm -rf venv && python3 -m venv venv && \
source venv/bin/activate && pip install -r requirements.txt pyinstaller
App launches but shows red "Backend failed to start" banner¶
In dev:
- If
backend-api/venvexists,npm run devruns uvicorn itself — check the[dev]lines in the terminal for a uvicorn startup failure. - Without a venv, the supervisor falls back to the bundled sidecar, so confirm
resources/phytograph_backend/exists (if not, runnpm run build:backend).
In packaged build: open macOS Console.app or Windows Event Viewer and
search for [Backend stderr]: lines from the supervisor.
Not every stderr line is an error
Python's logging writes to stderr by default, so INFO and WARNING
messages land there too. Read the message before assuming a failure.
Common causes:
- Quarantine bit on a fresh macOS install:
- A Windows application control policy refusing a bundled library — the log
shows
ImportError: DLL load failed while importing …: An Application Control policy has blocked this file. Restarting cannot fix it; see Native libraries are signed separately. - First-launch cold start (~30s with onedir, longer on slower disks)
- An orphaned backend from a previous session (it won't block the new instance, which picks its own port, but it does consume memory):
"Cannot remove quarantine" on macOS¶
The signed app's CodeSignature seal makes xattrs immutable. Either install via Finder drag (which clears quarantine on first launch via Gatekeeper approval), or use a signed+notarized CI build where this never comes up.
Renderer can't reach the backend¶
The renderer does not hardcode the port. initBackendUrl() in
src/renderer/utils/backendApi.ts fetches the real port from the main process
over the backend.getInfo IPC before the first render, and getBackendUrl()
returns that cached value. If requests are going to the wrong place, the
resolution step is what to inspect — don't edit getBackendUrl(). To pin a
specific port for both ends, set PHYTOGRAPH_BACKEND_PORT.
A tool fails with "… timed out. The backend did not respond within …"¶
Every request the renderer makes carries a client-side deadline (2 min for an
export, 5 min for a session op or a plant generate, 10 min for an import). When
one expires, abortOnTimeout() in src/renderer/utils/backendApi.ts aborts the
fetch with a BackendTimeoutError naming the endpoint and the budget, and that
message is what the toast shows.
The backend logs any request over 5 s, so start there:
Lower the threshold with PHYTOGRAPH_SLOW_REQUEST_SECONDS (in dev the line goes
to the npm run dev terminal alongside uvicorn's access log). If the request
isn't in the log at all it never arrived; if it's there and never finished, the
operation is stuck — restart the backend.
The trailing in-flight count should be uncorrelated with slowness. It wasn't
always: every handler used to be async def doing blocking work inline on the
single event loop, so one long operation starved every other request behind it —
a 7 M-point LAZ export (≈1–2 s of real work) died on its 2-minute deadline while
60 ms preview calls timed out at 60 s. Blocking handlers are now def and run in
the worker threadpool. If you see slow requests correlating with a high in-flight
count again, something has regressed — see
Concurrency model.
A user cancel is deliberately different: it aborts with no reason, stays a
plain AbortError, and callers swallow it silently. Don't collapse the two, or
a real timeout goes back to being invisible.
No suitable compiler found (gcc or clang) when building PyHelios¶
Almost always means xcode-select points at a broken Xcode, not that a
compiler is missing. A macOS update can leave stale Xcode support frameworks in
/Library/Developer/PrivateFrameworks/, after which the active developer
directory's xcrun/xcodebuild shim fails to load:
Symbol not found: _XPCTypeBool
Referenced from: /Library/Developer/PrivateFrameworks/CoreDevice.framework
Expected in: /Library/Apple/System/Library/PrivateFrameworks/Mercury.framework
/usr/bin/cc, /usr/bin/clang and /usr/bin/c++ are all shims through that
directory, so every one of them fails. PyHelios's build script probes
gcc --version then clang --version, sees both fail, and reports the missing
compiler — which is why the message is misleading.
Diagnose it in under a second:
Two symptoms confirm this specific failure:
- C compiles, C++ does not. With no working shim, clang resolves no sysroot
and therefore searches no SDK. The libc++ headers live only in the SDK, so
the build gets as far as ~78% (freetype, glfw, glew, libjpeg are all C) and
dies on the first C++ file with
fatal error: 'set' file not found. - The Command Line Tools installer keeps reopening. When the broken shim
can't resolve a tool it calls
xcode-select: Failed to locate 'clang++', requesting installation of command line developer tools. Every failed compile re-triggers it.
Reinstalling the Command Line Tools cannot fix this — the pointer is wrong, not the payload. Repoint it instead:
The Command Line Tools alone are sufficient to build Phytograph; a working Xcode
is not required. To repair Xcode itself, launch Xcode.app once so it reinstalls
its components.
Builds are not blocked while the machine is in this state:
scripts/build-pyhelios.mjs compiles a C++ sentinel before starting, and if the
active directory is unusable but the Command Line Tools work, it builds with
DEVELOPER_DIR pointed at them and prints a warning every build until you run
the command above. That override is per-process and changes nothing on the
system.
Don't trust pkgutil or the installer to confirm a healthy toolchain here — both
reported success throughout an outage where nothing could compile C++. Only
compiling proves it:
Plant generation / Helios features fail in dev only¶
PyHelios is built from the source submodule, not a pip wheel. Its native lib
(libhelios.dylib) lives at pyhelios/pyhelios_build/build/lib/ and must be
importable from the active Python env. If you replaced or recreated the venv,
or the submodule isn't initialized, rebuild from source:
git submodule update --init --recursive # if pyhelios/ is empty
source backend-api/venv/bin/activate
node scripts/build-pyhelios.mjs # compiles libhelios + editable install
The backend also auto-rebuilds libhelios on startup when the C++ source is
newer than the compiled lib, so a stale lib usually fixes itself on the next
backend restart. A clean rebuild: node scripts/build-pyhelios.mjs --clean.
"Cloud session not found … Re-import the cloud and try again"¶
A tool (export, triangulate, LAD, spacing check) refuses to run and names a
session id. The backend restarted since that cloud was imported — in dev, the
usual cause is uvicorn's --reload firing on a backend-api/*.py edit, which
replaces the process and drops every in-memory CloudSession. The renderer
still holds the old session id, and the on-disk octree keeps rendering, so
nothing looks wrong until a tool needs the actual points.
Fix: re-import the cloud.
Importing a 9th cloud is no longer a cause of this
It used to be. Only PHYTOGRAPH_MAX_CLOUD_SESSIONS (default 8) sessions are
held in RAM, and an evicted one used to be gone — so on a scene of twenty
scans, everything but the eight most recent answered this error. Eviction now
spills the session to disk and reads it back on demand
(Session eviction), so the
remaining causes are a backend restart, an explicit delete, a spill that
could not be written (disk full — the log says Could not spill cloud
session) or read back (corrupt file; Could not restore cloud session),
or one trimmed away under PHYTOGRAPH_SESSION_SPILL_MAX_BYTES. The first
use of a paged-out cloud is slower by one disk read.
This is deliberately a hard error rather than a fall back to the file the cloud was imported from. Once a file is imported, Phytograph treats it as if it no longer exists. The session arrays are the complete source of truth: they carry every edit (deletions, translation, filtering, segmentation labels) and every choice made in the import wizard (column roles, dropped columns, global shift, ASCII layout). Re-reading the file would not restore the cloud — it would substitute a different one and report success, which is worse than an error.
Some source paths are not readable point clouds at all. A RIEGL project "source
path" is the project directory, kept only as provenance; the older
fall-through surfaced it as the misleading Scan file not found:
/…/2018-02-23.002.riproject when the real problem was the dropped session.
The allow_file_source flag on PointSource / HeliosScanEntry /
ScanExportEntry is the deliberate opt-in for files that are genuinely not
live clouds (unit tests driving the loaders, external files). The app never
sets it.
Stale backend processes¶
Since ports are resolved per instance, a leftover backend no longer blocks the
next launch — it lands on a different free port. (The supervisor only kills a
process on its own resolved port, and only when /version mismatches, so a
running dev backend is never killed by a test run or a second instance.)
To clean up orphans:
To see what holds a specific port: lsof -ti :<port>.