Skip to main content

monaco_vscode_server/
lib.rs

1//! `monaco-vscode-server` is a Rust crate for managing the VSCode server backend.
2//! 
3//! It provides functionalities to download, start, stop, and manage the VSCode server,
4//! which is used by `monaco-vscode-api` to provide a Monaco editor with VSCode services.
5//! 
6//! ## Features
7//! - `embed`: Enables embedding the VSCode server binary directly into your application.
8//!            When this feature is active, the server can be extracted and run without
9//!            needing a separate download step at runtime, unless overridden.
10//! 
11//! ## Quick Start
12//! 
13//! ```rust,no_run
14//! use monaco_vscode_server::{VscodeServerManager, ServerConfig};
15//! 
16//! #[tokio::main]
17//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
18//!     let mut manager = VscodeServerManager::new().await?;
19//!     manager.ensure_server().await?; // Downloads if not present or embedded
20//!     manager.start().await?;
21//! 
22//!     println!("Server is running at {}", manager.url());
23//!     println!("Server info: {:?}", manager.info());
24//! 
25//!     // Keep the server running for a bit (e.g., in a real app, it runs until shutdown)
26//!     tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
27//! 
28//!     manager.stop().await?;
29//!     println!("Server stopped.");
30//! 
31//!     Ok(())
32//! }
33//! ```
34
35// Module declarations - these correspond to other files in src/
36mod download;
37mod platform;
38
39// Re-export commonly used types at the crate root
40pub use platform::Platform;
41
42// Standard library imports
43use std::path::{Path, PathBuf};
44use std::process::{Child, Command};
45use std::sync::Arc;
46use tokio::sync::Mutex;
47use serde::{Deserialize, Serialize};
48use thiserror::Error;
49
50/// Error type for the `monaco-vscode-server` crate.
51#[derive(Error, Debug)]
52pub enum ServerError {
53    #[error("IO error: {0}")]
54    Io(#[from] std::io::Error),
55    
56    /// An error occurred during network operations (e.g., downloading the server).
57    #[error("Network error: {0}")]
58    Network(#[from] reqwest::Error),
59    
60    /// The VSCode server executable or related files were not found where expected.
61    #[error("Server not found")]
62    ServerNotFound,
63    
64    /// An attempt was made to start a server that is already running.
65    #[error("Server already running")]
66    AlreadyRunning,
67    
68    /// An operation was attempted that requires the server to be running, but it is not.
69    #[error("Server not running")]
70    NotRunning,
71    
72    /// The server process failed to start.
73    #[error("Failed to start server: {0}")]
74    StartFailed(String),
75    
76    /// The current operating system or architecture is not supported.
77    #[error("Unsupported platform: {0}")]
78    UnsupportedPlatform(String),
79    
80    /// Failed to detect the required VSCode server version.
81    #[error("Version detection failed: {0}")]
82    VersionDetectionFailed(String),
83    
84    /// An error occurred while extracting the downloaded server archive.
85    #[error("Extraction failed: {0}")]
86    ExtractionFailed(String),
87    
88    /// An error occurred during the download process (e.g., HTTP error status).
89    #[error("Download failed: {0}")]
90    DownloadFailed(String),
91}
92
93/// Configuration for the VSCode server instance.
94///
95/// This struct allows customization of various server parameters such as port, host,
96/// installation directory, and command-line arguments.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ServerConfig {
99        /// The port number on which the server will listen.
100    pub port: u16,
101        /// The hostname or IP address to bind the server to.
102    pub host: String,
103        /// Additional command-line arguments to pass to the VSCode server executable.
104    pub args: Vec<String>,
105        /// The directory where the VSCode server will be installed or looked for.
106    pub server_dir: PathBuf,
107        /// If `true`, attempts to disable telemetry by passing relevant arguments to the server.
108    pub disable_telemetry: bool,
109        /// An optional connection token for securing the server.
110    pub connection_token: Option<String>,
111}
112
113/// Provides default settings for `ServerConfig`.
114/// - `port`: 8001
115/// - `host`: "127.0.0.1"
116/// - `args`: `["--accept-server-license-terms"]`
117/// - `server_dir`: A platform-specific cache directory or `./vscode-server`.
118/// - `disable_telemetry`: `true`
119/// - `connection_token`: `None`
120impl Default for ServerConfig {
121    fn default() -> Self {
122        Self {
123            port: 8001,
124            host: "127.0.0.1".to_string(),
125            args: vec!["--accept-server-license-terms".to_string()],
126            server_dir: default_server_dir(),
127            disable_telemetry: true,
128            connection_token: None,
129        }
130    }
131}
132
133/// Holds information about the detected or embedded VSCode server.
134///
135/// This includes the version of `monaco-vscode-api` it's compatible with,
136/// the specific VSCode commit SHA, the target platform, and the download URL.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ServerInfo {
139        /// The version of `monaco-vscode-api` this server is intended to work with.
140    pub monaco_api_version: String,
141        /// The specific commit SHA of the VSCode repository this server is built from.
142    pub vscode_commit: String,
143        /// The platform (OS and architecture) for which this server is intended.
144    pub platform: Platform,
145        /// The direct URL from which this version of the server can be downloaded.
146    pub download_url: String,
147}
148
149/// Manages the lifecycle of a VSCode server instance.
150///
151/// This struct is the primary entry point for interacting with the server. It handles
152/// downloading (if not embedded and not already present), starting, stopping, and querying
153/// the state of the VSCode server.
154///
155/// Instances are typically created using `VscodeServerManager::new()` or `VscodeServerManager::with_config()`.
156/// The server process is cleaned up when the `VscodeServerManager` instance is dropped.
157pub struct VscodeServerManager {
158    config: ServerConfig,
159    info: Option<ServerInfo>,
160    process: Arc<Mutex<Option<Child>>>,
161    server_path: Option<PathBuf>,
162}
163
164impl VscodeServerManager {
165    /// Creates a new `VscodeServerManager` with default configuration.
166    ///
167    /// This is an asynchronous operation as it may involve initial setup.
168    ///
169    /// # Errors
170    ///
171    /// Currently, this constructor does not return errors but is `async` for future compatibility
172    /// and consistency with `with_config`.
173    // Constructor
174    pub async fn new() -> Result<Self, ServerError> {
175        Self::with_config(ServerConfig::default()).await
176    }
177    
178        /// Creates a new `VscodeServerManager` with the specified configuration.
179    ///
180    /// This is an asynchronous operation.
181    ///
182    /// # Arguments
183    ///
184    /// * `config` - A `ServerConfig` struct with custom settings for the server.
185    ///
186    /// # Errors
187    ///
188    /// Currently, this constructor does not return errors but is `async` for future compatibility.
189    // Constructor with custom config
190    pub async fn with_config(config: ServerConfig) -> Result<Self, ServerError> {
191        Ok(Self {
192            config,
193            info: None,
194            process: Arc::new(Mutex::new(None)),
195            server_path: None,
196        })
197    }
198    
199        /// Ensures that the VSCode server is available, downloading it if necessary.
200    ///
201    /// This method performs the following steps:
202    /// 1. If the `embed` feature is enabled, it first tries to extract an embedded server.
203    /// 2. If no embedded server is found or the feature is disabled, it attempts to detect
204    ///    the latest compatible VSCode server version.
205    /// 3. It checks if this version is already present in the configured `server_dir`.
206    /// 4. If not present, it downloads and extracts the server.
207    ///
208    /// This method must be called before `start()` if the server's presence is not guaranteed.
209    /// It is an asynchronous operation due to potential network I/O.
210    ///
211    /// # Errors
212    ///
213    /// Returns `ServerError` if:
214    /// - Version detection fails (`ServerError::VersionDetectionFailed`).
215    /// - Downloading fails (`ServerError::Network`, `ServerError::DownloadFailed`).
216    /// - Extraction fails (`ServerError::ExtractionFailed`, `ServerError::Io`).
217    /// - The platform is unsupported (`ServerError::UnsupportedPlatform`).
218    // Ensure server is available (download if needed)
219    pub async fn ensure_server(&mut self) -> Result<(), ServerError> {        
220        // Otherwise download
221        let info = download::detect_version().await?;
222        self.info = Some(info.clone());
223        
224        let server_path = self.config.server_dir.join(&info.vscode_commit);
225        
226        if !server_path.exists() {
227            download::download_server(&info, &self.config.server_dir).await?;
228        }
229        
230        self.server_path = Some(server_path);
231        Ok(())
232    }
233    
234        /// Starts the VSCode server process.
235    ///
236    /// Before calling `start`, `ensure_server` should typically be called to make sure
237    /// the server binaries are available.
238    /// The server will be started with the configuration provided during the manager's creation.
239    ///
240    /// This is an asynchronous operation.
241    ///
242    /// # Errors
243    ///
244    /// Returns `ServerError` if:
245    /// - The server is already running (`ServerError::AlreadyRunning`).
246    /// - The server path has not been determined (e.g., `ensure_server` was not called) (`ServerError::ServerNotFound`).
247    /// - The server executable cannot be found at the expected path (`ServerError::ServerNotFound`).
248    /// - The server process fails to start (`ServerError::StartFailed`, `ServerError::Io`).
249    // Start the server
250    pub async fn start(&self) -> Result<(), ServerError> {
251        let mut process_guard = self.process.lock().await;
252        
253        if process_guard.is_some() {
254            return Err(ServerError::AlreadyRunning);
255        }
256        
257        let server_path = self.server_path.as_ref()
258            .ok_or(ServerError::ServerNotFound)?;
259        
260        let executable = self.get_executable_path(server_path)?;
261        
262        let mut cmd = Command::new(&executable);
263        
264        // Configure command
265        cmd.arg("--port").arg(self.config.port.to_string())
266           .arg("--host").arg(&self.config.host);
267        
268        if self.config.disable_telemetry {
269            cmd.arg("--disable-telemetry");
270        }
271        
272match &self.config.connection_token {
273    Some(token) => {
274        cmd.arg("--connection-token").arg(token);
275    }
276    None => {
277        cmd.arg("--without-connection-token");
278    }
279}
280        
281        for arg in &self.config.args {
282            cmd.arg(arg);
283        }
284        
285        // Start process
286        let child = cmd.spawn()
287            .map_err(|e| ServerError::StartFailed(e.to_string()))?;
288        
289        *process_guard = Some(child);
290        
291        // Wait for server to initialize
292        tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
293        
294        Ok(())
295    }
296    
297        /// Stops the VSCode server process if it is running.
298    ///
299    /// This is an asynchronous operation.
300    ///
301    /// # Errors
302    ///
303    /// Returns `ServerError::NotRunning` if the server was not running.
304    /// May return `ServerError::Io` if there's an issue killing the process, though this is rare.
305    // Stop the server
306    pub async fn stop(&self) -> Result<(), ServerError> {
307        let mut process_guard = self.process.lock().await;
308        
309        if let Some(mut child) = process_guard.take() {
310            child.kill()?;
311            child.wait()?;
312            Ok(())
313        } else {
314            Err(ServerError::NotRunning)
315        }
316    }
317    
318        /// Checks if the VSCode server process is currently running.
319    ///
320    /// This method checks the status of the underlying process.
321    /// It is an asynchronous operation as it involves locking the process state.
322    // Check if running
323    pub async fn is_running(&self) -> bool {
324        let mut process_guard = self.process.lock().await;
325        
326        if let Some(ref mut child) = *process_guard {
327            match child.try_wait() {
328                Ok(None) => true,
329                _ => {
330                    *process_guard = None;
331                    false
332                }
333            }
334        } else {
335            false
336        }
337    }
338    
339        /// Returns the URL (host and port) where the server is expected to be listening.
340    ///
341    /// This is constructed from the `host` and `port` in the `ServerConfig`.
342    /// It does not guarantee that the server is actually listening on this URL, only that
343    /// this is its configured address.
344    // Get server URL
345    pub fn url(&self) -> String {
346        format!("http://{}:{}", self.config.host, self.config.port)
347    }
348    
349        /// Returns a reference to the `ServerInfo` if the server version has been determined
350    /// (e.g., after `ensure_server` has been called).
351    ///
352    /// Returns `None` if server information is not yet available.
353    // Get server info
354    pub fn info(&self) -> Option<&ServerInfo> {
355        self.info.as_ref()
356    }
357    
358    // Helper to get executable path
359    fn get_executable_path(&self, server_path: &Path) -> Result<PathBuf, ServerError> {
360        let exe = if cfg!(target_os = "windows") {
361            server_path.join("bin").join("code-server.cmd")
362        } else {
363            server_path.join("bin").join("code-server")
364        };
365        
366        if !exe.exists() {
367            return Err(ServerError::ServerNotFound);
368        }
369        
370        Ok(exe)
371    }
372    
373    /// Returns a reference to the current `ServerConfig`.
374    pub fn config(&self) -> &ServerConfig {
375        &self.config
376    }
377}
378
379/// Ensures the server process is stopped when the `VscodeServerManager` goes out of scope.
380// Cleanup on drop
381impl Drop for VscodeServerManager {
382    fn drop(&mut self) {
383        if let Ok(mut process_guard) = self.process.try_lock() {
384            if let Some(mut child) = process_guard.take() {
385                let _ = child.kill();
386            }
387        }
388    }
389}
390
391/// Configuration specific to using `VscodeServerManager` within a Tauri application.
392///
393/// This struct wraps a `ServerConfig` and adds Tauri-specific options like auto-starting
394/// the server or stopping it on application exit.
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct TauriConfig {
397        /// If `true`, the VSCode server will be automatically started after `initialize` is called.
398    pub auto_start: bool,
399        /// If `true`, the VSCode server will be automatically stopped when the `TauriVscodeServer`
400    /// instance (or its underlying `VscodeServerManager`) is dropped. This is generally desired
401    /// for Tauri applications to clean up the server process on app exit.
402    pub stop_on_exit: bool,
403        /// The underlying `ServerConfig` to be used for the VSCode server instance.
404    pub server: ServerConfig,
405}
406
407/// Provides default settings for `TauriConfig`.
408/// - `auto_start`: `true`
409/// - `stop_on_exit`: `true`
410/// - `server`: `ServerConfig::default()`
411impl Default for TauriConfig {
412    fn default() -> Self {
413        Self {
414            auto_start: true,
415            stop_on_exit: true,
416            server: ServerConfig::default(),
417        }
418    }
419}
420
421/// A wrapper around `VscodeServerManager` tailored for use in Tauri applications.
422///
423/// This struct simplifies the integration of the VSCode server with Tauri by managing
424/// the server's lifecycle based on `TauriConfig` settings. It's designed to be held
425/// in Tauri's application state.
426///
427/// The underlying `VscodeServerManager` is wrapped in an `Arc<Mutex<>>` to allow
428/// shared mutable access from Tauri commands.
429pub struct TauriVscodeServer {
430    manager: Arc<Mutex<VscodeServerManager>>,
431    config: TauriConfig,
432}
433
434impl TauriVscodeServer {
435    /// Creates a new `TauriVscodeServer` with the given Tauri-specific configuration.
436    ///
437    /// This will internally create a `VscodeServerManager`.
438    ///
439    /// # Arguments
440    ///
441    /// * `config` - A `TauriConfig` struct.
442    ///
443    /// # Errors
444    ///
445    /// Propagates errors from `VscodeServerManager::with_config` (though currently it doesn't error).
446    pub async fn new(config: TauriConfig) -> Result<Self, ServerError> {
447        let manager = VscodeServerManager::with_config(config.server.clone()).await?;
448        
449        Ok(Self {
450            manager: Arc::new(Mutex::new(manager)),
451            config,
452        })
453    }
454    
455        /// Initializes the VSCode server.
456    ///
457    /// This involves ensuring the server is downloaded/extracted (via `ensure_server`)
458    /// and, if `config.auto_start` is true, starting the server.
459    ///
460    /// Returns information about the server if successful.
461    ///
462    /// # Errors
463    ///
464    /// Propagates errors from `VscodeServerManager::ensure_server` and `VscodeServerManager::start`.
465    pub async fn initialize(&self) -> Result<ServerInfo, ServerError> {
466        let mut manager = self.manager.lock().await;
467        manager.ensure_server().await?;
468        
469        if self.config.auto_start {
470            manager.start().await?;
471        }
472        
473        manager.info().cloned().ok_or(ServerError::ServerNotFound)
474    }
475    
476        /// Returns the URL where the VSCode server is expected to be listening.
477    ///
478    /// This is an asynchronous operation as it requires locking the underlying manager.
479    pub async fn get_url(&self) -> String {
480        let manager = self.manager.lock().await;
481        manager.url()
482    }
483    
484        /// Returns a JSON representation of the server's information and configuration.
485    ///
486    /// This is useful for providing server details to a Tauri frontend.
487    /// The JSON object includes `serverUrl`, `monacoApiVersion`, `vscodeCommit`,
488    /// `platform`, and `serviceConfig` (with `baseUrl` and `connectionToken`).
489    ///
490    /// # Errors
491    ///
492    /// Returns `ServerError::ServerNotFound` if the server info hasn't been determined yet.
493    pub async fn get_info(&self) -> Result<serde_json::Value, ServerError> {
494        let manager = self.manager.lock().await;
495        let info = manager.info().ok_or(ServerError::ServerNotFound)?;
496        
497        Ok(serde_json::json!({
498            "serverUrl": manager.url(),
499            "monacoApiVersion": info.monaco_api_version,
500            "vscodeCommit": info.vscode_commit,
501            "platform": info.platform.to_string(),
502            "serviceConfig": {
503                "baseUrl": manager.url(),
504                "connectionToken": manager.config().connection_token,
505            }
506        }))
507    }
508    
509    /// Stops the VSCode server process.
510    ///
511    /// This is an asynchronous operation.
512    ///
513    /// # Errors
514    ///
515    /// Propagates errors from `VscodeServerManager::stop`.
516    pub async fn stop(&self) -> Result<(), ServerError> {
517        let manager = self.manager.lock().await;
518        manager.stop().await
519    }
520    
521        /// Restarts the VSCode server process.
522    ///
523    /// This involves stopping the server, waiting briefly, and then starting it again.
524    /// This is an asynchronous operation.
525    ///
526    /// # Errors
527    ///
528    /// Propagates errors from `VscodeServerManager::stop` and `VscodeServerManager::start`.
529pub async fn restart(&self) -> Result<(), ServerError> {
530    self.stop().await?;
531     tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
532    {
533        let manager = self.manager.lock().await;
534        manager.start().await
535    }
536 }
537}
538
539// Private helper functions
540
541/// Determines the default directory for storing/finding the VSCode server.
542///
543/// The lookup order is:
544/// 1. The value of the `VSCODE_SERVER_DIR` environment variable, if set.
545/// 2. A subdirectory named `vscode-server-backend` within the system's cache directory
546///    (e.g., `~/.cache/vscode-server-backend` on Linux).
547/// 3. A local directory named `vscode-server` in the current working directory (`./vscode-server`)
548///    as a fallback if the system cache directory cannot be determined.
549fn default_server_dir() -> PathBuf {
550    if let Ok(dir) = std::env::var("VSCODE_SERVER_DIR") {
551        return PathBuf::from(dir);
552    }
553    
554    if let Some(cache_dir) = dirs::cache_dir() {
555        return cache_dir.join("vscode-server-backend");
556    }
557    
558    PathBuf::from("./vscode-server")
559}
560
561// Re-exports for convenience
562pub use download::download_server;