mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-03 18:23:51 +08:00
186 lines
6.0 KiB
Rust
186 lines
6.0 KiB
Rust
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<Self> {
|
|
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<TcpListener> {
|
|
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<TcpListener> {
|
|
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());
|
|
}
|
|
}
|