use std::process::Command;
use crate::config::{Result, TestbedError};
fn run_applescript(script: &str) -> Result<String> {
let output = Command::new("osascript")
.arg("-e")
.arg(script)
.output()
.map_err(|e| TestbedError::Qcow2Error {
message: format!("running osascript: {e}"),
})?;
if !output.status.success() {
return Err(TestbedError::Qcow2Error {
message: format!(
"osascript failed: {}",
String::from_utf8_lossy(&output.stderr)
),
});
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn find_emulated_nic(uuid: &str) -> Result<u32> {
let script = format!(
r#"
tell application "UTM"
set vm to virtual machine id "{uuid}"
set cfg to configuration of vm
set nis to network interfaces of cfg
repeat with ni in nis
if mode of ni is emulated then
return index of ni
end if
end repeat
return -1
end tell
"#
);
let result = run_applescript(&script)?;
let idx = result.trim().parse::<u32>().map_err(|_| {
TestbedError::Qcow2Error {
message: format!("parsing emulated NIC index from osascript output: {result:?}"),
}
})?;
if idx == u32::MAX || idx == 0 {
}
Ok(idx)
}
pub fn configure_port_forwards(
uuid: &str,
ssh_port: u16,
extra_forwards: &[(u16, u16)],
) -> Result<()> {
let nic_index = find_emulated_nic(uuid)?;
let mut rules = vec![format!(
"set newPortForward to {{protocol:\"TcPp\", guest address:\"\", guest port:\"22\", host address:\"127.0.0.1\", host port:\"{ssh_port}\"}}"
)];
for (guest_port, host_port) in extra_forwards {
rules.push(format!(
"set newPortForward to {{protocol:\"TcPp\", guest address:\"\", guest port:\"{guest_port}\", host address:\"127.0.0.1\", host port:\"{host_port}\"}}"
));
}
let set_rules: String = rules
.iter()
.map(|r| format!("\n {r}\n copy newPortForward to the end of portForwards"))
.collect();
let script = format!(
r#"
tell application "UTM"
set vm to virtual machine id "{uuid}"
set config to configuration of vm
set networkInterfaces to network interfaces of config
repeat with anInterface in networkInterfaces
if index of anInterface is {nic_index} then
set portForwards to {{}}
{set_rules}
set port forwards of anInterface to portForwards
end if
end repeat
update configuration of vm with config
end tell
"#
);
run_applescript(&script)?;
Ok(())
}
pub fn configure_resources(uuid: &str, memory_mib: u32, cpu_cores: u32) -> Result<()> {
let script = format!(
r#"
tell application "UTM"
set vm to virtual machine id "{uuid}"
set cfg to configuration of vm
set memory of cfg to {memory_mib}
set cpu cores of cfg to {cpu_cores}
update configuration of vm with cfg
end tell
"#
);
run_applescript(&script)?;
Ok(())
}
pub fn import_bundle(bundle_path: &str) -> Result<String> {
let before: std::collections::HashSet<String> = super::utmctl::list_vms()?
.into_iter()
.map(|e| e.uuid)
.collect();
let script = format!(
r#"tell application "UTM" to import new virtual machine from POSIX file "{}""#,
bundle_path
);
run_applescript(&script)?;
let start = std::time::Instant::now();
while start.elapsed().as_secs() < 30 {
std::thread::sleep(std::time::Duration::from_secs(2));
for entry in super::utmctl::list_vms()? {
if !before.contains(&entry.uuid) {
return Ok(entry.uuid);
}
}
}
Err(TestbedError::Qcow2Error {
message: "import succeeded but no new VM appeared in utmctl list within 30s".to_string(),
})
}
pub fn verify_imported_bundle(display_name: &str) -> bool {
dirs::home_dir()
.map(|home| {
home.join("Library/Containers/com.utmapp.UTM/Data/Documents")
.join(format!("{display_name}.utm"))
.exists()
})
.unwrap_or(false)
}
pub fn configure_shared_directory(uuid: &str, host_path: &str, mount_tag: &str, readonly: bool) -> Result<()> {
let read_only_str = if readonly { "true" } else { "false" };
let script = format!(
r#"
tell application "UTM"
set vm to first virtual machine where id is "{uuid}"
set sharedDirectory to {{sharedTag:"{mount_tag}", hostPath:"{host_path}", readOnly:{read_only_str}}}
set shared directories of vm to {{sharedDirectory}}
end tell
"#
);
run_applescript(&script)?;
Ok(())
}