use crate::context::Context;
use crate::provider::ProviderId;
use crate::service::Permit;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
const CEILING: Duration = Duration::from_secs(120);
pub(crate) struct Headless {
pub(crate) command: Command,
pub(crate) token: Option<String>,
}
pub(crate) use crate::host::Ran;
pub(crate) fn home(ctx: &Context, tool: ProviderId) -> PathBuf {
crate::home::dir(ctx).join("warm").join(tool.code())
}
pub(crate) fn prepare(permit: Permit, home: &Path) -> std::io::Result<()> {
if home.exists() {
crate::host::fs::remove_dir_all(permit, home)?;
}
if let Some(parent) = home.parent().filter(|parent| !parent.is_dir()) {
crate::host::fs::create_private_dir(permit, parent)?;
}
for dir in [
home.to_path_buf(),
home.join("home"),
home.join("tmp"),
home.join("work"),
] {
crate::host::fs::create_private_dir(permit, &dir)?;
}
Ok(())
}
pub(crate) fn clear(permit: Permit, home: &Path) {
let _ = crate::host::fs::remove_dir_all(permit, home);
}
pub(crate) fn command(ctx: &Context, permit: Permit, tool: ProviderId, home: &Path) -> Command {
let mut command = crate::provider::command(ctx, permit, tool);
let path: Option<OsString> = command
.get_envs()
.find(|(name, _)| *name == "PATH")
.and_then(|(_, value)| value.map(ToOwned::to_owned));
command.env_clear();
if let Some(path) = path {
command.env("PATH", path);
}
command
.env("HOME", home.join("home"))
.env("TMPDIR", home.join("tmp"))
.env("LANG", "en_US.UTF-8")
.env("TERM", "dumb")
.env("NO_COLOR", "1")
.current_dir(home.join("work"));
if let Some(user) = &ctx.user {
command.env("USER", user);
}
for (name, value) in ctx.proxy.variables() {
command.env(name, value);
}
command
}
#[cfg(any(test, feature = "test-support"))]
pub trait WarmScript: Send + Sync + std::fmt::Debug {
fn run(&self, started: &Started) -> Ran;
}
#[cfg(any(test, feature = "test-support"))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Started {
pub program: PathBuf,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
pub dir: Option<PathBuf>,
pub token: Option<String>,
}
pub(crate) fn run(ctx: &Context, headless: Headless) -> std::io::Result<Ran> {
#[cfg(any(test, feature = "test-support"))]
if let Some(script) = ctx.warm_script() {
let command = &headless.command;
let text = |s: &std::ffi::OsStr| s.to_string_lossy().into_owned();
let started = Started {
program: PathBuf::from(command.get_program()),
args: command.get_args().map(text).collect(),
env: command
.get_envs()
.filter_map(|(name, value)| Some((text(name), text(value?))))
.collect(),
dir: command.get_current_dir().map(Path::to_path_buf),
token: headless.token.clone(),
};
return Ok(script.run(&started));
}
#[cfg(not(any(test, feature = "test-support")))]
let _ = ctx;
let Headless { command, token } = headless;
crate::host::proc::run_within(command, token.as_deref().map(str::as_bytes), CEILING)
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
#[test]
fn a_home_is_made_afresh_for_each_run() {
let dir = std::env::temp_dir().join(format!("pitboard-warm-home-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let home = dir.join("warm").join("claude");
prepare(Permit::for_a_test(), &home).expect("made");
std::fs::write(home.join("home").join("left.json"), "{}").expect("written");
prepare(Permit::for_a_test(), &home).expect("made again");
assert!(!home.join("home").join("left.json").exists());
for sub in ["home", "tmp", "work"] {
assert!(home.join(sub).is_dir(), "{sub}");
}
clear(Permit::for_a_test(), &home);
assert!(!home.exists());
std::fs::remove_dir_all(&dir).expect("cleanup");
}
}