use std::ffi::OsString;
use std::path::{Path, PathBuf};
use arcbox_connect::v1::MachineExecRequest;
use arcbox_pty::RunAs;
use tokio::process::Command;
use crate::agent::exec_error::spawn_error;
use crate::agent::login_session::{Account, LoginProcess};
use crate::rpc::ErrorResponse;
const DEFAULT_LOGIN_USER: &str = "root";
pub(super) struct ProcessSpec {
program: PathBuf,
arg0: Option<OsString>,
args: Vec<String>,
clear_env: bool,
env: Vec<(String, String)>,
working_dir: Option<PathBuf>,
pub(super) run_as: Option<RunAs>,
}
impl ProcessSpec {
pub(super) fn resolve(req: &MachineExecRequest) -> Result<Self, ErrorResponse> {
if req.login {
Self::login(req)
} else {
Self::plain(req)
}
}
fn plain(req: &MachineExecRequest) -> Result<Self, ErrorResponse> {
let Some((program, args)) = req.cmd.split_first() else {
return Err(ErrorResponse::new(400, "cmd must not be empty"));
};
let run_as = if req.user.is_empty() {
None
} else {
Some(
arcbox_pty::resolve_user(&req.user)
.map_err(|e| ErrorResponse::new(400, e.to_string()))?,
)
};
Ok(Self {
program: PathBuf::from(program),
arg0: None,
args: args.to_vec(),
clear_env: false,
env: req
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
working_dir: (!req.working_dir.is_empty()).then(|| PathBuf::from(&req.working_dir)),
run_as,
})
}
fn login(req: &MachineExecRequest) -> Result<Self, ErrorResponse> {
let user = if req.user.is_empty() {
DEFAULT_LOGIN_USER
} else {
&req.user
};
let entry = lookup_account(user).map_err(|e| ErrorResponse::new(400, e))?;
let run_as = RunAs::for_account(&entry.name, entry.uid.as_raw(), entry.gid.as_raw())
.map_err(|e| ErrorResponse::new(500, format!("groups of {}: {e}", entry.name)))?;
let account = Account {
name: entry.name,
uid: entry.uid.as_raw(),
home: entry.dir,
shell: entry.shell,
};
let mut process = LoginProcess::plan(&account, &req.cmd, &req.env, &req.working_dir);
if req.working_dir.is_empty() && !process.working_dir.is_dir() {
process.working_dir = PathBuf::from("/");
}
Ok(Self {
program: process.program,
arg0: Some(process.arg0),
args: process.args,
clear_env: true,
env: process.env,
working_dir: Some(process.working_dir),
run_as: Some(run_as),
})
}
pub(super) fn append_path(&mut self, dir: &str) {
let current = match self.env.iter().position(|(k, _)| k == "PATH") {
Some(i) => Some(self.env.swap_remove(i).1),
None if self.clear_env => None,
None => std::env::var("PATH").ok(),
};
let path = match current {
Some(path) if !path.is_empty() => format!("{path}:{dir}"),
_ => dir.to_owned(),
};
self.env.push(("PATH".to_owned(), path));
}
pub(super) fn command(&self) -> Command {
let mut cmd = Command::new(&self.program);
if let Some(arg0) = &self.arg0 {
cmd.arg0(arg0);
}
cmd.args(&self.args);
if self.clear_env {
cmd.env_clear();
}
cmd.envs(self.env.iter().map(|(k, v)| (k, v)));
if let Some(dir) = &self.working_dir {
cmd.current_dir(dir);
}
cmd.kill_on_drop(true);
cmd
}
pub(super) fn spawn_error(&self, error: std::io::Error) -> ErrorResponse {
let path = self
.env
.iter()
.find_map(|(k, v)| (k == "PATH").then_some(v.as_str()));
let working_dir = self
.working_dir
.as_deref()
.and_then(Path::to_str)
.unwrap_or_default();
spawn_error(&self.program.to_string_lossy(), working_dir, path, error)
}
}
fn lookup_account(user: &str) -> Result<nix::unistd::User, String> {
let entry = match user.parse::<u32>() {
Ok(uid) => nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(uid)),
Err(_) => nix::unistd::User::from_name(user),
};
entry
.map_err(|e| format!("look up user {user}: {e}"))?
.ok_or_else(|| format!("unknown user: {user}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn spec(env: &[(&str, &str)]) -> ProcessSpec {
ProcessSpec::resolve(&MachineExecRequest {
cmd: vec!["sh".to_owned()],
env: env
.iter()
.map(|&(k, v)| (k.to_owned(), v.to_owned()))
.collect(),
..Default::default()
})
.unwrap()
}
fn path(spec: &ProcessSpec) -> Option<&str> {
spec.env
.iter()
.find_map(|(k, v)| (k == "PATH").then_some(v.as_str()))
}
#[test]
fn an_appended_dir_goes_last_on_the_path_the_process_would_get() {
let mut requested = spec(&[("PATH", "/opt/bin")]);
requested.append_path("/tools");
assert_eq!(path(&requested), Some("/opt/bin:/tools"));
let mut inherited = spec(&[]);
inherited.append_path("/tools");
let agent = std::env::var("PATH").unwrap();
assert_eq!(path(&inherited), Some(format!("{agent}:/tools").as_str()));
}
}