blob: e7d3445db80fb3c5ebdff4f5281265957e8a601b [file]
//! The `wslc` backend over the WSL containers C API, `wslcsdk.dll`: the same two
//! containers as over the command line, made through the API Microsoft supports for
//! production.
//!
//! A session made through the API is a VM of our own with its own image store, so the
//! image is provisioned into it first: loaded from a `wslc save` archive when the command
//! line's store has it, pulled from its registry otherwise, and only when the store does
//! not already hold that image id. A session lives while a handle to it is held, so the
//! VM, and everything in it, ends with this process, however it ends.
//!
//! One session per disk at a time, and one per name: a launch takes the first free slot,
//! each a name and a disk of its own with its own image store, so a second concurrent
//! launch provisions a second slot rather than waiting.
//!
//! The API gives a process no terminal and makes no networks, so an interactive launch
//! and the gateway inside the VM stay on the command line.
// the descriptions below are built and tested everywhere, and used only on Windows
#![cfg_attr(not(windows), allow(dead_code))]
use crate::requests::{RunRequest, REQUEST_DIR_INSIDE};
use crate::wslc::{self, Launch, INSIDE_HOST_HOME, INSIDE_ROOT, INSIDE_WORK};
use dm_sandbox::{INSIDE_HOME, INSIDE_PROJECTION};
use dm_wslc_api::{Bind, ContainerSpec, ImageInfo, Mount, Networking, ProcessSpec};
use std::path::{Path, PathBuf};
/// The slots' session names are this, a dash, and the slot number.
const SESSION_PREFIX: &str = "durable-memory";
/// How many launches may run at once, each in a slot of its own.
const SLOTS: usize = 8;
/// The shared volume's ceiling; it is dynamically sized and holds requests, exit codes,
/// captured output and the egress socket.
const VOLUME_BYTES: u64 = 4 << 30;
/// The harness user, which the image creates; the API has no user setting, so the
/// harness container's first process drops to it.
const HARNESS_UID: u32 = 1000;
/// The host directories a launch mounts, as Windows names them.
#[derive(Clone, Debug)]
pub(crate) struct Mounts {
/// The record root.
pub root: String,
/// The working tree.
pub work: String,
/// The harness's private home.
pub home: String,
/// The rendered projection.
pub projection: Option<String>,
/// The host home and the login directories under it, read-only.
pub host_home: Option<(String, Vec<String>)>,
}
/// The egress container: this binary as `amem inside`, the record, the shared volume,
/// the logins read-only, and the session's bridge as its only way out.
pub(crate) fn egress_spec(
l: &Launch,
name: &str,
volume: &str,
m: &Mounts,
extra: &[(String, String)],
) -> ContainerSpec {
let mut binds = vec![Bind {
host: m.root.clone(),
container: INSIDE_ROOT.into(),
read_only: false,
}];
if let Some((home, dirs)) = &m.host_home {
binds.extend(dirs.iter().map(|d| Bind {
host: format!("{home}{}{d}", std::path::MAIN_SEPARATOR),
container: format!("{INSIDE_HOST_HOME}/{d}"),
read_only: true,
}));
}
ContainerSpec {
image: l.image.clone(),
name: Some(name.to_string()),
networking: Networking::Bridged,
binds,
mounts: vec![shared(volume)],
init: ProcessSpec {
argv: wslc::inside_command(l),
env: wslc::egress_env(&wslc::tap_credentials(&l.root), extra),
cwd: None,
},
auto_remove: false,
}
}
/// A harness container, from one request: no network, the shared volume, the private
/// home, the working tree and the projection, run as the harness user, removed on exit.
pub(crate) fn harness_spec(
req: &RunRequest,
n: usize,
image: &str,
volume: &str,
m: &Mounts,
) -> ContainerSpec {
let mut binds = vec![
Bind {
host: m.home.clone(),
container: INSIDE_HOME.into(),
read_only: false,
},
Bind {
host: m.work.clone(),
container: INSIDE_WORK.into(),
read_only: false,
},
];
// the projection's path is the host's own, from the launch, never the request's:
// the request file sits in a volume the harness container can write
if let Some(p) = &m.projection {
binds.push(Bind {
host: p.clone(),
container: INSIDE_PROJECTION.into(),
read_only: true,
});
}
let mut argv = vec![
"/usr/bin/setpriv".to_string(),
format!("--reuid={HARNESS_UID}"),
format!("--regid={HARNESS_UID}"),
"--clear-groups".to_string(),
];
argv.extend(wslc::wrapped(req, n));
ContainerSpec {
image: image.to_string(),
name: None,
networking: Networking::None,
binds,
mounts: vec![shared(volume)],
init: ProcessSpec {
argv,
env: req
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
cwd: Some(req.cwd.clone()),
},
auto_remove: true,
}
}
/// A throwaway container that prints one file from the shared volume.
pub(crate) fn reader_spec(image: &str, volume: &str, file: &str) -> ContainerSpec {
ContainerSpec {
image: image.to_string(),
name: None,
networking: Networking::None,
binds: Vec::new(),
mounts: vec![shared(volume)],
init: ProcessSpec {
argv: vec!["cat".into(), format!("{REQUEST_DIR_INSIDE}/{file}")],
env: Vec::new(),
cwd: None,
},
auto_remove: true,
}
}
fn shared(volume: &str) -> Mount {
Mount {
volume: volume.to_string(),
container: REQUEST_DIR_INSIDE.into(),
read_only: false,
}
}
/// What getting the image into a session's store takes.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Provision {
/// The store holds it, with this id.
Present { id: String },
/// Load it from the command line's store; `stale` is a different image under the
/// same name, to delete first.
Load { stale: Option<String> },
/// Pull it from the registry its name gives.
Pull,
}
/// Decide how to provision `image` from what the session's store holds. The store is
/// trusted when it has the image, since asking the command line boots the command line's
/// VM when it has gone idle; `refresh` asks anyway, for after a rebuild. When asked,
/// the command line's copy is the newer truth: a rebuilt image replaces the session's,
/// and a name only a registry has is pulled again.
pub(crate) fn provision(
image: &str,
store: &[ImageInfo],
refresh: bool,
source_id: impl FnOnce() -> Option<String>,
) -> Provision {
let held = store.iter().find(|i| i.answers_to(image));
if let (Some(h), false) = (held, refresh) {
return Provision::Present { id: h.id.clone() };
}
match (held, source_id()) {
(Some(h), Some(src)) if h.id == dm_wslc_api::spec::bare_id(&src) => {
Provision::Present { id: h.id.clone() }
}
(held, Some(_)) => Provision::Load {
stale: held.map(|h| h.id.clone()),
},
(_, None) => Provision::Pull,
}
}
/// Where the sessions' disks live: given, or under the root's state when the root is on
/// a drive. A session disk cannot live on a `\\wsl.localhost` share, so a root there
/// needs one named.
///
/// # Errors
///
/// When the root is a network path and no storage was given.
pub(crate) fn storage_for(root: &Path, given: Option<&Path>) -> Result<PathBuf, String> {
if let Some(g) = given {
return Ok(g.to_path_buf());
}
let r = root.display().to_string();
if r.starts_with(r"\\") || r.starts_with("//") {
return Err(format!(
"the record root {r} is a network path, where a session disk cannot live; \
give a directory on a local drive with --wslc-storage or MEMORY_WSLC_STORAGE"
));
}
Ok(root.join("state").join("wslc-api"))
}
/// The creator's process id of the session `name`, from `wslc system session list`.
pub(crate) fn creator_of(listing: &str, name: &str) -> Option<u32> {
listing.lines().find_map(|line| {
let cols: Vec<&str> = line.split_whitespace().collect();
match cols.as_slice() {
[_, pid, n] if *n == name => pid.parse().ok(),
_ => None,
}
})
}
/// `ERROR_ALREADY_EXISTS`: another process holds a session by that name.
const ALREADY_EXISTS: u32 = 0x8007_00b7;
/// `ERROR_SHARING_VIOLATION`: another session holds that disk.
const SHARING_VIOLATION: u32 = 0x8007_0020;
/// Take the first free slot: `create` makes slot `n`'s session, and a slot whose name is
/// taken is offered to `reclaim`, which ends the session if its creator is gone. A slot
/// still busy is passed over. The session and its slot number, or `None` when every slot
/// is in use.
///
/// # Errors
///
/// A failure other than the slot being busy.
pub(crate) fn take_slot<S>(
slots: usize,
mut create: impl FnMut(usize) -> Result<S, dm_wslc_api::Error>,
mut reclaim: impl FnMut(usize) -> bool,
) -> Result<Option<(S, usize)>, dm_wslc_api::Error> {
let busy = |e: &dm_wslc_api::Error| e.hr == ALREADY_EXISTS || e.hr == SHARING_VIOLATION;
for n in 0..slots {
match create(n) {
Ok(s) => return Ok(Some((s, n))),
// a reclaimed session's disk may still be releasing; then it is busy too
Err(e) if e.hr == ALREADY_EXISTS && reclaim(n) => match create(n) {
Ok(s) => return Ok(Some((s, n))),
Err(e) if busy(&e) => continue,
Err(e) => return Err(e),
},
Err(e) if busy(&e) => continue,
Err(e) => return Err(e),
}
}
Ok(None)
}
/// The run volumes in a slot's `volumes` directory that are not this run's: a slot is
/// held by one launch at a time, so any other is left by a launch that was killed.
pub(crate) fn stale_volumes(files: impl IntoIterator<Item = String>, own: &str) -> Vec<String> {
files
.into_iter()
.filter_map(|f| f.strip_suffix(".vhdx").map(str::to_string))
.filter(|v| {
v.strip_prefix("memory-run-")
.is_some_and(|pid| !pid.is_empty() && pid.bytes().all(|b| b.is_ascii_digit()))
&& v != own
})
.collect()
}
/// Slot `n`'s session name.
pub(crate) fn slot_name(n: usize) -> String {
format!("{SESSION_PREFIX}-{n}")
}
#[cfg(windows)]
pub(crate) use on_windows::{check, run};
#[cfg(windows)]
mod on_windows {
use super::*;
use crate::requests::DONE;
use dm_wslc_api::{Api, Container, Running, Session, Sink};
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::time::Duration;
type Fallible<T> = Result<T, Box<dyn std::error::Error>>;
/// Open the first free slot's session. A slot whose session outlives the process that
/// made it, killed before it could end it, is taken back rather than passed over:
/// the API ends such a session by itself only a minute or so later.
fn open_slot(api: &Arc<Api>, storage: &Path) -> Fallible<(Session, PathBuf)> {
let dir = |n: usize| storage.join(n.to_string());
let taken = take_slot(
SLOTS,
|n| {
std::fs::create_dir_all(dir(n))
.map_err(|e| dm_wslc_api::Error::other("slot directory", e.to_string()))?;
Session::create(api, &slot_name(n), &dir(n))
},
|n| reclaim(&slot_name(n)),
)?;
let (session, n) = taken.ok_or_else(|| {
format!(
"all {SLOTS} session slots under {} are in use",
storage.display()
)
})?;
Ok((session, dir(n)))
}
/// Delete what killed launches left in this slot. The session's disk keeps their
/// containers, which hold their run volumes, and the volumes; a launch killed before
/// its harness exited leaves a container auto-removal never reached. A stale volume is
/// the sign: it is made before any container. The API lists no containers, so the
/// command line lists this session's, all of them stale while this launch, which has
/// started none yet, holds the slot.
fn sweep(session: &Session, slot: &Path, own: &str) {
let dir = slot.join("volumes");
let Ok(entries) = std::fs::read_dir(&dir) else {
return;
};
let files = entries
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned());
let stale = stale_volumes(files, own);
if stale.is_empty() {
return;
}
let name = format!(
"{SESSION_PREFIX}-{}",
slot.file_name().unwrap_or_default().to_string_lossy()
);
let args = ["--session", &name, "list", "-a", "-q"].map(String::from);
for id in wslc::run_ok(&args).unwrap_or_default().lines() {
if let Ok(c) = session.open_container(id.trim()) {
c.delete().ok();
}
}
for v in stale {
match session.delete_volume(&v) {
Ok(()) => eprintln!("amem: removed {v}, left by a launch that was killed"),
Err(e) => eprintln!("amem: {v}, left by a launch that was killed, stays: {e}"),
}
}
}
/// End the session `name` if the process that created it is gone. Whether it ended.
fn reclaim(name: &str) -> bool {
let Ok(listing) = wslc::run_ok(&["system".into(), "session".into(), "list".into()]) else {
return false;
};
let Some(pid) = creator_of(&listing, name) else {
return false;
};
if process_alive(pid) {
return false;
}
eprintln!("amem: ending {name}, left by process {pid}, which is gone");
let args = ["--session", name, "system", "session", "terminate"].map(String::from);
wslc::run_ok(&args).is_ok()
}
/// Whether a process with this id is running, as `tasklist` sees it.
fn process_alive(pid: u32) -> bool {
Command::new("tasklist")
.args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"])
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
// assume alive when it cannot be told: a live session is never ended
.map_or(true, |o| {
String::from_utf8_lossy(&o.stdout).contains(&format!("\"{pid}\""))
})
}
/// On Ctrl-C, end the session, and every container in it with it, then exit as an
/// interrupted process does; otherwise the session outlives this process for a while.
/// The run's projection goes too: no later run mounts or renders it again.
fn end_on_interrupt(
session: &Arc<Session>,
projection: PathBuf,
) -> Option<tokio::task::JoinHandle<()>> {
let rt = tokio::runtime::Handle::try_current().ok()?;
let s = session.clone();
Some(rt.spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
eprintln!("amem: interrupted; ending the session");
s.terminate().ok();
std::fs::remove_dir_all(&projection).ok();
std::process::exit(130);
}
}))
}
/// The image's id in the command line's store, when it is there.
fn cli_image_id(image: &str) -> Option<String> {
let out = Command::new(wslc::wslc())
.args(["image", "inspect", image])
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
.ok()?;
let v: serde_json::Value = serde_json::from_slice(&out.stdout).ok()?;
v.get(0)?.get("Id")?.as_str().map(str::to_string)
}
/// Make sure the session's store holds the image the launch names.
fn ensure_image(session: &Session, image: &str, refresh: bool, scratch: &Path) -> Fallible<()> {
let store = session.images()?;
match provision(image, &store, refresh, || cli_image_id(image)) {
Provision::Present { id } => {
eprintln!(
"amem: {image} {} from this session's store; after a rebuild, \
`amem wslc-api-check --refresh`",
id.get(..12).unwrap_or(&id)
);
Ok(())
}
Provision::Pull => {
eprintln!("amem: pulling {image} into this session's store");
session.pull_image(image, None).map_err(Into::into)
}
Provision::Load { stale } => {
if let Some(id) = stale {
session.delete_image(&id).ok();
}
load_from_cli(session, image, scratch)
}
}
}
/// `wslc save` the image to a file beside the session's disk, load it, and remove the
/// file.
fn load_from_cli(session: &Session, image: &str, scratch: &Path) -> Fallible<()> {
eprintln!("amem: loading {image} into this session's store; once per image");
let archive = scratch.join("image.tar");
let started = std::time::Instant::now();
let status = Command::new(wslc::wslc())
.args(["save", image])
.stdin(Stdio::null())
.stdout(std::fs::File::create(&archive)?)
.status()?;
let loaded: Fallible<()> = if status.success() {
session.load_image(&archive).map_err(Into::into)
} else {
Err(format!("wslc save {image}: {status}").into())
};
std::fs::remove_file(&archive).ok();
loaded?;
if !session.images()?.iter().any(|i| i.answers_to(image)) {
return Err(
format!("{image} was loaded but the session's store does not list it").into(),
);
}
eprintln!("amem: loaded in {:.0?}", started.elapsed());
Ok(())
}
fn mounts(l: &Launch) -> Fallible<Mounts> {
let home_dir = l.root.join("state/sandbox").join(l.harness.binary());
std::fs::create_dir_all(&home_dir)?;
let (host_home, dirs) = wslc::host_logins()?;
Ok(Mounts {
root: wslc::mount_path(&l.root)?,
work: wslc::mount_path(&l.workdir)?,
home: wslc::mount_path(&home_dir)?,
projection: wslc::projection_mount(l)?,
host_home: host_home.map(|h| (h, dirs)),
})
}
/// Run a launch through the C API. Returns the exit code the inside reports.
///
/// # Errors
///
/// When no slot is free, the image cannot be provisioned, or a container cannot be
/// started.
pub(crate) fn run(l: &Launch, api: &Arc<Api>, storage: &Path) -> Fallible<i32> {
let (session, slot) = open_slot(api, storage)?;
let session = Arc::new(session);
let interrupt = end_on_interrupt(&session, wslc::projection_dir(l));
let volume = format!("memory-run-{}", std::process::id());
sweep(&session, &slot, &volume);
ensure_image(&session, &l.image, false, &slot)?;
let m = mounts(l)?;
let pid = std::process::id();
session.create_volume(&volume, VOLUME_BYTES)?;
let result = (|| -> Fallible<i32> {
let spec = egress_spec(l, &format!("memory-egress-{pid}"), &volume, &m, &[]);
let egress = session.create_container(&spec)?;
// its output is ours, as `wslc logs -f` gives it over the command line
let running = egress.start(Sink::Inherit, Sink::Inherit)?;
let code = drive(&session, &egress, &running, &volume, &l.image, &m);
egress.delete().ok();
code
})();
session.delete_volume(&volume).ok();
if let Some(i) = interrupt {
i.abort();
}
// ended here rather than on drop: the caller exits the process straight after,
// and the interrupt task's reference may not have been dropped by then
session.terminate().ok();
result
}
/// A process in the egress container, its output captured.
fn exec(c: &Container, argv: &[&str]) -> Fallible<String> {
let spec = ProcessSpec {
argv: argv.iter().map(|a| (*a).to_string()).collect(),
env: Vec::new(),
cwd: None,
};
let r = c.exec(&spec, Sink::Capture, Sink::Capture)?;
let code = r.wait();
let (out, err) = r.output();
if code != 0 {
return Err(format!(
"{}: exit {code}: {}",
argv.join(" "),
String::from_utf8_lossy(&err).trim()
)
.into());
}
Ok(String::from_utf8_lossy(&out).into_owned())
}
/// Serve the inside's requests until it writes `done`.
fn drive(
session: &Session,
egress: &Container,
running: &Running,
volume: &str,
image: &str,
m: &Mounts,
) -> Fallible<i32> {
let mut n = 0;
loop {
let listing = match exec(egress, &["ls", REQUEST_DIR_INSIDE]) {
Ok(l) => l,
Err(e) => return after_exit(session, running, volume, image, &e.to_string()),
};
let names: Vec<&str> = listing.lines().map(str::trim).collect();
if names.contains(&DONE) {
let text = exec(egress, &["cat", &format!("{REQUEST_DIR_INSIDE}/{DONE}")])?;
return Ok(text.trim().parse().unwrap_or(1));
}
let req_name = RunRequest::file(n);
if names.contains(&req_name.as_str()) {
let text = exec(
egress,
&["cat", &format!("{REQUEST_DIR_INSIDE}/{req_name}")],
)?;
let req: RunRequest = serde_json::from_str(&text)?;
let harness = session.create_container(&harness_spec(&req, n, image, volume, m))?;
// the harness's output is ours; its exit code goes to the inside via the volume
let _ = harness.start(Sink::Inherit, Sink::Inherit)?.wait();
n += 1;
continue;
}
std::thread::sleep(Duration::from_millis(400));
}
}
/// The egress container has stopped, or cannot be reached: its `done` is still in the
/// volume if it finished properly, readable from a throwaway container.
fn after_exit(
session: &Session,
running: &Running,
volume: &str,
image: &str,
why: &str,
) -> Fallible<i32> {
let exited = running.wait_timeout(Duration::from_secs(5));
let reader = session.create_container(&reader_spec(image, volume, DONE))?;
let r = reader.start(Sink::Capture, Sink::Discard)?;
if r.wait() == 0 {
if let Ok(code) = String::from_utf8_lossy(&r.output().0).trim().parse::<i32>() {
return Ok(code);
}
}
Err(match exited {
Some(code) => format!("the egress container stopped early, exit {code}: {why}"),
None => format!("the egress container stopped answering: {why}"),
}
.into())
}
/// Prove the API end to end: load it, take a slot, provision `image`, and run one
/// container from it with no network, its output and exit code back.
///
/// # Errors
///
/// As [`run`].
pub(crate) fn check(storage: &Path, image: &str, refresh: bool) -> Fallible<String> {
let api = Api::load()?;
let version = api.version()?;
let mut t = Phases::default();
let (session, slot) = open_slot(&api, storage)?;
t.mark("session");
ensure_image(&session, image, refresh, &slot)?;
t.mark("vm and image");
let c = session.create_container(&probe_spec(image))?;
t.mark("create");
let r = c.start(Sink::Capture, Sink::Capture)?;
let code = r.wait();
t.mark("run");
let out = String::from_utf8_lossy(&r.output().0).trim().to_string();
drop((r, c, session));
t.mark("release");
Ok(format!(
"wslcsdk {version}; slot {}; {image} ran: {out:?}, exit {code}\n{t}",
slot.display(),
))
}
/// A container that says who it runs as and exits 3, with no network.
fn probe_spec(image: &str) -> ContainerSpec {
ContainerSpec {
image: image.to_string(),
name: None,
networking: Networking::None,
binds: Vec::new(),
mounts: Vec::new(),
init: ProcessSpec {
argv: vec![
"/bin/sh".into(),
"-c".into(),
"echo up as $(id -u); exit 3".into(),
],
env: Vec::new(),
cwd: None,
},
auto_remove: true,
}
}
/// How long each step took.
struct Phases {
last: std::time::Instant,
started: std::time::Instant,
steps: Vec<(&'static str, Duration)>,
}
impl Default for Phases {
fn default() -> Self {
let now = std::time::Instant::now();
Phases {
last: now,
started: now,
steps: Vec::new(),
}
}
}
impl Phases {
fn mark(&mut self, step: &'static str) {
let now = std::time::Instant::now();
self.steps.push((step, now - self.last));
self.last = now;
}
}
impl std::fmt::Display for Phases {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (step, d) in &self.steps {
write!(f, "{step} {d:.1?}; ")?;
}
write!(f, "total {:.1?}", self.last - self.started)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use dm_sandbox::{AuthMode, Harness};
use std::collections::BTreeMap;
fn mounts() -> Mounts {
Mounts {
root: r"C:\memory".into(),
work: r"C:\proj".into(),
home: r"C:\memory\state\sandbox\claude".into(),
projection: Some(r"C:\memory\state\projection\claude\run-7".into()),
host_home: Some((r"C:\Users\me".into(), vec![".claude".into()])),
}
}
fn launch() -> Launch {
Launch {
projection: "run-7".into(),
root: r"C:\memory".into(),
harness: Harness::Claude,
auth: AuthMode::Subscription,
provider: None,
server_auth: None,
server_provider: None,
workdir: r"C:\proj".into(),
image: "img:dev".into(),
inside_args: vec!["--prompt".into(), "hi".into()],
interactive: false,
via: wslc::Via::Api,
storage: None,
}
}
#[test]
fn a_harness_container_has_no_network_runs_as_the_harness_user_and_is_removed() {
let req = RunRequest {
argv: vec![
"/opt/memory/memory".into(),
"relay".into(),
"--".into(),
"claude".into(),
],
env: BTreeMap::from([("HOME".to_string(), "/home/user".to_string())]),
cwd: "/work".into(),
home: "state/sandbox/claude".into(),
interactive: false,
};
let s = harness_spec(&req, 2, "img:dev", "vol", &mounts());
assert_eq!(s.networking, Networking::None);
assert!(s.auto_remove);
assert_eq!(s.init.cwd.as_deref(), Some("/work"));
assert_eq!(
s.init.env,
vec![("HOME".to_string(), "/home/user".to_string())]
);
assert_eq!(
&s.init.argv[..4],
[
"/usr/bin/setpriv",
"--reuid=1000",
"--regid=1000",
"--clear-groups"
]
);
assert_eq!(&s.init.argv[4..6], ["/bin/sh", "-c"]);
assert!(
s.init.argv[6].ends_with("> /run/memory/req-2.out; echo $? > /run/memory/req-2.exit"),
"{}",
s.init.argv[6]
);
assert_eq!(&s.init.argv[7..], &req.argv[..]);
assert_eq!(s.mounts, vec![shared("vol")]);
let projection = s
.binds
.iter()
.find(|b| b.container == INSIDE_PROJECTION)
.unwrap();
assert!(projection.read_only, "the projection is read-only");
assert_eq!(projection.host, r"C:\memory\state\projection\claude\run-7");
assert!(
s.binds.iter().all(|b| b.host != mounts().root),
"never the record root, only its projection"
);
let none = harness_spec(
&req,
0,
"img:dev",
"vol",
&Mounts {
projection: None,
..mounts()
},
);
assert!(none.binds.iter().all(|b| b.container != INSIDE_PROJECTION));
}
#[test]
fn the_egress_container_has_the_record_the_volume_the_logins_read_only_and_the_bridge() {
let s = egress_spec(
&launch(),
"eg",
"vol",
&mounts(),
&[("K".into(), "v".into())],
);
assert_eq!(s.networking, Networking::Bridged);
assert!(!s.auto_remove);
assert_eq!(s.name.as_deref(), Some("eg"));
assert_eq!(
s.binds[0],
Bind {
host: r"C:\memory".into(),
container: "/memory".into(),
read_only: false
}
);
let login = &s.binds[1];
assert!(
login.read_only && login.container == "/host-home/.claude",
"{login:?}"
);
assert!(login.host.ends_with(".claude"));
assert_eq!(s.mounts, vec![shared("vol")]);
assert_eq!(
s.init.argv[..4],
["/opt/memory/memory", "--root", "/memory", "inside"]
);
assert!(s
.init
.argv
.ends_with(&["--prompt".to_string(), "hi".to_string()]));
assert!(s.init.env.contains(&("K".to_string(), "v".to_string())));
}
#[test]
fn a_reader_cats_one_file_from_the_volume_with_no_network() {
let r = reader_spec("img", "vol", "done");
assert_eq!(r.init.argv, ["cat", "/run/memory/done"]);
assert_eq!(r.networking, Networking::None);
}
fn info(name: &str, id: &str) -> ImageInfo {
ImageInfo {
name: name.into(),
id: id.into(),
size_bytes: 1,
}
}
#[test]
fn the_store_is_trusted_and_the_command_line_asked_only_when_it_lacks_the_image_or_on_refresh()
{
let store = [info("durable-memory/harness:dev", "aa11")];
let img = "durable-memory/harness:dev";
let present = Provision::Present { id: "aa11".into() };
let never = || -> Option<String> { panic!("the command line is not asked") };
assert_eq!(provision(img, &store, false, never), present);
assert_eq!(
provision(img, &store, true, || Some("sha256:AA11".into())),
present
);
assert_eq!(
provision(img, &store, true, || Some("sha256:bb22".into())),
Provision::Load {
stale: Some("aa11".into())
},
"a rebuild replaces the stored image"
);
assert_eq!(
provision(img, &[], false, || Some("bb22".into())),
Provision::Load { stale: None }
);
assert_eq!(
provision(img, &store, true, || None),
Provision::Pull,
"a registry's tag is pulled again"
);
assert_eq!(
provision("ghcr.io/x/harness@sha256:cc", &store, false, || None),
Provision::Pull
);
}
#[test]
fn a_session_disk_lives_under_the_root_unless_the_root_is_a_share() {
assert_eq!(
storage_for(Path::new(r"C:\memory"), None).unwrap(),
Path::new(r"C:\memory").join("state").join("wslc-api")
);
let e = storage_for(Path::new(r"\\wsl.localhost\Ubuntu\home\me\memory"), None).unwrap_err();
assert!(e.contains("--wslc-storage"), "{e}");
assert_eq!(
storage_for(Path::new(r"\\wsl.localhost\U\m"), Some(Path::new(r"D:\vm"))).unwrap(),
Path::new(r"D:\vm")
);
assert_eq!(slot_name(3), "durable-memory-3");
}
fn err(hr: u32) -> dm_wslc_api::Error {
dm_wslc_api::Error {
call: "WslcCreateSession",
hr,
message: String::new(),
}
}
#[test]
fn a_busy_slot_is_passed_over_even_after_a_reclaim_and_any_other_failure_stops() {
// slot 0: taken by a dead launcher, reclaimed, but its disk is still releasing
let mut calls = Vec::new();
let got = take_slot(
3,
|n| {
calls.push(n);
match (n, calls.iter().filter(|c| **c == n).count()) {
(0, 1) => Err(err(ALREADY_EXISTS)),
(0, _) => Err(err(SHARING_VIOLATION)),
_ => Ok(format!("session {n}")),
}
},
|n| n == 0,
);
assert_eq!(
got,
Ok(Some(("session 1".to_string(), 1))),
"the next slot, not a failed launch"
);
assert_eq!(calls, [0, 0, 1]);
// a reclaimed slot is taken when it comes free
let mut first = true;
let got = take_slot(
2,
|_| {
if std::mem::take(&mut first) {
Err(err(ALREADY_EXISTS))
} else {
Ok("s")
}
},
|_| true,
);
assert_eq!(got, Ok(Some(("s", 0))));
// every slot busy, and nothing to reclaim
assert_eq!(
take_slot(2, |_| Err::<(), _>(err(ALREADY_EXISTS)), |_| false),
Ok(None)
);
// anything else is the launch's failure
assert_eq!(
take_slot(2, |_| Err::<(), _>(err(0x8007_0005)), |_| false),
Err(err(0x8007_0005))
);
}
#[test]
fn a_killed_launchs_run_volume_is_swept_and_nothing_else() {
let files = [
"memory-run-33700.vhdx",
"memory-run-41.vhdx",
"memory-run-41.vhdx.lock",
"memory-run-99.vhdx",
"other.vhdx",
"memory-run-.vhdx",
]
.map(String::from);
let mut stale = stale_volumes(files, "memory-run-41");
stale.sort();
assert_eq!(stale, ["memory-run-33700", "memory-run-99"]);
}
#[test]
fn a_slot_session_is_traced_to_the_process_that_made_it() {
let listing = "ID Creator PID Display Name\n\
2 8984 wslc-cli-sstriker\n\
24 44972 durable-memory-0\n\
25 1200 durable-memory-10\n";
assert_eq!(creator_of(listing, "durable-memory-0"), Some(44972));
assert_eq!(
creator_of(listing, "durable-memory-1"),
None,
"not a prefix match"
);
assert_eq!(creator_of(listing, "ID"), None);
assert_eq!(creator_of("", "durable-memory-0"), None);
}
}