use std::fs::OpenOptions;
use std::io;
use std::os::windows::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Mutex, PoisonError};
use std::time::{Duration, Instant};
use crate::windows_backend::VolumePath;
const MOUNT_PATIENCE: Duration = Duration::from_secs(30);
static NEXT: AtomicU32 = AtomicU32::new(0);
static CREATING: Mutex<()> = Mutex::new(());
static HELD: Mutex<Vec<char>> = Mutex::new(Vec::new());
const DISKPART_FAILURE: &str = "DiskPart has encountered an error";
pub(crate) fn create_script(vhd: &Path, megabytes: u32, letter: char) -> String {
format!(
"create vdisk file=\"{vhd}\" maximum={megabytes} type=expandable\n\
select vdisk file=\"{vhd}\"\n\
attach vdisk\n\
create partition primary\n\
assign letter={letter}\n",
vhd = vhd.display()
)
}
pub(crate) fn detach_script(vhd: &Path) -> String {
format!(
"select vdisk file=\"{vhd}\"\ndetach vdisk\n",
vhd = vhd.display()
)
}
fn run_diskpart(script: &str, tag: &str) -> io::Result<String> {
let path = std::env::temp_dir().join(format!(
"dioring-diskpart-{tag}-{}-{}.txt",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
std::fs::write(&path, script)?;
let output = Command::new("diskpart").arg("/s").arg(&path).output();
let _ = std::fs::remove_file(&path);
let output = output?;
let printed = String::from_utf8_lossy(&output.stdout);
if output.status.success() && !printed.contains(DISKPART_FAILURE) {
Ok(printed.into_owned())
} else {
Err(io::Error::other(format!(
"diskpart ({}): {printed}",
output.status
)))
}
}
fn letter_is_taken(letter: char) -> bool {
Path::new(&format!("{letter}:\\")).exists()
|| OpenOptions::new()
.access_mode(0)
.open(format!(r"\\.\{letter}:"))
.is_ok()
}
fn free_letter() -> io::Result<char> {
let held = HELD.lock().unwrap_or_else(PoisonError::into_inner);
('D'..='Z')
.rev()
.find(|letter| !held.contains(letter) && !letter_is_taken(*letter))
.ok_or_else(|| io::Error::other("no free drive letter"))
}
fn reserve_letter() -> io::Result<char> {
let letter = free_letter()?;
HELD.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(letter);
Ok(letter)
}
fn release_letter(letter: char) {
HELD.lock()
.unwrap_or_else(PoisonError::into_inner)
.retain(|held| *held != letter);
}
pub(crate) fn scratch_directory() -> PathBuf {
let given = std::env::temp_dir();
match std::fs::canonicalize(&given) {
Ok(canonical) => {
let text = canonical.display().to_string();
match text.strip_prefix(r"\\?\") {
Some(plain) if plain.as_bytes().get(1) == Some(&b':') => PathBuf::from(plain),
_ => canonical,
}
}
Err(_) => given,
}
}
pub(crate) struct ScratchVolume {
vhd: PathBuf,
letter: char,
}
impl ScratchVolume {
pub(crate) fn create(megabytes: u32) -> io::Result<Self> {
let _creating = CREATING.lock().unwrap_or_else(PoisonError::into_inner);
let letter = reserve_letter()?;
let vhd = scratch_directory().join(format!(
"dioring-scratch-{}-{}.vhdx",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_file(&vhd);
if let Err(error) = run_diskpart(&create_script(&vhd, megabytes, letter), "create") {
let _ = run_diskpart(&detach_script(&vhd), "detach");
let _ = std::fs::remove_file(&vhd);
release_letter(letter);
return Err(error);
}
let volume = Self { vhd, letter };
volume.wait_until_mounted()?;
Ok(volume)
}
pub(crate) fn path(&self) -> VolumePath {
VolumePath::drive(self.letter).expect("a drive letter")
}
pub(crate) fn device_path(&self) -> String {
self.path().as_str().to_owned()
}
fn wait_until_mounted(&self) -> io::Result<()> {
let started = Instant::now();
loop {
match OpenOptions::new().access_mode(0).open(self.device_path()) {
Ok(_) => return Ok(()),
Err(error) if started.elapsed() >= MOUNT_PATIENCE => {
return Err(io::Error::other(format!(
"the scratch volume did not mount at {}: {error}",
self.device_path()
)));
}
Err(_) => std::thread::sleep(Duration::from_millis(250)),
}
}
}
}
impl Drop for ScratchVolume {
fn drop(&mut self) {
let _ = run_diskpart(&detach_script(&self.vhd), "detach");
let _ = std::fs::remove_file(&self.vhd);
release_letter(self.letter);
}
}
pub(crate) fn create_disk_script(vhd: &Path, megabytes: u32) -> String {
format!(
"create vdisk file=\"{vhd}\" maximum={megabytes} type=expandable\n\
select vdisk file=\"{vhd}\"\n\
attach vdisk\n\
detail vdisk\n",
vhd = vhd.display()
)
}
pub(crate) fn online_script(disk_number: u32) -> String {
format!(
"select disk {disk_number}\n\
online disk noerr\n\
attributes disk clear readonly noerr\n"
)
}
pub(crate) fn associated_disk(printed: &str, vhd: &Path) -> Result<u32, String> {
let value = |label: &str| -> Vec<&str> {
printed
.lines()
.filter_map(|line| line.trim().strip_prefix(label))
.map(str::trim)
.collect()
};
let names = value("Filename:");
let [name] = names.as_slice() else {
return Err(format!("expected one Filename, found {names:?}"));
};
if !name.eq_ignore_ascii_case(&vhd.display().to_string()) {
return Err(format!(
"diskpart describes {name:?}, not the disk made at {}",
vhd.display()
));
}
let disks = value("Associated disk#:");
let [disk] = disks.as_slice() else {
return Err(format!("expected one associated disk, found {disks:?}"));
};
match disk.parse::<u32>() {
Ok(0) => Err("the associated disk is disk 0, which is never a scratch disk".to_owned()),
Ok(number) => Ok(number),
Err(error) => Err(format!(
"the associated disk {disk:?} is not a number: {error}"
)),
}
}
pub(crate) struct ScratchDisk {
vhd: PathBuf,
disk_number: u32,
megabytes: u32,
}
impl ScratchDisk {
pub(crate) fn create(megabytes: u32) -> io::Result<Self> {
let _creating = CREATING.lock().unwrap_or_else(PoisonError::into_inner);
let vhd = scratch_directory().join(format!(
"dioring-scratch-disk-{}-{}.vhdx",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_file(&vhd);
let printed = run_diskpart(&create_disk_script(&vhd, megabytes), "create-disk");
let named = std::fs::canonicalize(&vhd).map(|path| {
path.display()
.to_string()
.trim_start_matches(r"\\?\")
.to_owned()
});
let disk = match (&printed, named) {
(Ok(printed), Ok(named)) => associated_disk(printed, Path::new(&named))
.map_err(|reason| format!("{reason}\n{printed}")),
(Err(error), _) => Err(error.to_string()),
(_, Err(error)) => Err(format!("the disk file was not made: {error}")),
};
let disk_number = match disk {
Ok(number) => number,
Err(reason) => {
let _ = run_diskpart(&detach_script(&vhd), "detach");
let _ = std::fs::remove_file(&vhd);
return Err(io::Error::other(reason));
}
};
let scratch = Self {
vhd,
disk_number,
megabytes,
};
run_diskpart(&online_script(disk_number), "online")?;
Ok(scratch)
}
pub(crate) fn disk_number(&self) -> u32 {
self.disk_number
}
pub(crate) fn bytes(&self) -> u64 {
u64::from(self.megabytes) * 1024 * 1024
}
}
impl Drop for ScratchDisk {
fn drop(&mut self) {
let _ = run_diskpart(&detach_script(&self.vhd), "detach");
let _ = std::fs::remove_file(&self.vhd);
}
}
#[cfg(test)]
mod tests;