microsandbox_server/
management.rs

1//! Management module for the microsandbox server.
2//!
3//! This module provides functionality for managing the microsandbox server lifecycle, including:
4//! - Starting and stopping the server
5//! - API key generation and management
6//! - Process management and signal handling
7//! - Server configuration and state management
8//!
9//! The module implements core server management features such as:
10//! - Secure server key generation and storage
11//! - PID file management for process tracking
12//! - Signal handling for graceful shutdown
13//! - JWT-based API key generation and formatting
14
15use std::{path::PathBuf, process::Stdio};
16
17use chrono::{Duration, Utc};
18use jsonwebtoken::{EncodingKey, Header};
19#[cfg(feature = "cli")]
20use microsandbox_utils::term;
21use microsandbox_utils::{
22    env, DEFAULT_MSBSERVER_EXE_PATH, MSBSERVER_EXE_ENV_VAR, NAMESPACES_SUBDIR, SERVER_KEY_FILE,
23    SERVER_PID_FILE,
24};
25use rand::{distr::Alphanumeric, Rng};
26use serde::{Deserialize, Serialize};
27use tokio::{fs, process::Command};
28
29use crate::{MicrosandboxServerError, MicrosandboxServerResult};
30
31//--------------------------------------------------------------------------------------------------
32// Constants
33//--------------------------------------------------------------------------------------------------
34
35/// Prefix for the API key
36pub const API_KEY_PREFIX: &str = "msb_";
37
38/// Length of the server key
39const SERVER_KEY_LENGTH: usize = 32;
40
41#[cfg(feature = "cli")]
42const START_SERVER_MSG: &str = "Start sandbox server";
43
44#[cfg(feature = "cli")]
45const STOP_SERVER_MSG: &str = "Stop sandbox server";
46
47#[cfg(feature = "cli")]
48const KEYGEN_MSG: &str = "Generate new API key";
49
50//--------------------------------------------------------------------------------------------------
51// Types
52//--------------------------------------------------------------------------------------------------
53
54/// Claims for the JWT token
55#[derive(Debug, Serialize, Deserialize)]
56pub struct Claims {
57    /// Expiration time
58    pub exp: u64,
59
60    /// Issued at time
61    pub iat: u64,
62
63    /// Namespace
64    pub namespace: String,
65}
66
67//--------------------------------------------------------------------------------------------------
68// Functions
69//--------------------------------------------------------------------------------------------------
70
71/// Start the sandbox server
72pub async fn start(
73    key: Option<String>,
74    host: Option<String>,
75    port: Option<u16>,
76    namespace_dir: Option<PathBuf>,
77    dev_mode: bool,
78    detach: bool,
79    reset_key: bool,
80) -> MicrosandboxServerResult<()> {
81    // Ensure microsandbox home directory exists
82    let microsandbox_home_path = env::get_microsandbox_home_path();
83    fs::create_dir_all(&microsandbox_home_path).await?;
84
85    // Ensure namespace directory exists
86    let namespace_path = microsandbox_home_path.join(NAMESPACES_SUBDIR);
87    fs::create_dir_all(&namespace_path).await?;
88
89    #[cfg(feature = "cli")]
90    let start_server_sp = term::create_spinner(START_SERVER_MSG.to_string(), None, None);
91
92    // Check if PID file exists, indicating a server might be running
93    let pid_file_path = microsandbox_home_path.join(SERVER_PID_FILE);
94    if pid_file_path.exists() {
95        // Read PID from file
96        let pid_str = fs::read_to_string(&pid_file_path).await?;
97        if let Ok(pid) = pid_str.trim().parse::<i32>() {
98            // Check if process is actually running
99            let process_running = unsafe { libc::kill(pid, 0) == 0 };
100
101            if process_running {
102                #[cfg(feature = "cli")]
103                term::finish_with_error(&start_server_sp);
104
105                #[cfg(feature = "cli")]
106                println!(
107                    "A sandbox server is already running (PID: {}) - Use {} to stop it",
108                    pid,
109                    console::style("msb server stop").yellow()
110                );
111
112                tracing::info!(
113                    "A sandbox server is already running (PID: {}). Use 'msb server stop' to stop it",
114                    pid
115                );
116
117                return Ok(());
118            } else {
119                // Process not running, clean up stale PID file
120                tracing::warn!("found stale PID file for process {}. Cleaning up.", pid);
121                clean(&pid_file_path).await?;
122            }
123        } else {
124            // Invalid PID in file, clean up
125            tracing::warn!("found invalid PID in server.pid file. Cleaning up.");
126            clean(&pid_file_path).await?;
127        }
128    }
129
130    // Get the path to the msbrun executable
131    let msbserver_path = microsandbox_utils::path::resolve_env_path(
132        MSBSERVER_EXE_ENV_VAR,
133        &*DEFAULT_MSBSERVER_EXE_PATH,
134    )
135    .map_err(|e| {
136        #[cfg(feature = "cli")]
137        term::finish_with_error(&start_server_sp);
138        e
139    })?;
140
141    let mut command = Command::new(msbserver_path);
142
143    if dev_mode {
144        command.arg("--dev");
145    }
146
147    if let Some(host) = host {
148        command.arg("--host").arg(host);
149    }
150
151    if let Some(port) = port {
152        command.arg("--port").arg(port.to_string());
153    }
154
155    if let Some(namespace_dir) = namespace_dir {
156        command.arg("--path").arg(namespace_dir);
157    }
158
159    // Handle secure non-dev mode
160    if !dev_mode {
161        // Create a key file with either the provided key or a generated one
162        let key_file_path = microsandbox_home_path.join(SERVER_KEY_FILE);
163
164        // Store if a key was provided before consuming the option
165        let key_provided = key.is_some();
166
167        let server_key = if let Some(key) = key {
168            // Use the provided key
169            command.arg("--key").arg(&key);
170            key
171        } else if key_file_path.exists() && !reset_key {
172            // Use existing key file if it exists and reset_key is not set
173            let existing_key = fs::read_to_string(&key_file_path).await.map_err(|e| {
174                #[cfg(feature = "cli")]
175                term::finish_with_error(&start_server_sp);
176
177                MicrosandboxServerError::StartError(format!(
178                    "failed to read existing key file {}: {}",
179                    key_file_path.display(),
180                    e
181                ))
182            })?;
183            command.arg("--key").arg(&existing_key);
184            existing_key
185        } else {
186            // Generate a new random key
187            let generated_key = generate_random_key();
188            command.arg("--key").arg(&generated_key);
189            generated_key
190        };
191
192        // Write the key to file (if it's a new key or we're resetting)
193        if !key_file_path.exists() || key_provided || reset_key {
194            fs::write(&key_file_path, &server_key).await.map_err(|e| {
195                #[cfg(feature = "cli")]
196                term::finish_with_error(&start_server_sp);
197
198                MicrosandboxServerError::StartError(format!(
199                    "failed to write key file {}: {}",
200                    key_file_path.display(),
201                    e
202                ))
203            })?;
204
205            tracing::info!("created server key file at {}", key_file_path.display());
206        }
207    }
208
209    if detach {
210        unsafe {
211            command.pre_exec(|| {
212                libc::setsid();
213                Ok(())
214            });
215        }
216
217        // TODO: Redirect to log file
218        // Redirect the i/o to /dev/null
219        command.stdout(Stdio::null());
220        command.stderr(Stdio::null());
221        command.stdin(Stdio::null());
222    }
223
224    // Only pass RUST_LOG if it's set in the environment
225    if let Ok(rust_log) = std::env::var("RUST_LOG") {
226        tracing::debug!("using existing RUST_LOG: {:?}", rust_log);
227        command.env("RUST_LOG", rust_log);
228    }
229
230    let mut child = command.spawn().map_err(|e| {
231        #[cfg(feature = "cli")]
232        term::finish_with_error(&start_server_sp);
233
234        MicrosandboxServerError::StartError(format!("failed to spawn server process: {}", e))
235    })?;
236
237    let pid = child.id().unwrap_or(0);
238    tracing::info!("started sandbox server process with PID: {}", pid);
239
240    // Create PID file
241    let pid_file_path = microsandbox_home_path.join(SERVER_PID_FILE);
242
243    // Ensure microsandbox home directory exists
244    fs::create_dir_all(&microsandbox_home_path).await?;
245
246    // Write PID to file
247    fs::write(&pid_file_path, pid.to_string())
248        .await
249        .map_err(|e| {
250            #[cfg(feature = "cli")]
251            term::finish_with_error(&start_server_sp);
252
253            MicrosandboxServerError::StartError(format!(
254                "failed to write PID file {}: {}",
255                pid_file_path.display(),
256                e
257            ))
258        })?;
259
260    #[cfg(feature = "cli")]
261    start_server_sp.finish();
262
263    if detach {
264        return Ok(());
265    }
266
267    // Set up signal handlers for graceful shutdown
268    let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
269        .map_err(|e| {
270            #[cfg(feature = "cli")]
271            term::finish_with_error(&start_server_sp);
272
273            MicrosandboxServerError::StartError(format!("failed to set up signal handlers: {}", e))
274        })?;
275
276    let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
277        .map_err(|e| {
278            #[cfg(feature = "cli")]
279            term::finish_with_error(&start_server_sp);
280
281            MicrosandboxServerError::StartError(format!("failed to set up signal handlers: {}", e))
282        })?;
283
284    // Wait for either child process to exit or signal to be received
285    tokio::select! {
286        status = child.wait() => {
287            if !status.as_ref().map_or(false, |s| s.success()) {
288                tracing::error!(
289                    "child process — sandbox server — exited with status: {:?}",
290                    status
291                );
292
293                // Clean up PID file if process fails
294                clean(&pid_file_path).await?;
295
296                #[cfg(feature = "cli")]
297                term::finish_with_error(&start_server_sp);
298
299                return Err(MicrosandboxServerError::StartError(format!(
300                    "child process — sandbox server — failed with exit status: {:?}",
301                    status
302                )));
303            }
304
305            // Clean up PID file on successful exit
306            clean(&pid_file_path).await?;
307        }
308        _ = sigterm.recv() => {
309            tracing::info!("received SIGTERM signal");
310
311            // Send SIGTERM to child process
312            if let Err(e) = child.kill().await {
313                tracing::error!("failed to send SIGTERM to child process: {}", e);
314            }
315
316            // Wait for child to exit after sending signal
317            if let Err(e) = child.wait().await {
318                tracing::error!("error waiting for child after SIGTERM: {}", e);
319            }
320
321            // Clean up PID file after signal
322            clean(&pid_file_path).await?;
323
324            // Exit with a message
325            tracing::info!("server terminated by SIGTERM signal");
326        }
327        _ = sigint.recv() => {
328            tracing::info!("received SIGINT signal");
329
330            // Send SIGTERM to child process
331            if let Err(e) = child.kill().await {
332                tracing::error!("failed to send SIGTERM to child process: {}", e);
333            }
334
335            // Wait for child to exit after sending signal
336            if let Err(e) = child.wait().await {
337                tracing::error!("error waiting for child after SIGINT: {}", e);
338            }
339
340            // Clean up PID file after signal
341            clean(&pid_file_path).await?;
342
343            // Exit with a message
344            tracing::info!("server terminated by SIGINT signal");
345        }
346    }
347
348    Ok(())
349}
350
351/// Stop the sandbox server
352pub async fn stop() -> MicrosandboxServerResult<()> {
353    let microsandbox_home_path = env::get_microsandbox_home_path();
354    let pid_file_path = microsandbox_home_path.join(SERVER_PID_FILE);
355
356    #[cfg(feature = "cli")]
357    let stop_server_sp = term::create_spinner(STOP_SERVER_MSG.to_string(), None, None);
358
359    // Check if PID file exists
360    if !pid_file_path.exists() {
361        #[cfg(feature = "cli")]
362        term::finish_with_error(&stop_server_sp);
363
364        return Err(MicrosandboxServerError::StopError(
365            "server is not running (PID file not found)".to_string(),
366        ));
367    }
368
369    // Read PID from file
370    let pid_str = fs::read_to_string(&pid_file_path).await?;
371    let pid = pid_str.trim().parse::<i32>().map_err(|_| {
372        MicrosandboxServerError::StopError("invalid PID found in server.pid file".to_string())
373    })?;
374
375    // Send SIGTERM to the process
376    unsafe {
377        if libc::kill(pid, libc::SIGTERM) != 0 {
378            // If process doesn't exist, clean up PID file and return error
379            if std::io::Error::last_os_error().raw_os_error().unwrap() == libc::ESRCH {
380                // Delete only the PID file
381                clean(&pid_file_path).await?;
382
383                #[cfg(feature = "cli")]
384                term::finish_with_error(&stop_server_sp);
385
386                return Err(MicrosandboxServerError::StopError(
387                    "server process not found (stale PID file removed)".to_string(),
388                ));
389            }
390
391            #[cfg(feature = "cli")]
392            term::finish_with_error(&stop_server_sp);
393
394            return Err(MicrosandboxServerError::StopError(format!(
395                "failed to stop server process (PID: {})",
396                pid
397            )));
398        }
399    }
400
401    // Clean up just the PID file
402    clean(&pid_file_path).await?;
403
404    #[cfg(feature = "cli")]
405    stop_server_sp.finish();
406
407    tracing::info!("stopped sandbox server process (PID: {})", pid);
408
409    Ok(())
410}
411
412/// Generate a new API key (JWT token)
413pub async fn keygen(
414    expire: Option<Duration>,
415    namespace: String,
416) -> MicrosandboxServerResult<String> {
417    let microsandbox_home_path = env::get_microsandbox_home_path();
418    let key_file_path = microsandbox_home_path.join(SERVER_KEY_FILE);
419
420    #[cfg(feature = "cli")]
421    let keygen_sp = term::create_spinner(KEYGEN_MSG.to_string(), None, None);
422
423    // Check if server key file exists
424    if !key_file_path.exists() {
425        #[cfg(feature = "cli")]
426        term::finish_with_error(&keygen_sp);
427
428        return Err(MicrosandboxServerError::KeyGenError(
429            "Server key file not found. Make sure the server is running in secure mode."
430                .to_string(),
431        ));
432    }
433
434    // Read the server key
435    let server_key = fs::read_to_string(&key_file_path).await.map_err(|e| {
436        #[cfg(feature = "cli")]
437        term::finish_with_error(&keygen_sp);
438
439        MicrosandboxServerError::KeyGenError(format!(
440            "Failed to read server key file {}: {}",
441            key_file_path.display(),
442            e
443        ))
444    })?;
445
446    // Determine token expiration (default: 24 hours)
447    let expire = expire.unwrap_or(Duration::hours(24));
448
449    // Generate JWT token with the specified expiration
450    let now = Utc::now();
451    let expiry = now + expire;
452
453    let claims = Claims {
454        exp: expiry.timestamp() as u64,
455        iat: now.timestamp() as u64,
456        namespace,
457    };
458
459    // Encode the token
460    let jwt_token = jsonwebtoken::encode(
461        &Header::default(),
462        &claims,
463        &EncodingKey::from_secret(server_key.as_bytes()),
464    )
465    .map_err(|e| {
466        #[cfg(feature = "cli")]
467        term::finish_with_error(&keygen_sp);
468
469        MicrosandboxServerError::KeyGenError(format!("Failed to generate token: {}", e))
470    })?;
471
472    // Convert the JWT token to our custom API key format
473    let custom_token = convert_jwt_to_api_key(&jwt_token)?;
474
475    // Store the token information for output
476    let token_str = custom_token.clone();
477    let expiry_str = expiry.to_rfc3339();
478
479    #[cfg(feature = "cli")]
480    keygen_sp.finish();
481
482    tracing::info!(
483        "Generated API token with namespace {} and expiry {}",
484        claims.namespace,
485        expiry_str
486    );
487
488    #[cfg(feature = "cli")]
489    {
490        println!("Token: {}", console::style(&token_str).cyan());
491        println!("Token expires: {}", console::style(&expiry_str).cyan());
492        println!("Namespace: {}", console::style(&claims.namespace).cyan());
493    }
494
495    Ok(token_str)
496}
497
498/// Clean up the PID file
499pub async fn clean(pid_file_path: &PathBuf) -> MicrosandboxServerResult<()> {
500    // Clean up PID file
501    if pid_file_path.exists() {
502        fs::remove_file(pid_file_path).await?;
503        tracing::info!("removed server PID file at {}", pid_file_path.display());
504    }
505
506    Ok(())
507}
508
509//--------------------------------------------------------------------------------------------------
510// Functions: Helpers
511//--------------------------------------------------------------------------------------------------
512
513/// Generate a random key for JWT token signing
514fn generate_random_key() -> String {
515    rand::rng()
516        .sample_iter(&Alphanumeric)
517        .take(SERVER_KEY_LENGTH)
518        .map(char::from)
519        .collect()
520}
521
522/// Convert a standard JWT token to our custom API key format
523/// Takes a standard JWT token (<header>.<payload>.<signature>) and returns
524/// our custom API key format (<API_KEY_PREFIX><full_jwt_token>)
525pub fn convert_jwt_to_api_key(jwt_token: &str) -> MicrosandboxServerResult<String> {
526    // Create custom API key format: API_KEY_PREFIX + full JWT token
527    Ok(format!("{}{}", API_KEY_PREFIX, jwt_token))
528}