use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::OnceLock;
use std::time::Duration;
pub const STORE_DIRECTORY: &str = "wrapper-store";
pub const PARTIAL_PREFIX: &str = ".partial-";
pub const IDLE_BUILD_AGE: Duration = Duration::from_secs(24 * 3600);
pub const PROGRESS_WINDOW: Duration = Duration::from_secs(10);
pub const WAIT_CAP: Duration = Duration::from_secs(120);
pub const WAIT_POLL: Duration = Duration::from_millis(25);
const LINK_ATTEMPTS: usize = 3;
pub const RESERVE_ATTEMPTS: usize = 64;
static PARTIALS_STARTED: AtomicUsize = AtomicUsize::new(0);
pub type BuildName = Result<String, (std::io::ErrorKind, String)>;
static BUILD_NAME: OnceLock<BuildName> = OnceLock::new();
const CHUNK_BYTES: usize = 1 << 20;
pub fn content_name(path: &Path) -> std::io::Result<String> {
use std::hash::Hasher;
use std::io::Read;
let mut file: std::fs::File = std::fs::File::open(path)?;
let mut hasher: std::hash::DefaultHasher = std::hash::DefaultHasher::new();
let mut chunk: Vec<u8> = vec![0; CHUNK_BYTES];
let mut length: u64 = 0;
loop {
let read: usize = file.read(chunk.as_mut_slice())?;
if read == 0 {
break;
}
hasher.write(&chunk[..read]);
length += u64::try_from(read).map_err(std::io::Error::other)?;
}
return Ok(format!("{:016x}-{length}-cli-git-native", hasher.finish()));
}
pub fn name_once(
cell: &OnceLock<BuildName>,
compute: impl FnOnce() -> std::io::Result<String>,
) -> std::io::Result<String> {
return cell
.get_or_init(|| { return compute().map_err(|error| { return (error.kind(), error.to_string()); }); })
.clone()
.map_err(|(kind, message)| { return std::io::Error::new(kind, message); });
}
fn build_name(built: &Path) -> std::io::Result<String> {
return name_once(&BUILD_NAME, || { return content_name(built); });
}
pub fn shared_copy(built: &Path, root: &Path) -> std::io::Result<PathBuf> {
let name: String = build_name(built)?;
return shared_copy_waiting(built, name.as_str(), root, &|| {}, &|store, stored| { return wait_for_publisher(store, stored); });
}
pub fn shared_copy_waiting(built: &Path, name: &str, root: &Path, before_publish: &dyn Fn(), wait: &dyn Fn(&Path, &Path) -> WaitEnd) -> std::io::Result<PathBuf> {
let store: PathBuf = root.join(STORE_DIRECTORY);
std::fs::create_dir_all(&store)?;
wait(store.as_path(), store.join(name).as_path());
return shared_copy_named(built, name, root, before_publish);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WaitEnd {
Published,
NobodyCopying,
CapReached,
}
pub trait WaitClock {
fn now(&self) -> std::time::SystemTime;
fn pause(&self, poll: Duration);
}
struct SystemClock;
impl WaitClock for SystemClock {
fn now(&self) -> std::time::SystemTime {
return std::time::SystemTime::now();
}
fn pause(&self, poll: Duration) {
std::thread::sleep(poll);
}
}
fn copy_in_progress(store: &Path, window: Duration, now: std::time::SystemTime) -> bool {
use std::os::unix::fs::MetadataExt;
let Ok(entries) = std::fs::read_dir(store) else {
return false;
};
return entries.flatten().any(|entry| {
let Ok(metadata) = entry.metadata() else {
return false;
};
if !entry.file_name().to_string_lossy().starts_with(PARTIAL_PREFIX) || metadata.nlink() != 1 {
return false;
}
return match metadata.modified().map(|modified| { return now.duration_since(modified); }) {
Ok(Ok(age)) => age < window,
Ok(Err(_)) => true,
Err(_) => false,
};
});
}
pub fn wait_for_publisher(store: &Path, stored: &Path) -> WaitEnd {
return wait_for_publisher_on(store, stored, [PROGRESS_WINDOW, WAIT_POLL, WAIT_CAP], &SystemClock);
}
pub fn wait_for_publisher_on(store: &Path, stored: &Path, limits: [Duration; 3], clock: &dyn WaitClock) -> WaitEnd {
let [window, poll, cap] = limits;
let began: std::time::SystemTime = clock.now();
loop {
if stored.exists() {
return WaitEnd::Published;
}
let now: std::time::SystemTime = clock.now();
if !copy_in_progress(store, window, now) {
return WaitEnd::NobodyCopying;
}
if now.duration_since(began).is_ok_and(|waited| { return waited >= cap; }) {
return WaitEnd::CapReached;
}
clock.pause(poll);
}
}
pub fn shared_copy_named(built: &Path, name: &str, root: &Path, before_publish: &dyn Fn()) -> std::io::Result<PathBuf> {
let store: PathBuf = root.join(STORE_DIRECTORY);
std::fs::create_dir_all(&store)?;
let stored: PathBuf = store.join(name);
if stored.exists() {
return Ok(stored);
}
let partial: PathBuf = reserve_partial(store.as_path(), &PARTIALS_STARTED)?;
super::support::copy_executable(built, partial.as_path());
std::fs::set_permissions(&partial, std::fs::metadata(built)?.permissions())?;
if content_name(partial.as_path())? != name {
let _ = std::fs::remove_file(&partial);
return Err(std::io::Error::other(format!(
"{} changed while it was copied: the copy does not have the name {name} read before",
built.display()
)));
}
std::fs::File::open(&partial)?.sync_all()?;
before_publish();
publish(partial.as_path(), stored.as_path())?;
remove_idle_builds(store.as_path(), stored.as_path(), IDLE_BUILD_AGE);
return Ok(stored);
}
pub fn reserve_partial(store: &Path, counter: &AtomicUsize) -> std::io::Result<PathBuf> {
for _ in 0..RESERVE_ATTEMPTS {
let candidate: PathBuf = store.join(format!(
"{PARTIAL_PREFIX}{}-{}",
std::process::id(),
counter.fetch_add(1, Ordering::SeqCst)
));
match std::fs::OpenOptions::new().write(true).create_new(true).open(&candidate) {
Ok(_) => return Ok(candidate),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error),
}
}
return Err(std::io::Error::other(format!(
"every one of {RESERVE_ATTEMPTS} partial names in {} exists",
store.display()
)));
}
pub fn publish(partial: &Path, stored: &Path) -> std::io::Result<()> {
let published: std::io::Result<()> = match std::fs::hard_link(partial, stored) {
Err(error) if error.kind() != std::io::ErrorKind::AlreadyExists => Err(error),
_ => Ok(()),
};
if let Err(error) = std::fs::remove_file(partial) {
eprintln!("wrapper store: could not remove {} ({error})", partial.display());
}
return published;
}
pub fn link_stored(
built: &Path,
root: &Path,
to: &Path,
after_found: &mut dyn FnMut(&Path),
) -> std::io::Result<()> {
for _ in 0..LINK_ATTEMPTS {
let stored: PathBuf = shared_copy(built, root)?;
after_found(stored.as_path());
match std::fs::hard_link(&stored, to) {
Ok(()) => return Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound && !stored.exists() => {}
Err(error) => return Err(error),
}
}
return Err(std::io::Error::other(format!(
"the stored copy of {} was gone {LINK_ATTEMPTS} times before it could be linked",
built.display()
)));
}
fn now_seconds() -> i64 {
return std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |since| { return i64::try_from(since.as_secs()).unwrap_or(i64::MAX); });
}
pub fn remove_idle_builds(store: &Path, keep: &Path, idle: Duration) {
remove_idle_builds_at(store, keep, idle, now_seconds(), &mut |_| {});
}
pub fn remove_idle_builds_at(store: &Path, keep: &Path, idle: Duration, now: i64, after_decision: &mut dyn FnMut(&Path)) {
use std::os::unix::fs::MetadataExt;
let Ok(entries) = std::fs::read_dir(store) else {
return;
};
let idle_seconds: i64 = i64::try_from(idle.as_secs()).unwrap_or(i64::MAX);
for entry in entries.flatten() {
let path: PathBuf = entry.path();
let Ok(metadata) = entry.metadata() else {
continue;
};
if now.saturating_sub(metadata.ctime()) < idle_seconds {
continue;
}
if entry.file_name().to_string_lossy().starts_with(PARTIAL_PREFIX) {
remove_name(&path);
} else if path != keep && metadata.nlink() == 1 {
after_decision(path.as_path());
remove_copy_nobody_linked(store, path.as_path());
}
}
}
fn remove_name(path: &Path) {
if let Err(error) = std::fs::remove_file(path)
&& error.kind() != std::io::ErrorKind::NotFound
{
eprintln!("wrapper store: could not remove {} ({error})", path.display());
}
}
fn remove_copy_nobody_linked(store: &Path, path: &Path) {
use std::os::unix::fs::MetadataExt;
let away: PathBuf = match reserve_partial(store, &PARTIALS_STARTED) {
Ok(reserved) => reserved,
Err(error) => {
eprintln!("wrapper store: could not set {} aside ({error})", path.display());
return;
}
};
if let Err(error) = std::fs::rename(path, &away) {
if error.kind() != std::io::ErrorKind::NotFound {
eprintln!("wrapper store: could not set {} aside ({error})", path.display());
}
remove_name(&away);
return;
}
if std::fs::metadata(&away).is_ok_and(|metadata| { return metadata.nlink() != 1; })
&& let Err(error) = std::fs::hard_link(&away, path)
&& error.kind() != std::io::ErrorKind::AlreadyExists
{
eprintln!("wrapper store: could not restore {} ({error})", path.display());
}
remove_name(&away);
}