Rename Castle -> Wild PC across the repo
Repo-side rename only (Phases 1-3 of the migration plan); the live box (~/.castle, systemd units, /data/castle, domains) is a separate cutover. - Slug `castle` -> `wildpc`: CLI command, module names (wildpc_core/cli/api), dist names, entry point `wildpc = wildpc_cli.main:main`. - Identifiers: CastleConfig/NATSClient/DirError/MDNS -> Wildpc*. - Env/constants: CASTLE_* -> WILDPC_*; ~/.castle -> ~/.wildpc, castle.yaml -> wildpc.yaml, /data/castle -> /data/wildpc. - Systemd UNIT_PREFIX castle- -> wildpc-; own programs castle-api/gateway/etc. - Display prose "Castle" -> "Wild PC" in docs, agent-guide files, README, frontend. - Package dirs and bootstrap yaml renamed via git mv; lockfiles regenerated; redundant nested uv.lock files dropped (workspace root lock is authoritative). Tests: core 273, cli 47, wildpc-api 120 all pass. Frontend type-checks + builds. Fixed a stale test fixture (secret_env_path kind arg) broken pre-rename.
This commit is contained in:
186
wildpc-api/src/wildpc_api/main.py
Normal file
186
wildpc-api/src/wildpc_api/main.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""Wild PC API — dashboard data, health aggregation, and service management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from wildpc_api.agent_sessions import manager as agent_session_manager
|
||||
from wildpc_api.agents import router as agents_router
|
||||
from wildpc_api.config import get_registry, settings
|
||||
from wildpc_api.config_editor import router as config_router
|
||||
from wildpc_api.deploy_routes import router as deploy_router
|
||||
from wildpc_api.graph import graph_router
|
||||
from wildpc_api.repos import repos_router
|
||||
from wildpc_api.logs import router as logs_router
|
||||
from wildpc_api.routes import router as dashboard_router
|
||||
from wildpc_api.secrets import router as secrets_router
|
||||
from wildpc_api.services import router as services_router
|
||||
from wildpc_api.stream import (
|
||||
close_all_subscribers,
|
||||
health_poll_loop,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
)
|
||||
from wildpc_api.nodes import router as nodes_router
|
||||
from wildpc_api.programs import programs_router
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Set by _watch_shutdown when uvicorn begins its shutdown sequence.
|
||||
_shutting_down = False
|
||||
|
||||
|
||||
async def _watch_shutdown(server: uvicorn.Server) -> None:
|
||||
"""Poll uvicorn's should_exit flag and close SSE subscribers promptly."""
|
||||
while not server.should_exit:
|
||||
await asyncio.sleep(0.5)
|
||||
global _shutting_down
|
||||
_shutting_down = True
|
||||
close_all_subscribers()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Application lifespan handler."""
|
||||
global _shutting_down
|
||||
_shutting_down = False
|
||||
|
||||
poll_task = asyncio.create_task(health_poll_loop())
|
||||
|
||||
# --- Mesh coordination (opt-in) ---
|
||||
nats_client = None
|
||||
mdns_service = None
|
||||
|
||||
if settings.nats_enabled:
|
||||
try:
|
||||
from wildpc_api.nats_client import WildpcNATSClient
|
||||
|
||||
registry = get_registry()
|
||||
nats_client = WildpcNATSClient(
|
||||
local_hostname=registry.node.hostname,
|
||||
local_registry=registry,
|
||||
servers=settings.nats_url,
|
||||
token=settings.nats_token,
|
||||
)
|
||||
await nats_client.start()
|
||||
app.state.nats_client = nats_client
|
||||
except Exception:
|
||||
logger.exception("Failed to start NATS mesh client")
|
||||
nats_client = None
|
||||
|
||||
if settings.mdns_enabled:
|
||||
try:
|
||||
from wildpc_api.mdns import WildpcMDNS
|
||||
|
||||
registry = get_registry()
|
||||
mdns_service = WildpcMDNS(
|
||||
hostname=registry.node.hostname,
|
||||
gateway_port=registry.node.gateway_port,
|
||||
api_port=settings.port,
|
||||
)
|
||||
mdns_service.start()
|
||||
app.state.mdns = mdns_service
|
||||
except Exception:
|
||||
logger.exception("Failed to start mDNS")
|
||||
mdns_service = None
|
||||
|
||||
yield
|
||||
|
||||
_shutting_down = True
|
||||
poll_task.cancel()
|
||||
|
||||
if nats_client:
|
||||
await nats_client.stop()
|
||||
if mdns_service:
|
||||
mdns_service.stop()
|
||||
|
||||
await agent_session_manager.close_all()
|
||||
close_all_subscribers()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="wildpc-api",
|
||||
description="Wild PC API and service management",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(config_router)
|
||||
app.include_router(dashboard_router)
|
||||
app.include_router(logs_router)
|
||||
app.include_router(nodes_router)
|
||||
app.include_router(secrets_router)
|
||||
app.include_router(services_router)
|
||||
app.include_router(programs_router)
|
||||
app.include_router(deploy_router)
|
||||
app.include_router(agents_router)
|
||||
app.include_router(graph_router)
|
||||
app.include_router(repos_router)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
"""Health check endpoint."""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/stream")
|
||||
async def sse_stream() -> StreamingResponse:
|
||||
"""SSE stream — pushes health updates and service action events."""
|
||||
q = subscribe()
|
||||
|
||||
async def event_generator() -> AsyncGenerator[str, None]:
|
||||
try:
|
||||
yield "event: connected\ndata: {}\n\n"
|
||||
while True:
|
||||
msg = await q.get()
|
||||
if not msg:
|
||||
break
|
||||
yield msg
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
unsubscribe(q)
|
||||
|
||||
return StreamingResponse(
|
||||
event_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
def run() -> None:
|
||||
"""Run the application with uvicorn."""
|
||||
config = uvicorn.Config(
|
||||
"wildpc_api.main:app",
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
reload=False,
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
async def serve_with_watcher() -> None:
|
||||
watcher = asyncio.create_task(_watch_shutdown(server))
|
||||
await server.serve()
|
||||
watcher.cancel()
|
||||
|
||||
asyncio.run(serve_with_watcher())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
Reference in New Issue
Block a user