use super::*;
pub(super) struct SessionStartArgs<'a> {
pub(super) client_pid: u32,
pub(super) working_dir: &'a Path,
pub(super) log_file: Option<NormalizedPath>,
pub(super) track_stats: bool,
pub(super) journal_path: Option<NormalizedPath>,
pub(super) profile: bool,
pub(super) private_daemon: Option<crate::protocol::PrivateDaemonSessionOptions>,
}
pub(super) async fn handle_session_start(
state: &SharedState,
args: SessionStartArgs<'_>,
) -> Response {
let (private_env, owner_pids) = match args.private_daemon {
Some(options) => {
if let Some(endpoint) = options.endpoint.as_deref() {
if endpoint != state.endpoint {
return Response::Error {
message: format!(
"private daemon endpoint mismatch: connected to {}, requested {endpoint}",
state.endpoint
),
};
}
}
if let Some(cache_dir) = options.cache_dir.as_ref() {
if cache_dir != &state.cache_dir {
return Response::Error {
message: format!(
"private daemon cache dir mismatch: connected to {}, requested {}",
state.cache_dir.display(),
cache_dir.display()
),
};
}
}
let owner_pids = if options.owner_pids.is_empty() {
vec![args.client_pid]
} else {
options.owner_pids
};
state
.private_daemon
.register_session(&owner_pids, &options.env)
.await;
(options.env, owner_pids)
}
None => (Vec::new(), Vec::new()),
};
let session_config = crate::depgraph::SessionConfig {
client_pid: args.client_pid,
working_dir: args.working_dir.into(),
log_file: args.log_file,
track_stats: args.track_stats,
journal_path: args.journal_path,
profile: args.profile,
private_env,
owner_pids,
};
let session_id = state.sessions.create(session_config);
state.session_worktree_roots.insert(
session_id,
SessionWorktreeRoot {
working_dir: args.working_dir.into(),
root: resolve_worktree_root(args.working_dir, None),
},
);
{
let warning_opt = {
let guard = state.depgraph_load_warning.lock().await;
guard.clone()
};
if let Some(warning) = warning_opt {
write_session_log(&state.sessions, &session_id, &warning);
}
}
watch_directory(state, args.working_dir).await;
let journal_path = state
.sessions
.get(&session_id)
.and_then(|s| s.journal_path.clone());
Response::SessionStarted {
session_id: session_id.to_string(),
journal_path,
}
}
pub(super) fn merge_session_private_env(
sessions: &SessionManager,
session_id: &SessionId,
client_env: Option<Vec<(String, String)>>,
) -> Option<Vec<(String, String)>> {
let Some(session) = sessions.get(session_id) else {
return client_env;
};
if session.private_env.is_empty() {
return client_env;
}
let mut merged = client_env.unwrap_or_default();
for (private_key, private_value) in session.private_env {
if let Some((_, value)) = merged.iter_mut().find(|(key, _)| key == &private_key) {
*value = private_value;
} else {
merged.push((private_key, private_value));
}
}
Some(merged)
}
pub(super) fn redact_session_private_env_for_journal(
sessions: &SessionManager,
session_id: &SessionId,
env: &Option<Vec<(String, String)>>,
) -> Option<Vec<(String, String)>> {
let Some(session) = sessions.get(session_id) else {
return env.clone();
};
if session.private_env.is_empty() {
return env.clone();
}
let private_keys: std::collections::HashSet<String> = session
.private_env
.iter()
.map(|(key, _)| key.clone())
.collect();
env.as_ref().map(|vars| {
vars.iter()
.map(|(key, value)| {
if private_keys.contains(key) {
(key.clone(), "<redacted>".to_string())
} else {
(key.clone(), value.clone())
}
})
.collect()
})
}
pub(super) fn record_session_stat(
sessions: &SessionManager,
session_id: &SessionId,
f: impl FnOnce(&mut crate::depgraph::SessionStatsTracker),
) {
sessions.mutate(session_id, |session| {
if let Some(ref mut tracker) = session.stats_tracker {
f(tracker);
}
});
}
pub(super) fn write_session_log(sessions: &SessionManager, session_id: &SessionId, message: &str) {
if let Some(session) = sessions.get(session_id) {
if let Some(ref log_path) = session.log_file {
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
{
let _ = writeln!(f, "{message}");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn merge_session_private_env_overlays_client_env() {
let sessions = SessionManager::new(Duration::from_secs(300));
let session_id = sessions.create(crate::depgraph::SessionConfig {
client_pid: 1,
working_dir: "/tmp/work".into(),
log_file: None,
track_stats: false,
journal_path: None,
profile: false,
private_env: vec![
("ZCCACHE_PATH_REMAP".to_string(), "auto".to_string()),
("ZCCACHE_PRIVATE_ONLY".to_string(), "1".to_string()),
],
owner_pids: vec![1],
});
let merged = merge_session_private_env(
&sessions,
&session_id,
Some(vec![
("ZCCACHE_PATH_REMAP".to_string(), "off".to_string()),
("OTHER".to_string(), "value".to_string()),
]),
)
.unwrap();
assert!(merged.contains(&("ZCCACHE_PATH_REMAP".to_string(), "auto".to_string())));
assert!(merged.contains(&("ZCCACHE_PRIVATE_ONLY".to_string(), "1".to_string())));
assert!(merged.contains(&("OTHER".to_string(), "value".to_string())));
assert!(!merged.contains(&("ZCCACHE_PATH_REMAP".to_string(), "off".to_string())));
}
#[test]
fn redact_session_private_env_for_journal_hides_values() {
let sessions = SessionManager::new(Duration::from_secs(300));
let session_id = sessions.create(crate::depgraph::SessionConfig {
client_pid: 1,
working_dir: "/tmp/work".into(),
log_file: None,
track_stats: false,
journal_path: None,
profile: false,
private_env: vec![("ZCCACHE_PRIVATE".to_string(), "secret".to_string())],
owner_pids: vec![1],
});
let redacted = redact_session_private_env_for_journal(
&sessions,
&session_id,
&Some(vec![
("ZCCACHE_PRIVATE".to_string(), "secret".to_string()),
("OTHER".to_string(), "value".to_string()),
]),
)
.unwrap();
assert!(redacted.contains(&("ZCCACHE_PRIVATE".to_string(), "<redacted>".to_string())));
assert!(redacted.contains(&("OTHER".to_string(), "value".to_string())));
assert!(!redacted.contains(&("ZCCACHE_PRIVATE".to_string(), "secret".to_string())));
}
}