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/// Server configuration.
42#[derive(Debug, Clone)]
43pub struct ServerConfig {
44    /// Host address to bind to.
45    pub host: String,
46    /// Port to listen on.
47    pub port: u16,
48    /// Whether to open browser on start.
49    pub open_browser: bool,
50}
51
52impl Default for ServerConfig {
53    fn default() -> Self {
54        Self {
55            host: "127.0.0.1".to_string(),
56            port: 3000,
57            open_browser: false,
58        }
59    }
60}
61
62/// Start the Venus server for a notebook.
63pub async fn serve(notebook_path: impl AsRef<Path>, config: ServerConfig) -> ServerResult<()> {
64    let path = notebook_path.as_ref();
65
66    // Create shared interrupt flag (AtomicBool for lock-free access)
67    let interrupted = Arc::new(AtomicBool::new(false));
68
69    // Create session with shared interrupt flag
70    let (session, _rx) = NotebookSession::new(path, interrupted.clone())?;
71    let session = Arc::new(RwLock::new(session));
72
73    // Create app state with shared kill handle and interrupt flag
74    let state = Arc::new(AppState {
75        session: session.clone(),
76        kill_handle: Arc::new(TokioMutex::new(None)),
77        interrupted,
78    });
79
80    // Create router
81    let app = create_router(state);
82
83    // Create file watcher
84    let mut watcher = FileWatcher::new(path)?;
85
86    // Spawn watcher task
87    let session_clone = session.clone();
88    tokio::spawn(async move {
89        while let Some(event) = watcher.recv().await {
90            match event {
91                FileEvent::Modified(_) => {
92                    tracing::info!("Notebook file changed, reloading...");
93                    let mut session = session_clone.write().await;
94                    if let Err(e) = session.reload() {
95                        tracing::error!("Failed to reload notebook: {}", e);
96                    }
97                }
98                FileEvent::Removed(path) => {
99                    tracing::warn!("Notebook file removed: {}", path.display());
100                }
101                FileEvent::Created(_) => {}
102            }
103        }
104    });
105
106    // Build address
107    let addr: SocketAddr = format!("{}:{}", config.host, config.port)
108        .parse()
109        .map_err(|_| ServerError::Io {
110            path: std::path::PathBuf::new(),
111            message: format!("Invalid address: {}:{}", config.host, config.port),
112        })?;
113
114    tracing::info!("Starting Venus server at http://{}", addr);
115
116    // Open browser if requested
117    if config.open_browser {
118        tracing::info!("Open http://{} in your browser", addr);
119    }
120
121    // Start server
122    let listener = tokio::net::TcpListener::bind(addr).await?;
123    axum::serve(listener, app).await?;
124
125    Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_default_config() {
134        let config = ServerConfig::default();
135        assert_eq!(config.host, "127.0.0.1");
136        assert_eq!(config.port, 3000);
137        assert!(!config.open_browser);
138    }
139}