use std::fmt;
use std::fs::{self, File, TryLockError};
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use crate::config;
use crate::issue::Repo;
#[derive(Debug, Clone, Copy)]
pub enum Sharing {
UpToTheLimit,
OneOfAKind(PassKind),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PassKind {
Architect,
Security,
}
impl PassKind {
fn name(self) -> &'static str {
match self {
PassKind::Architect => "an Architect run",
PassKind::Security => "a Security run",
}
}
pub fn slug(self) -> &'static str {
match self {
PassKind::Architect => "architect",
PassKind::Security => "security",
}
}
}
#[derive(Debug)]
pub struct AlreadyRunning {
pub repo: Repo,
limit: NonZeroUsize,
why: Why,
}
#[derive(Debug)]
enum Why {
AtLimit { running: usize },
SameKind(PassKind),
Older,
}
impl fmt::Display for AlreadyRunning {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let (repo, limit) = (self.repo.slug(), self.limit);
match self.why {
Why::AtLimit { running } => write!(
f,
"at the Command limit on {repo}: {}, pass.command_limit is {limit}",
commands_running(running)
),
Why::SameKind(kind) => write!(f, "{} is already running on {repo}", kind.name()),
Why::Older => write!(f, "an older thirdshift's Pass is running on {repo}"),
}
}
}
fn commands_running(n: usize) -> String {
match n {
1 => "1 Command is running".to_string(),
n => format!("{n} Commands are running"),
}
}
pub fn hold_slot(repo: &Repo) {
let taken = (|| {
let slots = Slots::of(repo)?;
let _counting = slots.counting()?;
let free = slots.count()?.free;
slots.take(free)
})();
if let Err(error) = taken {
crate::progress::warn(
&error,
format_args!("this Command does not count toward the Command limit"),
);
}
}
pub fn hold_pass(
repo: &Repo,
limit: NonZeroUsize,
sharing: Sharing,
) -> Result<Option<AlreadyRunning>> {
let slots = Slots::of(repo)?;
let _counting = slots.counting()?;
let skipped = |why| {
Ok(Some(AlreadyRunning {
repo: repo.clone(),
limit,
why,
}))
};
let kind = match sharing {
Sharing::UpToTheLimit => None,
Sharing::OneOfAKind(kind) => match slots.try_kind(kind)? {
Some(lock) => Some((kind, lock)),
None => return skipped(Why::SameKind(kind)),
},
};
let held = slots.count()?;
let legacy = try_lock(&legacy_path(repo)?, File::try_lock_shared)?;
let running = held.held + usize::from(legacy.is_none());
if running >= limit.get() {
return skipped(Why::AtLimit { running });
}
let Some(legacy) = legacy else {
return skipped(Why::Older);
};
slots.take(held.free)?;
if let Some((_, lock)) = kind {
hold(lock);
}
hold(legacy);
Ok(None)
}
struct Slots {
dir: PathBuf,
}
struct Count {
held: usize,
free: usize,
}
impl Slots {
fn of(repo: &Repo) -> Result<Self> {
let dir = config::home()?
.join(".thirdshift/command-slots")
.join(repo.owner.to_ascii_lowercase())
.join(repo.name.to_ascii_lowercase());
fs::create_dir_all(&dir).with_context(|| format!("can't create {}", dir.display()))?;
Ok(Slots { dir })
}
fn counting(&self) -> Result<File> {
let path = self.dir.join("count.lock");
let file = open(&path)?;
file.lock()
.with_context(|| format!("can't lock {}", path.display()))?;
Ok(file)
}
fn count(&self) -> Result<Count> {
let mut count = Count { held: 0, free: 0 };
let mut free = None;
for n in 0.. {
let path = self.slot(n);
if !path.exists() {
count.free = free.unwrap_or(n);
return Ok(count);
}
match try_lock(&path, File::try_lock)? {
Some(_) => free = free.or(Some(n)),
None => count.held += 1,
}
}
unreachable!("the slots ran out")
}
fn take(&self, n: usize) -> Result<()> {
let path = self.slot(n);
let slot = try_lock(&path, File::try_lock)?
.with_context(|| format!("{} was taken while counting", path.display()))?;
hold(slot);
Ok(())
}
fn slot(&self, n: usize) -> PathBuf {
self.dir.join(format!("{n}.slot"))
}
fn try_kind(&self, kind: PassKind) -> Result<Option<File>> {
try_lock(
&self.dir.join(format!("{}.lock", kind.slug())),
File::try_lock,
)
}
}
fn legacy_path(repo: &Repo) -> Result<PathBuf> {
let dir = config::home()?
.join(".thirdshift/architect-locks")
.join(repo.owner.to_ascii_lowercase());
fs::create_dir_all(&dir).with_context(|| format!("can't create {}", dir.display()))?;
Ok(dir.join(format!("{}.lock", repo.name.to_ascii_lowercase())))
}
fn open(path: &Path) -> Result<File> {
File::options()
.create(true)
.truncate(false)
.write(true)
.open(path)
.with_context(|| format!("can't open {}", path.display()))
}
fn try_lock(path: &Path, locking: fn(&File) -> Result<(), TryLockError>) -> Result<Option<File>> {
let file = open(path)?;
match locking(&file) {
Ok(()) => Ok(Some(file)),
Err(TryLockError::WouldBlock) => Ok(None),
Err(TryLockError::Error(error)) => {
Err(error).with_context(|| format!("can't lock {}", path.display()))
}
}
}
fn hold(lock: File) {
std::mem::forget(lock);
}