use crate::call_catching_panics::call_catching_panics;
use std::sync::{Arc, Mutex};
use std::thread::{Scope, ScopedJoinHandle};
#[cfg(test)]
thread_local! {
pub(crate) static STARTS_FAIL_ON_THIS_THREAD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
pub(crate) struct RefusedStarts;
#[cfg(test)]
impl RefusedStarts {
pub(crate) fn here() -> Self {
STARTS_FAIL_ON_THIS_THREAD.with(|flag| { flag.set(true); });
return Self;
}
}
#[cfg(test)]
impl Drop for RefusedStarts {
fn drop(&mut self) {
STARTS_FAIL_ON_THIS_THREAD.with(|flag| { flag.set(false); });
}
}
pub enum Started<'scope, T> {
Thread(ScopedJoinHandle<'scope, T>),
Inline(std::thread::Result<T>),
}
impl<T> Started<'_, T> {
pub fn join(self) -> std::thread::Result<T> {
return match self {
Started::Thread(handle) => handle.join(),
Started::Inline(done) => done,
};
}
pub fn ran_inline(&self) -> bool {
return matches!(self, Started::Inline(_));
}
}
fn take_job<J>(slot: &Mutex<Option<J>>) -> Option<J> {
return match slot.lock() {
Ok(mut guard) => guard.take(),
Err(poisoned) => poisoned.into_inner().take(),
};
}
pub fn start_or_run_with<'scope, 'env, S, T>(
scope: &'scope Scope<'scope, 'env>,
state: S,
work: fn(S) -> T,
) -> Started<'scope, T>
where
S: Send + 'scope,
T: Send + 'scope,
{
let slot: Arc<Mutex<Option<(S, fn(S) -> T)>>> = Arc::new(Mutex::new(Some((state, work))));
let theirs: Arc<Mutex<Option<(S, fn(S) -> T)>>> = Arc::clone(&slot);
#[cfg_attr(
dylint_lib = "monochromatic_lint_closure",
expect(
avoidable_closure,
reason = "`Builder::spawn_scoped` accepts only a callable bounded by the scope, and it must carry the slot that holds the job; see package/git-policy/cli/doc/closure-lint-suppressions.md"
)
)]
let outcome: std::io::Result<ScopedJoinHandle<'scope, T>> = match starts() {
Ok(()) => std::thread::Builder::new().spawn_scoped(scope, move || {
let taken: Option<(S, fn(S) -> T)> = take_job(&theirs);
return match taken {
Some((their_state, their_work)) => their_work(their_state),
None => panic!("the job of a started thread was taken by the thread that started it"),
};
}),
Err(error) => Err(error),
};
return match outcome {
Ok(handle) => Started::Thread(handle),
Err(_refused) => {
let taken: Option<(S, fn(S) -> T)> = take_job(&slot);
match taken {
Some((our_state, our_work)) => Started::Inline(call_catching_panics(our_state, our_work)),
None => Started::Inline(Err(Box::new("the job of a thread that did not start was already taken"))),
}
}
};
}
pub fn spawn_named<F, T>(name: &str, work: F) -> std::io::Result<std::thread::JoinHandle<T>>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
starts()?;
return std::thread::Builder::new().name(String::from(name)).spawn(work);
}
fn starts() -> std::io::Result<()> {
#[cfg(test)]
if STARTS_FAIL_ON_THIS_THREAD.with(std::cell::Cell::get) {
return Err(std::io::Error::from_raw_os_error(libc::EAGAIN));
}
return Ok(());
}
#[cfg(test)]
#[path = "scoped_start_tests.rs"]
mod tests;