use crate::commands::{OP_TIMEOUT, ready_instance_locked};
use crate::errors;
use crate::images::{self, ImageMeta};
use crate::runtime;
use crate::sdk::Sdk;
use crate::store::{State, Store, now_secs};
use crate::style::Style;
use crate::units::ImageRef;
use anyhow::{Context, Result, bail};
use sha2::{Digest, Sha256};
use std::time::Duration;
const DEFAULT_SNAPSHOT: &str = "committed";
pub fn commit_id(parent: &str, instance_id: &str, now: u64) -> String {
let mut h = Sha256::new();
h.update(format!("commit\n{parent}\n{instance_id}\n{now}\n"));
h.finalize()
.iter()
.take(6)
.map(|b| format!("{b:02x}"))
.collect()
}
pub fn run(instance: &str, tag: Option<&str>, json: bool) -> Result<i32> {
let tag = tag.map(images::normalize_tag).transpose()?;
let sdk = Sdk::locate().map_err(errors::with(errors::DEPENDENCY))?;
let store = Store::open()?;
let found = store.find(instance)?;
let _op = store.lock_instance(&found.id, OP_TIMEOUT)?;
let mut inst = ready_instance_locked(&store, &found.id)?;
let st = Style::stderr();
let total = 4;
let step = |n: usize, text: &str| eprintln!("{} {text}", st.cyan(&format!("[{n}/{total}]")));
let parent = inst
.image_id
.as_deref()
.and_then(|id| images::find(&store, id).ok());
let snapshot = inst
.snapshot
.clone()
.or_else(|| parent.as_ref().and_then(|p| p.snapshot.clone()))
.unwrap_or_else(|| DEFAULT_SNAPSHOT.to_string());
let now = now_secs();
let parent_key = parent.as_ref().map_or(inst.image.clone(), |p| p.id.clone());
let id = commit_id(&parent_key, &inst.id, now);
let mut meta = match &parent {
Some(p) => p.clone(),
None => {
let sys = sdk
.resolve(&ImageRef {
api: inst.api,
minor: None,
flavour: Some(inst.flavour.clone()),
})
.map_err(errors::with(errors::DEPENDENCY))?;
ImageMeta {
id: id.clone(),
fingerprint: String::new(),
tags: vec![],
api: sys.api,
flavour: sys.flavour.clone(),
abi: sys.abi.clone(),
system_image: sys.package.clone(),
system_image_revision: sdk.system_image_revision(&sys)?,
emulator_version: sdk.emulator_version()?,
memory_mb: inst.memory_mb,
cpus: inst.cpus,
disk_mb: inst.disk_mb,
device: inst.device.clone(),
width: inst.width,
height: inst.height,
headless: inst.headless,
gpu: inst.gpu.clone(),
snapshot: None,
launch: None,
created_at: now,
last_used: None,
}
}
};
meta.id = id.clone();
meta.fingerprint = format!("commit:{}:{}:{now}", parent_key, inst.id);
meta.tags = vec![];
meta.snapshot = Some(snapshot.clone());
meta.created_at = now;
meta.last_used = None;
meta.memory_mb = inst.memory_mb;
meta.cpus = inst.cpus;
meta.gpu = inst.gpu.clone();
let avd = store.instance_dir(&inst.id).join("avd");
runtime::ensure_free_space(
&store.root(),
images::dir_private_usage(&avd),
"commit an image",
)?;
step(
1,
&format!("Saving a snapshot of {} ({snapshot:?})", inst.name),
);
crate::build::adb_checked(&sdk, &inst.serial(), &["shell", "sync"], "flushing to disk")?;
let out = crate::build::adb_checked(
&sdk,
&inst.serial(),
&["emu", "avd", "snapshot", "save", &snapshot],
"saving the snapshot",
)?;
if out.lines().any(|l| l.starts_with("KO")) {
bail!("the emulator refused to save the snapshot: {}", out.trim());
}
let snap_dir = avd
.join(format!("{}.avd", runtime::AVD_NAME))
.join("snapshots")
.join(&snapshot);
if !snap_dir.is_dir() {
bail!(
"the snapshot was not created (expected {})",
snap_dir.display()
);
}
step(
2,
"Stopping the instance (its files must not change while they are copied)",
);
inst.set_state(State::Stopping)?;
store.save(&inst)?;
runtime::kill_instance(&sdk, &inst, Duration::from_secs(20));
inst.set_state(State::Stopped)?;
inst.pid = None;
store.save(&inst)?;
step(3, "Copying the instance into a new image");
let partial = store.images_dir().join(format!(".{id}.partial"));
let _ = std::fs::remove_dir_all(&partial);
std::fs::create_dir_all(&partial)?;
let copied = runtime::clone_avd_home(&avd, &partial.join("avd"), meta.api).and_then(|()| {
std::fs::write(
partial.join("image.json"),
serde_json::to_vec_pretty(&meta)?,
)
.context("writing image.json")
});
if let Err(e) = copied {
let _ = std::fs::remove_dir_all(&partial);
return Err(e);
}
remove_locks(&partial.join("avd"));
std::fs::rename(&partial, images::image_dir(&store, &id)).context("publishing the image")?;
step(4, "Done");
if let Some(t) = &tag {
images::apply_tag(&store, &mut meta, t)?;
}
let shown = tag.clone().unwrap_or_else(|| id.clone());
if json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"id": id, "tag": tag, "snapshot": snapshot,
"instance": inst.name, "instance_status": inst.state.as_str(),
}))?
);
} else {
println!("{id}");
}
eprintln!(
"Committed {} as image {shown}. The instance is stopped: `dorker start {}` boots it \
again (cold boot, keeps its data), or `dorker run {shown}` makes a new one that resumes \
from the saved state.",
inst.name, inst.name
);
if inst.auto_remove {
store.remove(&inst)?;
}
Ok(0)
}
fn remove_locks(dir: &std::path::Path) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
remove_locks(&p);
} else if p.extension().is_some_and(|x| x == "lock") {
let _ = std::fs::remove_file(&p);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn commit_ids_are_stable_for_the_same_inputs_and_differ_otherwise() {
let a = commit_id("img", "inst", 100);
assert_eq!(a.len(), 12);
assert_eq!(a, commit_id("img", "inst", 100));
assert_ne!(a, commit_id("img", "inst", 101));
assert_ne!(a, commit_id("img", "other", 100));
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn lock_files_are_removed_recursively() {
let d = crate::testutil::Dir::new();
std::fs::create_dir_all(d.path().join("a/b")).unwrap();
std::fs::write(d.path().join("a/b/x.lock"), b"").unwrap();
std::fs::write(d.path().join("a/keep.img"), b"x").unwrap();
std::fs::write(d.path().join("top.lock"), b"").unwrap();
remove_locks(d.path());
assert!(!d.path().join("a/b/x.lock").exists());
assert!(!d.path().join("top.lock").exists());
assert!(d.path().join("a/keep.img").exists());
}
}