use crate::self_update::{self, Announcement, UpdateOutcome};
use anyhow::Result;
use std::io::{IsTerminal, Write};
use std::sync::mpsc::{Receiver, RecvTimeoutError};
use std::time::Duration;
const CHECK_WAIT: Duration = Duration::from_millis(300);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PreAnalysisPrompt {
Ask,
Skip,
}
fn decide_pre_analysis_prompt(update_available: bool, stdin_is_tty: bool) -> PreAnalysisPrompt {
if update_available && stdin_is_tty {
PreAnalysisPrompt::Ask
} else {
PreAnalysisPrompt::Skip
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum PreAnalysisOutcome<R = Receiver<String>> {
NotAsked(Option<R>),
Declined,
}
impl PreAnalysisOutcome {
#[cfg(test)]
fn shape(&self) -> PreAnalysisOutcome<()> {
match self {
Self::NotAsked(receiver) => PreAnalysisOutcome::NotAsked(receiver.as_ref().map(|_| ())),
Self::Declined => PreAnalysisOutcome::Declined,
}
}
}
pub(crate) fn offer_pre_analysis_update(
update_check: Option<Receiver<String>>,
before_reexec: impl FnOnce(),
) -> Result<PreAnalysisOutcome> {
let Some(receiver) = update_check else {
return Ok(PreAnalysisOutcome::NotAsked(None));
};
let version = match receiver.recv_timeout(CHECK_WAIT) {
Ok(version) => version,
Err(RecvTimeoutError::Timeout) => {
return Ok(PreAnalysisOutcome::NotAsked(Some(receiver)));
}
Err(RecvTimeoutError::Disconnected) => {
return Ok(PreAnalysisOutcome::NotAsked(None));
}
};
if decide_pre_analysis_prompt(true, std::io::stdin().is_terminal()) == PreAnalysisPrompt::Skip {
return Ok(PreAnalysisOutcome::NotAsked(None));
}
let current_version = env!("CARGO_PKG_VERSION");
println!("New release found: v{current_version} -> v{version}");
print!("Update to v{version} and re-run? [y/N]: ");
std::io::stdout().flush()?;
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
if !self_update::is_affirmative(&answer) {
return Ok(PreAnalysisOutcome::Declined);
}
update_and_reexec(before_reexec)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AfterUpdate {
Reexec,
ContinueToAnalysis,
}
fn decide_after_update(outcome: UpdateOutcome) -> AfterUpdate {
match outcome {
UpdateOutcome::Updated => AfterUpdate::Reexec,
UpdateOutcome::NotUpdated => AfterUpdate::ContinueToAnalysis,
}
}
fn update_and_reexec(before_reexec: impl FnOnce()) -> Result<PreAnalysisOutcome> {
let outcome = self_update::run_self_update(true, Announcement::AlreadyAnnounced)?;
match decide_after_update(outcome) {
AfterUpdate::Reexec => {
before_reexec();
reexec_current_command()
}
AfterUpdate::ContinueToAnalysis => Ok(PreAnalysisOutcome::NotAsked(None)),
}
}
#[cfg(unix)]
fn reexec_current_command() -> Result<PreAnalysisOutcome> {
use std::os::unix::process::CommandExt;
let exe = std::env::current_exe()?;
let args: Vec<std::ffi::OsString> = std::env::args_os().skip(1).collect();
Err(anyhow::Error::from(
std::process::Command::new(exe).args(args).exec(),
))
}
#[cfg(not(unix))]
fn reexec_current_command() -> Result<PreAnalysisOutcome> {
println!("Re-run rinkaku to analyze with the updated binary");
std::process::exit(0);
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case::should_ask_when_an_update_is_available_and_stdin_is_a_tty(
true,
true,
PreAnalysisPrompt::Ask
)]
#[case::should_skip_when_no_update_is_available(false, true, PreAnalysisPrompt::Skip)]
#[case::should_skip_when_stdin_is_not_a_tty(true, false, PreAnalysisPrompt::Skip)]
#[case::should_skip_when_no_update_is_available_and_stdin_is_not_a_tty(
false,
false,
PreAnalysisPrompt::Skip
)]
fn should_decide_pre_analysis_prompt(
#[case] update_available: bool,
#[case] stdin_is_tty: bool,
#[case] expected: PreAnalysisPrompt,
) {
let actual = decide_pre_analysis_prompt(update_available, stdin_is_tty);
assert_eq!(expected, actual);
}
#[rstest]
#[case::should_reexec_when_the_binary_was_replaced(UpdateOutcome::Updated, AfterUpdate::Reexec)]
#[case::should_continue_to_analysis_when_nothing_was_replaced(
UpdateOutcome::NotUpdated,
AfterUpdate::ContinueToAnalysis
)]
fn should_decide_what_follows_an_accepted_update(
#[case] outcome: UpdateOutcome,
#[case] expected: AfterUpdate,
) {
let actual = decide_after_update(outcome);
assert_eq!(expected, actual);
}
#[test]
fn should_not_ask_when_there_is_no_version_check_at_all() {
let actual = offer_pre_analysis_update(None, || {}).expect("offer");
assert_eq!(PreAnalysisOutcome::NotAsked(None), actual.shape());
}
#[test]
fn should_hand_the_receiver_back_when_the_check_thread_is_still_running() {
let (sender, receiver) = std::sync::mpsc::channel::<String>();
let started_at = std::time::Instant::now();
let actual = offer_pre_analysis_update(Some(receiver), || {}).expect("offer");
let waited = started_at.elapsed();
assert_eq!(PreAnalysisOutcome::NotAsked(Some(())), actual.shape());
assert_eq!(true, waited >= CHECK_WAIT, "waited {waited:?}");
drop(sender);
}
#[test]
fn should_not_hand_the_receiver_back_when_the_check_found_nothing() {
let (sender, receiver) = std::sync::mpsc::channel::<String>();
drop(sender);
let actual = offer_pre_analysis_update(Some(receiver), || {}).expect("offer");
assert_eq!(PreAnalysisOutcome::NotAsked(None), actual.shape());
}
#[test]
fn should_not_prompt_when_a_version_arrives_in_time_but_stdin_is_not_a_tty() {
let (sender, receiver) = std::sync::mpsc::channel::<String>();
sender.send("9.9.9".to_string()).expect("send");
let actual = offer_pre_analysis_update(Some(receiver), || {}).expect("offer");
assert_eq!(PreAnalysisOutcome::NotAsked(None), actual.shape());
}
}