bharatgraph / packaging /desktop_entry.py
abinazebinoy's picture
fix: chdir into app-data dir at desktop startup, fixing 30+ writes at once (P-1)
45cee30
Raw History Blame Contribute Delete
4.73 kB
"""
BharatGraph Desktop -- PyInstaller entry point (Phase D2)
Why this file exists, separately from app.py: the Docker deployment starts
the server via `uvicorn app:app`, a CLI command that resolves the string
"app:app" to a module and attribute at runtime using Python's normal import
machinery. A PyInstaller-frozen executable does not have that machinery
available the same way -- there is no `app.py` file on disk to resolve a
string against once everything is bundled into a single binary. The
standard, reliable fix (used by PyInstaller's own documentation for
freezing ASGI apps) is to import the app object directly in Python code and
call uvicorn.run() programmatically instead of via its CLI string-lookup
path. This file is that direct entry point.
Sets BHARATGRAPH_DESKTOP_MODE=true before importing anything else, so the
Phase D1 admin-auth hardening in api/routes/admin.py (auto-generated,
persisted per-install secret; no DEBUG_MODE bypass path) applies correctly
whenever the app is started this way, and so config/settings.py's local
config-file loading is the primary configuration path a desktop user
actually has, rather than a code path that only matters at packaging time.
P-1 fix: also chdir()s into the OS-appropriate app-data directory before
any other import. Grepped every bare (non-__file__-anchored) CWD-relative
path in the codebase: 30+ real call sites across all 19 scrapers'
data/samples/*.json sample-fallback output, processing/pipeline.py,
ai/multi_investigator.py, ai/risk_scorer.py, ai/self_learning/
schema_learner.py, processing/alias_graph.py, and graph/loader.py's own
glob() read of pipeline output -- none of which reference the (equally
dead) DATA_RAW_PATH/DATA_PROCESSED_PATH/DATA_SAMPLES_PATH constants in
config/settings.py; each just hardcodes its own "data/processed/..." or
"data/samples/..." string directly. On a real Windows install, a
double-clicked app's CWD defaults to (and the install directory itself is
typically read-only for) a non-admin user -- meaning every one of those
30+ writes would fail outright, not just lose regeneratable cache data.
Verified every one of them is genuinely CWD-relative (confirmed via
direct testing that python-dotenv's own load_dotenv() calls elsewhere in
the codebase are NOT affected by this, since dotenv resolves relative to
the calling file's own frame, not CWD) -- so a single chdir() here fixes
all 30+ call sites identically, with zero changes needed to any of them,
rather than repeating the explicit per-file redirection this branch's
sibling commits used for case_memory.json/logs/dossier output (which
were __file__-anchored, not CWD-relative, and so could NOT have been
fixed this way). Web-deployment behavior is unchanged -- this file is
never imported by the Docker/app.py path.
Binds to 127.0.0.1 only, matching the target architecture ("bound to
127.0.0.1 only") -- this process is meant to be reached only by the Tauri
shell's own webview (Phase D4), never by the network.
"""
import os
import sys
# Must happen before importing api.main (or anything that transitively
# imports config.settings), so the desktop-mode admin-auth branch and the
# local config-file loading are both active from the very first import.
os.environ.setdefault("BHARATGRAPH_DESKTOP_MODE", "true")
# PyInstaller sets sys.frozen and sys._MEIPASS when running from a frozen
# bundle; ensure the app's own package root is importable either way, so
# this entry point works identically when run as a plain script (for local
# testing, exactly as this was verified) and when frozen.
if getattr(sys, "frozen", False):
_app_root = sys._MEIPASS # type: ignore[attr-defined]
else:
_app_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, _app_root)
# P-1 fix: chdir into a guaranteed-writable directory before any other
# import that might (transitively, later, at request time) write a bare
# CWD-relative path. Must come after sys.path is set up (needs to import
# config.settings) but before uvicorn/api.main are imported.
from config.settings import get_app_data_dir
_workdir = get_app_data_dir()
os.makedirs(_workdir, exist_ok=True)
os.chdir(_workdir)
import uvicorn
from api.main import app
def main() -> None:
port = int(os.getenv("BHARATGRAPH_DESKTOP_PORT", "8000"))
uvicorn.run(
app,
host="127.0.0.1",
port=port,
log_level=os.getenv("BHARATGRAPH_LOG_LEVEL", "info"),
# Single worker: matches the existing Docker deployment's own
# --workers 1 (api/main.py's docstring/Dockerfile), and a desktop
# install is single-user by definition -- no reason to diverge.
workers=1,
)
if __name__ == "__main__":
main()