pub const ENV_ORIGINATOR: &str = "RUNNING_PROCESS_ORIGINATOR";
pub const ENV_LINEAGE: &str = "ZCCACHE_LINEAGE";
pub const ENV_PARENT_PID: &str = "ZCCACHE_PARENT_PID";
pub const ENV_DAEMON_PID: &str = "ZCCACHE_DAEMON_PID";
pub const ENV_CLIENT_PID: &str = "ZCCACHE_CLIENT_PID";
pub const ENV_SESSION_ID: &str = "ZCCACHE_SESSION_ID";
pub const ALL: &[&str] = &[
ENV_ORIGINATOR,
ENV_LINEAGE,
ENV_PARENT_PID,
ENV_DAEMON_PID,
ENV_CLIENT_PID,
ENV_SESSION_ID,
];
#[derive(Clone, Debug)]
pub struct Lineage {
pub daemon_pid: u32,
pub client_pid: Option<u32>,
pub session_id: Option<String>,
}
impl Lineage {
#[must_use]
pub fn current(client_pid: Option<u32>, session_id: Option<String>) -> Self {
Self {
daemon_pid: std::process::id(),
client_pid,
session_id,
}
}
#[must_use]
pub fn env_for_child(
&self,
incoming_env: Option<&[(String, String)]>,
) -> Vec<(String, String)> {
fn lookup<'a>(env: Option<&'a [(String, String)]>, key: &str) -> Option<&'a str> {
env.and_then(|e| e.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str()))
}
let originator = lookup(incoming_env, ENV_ORIGINATOR)
.map(str::to_owned)
.unwrap_or_else(|| format!("zccache:{}", self.daemon_pid));
let mut chain: Vec<String> = match lookup(incoming_env, ENV_LINEAGE) {
Some(existing) if !existing.is_empty() => {
existing.split('>').map(str::to_owned).collect()
}
_ => Vec::new(),
};
let push_unique = |chain: &mut Vec<String>, pid: u32| {
let s = pid.to_string();
if chain.last().map(String::as_str) != Some(s.as_str()) {
chain.push(s);
}
};
if let Some(pid) = self.client_pid {
push_unique(&mut chain, pid);
}
push_unique(&mut chain, self.daemon_pid);
let lineage = chain.join(">");
let mut out = vec![
(ENV_ORIGINATOR.into(), originator),
(ENV_LINEAGE.into(), lineage),
(ENV_DAEMON_PID.into(), self.daemon_pid.to_string()),
(ENV_PARENT_PID.into(), self.daemon_pid.to_string()),
];
if let Some(pid) = self.client_pid {
out.push((ENV_CLIENT_PID.into(), pid.to_string()));
}
if let Some(ref sid) = self.session_id {
out.push((ENV_SESSION_ID.into(), sid.clone()));
}
out
}
pub fn apply_to_tokio(
&self,
cmd: &mut tokio::process::Command,
incoming_env: Option<&[(String, String)]>,
) {
for (k, v) in self.env_for_child(incoming_env) {
cmd.env(k, v);
}
}
pub fn apply_to_sync(
&self,
cmd: &mut std::process::Command,
incoming_env: Option<&[(String, String)]>,
) {
for (k, v) in self.env_for_child(incoming_env) {
cmd.env(k, v);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pairs(items: &[(&str, &str)]) -> Vec<(String, String)> {
items
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect()
}
fn get<'a>(vars: &'a [(String, String)], key: &str) -> Option<&'a str> {
vars.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
}
#[test]
fn empty_incoming_starts_chain_with_daemon_only() {
let l = Lineage {
daemon_pid: 100,
client_pid: None,
session_id: None,
};
let out = l.env_for_child(None);
assert_eq!(get(&out, ENV_LINEAGE), Some("100"));
assert_eq!(get(&out, ENV_ORIGINATOR), Some("zccache:100"));
assert_eq!(get(&out, ENV_DAEMON_PID), Some("100"));
assert_eq!(get(&out, ENV_PARENT_PID), Some("100"));
assert_eq!(get(&out, ENV_CLIENT_PID), None);
assert_eq!(get(&out, ENV_SESSION_ID), None);
}
#[test]
fn client_pid_appears_in_chain_before_daemon() {
let l = Lineage {
daemon_pid: 200,
client_pid: Some(150),
session_id: None,
};
let out = l.env_for_child(None);
assert_eq!(get(&out, ENV_LINEAGE), Some("150>200"));
assert_eq!(get(&out, ENV_CLIENT_PID), Some("150"));
}
#[test]
fn existing_lineage_is_preserved_and_extended() {
let incoming = pairs(&[(ENV_LINEAGE, "10>20"), (ENV_ORIGINATOR, "build:10")]);
let l = Lineage {
daemon_pid: 200,
client_pid: Some(150),
session_id: None,
};
let out = l.env_for_child(Some(&incoming));
assert_eq!(get(&out, ENV_LINEAGE), Some("10>20>150>200"));
assert_eq!(get(&out, ENV_ORIGINATOR), Some("build:10"));
}
#[test]
fn session_id_propagated_when_present() {
let l = Lineage {
daemon_pid: 100,
client_pid: None,
session_id: Some("abc-123".into()),
};
let out = l.env_for_child(None);
assert_eq!(get(&out, ENV_SESSION_ID), Some("abc-123"));
}
#[test]
fn duplicate_trailing_pid_is_collapsed() {
let incoming = pairs(&[(ENV_LINEAGE, "10>150")]);
let l = Lineage {
daemon_pid: 200,
client_pid: Some(150),
session_id: None,
};
let out = l.env_for_child(Some(&incoming));
assert_eq!(get(&out, ENV_LINEAGE), Some("10>150>200"));
}
#[test]
fn current_uses_running_daemon_pid() {
let l = Lineage::current(Some(42), Some("sid".into()));
assert_eq!(l.daemon_pid, std::process::id());
assert_eq!(l.client_pid, Some(42));
assert_eq!(l.session_id.as_deref(), Some("sid"));
}
#[test]
fn apply_to_tokio_sets_lineage_env() {
let l = Lineage {
daemon_pid: 100,
client_pid: Some(50),
session_id: None,
};
let mut cmd = tokio::process::Command::new("echo");
l.apply_to_tokio(&mut cmd, None);
let envs: Vec<(String, String)> = cmd
.as_std()
.get_envs()
.filter_map(|(k, v)| {
Some((
k.to_string_lossy().into_owned(),
v?.to_string_lossy().into_owned(),
))
})
.collect();
assert_eq!(get(&envs, ENV_LINEAGE), Some("50>100"));
assert_eq!(get(&envs, ENV_ORIGINATOR), Some("zccache:100"));
}
#[test]
fn apply_to_sync_sets_lineage_env() {
let l = Lineage {
daemon_pid: 100,
client_pid: None,
session_id: None,
};
let mut cmd = std::process::Command::new("echo");
l.apply_to_sync(&mut cmd, None);
let envs: Vec<(String, String)> = cmd
.get_envs()
.filter_map(|(k, v)| {
Some((
k.to_string_lossy().into_owned(),
v?.to_string_lossy().into_owned(),
))
})
.collect();
assert_eq!(get(&envs, ENV_DAEMON_PID), Some("100"));
}
#[test]
fn empty_existing_lineage_treated_as_unset() {
let incoming = pairs(&[(ENV_LINEAGE, "")]);
let l = Lineage {
daemon_pid: 100,
client_pid: None,
session_id: None,
};
let out = l.env_for_child(Some(&incoming));
assert_eq!(get(&out, ENV_LINEAGE), Some("100"));
}
#[test]
fn no_client_pid_starts_chain_with_daemon_only_even_when_session_set() {
let l = Lineage {
daemon_pid: 99,
client_pid: None,
session_id: Some("probe".into()),
};
let out = l.env_for_child(None);
assert_eq!(get(&out, ENV_LINEAGE), Some("99"));
assert_eq!(get(&out, ENV_CLIENT_PID), None);
assert_eq!(get(&out, ENV_SESSION_ID), Some("probe"));
}
#[test]
fn all_constants_are_unique_and_present() {
let mut seen = std::collections::HashSet::new();
for var in ALL {
assert!(seen.insert(*var), "duplicate var in ALL: {var}");
}
assert!(ALL.contains(&ENV_ORIGINATOR));
assert!(ALL.contains(&ENV_LINEAGE));
assert!(ALL.contains(&ENV_DAEMON_PID));
assert!(ALL.contains(&ENV_PARENT_PID));
assert!(ALL.contains(&ENV_CLIENT_PID));
assert!(ALL.contains(&ENV_SESSION_ID));
}
#[test]
fn deeply_nested_chain_is_extended() {
let incoming = pairs(&[(ENV_LINEAGE, "1>2>3>4>5")]);
let l = Lineage {
daemon_pid: 700,
client_pid: Some(600),
session_id: None,
};
let out = l.env_for_child(Some(&incoming));
assert_eq!(get(&out, ENV_LINEAGE), Some("1>2>3>4>5>600>700"));
}
#[test]
fn originator_is_independent_of_lineage_chain() {
let incoming = pairs(&[(ENV_ORIGINATOR, "myrunner:99999"), (ENV_LINEAGE, "100>200")]);
let l = Lineage {
daemon_pid: 400,
client_pid: None,
session_id: None,
};
let out = l.env_for_child(Some(&incoming));
assert_eq!(get(&out, ENV_ORIGINATOR), Some("myrunner:99999"));
assert_eq!(get(&out, ENV_LINEAGE), Some("100>200>400"));
}
#[test]
fn overlay_after_replay_extends_chain_and_preserves_unrelated_env() {
let incoming = pairs(&[(ENV_LINEAGE, "10>20"), ("PATH", "/usr/bin")]);
let l = Lineage {
daemon_pid: 300,
client_pid: Some(200),
session_id: None,
};
let mut cmd = std::process::Command::new("echo");
for (k, v) in &incoming {
cmd.env(k, v);
}
l.apply_to_sync(&mut cmd, Some(&incoming));
let envs: Vec<(String, String)> = cmd
.get_envs()
.filter_map(|(k, v)| {
Some((
k.to_string_lossy().into_owned(),
v?.to_string_lossy().into_owned(),
))
})
.collect();
assert_eq!(get(&envs, ENV_LINEAGE), Some("10>20>200>300"));
assert_eq!(get(&envs, "PATH"), Some("/usr/bin"));
}
#[test]
fn lineage_is_clone_and_debug() {
let l = Lineage {
daemon_pid: 1,
client_pid: Some(2),
session_id: Some("s".into()),
};
let cloned = l.clone();
let _ = format!("{cloned:?}");
}
}