microsandbox_server/
management.rs1use 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
31pub const API_KEY_PREFIX: &str = "msb_";
37
38const 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#[derive(Debug, Serialize, Deserialize)]
56pub struct Claims {
57 pub exp: u64,
59
60 pub iat: u64,
62
63 pub namespace: String,
65}
66
67pub 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 let microsandbox_home_path = env::get_microsandbox_home_path();
83 fs::create_dir_all(µsandbox_home_path).await?;
84
85 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 let pid_file_path = microsandbox_home_path.join(SERVER_PID_FILE);
94 if pid_file_path.exists() {
95 let pid_str = fs::read_to_string(&pid_file_path).await?;
97 if let Ok(pid) = pid_str.trim().parse::<i32>() {
98 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 tracing::warn!("found stale PID file for process {}. Cleaning up.", pid);
121 clean(&pid_file_path).await?;
122 }
123 } else {
124 tracing::warn!("found invalid PID in server.pid file. Cleaning up.");
126 clean(&pid_file_path).await?;
127 }
128 }
129
130 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 if !dev_mode {
161 let key_file_path = microsandbox_home_path.join(SERVER_KEY_FILE);
163
164 let key_provided = key.is_some();
166
167 let server_key = if let Some(key) = key {
168 command.arg("--key").arg(&key);
170 key
171 } else if key_file_path.exists() && !reset_key {
172 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 let generated_key = generate_random_key();
188 command.arg("--key").arg(&generated_key);
189 generated_key
190 };
191
192 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 command.stdout(Stdio::null());
220 command.stderr(Stdio::null());
221 command.stdin(Stdio::null());
222 }
223
224 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 let pid_file_path = microsandbox_home_path.join(SERVER_PID_FILE);
242
243 fs::create_dir_all(µsandbox_home_path).await?;
245
246 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 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 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(&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(&pid_file_path).await?;
307 }
308 _ = sigterm.recv() => {
309 tracing::info!("received SIGTERM signal");
310
311 if let Err(e) = child.kill().await {
313 tracing::error!("failed to send SIGTERM to child process: {}", e);
314 }
315
316 if let Err(e) = child.wait().await {
318 tracing::error!("error waiting for child after SIGTERM: {}", e);
319 }
320
321 clean(&pid_file_path).await?;
323
324 tracing::info!("server terminated by SIGTERM signal");
326 }
327 _ = sigint.recv() => {
328 tracing::info!("received SIGINT signal");
329
330 if let Err(e) = child.kill().await {
332 tracing::error!("failed to send SIGTERM to child process: {}", e);
333 }
334
335 if let Err(e) = child.wait().await {
337 tracing::error!("error waiting for child after SIGINT: {}", e);
338 }
339
340 clean(&pid_file_path).await?;
342
343 tracing::info!("server terminated by SIGINT signal");
345 }
346 }
347
348 Ok(())
349}
350
351pub 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 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 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 unsafe {
377 if libc::kill(pid, libc::SIGTERM) != 0 {
378 if std::io::Error::last_os_error().raw_os_error().unwrap() == libc::ESRCH {
380 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(&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
412pub 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 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 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 let expire = expire.unwrap_or(Duration::hours(24));
448
449 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 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 let custom_token = convert_jwt_to_api_key(&jwt_token)?;
474
475 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
498pub async fn clean(pid_file_path: &PathBuf) -> MicrosandboxServerResult<()> {
500 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
509fn generate_random_key() -> String {
515 rand::rng()
516 .sample_iter(&Alphanumeric)
517 .take(SERVER_KEY_LENGTH)
518 .map(char::from)
519 .collect()
520}
521
522pub fn convert_jwt_to_api_key(jwt_token: &str) -> MicrosandboxServerResult<String> {
526 Ok(format!("{}{}", API_KEY_PREFIX, jwt_token))
528}