wsgi.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. """Composition root and server entrypoint.
  2. This is the ONLY module that constructs the store/queue/prefs and starts the worker
  3. thread. `python wsgi.py` runs the app. Tests import `app.create_app` directly and never
  4. touch this file, so importing the app never reads the real config or starts a thread.
  5. Deployment invariant: run as a SINGLE process. The worker thread and the SQLite
  6. connections live in-process; multiple processes would run competing queue workers and an
  7. in-process config lock that doesn't coordinate file writes. Do not add web-server workers.
  8. """
  9. from __future__ import annotations
  10. import logging
  11. import os
  12. import sys
  13. from logging.handlers import RotatingFileHandler
  14. from app import create_app
  15. from config_store import ConfigStore
  16. from prefs import PrefStore
  17. from worker import JobQueue
  18. _PLACEHOLDER_SECRET = "change-me-to-a-random-string"
  19. def _configure_logging(app_cfg) -> None:
  20. level = getattr(logging, str(app_cfg.get("log_level", "INFO")).upper(), logging.INFO)
  21. root = logging.getLogger()
  22. root.setLevel(level)
  23. fmt = logging.Formatter("%(asctime)s %(levelname)-7s %(name)s: %(message)s")
  24. sh = logging.StreamHandler()
  25. sh.setFormatter(fmt)
  26. root.addHandler(sh)
  27. # App log to a rotating file too, so admin errors are viewable in the panel.
  28. fh = RotatingFileHandler(
  29. app_cfg.get("app_log", "app.log"), maxBytes=5_000_000, backupCount=5, encoding="utf-8"
  30. )
  31. fh.setFormatter(fmt)
  32. root.addHandler(fh)
  33. def build():
  34. store = ConfigStore()
  35. snap = store.snapshot()
  36. _configure_logging(snap.app)
  37. log = logging.getLogger("pps.wsgi")
  38. secret = snap.app.get("secret_key", "")
  39. if secret == _PLACEHOLDER_SECRET or len(secret) < 32:
  40. raise SystemExit(
  41. "app.secret_key is missing/weak. Generate one with:\n"
  42. " python -c \"import secrets; print(secrets.token_urlsafe(48))\""
  43. )
  44. mode = snap.auth.get("mode", "static")
  45. if mode == "static":
  46. log.warning("=" * 72)
  47. log.warning("AUTH MODE = static — anyone with the shared password is an ADMIN.")
  48. log.warning('This is a DEVELOPMENT mode. Set [auth] mode = "oidc" for production.')
  49. log.warning("=" * 72)
  50. listen = snap.app.get("listen", "127.0.0.1")
  51. if listen not in ("127.0.0.1", "localhost", "::1") and not os.environ.get(
  52. "PPSQ_ALLOW_INSECURE_AUTH"
  53. ):
  54. raise SystemExit(
  55. f"Refusing to serve static auth on a non-loopback interface ({listen}). "
  56. 'Use [auth] mode = "oidc", or set PPSQ_ALLOW_INSECURE_AUTH=1 for dev.'
  57. )
  58. db_path = snap.app.get("db_path", "jobs.db")
  59. queue = JobQueue(
  60. db_path,
  61. pps_provider=store.pps,
  62. cfg_provider=lambda: store.snapshot().quarantine,
  63. ops_log_path=snap.app.get("worker_log", "worker.log"),
  64. )
  65. prefs = PrefStore(db_path)
  66. app = create_app(store, queue, prefs)
  67. return app, queue
  68. def main() -> int:
  69. from waitress import serve
  70. app, queue = build()
  71. queue.start()
  72. store = app.config["STORE"]
  73. snap = store.snapshot()
  74. host = snap.app.get("listen", "127.0.0.1")
  75. port = int(snap.app.get("port", 8080))
  76. logging.getLogger("pps.wsgi").info(
  77. "PPS Quarantine Manager listening on http://%s:%s", host, port
  78. )
  79. serve(app, host=host, port=port, threads=8)
  80. return 0
  81. if __name__ == "__main__":
  82. sys.exit(main())