use std::env;
#[cfg(any(feature = "frontend", test))]
use crate::args::InitArgs;
use crate::ast::AndOrList;
#[cfg(any(feature = "frontend", test))]
use crate::policy::ShellIdentity;
use crate::sys;
use crate::sys::Runtime;
use super::{BackgroundMachinePayload, ShellState};
pub(super) const MAX_MACHINE_PAYLOAD_BYTES: usize = 16 * 1024 * 1024;
const MACHINE_PAYLOAD_CHILD_FD: sys::FileDescriptor = sys::FileDescriptor::new(255);
pub(super) fn encode_background_payload(
state: &ShellState,
and_or_list: &AndOrList,
command_id: &str,
) -> Result<String, String> {
let payload = BackgroundMachinePayload {
state: state.background_snapshot(),
and_or_list: and_or_list.clone(),
command_id: command_id.to_string(),
};
let payload_text =
serde_json::to_string(&payload).map_err(|err| format!("invalid machine payload: {err}"))?;
validate_machine_payload_size(payload_text.len())?;
Ok(payload_text)
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(super) fn parse_machine_payload(
payload_text: &str,
) -> Result<BackgroundMachinePayload, String> {
serde_json::from_str(payload_text).map_err(|err| format!("invalid machine payload: {err}"))
}
fn validate_machine_payload_size(len: usize) -> Result<(), String> {
if len <= MAX_MACHINE_PAYLOAD_BYTES {
Ok(())
} else {
Err(format!(
"machine payload too large: {len} bytes exceeds limit of {MAX_MACHINE_PAYLOAD_BYTES} bytes"
))
}
}
fn background_machine_env(state: &ShellState) -> Vec<(String, String)> {
let payload_env = state.definition.identity.machine_payload_fd_env_var();
let mut env = state.exported_env();
env.retain(|(key, _)| key != payload_env);
env.push((
payload_env.to_string(),
MACHINE_PAYLOAD_CHILD_FD.as_i32().to_string(),
));
env
}
#[cfg(any(feature = "frontend", test))]
pub(crate) fn load_machine_payload_text(
init: &InitArgs,
identity: &ShellIdentity,
) -> Result<String, String> {
let payload_env = identity.machine_payload_fd_env_var();
if let Some(payload_text) = init.command_str.as_ref() {
validate_machine_payload_size(payload_text.len())?;
return Ok(payload_text.clone());
}
if let Ok(raw_fd) = env::var(payload_env) {
let fd_num = raw_fd
.parse::<i32>()
.map_err(|_| format!("{}: invalid {payload_env} value: {raw_fd}", identity.name()))?;
if fd_num < 0 {
return Err(format!(
"{}: invalid {payload_env} value: {fd_num}",
identity.name()
));
}
let payload_text = sys::FileDescriptor::from(fd_num)
.read_to_string()
.map_err(|err| {
format!(
"{}: failed to read machine payload fd {fd_num}: {err}",
identity.name()
)
})?;
validate_machine_payload_size(payload_text.len())
.map_err(|err| format!("{}: {err}", identity.name()))?;
return Ok(payload_text);
}
Err(format!(
"{}: --machine requires -c <payload> or {payload_env}=<fd>",
identity.name()
))
}
pub(crate) fn resolve_machine_program_path(shell_name: &str) -> Option<String> {
let cargo_bin_env = format!("CARGO_BIN_EXE_{shell_name}");
if let Ok(path) = env::var(&cargo_bin_env) {
return Some(path);
}
let current = env::current_exe().ok()?;
if let Some(parent) = current.parent()
&& parent.file_name().is_some_and(|name| name == "deps")
&& let Some(bin_dir) = parent.parent()
{
let candidate = bin_dir.join(shell_name);
if candidate.is_file() {
return Some(candidate.to_string_lossy().into_owned());
}
}
Some(current.to_string_lossy().into_owned())
}
pub(super) fn spawn_background_job<R: Runtime>(
state: &ShellState,
runtime: &mut R,
and_or_list: &AndOrList,
command_id: &str,
) -> Result<sys::SpawnedProcess, String> {
let payload_text = encode_background_payload(state, and_or_list, command_id)?;
let Some((program, argv)) = state
.definition
.background_launcher
.machine_command(state.shell_name())
else {
return Err("failed to resolve machine executable path".to_string());
};
let payload_pipe = sys::OsPipe::new()
.map_err(|err| format!("failed to create machine payload pipe: {err}"))?;
let env = background_machine_env(state);
let mut passed_fds = state.inherited_fds();
passed_fds.push(sys::PassedFileDescriptor {
parent_fd: payload_pipe.read_fd,
child_fd: MACHINE_PAYLOAD_CHILD_FD,
});
let spawned = match runtime.spawn_external_command(
&sys::ExternalCommand {
program,
argv,
env,
cwd: state.cwd.clone(),
create_process_group: true,
passed_fds,
},
sys::SpawnStdio {
stdin_fd: state.stdin_fd,
stdout_fd: state.stdout_fd,
stderr_fd: state.stderr_fd,
},
&[],
sys::SpawnMode::BackgroundJob,
) {
Ok(spawned) => spawned,
Err(err) => {
payload_pipe.read_fd.close();
payload_pipe.write_fd.close();
return Err(format!("failed to spawn background job: {err}"));
}
};
if let Err(err) = payload_pipe.write_fd.write_all(payload_text.as_bytes()) {
payload_pipe.read_fd.close();
payload_pipe.write_fd.close();
let _ = runtime.signal_process_group(spawned.handle, sys::RuntimeSignal::Terminate);
let _ = runtime.wait_child(spawned.handle);
return Err(format!("failed to write machine payload: {err}"));
}
payload_pipe.read_fd.close();
payload_pipe.write_fd.close();
Ok(spawned)
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
static PAYLOAD_ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn load_machine_payload_text_reads_payload_from_fd_env() {
let _guard = PAYLOAD_ENV_LOCK.lock().expect("payload env lock poisoned");
let identity = ShellIdentity::default();
let payload_env = identity.machine_payload_fd_env_var();
let saved_fd = env::var(payload_env).ok();
let pipe = sys::OsPipe::new().expect("payload pipe");
pipe.write_fd
.write_all(br#"{"kind":"payload"}"#)
.expect("write payload");
pipe.write_fd.close();
unsafe {
env::set_var(payload_env, pipe.read_fd.as_i32().to_string());
}
let init = InitArgs::default();
let payload = load_machine_payload_text(&init, &identity).expect("load payload from fd");
assert_eq!(payload, r#"{"kind":"payload"}"#);
unsafe {
if let Some(value) = saved_fd {
env::set_var(payload_env, value);
} else {
env::remove_var(payload_env);
}
}
}
}