use std::{
env,
ffi::{OsStr, OsString},
panic,
sync::mpsc,
thread,
};
use thiserror::Error;
use super::{ENV_LOCK, ScopedEnv, remove_env_var_unlocked, set_env_var_unlocked};
#[derive(Debug, Error)]
pub(super) enum GuardThreadError {
#[error("the {signal} signal was not received")]
Receive {
signal: &'static str,
#[source]
source: mpsc::RecvError,
},
#[error("the {signal} signal could not be sent")]
Send {
signal: &'static str,
},
}
fn receive_signal(
receiver: &mpsc::Receiver<()>,
signal: &'static str,
) -> Result<(), GuardThreadError> {
receiver
.recv()
.map_err(|source| GuardThreadError::Receive { signal, source })
}
fn send_signal<T>(
sender: &mpsc::Sender<T>,
value: T,
signal: &'static str,
) -> Result<(), GuardThreadError> {
sender
.send(value)
.map_err(|_| GuardThreadError::Send { signal })
}
pub(super) fn join_guard_thread(
handle: thread::JoinHandle<Result<(), GuardThreadError>>,
) -> Result<(), GuardThreadError> {
match handle.join() {
Ok(result) => result,
Err(payload) => panic::resume_unwind(payload),
}
}
pub(super) struct ReleaseOnDrop {
pub(super) sender: Option<mpsc::Sender<()>>,
}
impl Drop for ReleaseOnDrop {
fn drop(&mut self) {
if let Some(sender) = self.sender.take() {
#[expect(
clippy::let_underscore_must_use,
reason = "Receiver may have dropped after a test failure."
)]
let _ = sender.send(());
}
}
}
pub(super) struct RestoreEnv {
pub(super) key: String,
pub(super) original: Option<OsString>,
}
impl Drop for RestoreEnv {
fn drop(&mut self) {
let _guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match &self.original {
Some(value) => set_env_var_unlocked(OsStr::new(&self.key), value.as_os_str()),
None => remove_env_var_unlocked(OsStr::new(&self.key)),
}
}
}
pub(super) struct ThreadAChannels {
pub(super) proceed_rx: mpsc::Receiver<()>,
pub(super) ready_tx: mpsc::Sender<()>,
pub(super) release_rx: mpsc::Receiver<()>,
pub(super) done_tx: mpsc::Sender<()>,
}
pub(super) struct ThreadBChannels {
pub(super) start_rx: mpsc::Receiver<()>,
pub(super) attempt_tx: mpsc::Sender<()>,
pub(super) acquired_tx: mpsc::Sender<Option<String>>,
pub(super) done_tx: mpsc::Sender<()>,
}
pub(super) fn spawn_inner_guard_thread(
key: String,
channels: ThreadBChannels,
) -> thread::JoinHandle<Result<(), GuardThreadError>> {
let ThreadBChannels {
start_rx,
attempt_tx,
acquired_tx,
done_tx,
} = channels;
thread::spawn(move || {
receive_signal(&start_rx, "start")?;
send_signal(&attempt_tx, (), "attempt")?;
let guard = ScopedEnv::apply(&[(key.clone(), Some(String::from("two")))]);
let value = env::var(&key).ok();
send_signal(&acquired_tx, value, "acquired value")?;
drop(guard);
send_signal(&done_tx, (), "completion")
})
}
pub(super) fn spawn_outer_guard_thread(
key: String,
channels: ThreadAChannels,
) -> thread::JoinHandle<Result<(), GuardThreadError>> {
let ThreadAChannels {
proceed_rx,
ready_tx,
release_rx,
done_tx,
} = channels;
thread::spawn(move || {
let guard = ScopedEnv::apply(&[(key, Some(String::from("one")))]);
send_signal(&ready_tx, (), "ready")?;
receive_signal(&proceed_rx, "proceed")?;
receive_signal(&release_rx, "release")?;
drop(guard);
send_signal(&done_tx, (), "completion")
})
}