main.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. """zhuce6 unified entrypoint."""
  2. from __future__ import annotations
  3. import argparse
  4. from contextlib import asynccontextmanager
  5. from dataclasses import replace
  6. import json
  7. import os
  8. from pathlib import Path
  9. import subprocess
  10. import sys
  11. import threading
  12. import time
  13. from typing import Any
  14. from core.env_loader import bootstrap_env, load_env_file as _load_env_file
  15. bootstrap_env(Path(__file__).resolve().parent)
  16. try:
  17. from fastapi import FastAPI, HTTPException, Request, Response
  18. from fastapi.responses import HTMLResponse
  19. from pydantic import BaseModel, Field
  20. import uvicorn
  21. WEB_RUNTIME_IMPORT_ERROR: ModuleNotFoundError | None = None
  22. except ModuleNotFoundError as exc:
  23. WEB_RUNTIME_IMPORT_ERROR = exc
  24. FastAPI = Any # type: ignore[assignment]
  25. Request = Any # type: ignore[assignment]
  26. Response = Any # type: ignore[assignment]
  27. HTMLResponse = Any # type: ignore[assignment]
  28. class HTTPException(Exception):
  29. def __init__(self, status_code: int, detail: str = "") -> None:
  30. super().__init__(detail)
  31. self.status_code = status_code
  32. self.detail = detail
  33. class BaseModel: # type: ignore[no-redef]
  34. pass
  35. def Field(*, default=None, alias=None): # type: ignore[no-redef]
  36. return default
  37. uvicorn = None # type: ignore[assignment]
  38. try:
  39. from core import process_manager
  40. from core.chatgpt_flow_runner import (
  41. run_chatgpt_callback_exchange,
  42. run_chatgpt_preflight,
  43. run_chatgpt_register_once,
  44. )
  45. from core.doctor import collect_doctor_report, format_doctor_report
  46. from core.paths import DEFAULT_DASHBOARD_LOG_FILE
  47. from core.registration import RegistrationBurstScheduler, RegistrationLoop
  48. from core.registry import list_platforms, load_all
  49. from core.settings import AppSettings
  50. from core.setup_wizard import run_setup_wizard
  51. from dashboard.api import (
  52. _account_survival_payload,
  53. _build_background_tasks,
  54. _cpa_dependency_payload,
  55. _cfmail_dependency_payload,
  56. _count_cpa_files,
  57. _count_today_new,
  58. _fetch_management_auth_files,
  59. _parse_settings_patch,
  60. _persist_env_updates,
  61. _proxy_pool_dependency_payload,
  62. _recent_pool_files,
  63. _runtime_payload,
  64. _settings_payload,
  65. _sub2api_dependency_payload,
  66. _summary_payload,
  67. )
  68. from ops.rotate_log import _rotate_log_tail
  69. APP_IMPORT_ERROR: ModuleNotFoundError | None = None
  70. except ModuleNotFoundError as exc:
  71. process_manager = Any # type: ignore[assignment]
  72. run_chatgpt_callback_exchange = Any # type: ignore[assignment]
  73. run_chatgpt_preflight = Any # type: ignore[assignment]
  74. run_chatgpt_register_once = Any # type: ignore[assignment]
  75. collect_doctor_report = Any # type: ignore[assignment]
  76. format_doctor_report = Any # type: ignore[assignment]
  77. DEFAULT_DASHBOARD_LOG_FILE = Path("dashboard.log")
  78. RegistrationBurstScheduler = Any # type: ignore[assignment]
  79. RegistrationLoop = Any # type: ignore[assignment]
  80. list_platforms = Any # type: ignore[assignment]
  81. load_all = Any # type: ignore[assignment]
  82. AppSettings = Any # type: ignore[assignment]
  83. run_setup_wizard = Any # type: ignore[assignment]
  84. _account_survival_payload = Any # type: ignore[assignment]
  85. _build_background_tasks = Any # type: ignore[assignment]
  86. _cpa_dependency_payload = Any # type: ignore[assignment]
  87. _cfmail_dependency_payload = Any # type: ignore[assignment]
  88. _count_cpa_files = Any # type: ignore[assignment]
  89. _count_today_new = Any # type: ignore[assignment]
  90. _fetch_management_auth_files = Any # type: ignore[assignment]
  91. _parse_settings_patch = Any # type: ignore[assignment]
  92. _persist_env_updates = Any # type: ignore[assignment]
  93. _proxy_pool_dependency_payload = Any # type: ignore[assignment]
  94. _recent_pool_files = Any # type: ignore[assignment]
  95. _runtime_payload = Any # type: ignore[assignment]
  96. _settings_payload = Any # type: ignore[assignment]
  97. _sub2api_dependency_payload = Any # type: ignore[assignment]
  98. _summary_payload = Any # type: ignore[assignment]
  99. _rotate_log_tail = Any # type: ignore[assignment]
  100. APP_IMPORT_ERROR = exc
  101. DASHBOARD_MODES = {"full", "dashboard", "lite"}
  102. WORKER_MODES = {"register-loop", "burst-scheduler"}
  103. ALL_RUNTIME_MODES = DASHBOARD_MODES | WORKER_MODES
  104. DASHBOARD_CORS_PATHS = frozenset({"/api/runtime", "/api/summary"})
  105. class _ValidateOpsProxy:
  106. def __getattr__(self, name: str) -> object:
  107. from ops import validate as validate_module
  108. return getattr(validate_module, name)
  109. validate_ops = _ValidateOpsProxy()
  110. def classify_token_file(*args, **kwargs): # type: ignore[no-untyped-def]
  111. from ops.scan import classify_token_file as _classify_token_file
  112. return _classify_token_file(*args, **kwargs)
  113. def _ensure_web_runtime_available() -> None:
  114. if WEB_RUNTIME_IMPORT_ERROR is not None or uvicorn is None:
  115. raise ModuleNotFoundError(
  116. "Web runtime dependencies are unavailable. Run `uv sync` or use `uv run python main.py ...`."
  117. ) from WEB_RUNTIME_IMPORT_ERROR
  118. def _ensure_app_dependencies_available() -> None:
  119. if APP_IMPORT_ERROR is not None:
  120. raise ModuleNotFoundError(
  121. f"Missing dependency: {APP_IMPORT_ERROR.name or APP_IMPORT_ERROR}. Run `uv sync` first."
  122. ) from APP_IMPORT_ERROR
  123. def _handle_missing_dependency_import() -> None:
  124. exc = APP_IMPORT_ERROR or WEB_RUNTIME_IMPORT_ERROR
  125. missing = exc.name if isinstance(exc, ModuleNotFoundError) else "dependency"
  126. print(
  127. "缺少运行依赖, 当前命令无法继续.\n"
  128. f"missing: {missing}\n"
  129. "请先执行: uv sync",
  130. file=sys.stderr,
  131. )
  132. raise SystemExit(1)
  133. def _run_uv_sync() -> bool:
  134. result = subprocess.run(
  135. ["uv", "sync"],
  136. cwd=Path(__file__).resolve().parent,
  137. check=False,
  138. text=True,
  139. )
  140. return result.returncode == 0
  141. class ChatGPTPreflightRequest(BaseModel):
  142. email: str | None = None
  143. password: str | None = None
  144. proxy: str | None = None
  145. mail_provider: str = Field(default="cfmail")
  146. class ChatGPTCallbackExchangeRequest(BaseModel):
  147. callback_url: str
  148. expected_state: str = Field(alias="state")
  149. code_verifier: str
  150. proxy: str | None = None
  151. write_pool: bool = True
  152. class ChatGPTRegisterRequest(BaseModel):
  153. email: str | None = None
  154. password: str | None = None
  155. proxy: str | None = None
  156. mail_provider: str = Field(default="cfmail")
  157. write_pool: bool = True
  158. class RegisterControlRequest(BaseModel):
  159. action: str
  160. def _apply_runtime_mode(settings: AppSettings, mode: str) -> AppSettings:
  161. normalized = mode if mode in ALL_RUNTIME_MODES else "full"
  162. updated = replace(settings, runtime_mode=normalized)
  163. if normalized == "dashboard":
  164. updated = replace(updated, register_enabled=False)
  165. elif normalized in {"full", "lite", "register-loop", "burst-scheduler"}:
  166. updated = replace(updated, register_enabled=True)
  167. if normalized == "lite":
  168. updated = replace(
  169. updated,
  170. cleanup_enabled=False,
  171. d1_cleanup_enabled=False,
  172. validate_enabled=False,
  173. rotate_enabled=False,
  174. account_survival_enabled=False,
  175. )
  176. elif normalized == "full" and updated.register_enabled and str(updated.backend or "").strip().lower() == "cpa":
  177. updated = replace(updated, account_survival_enabled=True)
  178. return updated
  179. def _dashboard_cors_headers(origin: str) -> dict[str, str]:
  180. return {
  181. "Access-Control-Allow-Origin": origin,
  182. "Access-Control-Allow-Methods": "GET, OPTIONS",
  183. "Access-Control-Allow-Headers": "Accept, Content-Type",
  184. "Access-Control-Max-Age": "600",
  185. "Vary": "Origin",
  186. }
  187. def create_app(enable_background_tasks: bool = True, mode: str = "full") -> FastAPI:
  188. _ensure_app_dependencies_available()
  189. _ensure_web_runtime_available()
  190. settings = _apply_runtime_mode(AppSettings.from_env(), mode)
  191. @asynccontextmanager
  192. async def lifespan(app: FastAPI):
  193. load_all()
  194. app.state.settings = settings
  195. app.state.dashboard_overview_cache = None
  196. app.state.background_tasks = _build_background_tasks(settings) if enable_background_tasks else []
  197. app.state.registration_loop = None
  198. for task in app.state.background_tasks:
  199. task.start()
  200. if settings.register_enabled and settings.runtime_mode in {"full", "lite"}:
  201. reg_loop = RegistrationLoop(settings)
  202. reg_loop.start()
  203. app.state.registration_loop = reg_loop
  204. try:
  205. yield
  206. finally:
  207. if app.state.registration_loop:
  208. app.state.registration_loop.stop()
  209. for task in app.state.background_tasks:
  210. task.stop()
  211. app = FastAPI(title="zhuce6", version="0.1.0", lifespan=lifespan)
  212. @app.middleware("http")
  213. async def dashboard_cors_middleware(request: Request, call_next): # type: ignore[no-untyped-def]
  214. origin = request.headers.get("origin", "").strip().rstrip("/")
  215. allowed_origins = {
  216. str(item or "").strip().rstrip("/")
  217. for item in getattr(settings, "dashboard_allowed_origins", ())
  218. if str(item or "").strip()
  219. }
  220. if request.url.path not in DASHBOARD_CORS_PATHS or not origin or origin not in allowed_origins:
  221. return await call_next(request)
  222. if request.method == "OPTIONS":
  223. return Response(status_code=204, headers=_dashboard_cors_headers(origin))
  224. response = await call_next(request)
  225. for key, value in _dashboard_cors_headers(origin).items():
  226. response.headers[key] = value
  227. return response
  228. @app.get("/healthz")
  229. def healthz() -> dict[str, str]:
  230. return {"status": "ok"}
  231. @app.get("/api/platforms")
  232. def api_platforms() -> list[dict[str, str]]:
  233. return list_platforms()
  234. @app.get("/api/runtime")
  235. def api_runtime() -> dict[str, object]:
  236. return _runtime_payload(app)
  237. @app.get("/api/summary")
  238. def api_summary() -> dict[str, object]:
  239. return _summary_payload(app)
  240. @app.get("/api/account-survival")
  241. def api_account_survival() -> dict[str, object]:
  242. return _account_survival_payload(app.state.settings)
  243. @app.get("/api/settings")
  244. def api_settings() -> dict[str, object]:
  245. return _settings_payload(app)
  246. @app.put("/api/settings")
  247. async def api_settings_update(request: Request) -> dict[str, object]:
  248. payload = await request.json()
  249. if not isinstance(payload, dict):
  250. raise HTTPException(status_code=400, detail="settings patch must be an object")
  251. updates, env_updates = _parse_settings_patch(payload)
  252. app.state.settings = replace(app.state.settings, **updates)
  253. app.state.dashboard_overview_cache = None
  254. env_file_path = Path(
  255. str(os.getenv("ZHUCE6_ENV_FILE", str(app.state.settings.env_file))).strip()
  256. or str(app.state.settings.env_file)
  257. ).expanduser().resolve()
  258. _persist_env_updates(env_file_path, env_updates)
  259. app.state.settings = replace(app.state.settings, env_file=env_file_path)
  260. response = _settings_payload(app)
  261. response["restart_required"] = bool(env_updates)
  262. return response
  263. @app.post("/api/control/register")
  264. async def api_control_register(request: RegisterControlRequest) -> dict[str, object]:
  265. action = str(request.action or "").strip().lower()
  266. if action not in {"start", "stop", "restart"}:
  267. raise HTTPException(status_code=400, detail=f"unsupported action: {action}")
  268. active_loop = getattr(app.state, "registration_loop", None)
  269. if action in {"stop", "restart"} and active_loop is not None:
  270. active_loop.stop()
  271. app.state.registration_loop = None
  272. app.state.settings = replace(app.state.settings, register_enabled=False)
  273. if action in {"start", "restart"}:
  274. next_settings = replace(app.state.settings, register_enabled=True)
  275. reg_loop = RegistrationLoop(next_settings)
  276. reg_loop.start()
  277. app.state.settings = next_settings
  278. app.state.registration_loop = reg_loop
  279. status_map = {"start": "started", "stop": "stopped", "restart": "restarted"}
  280. return {"status": status_map[action], "register_enabled": bool(app.state.registration_loop is not None)}
  281. @app.get("/api/health/dependencies")
  282. def api_health_dependencies() -> dict[str, object]:
  283. settings = app.state.settings
  284. return {
  285. "cfmail": _cfmail_dependency_payload(settings),
  286. "proxy_pool": _proxy_pool_dependency_payload(app),
  287. "cpa": _cpa_dependency_payload(settings),
  288. "sub2api": _sub2api_dependency_payload(settings),
  289. }
  290. @app.post("/api/account-survival/reseed")
  291. def api_account_survival_reseed() -> dict[str, object]:
  292. from ops.responses_survival import responses_survival_once
  293. payload = responses_survival_once(
  294. pool_dir=app.state.settings.pool_dir,
  295. state_file=app.state.settings.responses_survival_state_file,
  296. cohort_size=app.state.settings.account_survival_cohort_size,
  297. proxy=app.state.settings.account_survival_proxy,
  298. timeout_seconds=app.state.settings.account_survival_timeout_seconds,
  299. reseed=True,
  300. settings=app.state.settings,
  301. require_provenance=app.state.settings.responses_survival_require_provenance,
  302. recent_window_seconds=app.state.settings.responses_survival_recent_window_seconds,
  303. warmup_min_age_seconds=app.state.settings.warmup_min_age_seconds,
  304. warmup_min_successful_probes=app.state.settings.warmup_min_successful_probes,
  305. )
  306. payload["enabled"] = app.state.settings.account_survival_enabled
  307. payload["available"] = True
  308. payload["path"] = str(app.state.settings.responses_survival_state_file)
  309. return payload
  310. @app.get("/api/platforms/chatgpt/actions")
  311. def api_chatgpt_actions() -> list[dict[str, Any]]:
  312. from core.registry import get
  313. platform_cls = get("chatgpt")
  314. platform = platform_cls()
  315. return platform.get_platform_actions()
  316. @app.post("/api/register/chatgpt/preflight")
  317. def api_chatgpt_preflight(request: ChatGPTPreflightRequest) -> dict[str, object]:
  318. return run_chatgpt_preflight(
  319. email=request.email,
  320. password=request.password,
  321. mail_provider=request.mail_provider,
  322. proxy=request.proxy,
  323. )
  324. @app.post("/api/register/chatgpt/run")
  325. def api_chatgpt_register_once(request: ChatGPTRegisterRequest) -> dict[str, object]:
  326. return run_chatgpt_register_once(
  327. email=request.email,
  328. password=request.password,
  329. mail_provider=request.mail_provider,
  330. proxy=request.proxy,
  331. write_pool=request.write_pool,
  332. pool_dir=app.state.settings.pool_dir,
  333. )
  334. @app.post("/api/register/chatgpt/callback-exchange")
  335. def api_chatgpt_callback_exchange(request: ChatGPTCallbackExchangeRequest) -> dict[str, object]:
  336. return run_chatgpt_callback_exchange(
  337. callback_url=request.callback_url,
  338. expected_state=request.expected_state,
  339. code_verifier=request.code_verifier,
  340. proxy=request.proxy,
  341. write_pool=request.write_pool,
  342. pool_dir=app.state.settings.pool_dir,
  343. )
  344. @app.get("/zhuce6", response_class=HTMLResponse)
  345. def zhuce6_page() -> str:
  346. html_path = Path(__file__).parent / "dashboard" / "zhuce6.html"
  347. if html_path.is_file():
  348. return html_path.read_text(encoding="utf-8")
  349. return "<h1>zhuce6.html not found</h1>"
  350. return app
  351. app = create_app() if WEB_RUNTIME_IMPORT_ERROR is None and APP_IMPORT_ERROR is None else None
  352. def build_arg_parser() -> argparse.ArgumentParser:
  353. parser = argparse.ArgumentParser(description="Start the zhuce6 service runtime")
  354. parser.add_argument(
  355. "command",
  356. nargs="?",
  357. choices=("run", "stop", "status", "init", "doctor"),
  358. default="run",
  359. help="Lifecycle command",
  360. )
  361. parser.add_argument("--mode", choices=sorted(ALL_RUNTIME_MODES), default="full", help="Runtime mode")
  362. parser.add_argument("--host", default="127.0.0.1", help="Bind host")
  363. parser.add_argument("--port", type=int, default=8000, help="Bind port")
  364. parser.add_argument("--reload", action="store_true", help="Enable reload mode for development")
  365. parser.add_argument("--no-background-tasks", action="store_true", help="Disable internal background loops")
  366. parser.add_argument("--register-loop", action="store_true", help="Enable continuous registration loop")
  367. parser.add_argument("--register-loop-only", action="store_true", help="Run only the continuous registration loop")
  368. parser.add_argument(
  369. "--register-burst-scheduler-only",
  370. action="store_true",
  371. help="Run only the burst registration scheduler",
  372. )
  373. parser.add_argument("--register-threads", type=int, default=None, help="Number of registration threads")
  374. parser.add_argument("--target-count", type=int, default=None, help="Stop registration after N successes")
  375. parser.add_argument("--batch-threads", type=int, default=None, help="Number of threads per burst batch")
  376. parser.add_argument("--batch-target-count", type=int, default=None, help="Target successes per burst batch")
  377. parser.add_argument("--batch-interval-seconds", type=int, default=None, help="Seconds between burst starts")
  378. parser.add_argument("--fix", action="store_true", help="For doctor/init: run `uv sync` before re-checking")
  379. return parser
  380. def _warn_deprecated_flag(flag: str, replacement: str) -> None:
  381. print(f"warning: {flag} is deprecated; use {replacement} instead.", file=sys.stderr)
  382. def _resolve_mode(args: argparse.Namespace) -> str:
  383. mode = str(args.mode or "full")
  384. if args.register_burst_scheduler_only:
  385. _warn_deprecated_flag("--register-burst-scheduler-only", "--mode burst-scheduler")
  386. return "burst-scheduler"
  387. if args.register_loop_only:
  388. _warn_deprecated_flag("--register-loop-only", "--mode register-loop")
  389. return "register-loop"
  390. if args.register_loop:
  391. _warn_deprecated_flag("--register-loop", "--mode full")
  392. return "full"
  393. return mode
  394. def _apply_cli_env_overrides(args: argparse.Namespace, mode: str) -> None:
  395. os.environ["ZHUCE6_RUNTIME_MODE"] = mode
  396. os.environ["ZHUCE6_HOST"] = str(args.host)
  397. os.environ["ZHUCE6_PORT"] = str(args.port)
  398. os.environ["ZHUCE6_DASHBOARD_PORT"] = str(args.port)
  399. os.environ["ZHUCE6_REGISTER_ENABLED"] = "true" if mode in {"full", "lite", "register-loop", "burst-scheduler"} else "false"
  400. if args.register_threads is not None:
  401. os.environ["ZHUCE6_REGISTER_THREADS"] = str(args.register_threads)
  402. if args.target_count is not None:
  403. os.environ["ZHUCE6_REGISTER_TARGET_COUNT"] = str(args.target_count)
  404. if args.batch_threads is not None:
  405. os.environ["ZHUCE6_REGISTER_BATCH_THREADS"] = str(args.batch_threads)
  406. if args.batch_target_count is not None:
  407. os.environ["ZHUCE6_REGISTER_BATCH_TARGET_COUNT"] = str(args.batch_target_count)
  408. if args.batch_interval_seconds is not None:
  409. os.environ["ZHUCE6_REGISTER_BATCH_INTERVAL_SECONDS"] = str(args.batch_interval_seconds)
  410. def _pid_name_for_mode(mode: str) -> str:
  411. return mode if mode in {"register-loop", "burst-scheduler"} else "main"
  412. def _ensure_runtime_cfmail_env(settings: AppSettings, mode: str) -> None:
  413. if mode not in {"full", "lite", "register-loop", "burst-scheduler"}:
  414. return
  415. providers = {part.strip() for part in settings.register_mail_provider.split(",") if part.strip()}
  416. if "cfmail" not in providers:
  417. return
  418. missing = settings.validate_cfmail_env()
  419. if missing:
  420. raise SystemExit(
  421. "Missing cfmail provisioning env: "
  422. + " ".join(missing)
  423. + f"\nExpected env file: {os.getenv('ZHUCE6_CFMAIL_ENV_FILE', str(settings.config_dir / 'cfmail_provision.env'))}"
  424. )
  425. def main(argv: list[str] | None = None) -> None:
  426. args = build_arg_parser().parse_args(argv)
  427. if args.command == "stop":
  428. _ensure_app_dependencies_available()
  429. print(json.dumps({"stopped": process_manager.stop_all()}, ensure_ascii=False, indent=2))
  430. return
  431. if args.command == "status":
  432. _ensure_app_dependencies_available()
  433. print(json.dumps({"processes": process_manager.status_all()}, ensure_ascii=False, indent=2))
  434. return
  435. if args.command == "init":
  436. if APP_IMPORT_ERROR is not None:
  437. _handle_missing_dependency_import()
  438. run_setup_wizard()
  439. _run_uv_sync()
  440. return
  441. if args.command == "doctor":
  442. if APP_IMPORT_ERROR is not None:
  443. _handle_missing_dependency_import()
  444. if args.fix:
  445. _run_uv_sync()
  446. print(format_doctor_report(collect_doctor_report()))
  447. return
  448. mode = _resolve_mode(args)
  449. _apply_cli_env_overrides(args, mode)
  450. settings = _apply_runtime_mode(AppSettings.from_env(), mode)
  451. _ensure_runtime_cfmail_env(settings, mode)
  452. process_manager.stop_all()
  453. pid_name = _pid_name_for_mode(mode)
  454. process_manager.write_pid(pid_name)
  455. try:
  456. if mode == "register-loop":
  457. reg_loop = RegistrationLoop(settings)
  458. reg_loop.start()
  459. try:
  460. while True:
  461. time.sleep(5)
  462. except KeyboardInterrupt:
  463. pass
  464. finally:
  465. reg_loop.stop()
  466. return
  467. if mode == "burst-scheduler":
  468. scheduler = RegistrationBurstScheduler(settings)
  469. try:
  470. scheduler.run()
  471. except KeyboardInterrupt:
  472. pass
  473. finally:
  474. scheduler.stop()
  475. return
  476. create_mode = mode if mode in DASHBOARD_MODES else "full"
  477. uvicorn.run(
  478. create_app(enable_background_tasks=not args.no_background_tasks, mode=create_mode),
  479. host=args.host,
  480. port=args.port,
  481. reload=args.reload,
  482. )
  483. finally:
  484. process_manager.remove_pid(pid_name)
  485. if __name__ == "__main__":
  486. main()