venus_server/
lib.rs

1//! Venus interactive notebook server.
2//!
3//! Provides a WebSocket server for real-time notebook interaction.
4//!
5//! # Architecture
6//!
7//! The server consists of:
8//! - **Session**: Manages notebook state, compilation, and execution
9//! - **Protocol**: Defines client/server message types
10//! - **Routes**: HTTP and WebSocket handlers
11//! - **Watcher**: File system monitoring for external changes
12//!
13//! # Features
14//!
15//! - `embedded-frontend` (default): Embeds the web UI for standalone use
16
17#[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
41// Re-export LSP cleanup function
42pub use lsp::kill_all_processes as kill_all_lsp_processes;
43
44/// Server configuration.
45#[derive(Debug, Clone)]
46pub struct ServerConfig {
47    /// Host address to bind to.
48    pub host: String,
49    /// Port to listen on.
50    pub port: u16,
51    /// Whether to open browser on start.
52    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
65/// Start the Venus server for a notebook.
66pub async fn serve(notebook_path: impl AsRef<Path>, config: ServerConfig) -> ServerResult<()> {
67    let path = notebook_path.as_ref();
68
69    // Create shared interrupt flag (AtomicBool for lock-free access)
70    let interrupted = Arc::new(AtomicBool::new(false));
71
72    // Create session with shared interrupt flag
73    let (session, _rx) = NotebookSession::new(path, interrupted.clone())?;
74
75    // Get the kill handle from the executor - it's an Arc so it will see
76    // updates when workers are spawned during execution
77    let kill_handle = session.get_kill_handle();
78
79    let session = Arc::new(RwLock::new(session));
80
81    // Create app state with shared kill handle and interrupt flag
82    let state = Arc::new(AppState {
83        session: session.clone(),
84        kill_handle: Arc::new(TokioMutex::new(kill_handle)),
85        interrupted,
86    });
87
88    // Create router
89    let app = create_router(state);
90
91    // Create file watcher
92    let mut watcher = FileWatcher::new(path)?;
93
94    // Spawn watcher task and store handle for cleanup
95    let watcher_task = tokio::spawn(async move {
96        while let Some(event) = watcher.recv().await {
97            match event {
98                FileEvent::Modified(_) => {
99                    // NOTE: We do NOT auto-reload here. External file changes should be picked up
100                    // manually via "Restart Kernel" button. Auto-reloading causes infinite loops
101                    // when editors perform frequent auto-saves or temporary file operations.
102                    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    // Build address
113    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    // Open browser if requested
123    if config.open_browser {
124        tracing::info!("Open http://{} in your browser", addr);
125    }
126
127    // Start server with graceful shutdown
128    let listener = tokio::net::TcpListener::bind(addr).await?;
129
130    // Create shutdown signal channel
131    let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
132
133    // Handle Ctrl+C for graceful shutdown
134    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    // Serve with graceful shutdown
142    let server = axum::serve(listener, app).with_graceful_shutdown(async move {
143        let _ = shutdown_rx.await;
144    });
145
146    server.await?;
147
148    // Clean up file watcher task
149    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}