use crate::sandbox::Sandbox;
use anyhow::{Context, bail};
use std::fs;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Command, ExitStatus, Stdio};
use tracing::{debug, info, warn};
const PROCESS_SKIP_LIST: &[&str] = &[
"kextd",
"mds",
"mds_stores",
"mdworker",
"mdworker_shared",
"notifyd",
];
impl Sandbox {
pub fn mount_bindfs(
&self,
src: &Path,
dest: &Path,
opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let cmd = self.config.bindfs();
Ok(Some(
Command::new(cmd)
.args(opts)
.arg(src)
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
pub fn mount_devfs(
&self,
_src: &Path,
dest: &Path,
opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let cmd = "/sbin/mount_devfs";
Ok(Some(
Command::new(cmd)
.arg("devfs")
.args(opts)
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
pub fn mount_fdfs(
&self,
_src: &Path,
_dest: &Path,
_opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
bail!("fd mounts are not supported on macOS");
}
pub fn mount_nfs(
&self,
src: &Path,
dest: &Path,
opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let cmd = "/sbin/mount_nfs";
Ok(Some(
Command::new(cmd)
.args(opts)
.arg(src)
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
pub fn mount_procfs(
&self,
_src: &Path,
_dest: &Path,
_opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
bail!("procfs mounts are not supported on macOS");
}
pub fn mount_tmpfs(
&self,
_src: &Path,
dest: &Path,
opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let cmd = "/sbin/mount_tmpfs";
Ok(Some(
Command::new(cmd)
.args(opts)
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
fn unmount_common(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
let cmd = "/usr/sbin/diskutil";
let max_retries = 180;
let mut last_status = None;
for attempt in 0..max_retries {
let status = Command::new(cmd)
.arg("unmount")
.arg(dest)
.stdout(Stdio::null())
.stderr(Stdio::null())
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?;
if status.success() {
if attempt > 0 {
debug!(
path = %dest.display(),
retries = attempt,
"Unmount succeeded after retries"
);
}
return Ok(Some(status));
}
last_status = Some(status);
if attempt < max_retries - 1 {
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
Ok(last_status)
}
pub fn unmount_bindfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn unmount_devfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
let cmd = "/sbin/umount";
Ok(Some(
Command::new(cmd)
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
pub fn unmount_fdfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn unmount_nfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn unmount_procfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn unmount_tmpfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn kill_processes_for_path(&self, path: &Path) {
for iteration in 0..super::KILL_PROCESSES_MAX_RETRIES {
let output = Command::new("fuser")
.arg("-c")
.arg(path)
.process_group(0)
.output();
let Ok(out) = output else { return };
let stdout = String::from_utf8_lossy(&out.stdout);
let pids: Vec<&str> = stdout.split_whitespace().collect();
if pids.is_empty() {
return;
}
let pids_to_kill = self.filter_skip_list(&pids);
if pids_to_kill.is_empty() {
debug!(
path = %path.display(),
"All processes in skip list, skipping"
);
return;
}
debug!(
path = %path.display(),
pids = %pids_to_kill.join(" "),
"Killing processes for mount"
);
let _ = Command::new("kill")
.arg("-9")
.args(&pids_to_kill)
.stderr(std::process::Stdio::null())
.process_group(0)
.status();
let delay_ms = super::KILL_PROCESSES_INITIAL_DELAY_MS << iteration;
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
}
fn filter_skip_list(&self, pids: &[&str]) -> Vec<String> {
if PROCESS_SKIP_LIST.is_empty() {
return pids.iter().map(|s| (*s).to_string()).collect();
}
let output = Command::new("ps")
.arg("-o")
.arg("pid=,comm=")
.arg("-p")
.arg(pids.join(","))
.process_group(0)
.output();
let Ok(out) = output else {
return pids.iter().map(|s| (*s).to_string()).collect();
};
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|line| {
let mut parts = line.split_whitespace();
let pid = parts.next()?;
let comm = parts.next()?;
if PROCESS_SKIP_LIST.contains(&comm) {
debug!(pid, name = comm, "Skipping protected process");
return None;
}
Some(pid.to_string())
})
.collect()
}
pub fn kill_processes(&self, sandbox: &Path) {
for iteration in 0..super::KILL_PROCESSES_MAX_RETRIES {
let output = Command::new("lsof")
.arg("+D")
.arg(sandbox)
.process_group(0)
.output();
let Ok(out) = output else {
return;
};
let stdout = String::from_utf8_lossy(&out.stdout);
let pids: Vec<&str> = stdout
.lines()
.skip(1)
.filter_map(|line| line.split_whitespace().nth(1))
.collect();
if pids.is_empty() {
debug!(retries = iteration, "No processes found in sandbox");
return;
}
info!(pids = %pids.join(" "), "Killed processes using sandbox");
let _ = Command::new("kill")
.arg("-9")
.args(&pids)
.stderr(std::process::Stdio::null())
.process_group(0)
.status();
let delay_ms = super::KILL_PROCESSES_INITIAL_DELAY_MS << iteration;
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
let proc_info = self.get_process_info(sandbox);
warn!(
max_retries = super::KILL_PROCESSES_MAX_RETRIES,
remaining = %proc_info,
"Gave up killing processes after max retries"
);
}
fn get_process_info(&self, sandbox: &Path) -> String {
let output = Command::new("lsof")
.arg("+D")
.arg(sandbox)
.process_group(0)
.output();
let Ok(out) = output else {
return String::from("(failed to query)");
};
let stdout = String::from_utf8_lossy(&out.stdout);
let pids: Vec<&str> = stdout
.lines()
.skip(1)
.filter_map(|line| line.split_whitespace().nth(1))
.collect();
if pids.is_empty() {
return String::from("(none)");
}
let ps_output = Command::new("ps")
.arg("-ww")
.arg("-o")
.arg("pid,args")
.arg("-p")
.arg(pids.join(","))
.process_group(0)
.output();
match ps_output {
Ok(out) => String::from_utf8_lossy(&out.stdout)
.lines()
.skip(1)
.filter_map(|line| {
let mut parts = line.split_whitespace();
let pid = parts.next()?;
let cmd: String = parts.collect::<Vec<_>>().join(" ");
Some(format!("pid={} cmd='{}'", pid, cmd))
})
.collect::<Vec<_>>()
.join(", "),
Err(_) => pids
.iter()
.map(|p| format!("pid={}", p))
.collect::<Vec<_>>()
.join(", "),
}
}
}