use std::path::{Path, PathBuf};
use mj_core::state::{State, TargetLocator};
use mj_core::worker_launch::installed_harness_home;
use crate::targets::{CommandExecutor, CommandSpec};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkedProfileHome {
pub session_id: String,
pub staged_home: PathBuf,
pub profile_home: PathBuf,
}
pub fn link_profile_homes_of_earlier_sessions(state: &State) -> Vec<LinkedProfileHome> {
let mut linked = Vec::new();
for session in state.sessions.values() {
let Some(TargetLocator::LocalBare { worker_root }) = &session.target else {
continue;
};
if session.harness_kind.nested_home() {
continue;
}
match link_profile_home(worker_root) {
Ok(Some(profile_home)) => {
tracing::info!(
session_id = %session.id,
profile_home = %profile_home.display(),
"this session's worker was started by an earlier release from the profile \
home; its staged home now links there until the session is next staged"
);
linked.push(LinkedProfileHome {
session_id: session.id.clone(),
staged_home: worker_root.join("profile"),
profile_home,
});
}
Ok(None) => {}
Err(error) => tracing::warn!(
session_id = %session.id,
"could not link the staged home of a session an earlier release started: {error:#}"
),
}
}
linked
}
fn link_profile_home(worker_root: &Path) -> anyhow::Result<Option<PathBuf>> {
let staged_home = worker_root.join("profile");
match std::fs::symlink_metadata(&staged_home) {
Ok(_) => return Ok(None),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
let Some(installed_home) = installed_harness_home(&worker_root.join("launch.json"))? else {
return Ok(None);
};
if installed_home.starts_with(worker_root) || !installed_home.is_dir() {
return Ok(None);
}
link_directory(&installed_home, &staged_home)?;
Ok(Some(installed_home))
}
#[cfg(unix)]
fn link_directory(target: &Path, link: &Path) -> std::io::Result<()> {
std::os::unix::fs::symlink(target, link)
}
#[cfg(not(unix))]
fn link_directory(_target: &Path, _link: &Path) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"linking a staged home needs a Unix file system",
))
}
pub fn session_has_a_staged_home_of_its_own(session: &mj_core::state::SessionRecord) -> bool {
let Some(TargetLocator::LocalBare { worker_root }) = &session.target else {
return true;
};
if session.harness_kind.nested_home() {
return true;
}
std::fs::symlink_metadata(worker_root.join("profile")).is_ok_and(|metadata| metadata.is_dir())
}
pub fn running_process_arguments(executor: &impl CommandExecutor) -> Option<Vec<String>> {
let command = CommandSpec::new("ps", ["-A", "-ww", "-o", "args="])
.purpose("list running processes before removing files they may be using");
match executor.execute(&command) {
Ok(output) if output.status == 0 => Some(
String::from_utf8_lossy(&output.stdout)
.lines()
.map(str::to_owned)
.collect(),
),
Ok(output) => {
tracing::warn!(
status = output.status,
"could not list running processes: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
None
}
Err(error) => {
tracing::warn!("could not list running processes: {error:#}");
None
}
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use mj_core::config::HarnessKind;
fn session(
id: &str,
harness: HarnessKind,
worker_root: &Path,
) -> mj_core::state::SessionRecord {
let mut session = crate::controller::test_support::checkpoint_test_session(id);
session.harness_kind = harness;
session.target = Some(TargetLocator::LocalBare {
worker_root: worker_root.to_path_buf(),
});
session
}
fn install_launch(worker_root: &Path, launch: serde_json::Value) {
std::fs::create_dir_all(worker_root).unwrap();
std::fs::write(
worker_root.join("launch.json"),
serde_json::to_vec(&launch).unwrap(),
)
.unwrap();
}
#[test]
fn an_earlier_session_s_staged_home_links_to_the_home_its_worker_uses() {
let directory = tempfile::tempdir().unwrap();
let profile_home = directory.path().join(".codex");
std::fs::create_dir_all(profile_home.join("sessions")).unwrap();
std::fs::write(profile_home.join("auth.json"), "{}").unwrap();
let earlier = directory.path().join("workers/earlier");
install_launch(
&earlier,
serde_json::json!({
"harness": "codex",
"harness_home": profile_home,
"environment": {},
}),
);
let older = directory.path().join("workers/older");
install_launch(
&older,
serde_json::json!({
"harness": "codex",
"environment": { "CODEX_HOME": profile_home },
}),
);
let staged = directory.path().join("workers/staged");
std::fs::create_dir_all(staged.join("profile")).unwrap();
install_launch(
&staged,
serde_json::json!({
"harness": "codex",
"harness_home": staged.join("profile"),
"environment": {},
}),
);
let provisioning = directory.path().join("workers/provisioning");
std::fs::create_dir_all(&provisioning).unwrap();
let mut state = State::default();
for (id, root) in [
("earlier", &earlier),
("older", &older),
("staged", &staged),
("provisioning", &provisioning),
] {
state
.sessions
.insert(id.to_owned(), session(id, HarnessKind::Codex, root));
}
let linked = link_profile_homes_of_earlier_sessions(&state);
assert_eq!(
linked
.iter()
.map(|link| link.session_id.as_str())
.collect::<Vec<_>>(),
["earlier", "older"]
);
for id in ["earlier", "older"] {
let TargetLocator::LocalBare { worker_root } =
state.sessions[id].target.as_ref().unwrap()
else {
unreachable!()
};
let link = worker_root.join("profile");
assert_eq!(std::fs::read_link(&link).unwrap(), profile_home);
assert!(link.join("auth.json").is_file());
assert!(!session_has_a_staged_home_of_its_own(&state.sessions[id]));
}
assert!(staged.join("profile").is_dir());
assert!(session_has_a_staged_home_of_its_own(
&state.sessions["staged"]
));
assert!(!provisioning.join("profile").exists());
assert!(link_profile_homes_of_earlier_sessions(&state).is_empty());
}
}