1#[cfg(feature = "embedded-frontend")]
18pub mod embedded_frontend;
19pub mod error;
20pub mod lsp;
21pub mod protocol;
22pub mod routes;
23pub mod rust_analyzer;
24pub mod session;
25pub mod undo;
26pub mod watcher;
27
28use std::net::SocketAddr;
29use std::path::Path;
30use std::sync::Arc;
31use std::sync::atomic::AtomicBool;
32
33use tokio::sync::{Mutex as TokioMutex, RwLock};
34
35pub use error::{ServerError, ServerResult};
36pub use protocol::{ClientMessage, ServerMessage};
37pub use routes::{AppState, create_router};
38pub use session::{NotebookSession, SessionHandle};
39pub use watcher::{FileEvent, FileWatcher};
40
41pub use lsp::kill_all_processes as kill_all_lsp_processes;
43
44#[derive(Debug, Clone)]
46pub struct ServerConfig {
47 pub host: String,
49 pub port: u16,
51 pub open_browser: bool,
53}
54
55impl Default for ServerConfig {
56 fn default() -> Self {
57 Self {
58 host: "127.0.0.1".to_string(),
59 port: 3000,
60 open_browser: false,
61 }
62 }
63}
64
65pub async fn serve(notebook_path: impl AsRef<Path>, config: ServerConfig) -> ServerResult<()> {
67 let path = notebook_path.as_ref();
68
69 let interrupted = Arc::new(AtomicBool::new(false));
71
72 let (session, _rx) = NotebookSession::new(path, interrupted.clone())?;
74
75 let kill_handle = session.get_kill_handle();
78
79 let session = Arc::new(RwLock::new(session));
80
81 let state = Arc::new(AppState {
83 session: session.clone(),
84 kill_handle: Arc::new(TokioMutex::new(kill_handle)),
85 interrupted,
86 });
87
88 let app = create_router(state);
90
91 let mut watcher = FileWatcher::new(path)?;
93
94 let watcher_task = tokio::spawn(async move {
96 while let Some(event) = watcher.recv().await {
97 match event {
98 FileEvent::Modified(_) => {
99 tracing::debug!("Notebook file changed externally (ignored, use Restart Kernel to apply)");
103 }
104 FileEvent::Removed(path) => {
105 tracing::warn!("Notebook file removed: {}", path.display());
106 }
107 FileEvent::Created(_) => {}
108 }
109 }
110 });
111
112 let addr: SocketAddr = format!("{}:{}", config.host, config.port)
114 .parse()
115 .map_err(|_| ServerError::Io {
116 path: std::path::PathBuf::new(),
117 message: format!("Invalid address: {}:{}", config.host, config.port),
118 })?;
119
120 tracing::info!("Starting Venus server at http://{}", addr);
121
122 if config.open_browser {
124 tracing::info!("Open http://{} in your browser", addr);
125 }
126
127 let listener = tokio::net::TcpListener::bind(addr).await?;
129
130 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
132
133 tokio::spawn(async move {
135 if tokio::signal::ctrl_c().await.is_ok() {
136 tracing::info!("Received shutdown signal");
137 let _ = shutdown_tx.send(());
138 }
139 });
140
141 let server = axum::serve(listener, app).with_graceful_shutdown(async move {
143 let _ = shutdown_rx.await;
144 });
145
146 server.await?;
147
148 watcher_task.abort();
150 let _ = watcher_task.await;
151
152 tracing::info!("Server shutdown complete");
153
154 Ok(())
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_default_config() {
163 let config = ServerConfig::default();
164 assert_eq!(config.host, "127.0.0.1");
165 assert_eq!(config.port, 3000);
166 assert!(!config.open_browser);
167 }
168}