use crate::wait_progress::{Cadence, WaitReporter};
use crate::quiet_ledger;
use crate::thread_quiet::{ThreadClock, current_thread_id};
use monochromatic_lint::run_workers::lint_thread;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::io::Write;
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
use std::time::{Duration, Instant};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GuardFailure {
Start(String),
Panicked(String),
Quiet(Duration),
Ceiling(Duration),
Vanished,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GuardLimits {
pub window: Duration,
pub ceiling: Duration,
}
fn panic_text(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<String>() {
return message.clone();
}
if let Some(message) = payload.downcast_ref::<&str>() {
return String::from(*message);
}
return String::from("a panic payload that is not text");
}
fn notice_line(waiting_for: &str, waited: Duration, window: Duration, repeat: bool) -> String {
let hint: &str = if repeat {
""
} else {
" Hint: it is still working; if it looks stuck, wait for the window or set the policy markdown/autofix to off for this command."
};
return format!(
"cli-git: still waiting for {waiting_for} after {} seconds; giving up on it only after {} seconds with no sign of work.{hint}",
waited.as_secs(),
window.as_secs()
);
}
fn wait_for<T>(
receiver: &Receiver<Result<T, String>>,
thread_numbers: &Receiver<i32>,
limits: GuardLimits,
waiting_for: &str,
reporter: &mut WaitReporter<'_>,
) -> (Result<T, GuardFailure>, Duration) {
let window: Duration = limits.window;
let began: Instant = Instant::now();
let mut clock: Option<ThreadClock> = None;
let mut longest: Duration = Duration::ZERO;
loop {
reporter.poll(|waited, repeat| {
return notice_line(waiting_for, waited, window, repeat);
});
if clock.is_none()
&& let Ok(number) = thread_numbers.try_recv()
{
clock = Some(ThreadClock::start(number, window, false));
}
if let Some(watching) = clock.as_mut() {
let quiet: bool = watching.quiet();
longest = watching.longest_quiet();
if quiet {
return (Err(GuardFailure::Quiet(window)), longest);
}
}
if began.elapsed() >= limits.ceiling {
return (Err(GuardFailure::Ceiling(limits.ceiling)), longest);
}
match receiver.recv_timeout(NOTICE_POLL) {
Ok(Ok(value)) => return (Ok(value), longest),
Ok(Err(reason)) => return (Err(GuardFailure::Panicked(reason)), longest),
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => return (Err(GuardFailure::Vanished), longest),
}
}
}
const NOTICE_POLL: Duration = Duration::from_millis(50);
pub fn run_guarded<T: Send + 'static>(
job: impl FnOnce() -> T + Send + 'static,
limits: GuardLimits,
cadence: Cadence,
waiting_for: &str,
notices: &mut dyn Write,
) -> Result<T, GuardFailure> {
let (sender, receiver): (Sender<Result<T, String>>, Receiver<Result<T, String>>) = channel();
let (numbers, thread_numbers): (Sender<i32>, Receiver<i32>) = channel();
let spawned = lint_thread().name(String::from("cli-git-markdown-lint")).spawn(move || {
if let Some(number) = current_thread_id() {
let _told = numbers.send(number);
}
let outcome: Result<T, String> = match catch_unwind(AssertUnwindSafe(job)) {
Ok(value) => Ok(value),
Err(payload) => Err(panic_text(payload.as_ref())),
};
let _abandoned = sender.send(outcome);
});
if let Err(error) = spawned {
return Err(GuardFailure::Start(error.to_string()));
}
let mut reporter: WaitReporter<'_> = WaitReporter::start(cadence, notices);
let (result, longest): (Result<T, GuardFailure>, Duration) = wait_for(&receiver, &thread_numbers, limits, waiting_for, &mut reporter);
quiet_ledger::note("monochromatic-lint", longest, matches!(result, Err(GuardFailure::Quiet(_))));
return result;
}
#[cfg(test)]
#[path = "markdown_linter_guard_tests.rs"]
mod tests;