use std::{future::IntoFuture, net::SocketAddr, time::Duration}; use tokio::net::TcpListener; use tokio_util::sync::CancellationToken; use crate::{ config::{Config, ConsoleSource}, control, cursor::{ handlers, prompting::{PromptAssets, PromptCompiler}, CursorSessionRegistry, }, harness::CursorHarness, provider::ProviderRouter, run::RunRegistry, store::Store, Result, }; pub struct App { config: Config, router: axum::Router, registry: CursorSessionRegistry, harness: CursorHarness, store: Store, } impl App { pub async fn new(mut config: Config) -> Result { let store = Store::connect(&config.database_url).await?; if config.use_persisted_ports { config .listen_addr .set_port(store.port_settings().await?.service_port); } let assets = PromptAssets::embedded()?; let compiler = PromptCompiler::new(assets); let provider = std::sync::Arc::new(ProviderRouter::new( store.clone(), config.provider_request_timeout, )); let run_registry = RunRegistry::default(); let registry = CursorSessionRegistry::new(store.clone(), provider.clone(), compiler, run_registry); let control = control::ControlService::new(store.clone(), provider)?; let harness = control.cursor_harness().clone(); let mut router = handlers::router(registry.clone())?; router = match &config.console { Some(ConsoleSource::Directory(directory)) => { router.merge(control::web_router(control.clone(), directory)) } Some(ConsoleSource::Proxy(target)) => { router.merge(control::proxy_web_router(control.clone(), target.clone())) } None => router.merge(control::api_router(control.clone())), }; Ok(Self { router, registry, harness, store, config, }) } pub fn merge_router(mut self, router: axum::Router) -> Self { self.router = self.router.merge(router); self } pub async fn bind(&self) -> Result { let requested = self.config.listen_addr; let listener = bind_service_listener(requested, self.config.use_persisted_ports).await?; if self.config.use_persisted_ports { self.store .set_service_port(listener.local_addr()?.port()) .await?; } Ok(listener) } pub fn harness(&self) -> CursorHarness { self.harness.clone() } pub fn store(&self) -> Store { self.store.clone() } pub async fn serve(self) -> Result<()> { let listener = self.bind().await?; let shutdown = CancellationToken::new(); let signal_shutdown = shutdown.clone(); let running = self.serve_on(listener, shutdown); tokio::pin!(running); tokio::select! { result = &mut running => result, () = shutdown_signal() => { tracing::info!("shutdown signal received; cancelling active runs"); signal_shutdown.cancel(); running.await } } } pub async fn serve_on(self, listener: TcpListener, shutdown: CancellationToken) -> Result<()> { let address = listener.local_addr()?; self.harness.set_backend_addr(address); tracing::info!(%address, "cursor server listening"); let registry = self.registry; let harness = self.harness; let graceful = shutdown.clone(); let server = axum::serve(listener, self.router) .with_graceful_shutdown(async move { graceful.cancelled().await; }) .into_future(); tokio::pin!(server); tokio::select! { result = &mut server => { if let Err(error) = harness.disable().await { tracing::warn!(%error, "failed to disable Cursor harness after server stop"); } result? }, () = shutdown.cancelled() => { if let Err(error) = harness.disable().await { tracing::warn!(%error, "failed to disable Cursor harness during shutdown"); } registry.shutdown().await; match tokio::time::timeout(Duration::from_secs(10), &mut server).await { Ok(result) => result?, Err(_) => tracing::warn!("graceful shutdown timed out; forcing server close"), } } } Ok(()) } } async fn bind_service_listener( requested: SocketAddr, allow_random_fallback: bool, ) -> Result { match TcpListener::bind(requested).await { Ok(listener) => Ok(listener), Err(error) if allow_random_fallback && requested.port() != 0 => { tracing::warn!(%requested, %error, "configured service port unavailable; selecting a random port"); Ok(TcpListener::bind(SocketAddr::new(requested.ip(), 0)).await?) } Err(error) => Err(error.into()), } } async fn shutdown_signal() { let ctrl_c = async { let _ = tokio::signal::ctrl_c().await; }; #[cfg(unix)] let terminate = async { if let Ok(mut signal) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { signal.recv().await; } }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = terminate => {} } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn service_listener_falls_back_when_configured_port_is_busy() { let occupied = TcpListener::bind("127.0.0.1:0").await.unwrap(); let requested = occupied.local_addr().unwrap(); let listener = bind_service_listener(requested, true).await.unwrap(); assert_ne!(listener.local_addr().unwrap().port(), requested.port()); } }