| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- """Composition root and server entrypoint.
- This is the ONLY module that constructs the store/queue/prefs and starts the worker
- thread. `python wsgi.py` runs the app. Tests import `app.create_app` directly and never
- touch this file, so importing the app never reads the real config or starts a thread.
- Deployment invariant: run as a SINGLE process. The worker thread and the SQLite
- connections live in-process; multiple processes would run competing queue workers and an
- in-process config lock that doesn't coordinate file writes. Do not add web-server workers.
- """
- from __future__ import annotations
- import logging
- import os
- import sys
- from logging.handlers import RotatingFileHandler
- from app import create_app
- from config_store import ConfigStore
- from prefs import PrefStore
- from worker import JobQueue
- _PLACEHOLDER_SECRET = "change-me-to-a-random-string"
- def _configure_logging(app_cfg) -> None:
- level = getattr(logging, str(app_cfg.get("log_level", "INFO")).upper(), logging.INFO)
- root = logging.getLogger()
- root.setLevel(level)
- fmt = logging.Formatter("%(asctime)s %(levelname)-7s %(name)s: %(message)s")
- sh = logging.StreamHandler()
- sh.setFormatter(fmt)
- root.addHandler(sh)
- # App log to a rotating file too, so admin errors are viewable in the panel.
- fh = RotatingFileHandler(
- app_cfg.get("app_log", "app.log"), maxBytes=5_000_000, backupCount=5, encoding="utf-8"
- )
- fh.setFormatter(fmt)
- root.addHandler(fh)
- def build():
- store = ConfigStore()
- snap = store.snapshot()
- _configure_logging(snap.app)
- log = logging.getLogger("pps.wsgi")
- secret = snap.app.get("secret_key", "")
- if secret == _PLACEHOLDER_SECRET or len(secret) < 32:
- raise SystemExit(
- "app.secret_key is missing/weak. Generate one with:\n"
- " python -c \"import secrets; print(secrets.token_urlsafe(48))\""
- )
- mode = snap.auth.get("mode", "static")
- if mode == "static":
- log.warning("=" * 72)
- log.warning("AUTH MODE = static — anyone with the shared password is an ADMIN.")
- log.warning('This is a DEVELOPMENT mode. Set [auth] mode = "oidc" for production.')
- log.warning("=" * 72)
- listen = snap.app.get("listen", "127.0.0.1")
- if listen not in ("127.0.0.1", "localhost", "::1") and not os.environ.get(
- "PPSQ_ALLOW_INSECURE_AUTH"
- ):
- raise SystemExit(
- f"Refusing to serve static auth on a non-loopback interface ({listen}). "
- 'Use [auth] mode = "oidc", or set PPSQ_ALLOW_INSECURE_AUTH=1 for dev.'
- )
- db_path = snap.app.get("db_path", "jobs.db")
- queue = JobQueue(
- db_path,
- pps_provider=store.pps,
- cfg_provider=lambda: store.snapshot().quarantine,
- ops_log_path=snap.app.get("worker_log", "worker.log"),
- )
- prefs = PrefStore(db_path)
- app = create_app(store, queue, prefs)
- return app, queue
- def main() -> int:
- from waitress import serve
- app, queue = build()
- queue.start()
- store = app.config["STORE"]
- snap = store.snapshot()
- host = snap.app.get("listen", "127.0.0.1")
- port = int(snap.app.get("port", 8080))
- logging.getLogger("pps.wsgi").info(
- "PPS Quarantine Manager listening on http://%s:%s", host, port
- )
- serve(app, host=host, port=port, threads=8)
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|