use privesc::{PrivilegedChild, PrivilegedCommand};
use quincy::{QuincyError, Result};
use std::env;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::timeout;
use tracing::{error, info, warn};
use super::types::QuincyInstance;
use crate::ipc::{
ConnectionMetrics, ConnectionStatus, IpcMessage, IpcServer, get_ipc_socket_path,
get_log_file_path,
};
use crate::validation;
impl QuincyInstance {
const DAEMON_START_TIMEOUT: Duration = Duration::from_secs(15);
pub async fn start(
name: String,
config_path: PathBuf,
) -> Result<(Self, Option<ConnectionMetrics>)> {
validation::validate_instance_name(&name)?;
info!("Starting client daemon process for: {}", name);
let socket_path = get_ipc_socket_path(&name);
let log_path = get_log_file_path(&name);
let ipc_server = IpcServer::new(&socket_path)?;
let daemon_binary = Self::get_daemon_binary_path()?;
let handle = Self::spawn_daemon_process(
&daemon_binary,
&name,
&config_path,
&socket_path,
&log_path,
)?;
let connection = match timeout(Self::DAEMON_START_TIMEOUT, ipc_server.accept()).await {
Ok(Ok(conn)) => conn,
Ok(Err(e)) => return Err(e),
Err(_) => {
warn!(
"Timed out after {:?} while waiting for daemon IPC connection",
Self::DAEMON_START_TIMEOUT
);
return Err(QuincyError::system(Self::get_spawn_error(handle)));
}
};
let instance = Self::new(name, Some(Arc::new(Mutex::new(connection))));
instance.send_start_command(&config_path).await;
let metrics = instance.fetch_initial_status().await?;
Ok((instance, metrics))
}
fn get_daemon_binary_path() -> Result<PathBuf> {
Ok(env::current_exe()?
.parent()
.ok_or_else(|| QuincyError::system("Could not determine parent directory"))?
.join("quincy-client-daemon"))
}
fn spawn_daemon_process(
daemon_binary: &Path,
name: &str,
config_path: &Path,
socket_path: &Path,
log_path: &Path,
) -> Result<PrivilegedChild> {
let config_str = config_path.to_string_lossy();
let socket_str = socket_path.to_string_lossy();
let log_str = log_path.to_string_lossy();
PrivilegedCommand::new(daemon_binary)
.arg("--instance-name")
.arg(name)
.arg("--config-path")
.arg(config_str)
.arg("--socket-path")
.arg(socket_str)
.arg("--log-path")
.arg(log_str)
.gui(true)
.prompt("Quincy needs administrator privileges to create network interfaces.")
.spawn()
.map_err(|err| QuincyError::system(format!("{err}")))
}
fn get_spawn_error(mut handle: PrivilegedChild) -> String {
if handle.try_wait().ok().flatten().is_none() {
return "Timed out waiting for daemon IPC connection".to_string();
}
let Some(output) = handle.wait().ok() else {
return "Timed out waiting for daemon IPC connection".to_string();
};
if output.status.success() {
return "Timed out waiting for daemon IPC connection".to_string();
}
match output.stderr_str() {
Some(stderr) if !stderr.is_empty() => {
format!("Daemon process failed: {}", stderr.trim())
}
_ => format!("Daemon process exited with status: {}", output.status),
}
}
async fn send_start_command(&self, config_path: &Path) {
if let Some(ref ipc_connection) = self.ipc_client {
let mut connection = ipc_connection.lock().await;
if let Err(e) = connection
.send(&IpcMessage::StartClient {
config_path: config_path.to_path_buf(),
})
.await
{
error!("Failed to send start command to daemon: {}", e);
}
}
}
async fn fetch_initial_status(&self) -> Result<Option<ConnectionMetrics>> {
let Some(ref ipc_connection) = self.ipc_client else {
return Ok(None);
};
let mut connection = ipc_connection.lock().await;
connection.send(&IpcMessage::GetStatus).await?;
match connection.recv().await? {
IpcMessage::StatusUpdate(status) => match status.status {
ConnectionStatus::Connected => Ok(status.metrics),
ConnectionStatus::Connecting => Ok(None),
ConnectionStatus::Disconnected => Err(QuincyError::system("Daemon disconnected")),
ConnectionStatus::Error(err) => Err(QuincyError::system(err.to_string())),
},
IpcMessage::Error(err) => Err(QuincyError::system(err.to_string())),
other => {
warn!("Unexpected response to status request: {:?}", other);
Ok(None)
}
}
}
pub async fn stop(&mut self) -> Result<()> {
info!("Stopping client daemon process for: {}", self.name);
self.send_shutdown_message().await;
self.close_connection();
Ok(())
}
async fn send_shutdown_message(&self) {
if let Some(ref ipc_connection) = self.ipc_client {
let mut connection = ipc_connection.lock().await;
match connection.send(&IpcMessage::Shutdown).await {
Ok(()) => info!("Sent graceful shutdown message to daemon"),
Err(e) => warn!("Failed to send shutdown message to daemon: {}", e),
}
}
}
fn close_connection(&mut self) {
self.ipc_client = None;
info!("IPC connection closed - daemon will detect disconnection");
}
}