use std::{path::PathBuf, process::Stdio, sync::Arc};
use tokio::process::Command;
use tracing::debug;
use crate::{
config::{EffectiveConfig, sanitize_package_name},
error::NpxcError,
paths::SessionState,
};
use super::proc::ContainerProcess;
pub struct Session {
pub state: Arc<SessionState>,
pub session_dir: PathBuf,
container: Option<ContainerProcess>,
cleaned: bool,
}
impl Session {
pub fn start(
pkg_name: &str,
image_tag: &str,
config: &EffectiveConfig,
extra_ro_mount: Option<&std::path::Path>,
session_dir_parent: Option<&std::path::Path>,
) -> Result<Self, NpxcError> {
let sanitized = sanitize_package_name(pkg_name);
let parent =
session_dir_parent.map_or_else(std::env::temp_dir, std::path::Path::to_path_buf);
let session_dir = tempfile::Builder::new()
.prefix(&format!("npxc-{sanitized}-"))
.tempdir_in(&parent)
.map_err(NpxcError::Io)?
.keep();
let mount_spec = format!("{}:/workspace:{}", session_dir.display(), config.mount_mode);
let container_name = format!("npxc-{sanitized}-{}", std::process::id());
let mut cmd = Command::new(&config.container_cli);
cmd.args([
"run",
"--rm",
"-i",
"--progress",
"none",
"--name",
&container_name,
"--network",
&config.network,
"--read-only",
"--tmpfs",
"/tmp",
"--cap-drop",
"ALL",
"-m",
&config.memory,
"-c",
&config.cpus,
"-v",
&mount_spec,
]);
if let Some(dir) = extra_ro_mount {
let canonical = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
let spec = format!("{0}:{0}:ro", canonical.display());
cmd.args(["-v", &spec]);
}
cmd.arg(image_tag);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
cmd.kill_on_drop(true);
debug!(cmd = ?cmd, "running container command");
let child = cmd.spawn().map_err(|e| {
NpxcError::RuntimeNotAvailable(format!(
"failed to spawn '{}': {e}",
config.container_cli
))
})?;
let container = ContainerProcess::from_child(child);
Ok(Session {
state: Arc::new(SessionState::new()),
session_dir,
container: Some(container),
cleaned: false,
})
}
pub fn take_container(&mut self) -> Option<ContainerProcess> {
self.container.take()
}
pub async fn teardown(mut self) {
if let Some(ref mut c) = self.container {
c.kill_and_wait().await;
}
let _ = tokio::fs::remove_dir_all(&self.session_dir).await;
self.cleaned = true;
}
}
impl Drop for Session {
fn drop(&mut self) {
if self.cleaned {
return;
}
if let Some(ref mut c) = self.container {
c.kill_now();
}
let _ = std::fs::remove_dir_all(&self.session_dir);
}
}