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;