use crate::config::ServerConfig;
use crate::error::{Error, Result};
use async_process::{Child, Command, Stdio};
use std::fmt;
use tracing;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ServerId(Uuid);
impl ServerId {
pub(crate) fn new() -> Self {
Self(Uuid::new_v4())
}
}
impl fmt::Display for ServerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServerStatus {
Starting,
Running,
Stopping,
Stopped,
Failed,
}
pub struct ServerProcess {
config: ServerConfig,
name: String,
id: ServerId,
child: Option<Child>,
status: ServerStatus,
}
impl ServerProcess {
#[tracing::instrument(skip(config), fields(server_name = %name))]
pub fn new(name: String, config: ServerConfig) -> Self {
Self {
config,
name,
id: ServerId::new(),
child: None,
status: ServerStatus::Stopped,
}
}
pub fn id(&self) -> ServerId {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
#[tracing::instrument(skip(self), fields(server_name = %self.name, server_id = %self.id))]
pub fn status(&self) -> ServerStatus {
self.status
}
#[tracing::instrument(skip(self), fields(server_name = %self.name, server_id = %self.id))]
pub async fn start(&mut self) -> Result<()> {
if self.child.is_some() {
tracing::warn!("Attempted to start an already running server");
return Err(Error::AlreadyRunning);
}
tracing::info!("Starting server process");
self.status = ServerStatus::Starting;
let mut command = Command::new(&self.config.command);
command.args(&self.config.args);
for (key, value) in &self.config.env {
command.env(key, value);
}
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
tracing::debug!(command = ?self.config.command, args = ?self.config.args, env = ?self.config.env, "Spawning process");
let child = command.spawn().map_err(|e| {
tracing::error!("Failed to spawn process: {}", e);
Error::Process(format!("Failed to start process: {}", e))
})?;
self.child = Some(child);
self.status = ServerStatus::Running;
tracing::info!("Server process started successfully");
Ok(())
}
#[tracing::instrument(skip(self), fields(server_name = %self.name, server_id = %self.id))]
pub async fn stop(&mut self) -> Result<()> {
if let Some(mut child) = self.child.take() {
tracing::info!("Stopping server process");
self.status = ServerStatus::Stopping;
if let Err(e) = child.kill() {
tracing::error!("Failed to kill process: {}", e);
}
match child.status().await {
Ok(status) => tracing::info!(exit_status = ?status, "Server process stopped"),
Err(e) => tracing::warn!("Failed to get exit status after stopping: {}", e),
}
self.status = ServerStatus::Stopped;
Ok(())
} else {
tracing::warn!("Attempted to stop a server that was not running");
Err(Error::NotRunning)
}
}
#[tracing::instrument(skip(self), fields(server_name = %self.name, server_id = %self.id))]
pub fn take_stdin(&mut self) -> Result<async_process::ChildStdin> {
if let Some(child) = &mut self.child {
child.stdin.take().ok_or_else(|| {
Error::Process("Failed to get stdin pipe from child process".to_string())
})
} else {
Err(Error::NotRunning)
}
}
#[tracing::instrument(skip(self), fields(server_name = %self.name, server_id = %self.id))]
pub fn take_stdout(&mut self) -> Result<async_process::ChildStdout> {
if let Some(child) = &mut self.child {
child.stdout.take().ok_or_else(|| {
Error::Process("Failed to get stdout pipe from child process".to_string())
})
} else {
Err(Error::NotRunning)
}
}
pub fn take_stderr(&mut self) -> Result<async_process::ChildStderr> {
if let Some(child) = &mut self.child {
child.stderr.take().ok_or_else(|| {
Error::Process("Failed to get stderr pipe from child process".to_string())
})
} else {
Err(Error::NotRunning)
}
}
}
impl Clone for ServerProcess {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
name: self.name.clone(),
id: self.id,
child: None, status: self.status,
}
}
}