use self_update::Status;
use self_update::errors::{Error, Result};
use self_update::update::{Release, ReleaseUpdate, UpdateStatus};
const DEFAULT_GUARD_ENV: &str = "RESTART_GUARD";
#[derive(Default)]
pub struct UpdateBuilder {
release_update: Option<Box<dyn ReleaseUpdate>>,
guard_env: Option<String>,
}
impl UpdateBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn release_update(&mut self, release_update: Box<dyn ReleaseUpdate>) -> &mut Self {
self.release_update = Some(release_update);
self
}
pub fn guard_env(&mut self, env: &str) -> &mut Self {
self.guard_env = Some(env.to_owned());
self
}
pub fn build(&mut self) -> Result<Box<dyn ReleaseUpdate>> {
let inner = self
.release_update
.take()
.ok_or_else(|| Error::Config("`release_update` required".to_owned()))?;
Ok(Box::new(Update {
inner,
guard_env: self
.guard_env
.clone()
.unwrap_or_else(|| DEFAULT_GUARD_ENV.to_owned()),
}))
}
}
pub struct Update {
inner: Box<dyn ReleaseUpdate>,
guard_env: String,
}
impl Update {
pub fn configure() -> UpdateBuilder {
UpdateBuilder::new()
}
pub fn guard_env(&self) -> &str {
&self.guard_env
}
fn restart(&self) -> Result<()> {
let args: Vec<std::ffi::OsString> = std::env::args_os().skip(1).collect();
let mut command = std::process::Command::new(self.inner.bin_install_path());
command.args(&args).env(self.guard_env.clone(), "1");
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let err = command.exec();
Err(Error::Release(format!(
"re-executing updated binary: {}",
err
)))
}
#[cfg(windows)]
{
let status = command
.status()
.map_err(|e| Error::Release(format!("re-executing updated binary: {}", e)))?;
std::process::exit(status.code().unwrap_or(0));
}
}
}
impl ReleaseUpdate for Update {
fn get_latest_release(&self) -> Result<Release> {
self.inner.get_latest_release()
}
fn get_latest_releases(&self, current_version: &str) -> Result<Vec<Release>> {
self.inner.get_latest_releases(current_version)
}
fn get_release_version(&self, ver: &str) -> Result<Release> {
self.inner.get_release_version(ver)
}
fn current_version(&self) -> String {
self.inner.current_version()
}
fn target(&self) -> String {
self.inner.target()
}
fn target_version(&self) -> Option<String> {
self.inner.target_version()
}
fn bin_name(&self) -> String {
self.inner.bin_name()
}
fn bin_install_path(&self) -> std::path::PathBuf {
self.inner.bin_install_path()
}
fn bin_path_in_archive(&self) -> String {
self.inner.bin_path_in_archive()
}
fn show_download_progress(&self) -> bool {
self.inner.show_download_progress()
}
fn show_output(&self) -> bool {
self.inner.show_output()
}
fn no_confirm(&self) -> bool {
self.inner.no_confirm()
}
fn progress_template(&self) -> String {
self.inner.progress_template()
}
fn progress_chars(&self) -> String {
self.inner.progress_chars()
}
fn auth_token(&self) -> Option<String> {
self.inner.auth_token()
}
fn update(&self) -> Result<Status> {
let current_version = self.current_version();
self.update_extended()
.map(|s| s.into_status(current_version))
}
fn update_extended(&self) -> Result<UpdateStatus> {
if std::env::var_os(&self.guard_env).is_some() {
return Ok(UpdateStatus::UpToDate);
}
let status = self.inner.update_extended()?;
if let UpdateStatus::Updated(_) = status {
self.restart()?;
}
Ok(status)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::MockRelease;
#[test]
#[serial_test::serial(env_guard)]
fn update_is_skipped_when_guard_env_is_set() {
let guard = "AUTO_UPDATE_TEST_GUARD_SET";
unsafe { std::env::set_var(guard, "1") };
let mock = MockRelease::new("mock-guard-set").report_updated(true);
let calls = mock.call_counter();
let updater = Update::configure()
.release_update(Box::new(mock))
.guard_env(guard)
.build()
.unwrap();
let status = updater.update().unwrap();
assert!(matches!(status, Status::UpToDate(_)));
assert_eq!(
calls.get(),
0,
"inner update must be skipped when the guard is set"
);
unsafe { std::env::remove_var(guard) };
}
#[test]
#[serial_test::serial(env_guard)]
fn update_extended_is_skipped_when_guard_env_is_set() {
let guard = "AUTO_UPDATE_TEST_GUARD_EXT";
unsafe { std::env::set_var(guard, "1") };
let mock = MockRelease::new("mock-guard-ext");
let calls = mock.call_counter();
let updater = Update::configure()
.release_update(Box::new(mock))
.guard_env(guard)
.build()
.unwrap();
let status = updater.update_extended().unwrap();
assert!(matches!(status, UpdateStatus::UpToDate));
assert_eq!(calls.get(), 0);
unsafe { std::env::remove_var(guard) };
}
#[test]
#[serial_test::serial(env_guard)]
fn update_runs_inner_when_guard_env_is_unset() {
let guard = "AUTO_UPDATE_TEST_GUARD_UNSET";
unsafe { std::env::remove_var(guard) };
let mock = MockRelease::new("mock-guard-unset");
let calls = mock.call_counter();
let updater = Update::configure()
.release_update(Box::new(mock))
.guard_env(guard)
.build()
.unwrap();
let status = updater.update().unwrap();
assert!(matches!(status, Status::UpToDate(_)));
assert_eq!(calls.get(), 1);
}
#[test]
fn guard_env_getter_reports_the_configured_guard() {
let updater = Update {
inner: Box::new(MockRelease::new("mock-guard-getter")),
guard_env: "CUSTOM_GUARD".to_owned(),
};
assert_eq!(updater.guard_env(), "CUSTOM_GUARD");
}
#[test]
fn build_requires_a_release_update() {
assert!(Update::configure().build().is_err());
}
#[test]
fn metadata_methods_delegate_to_inner() {
let updater = Update::configure()
.release_update(Box::new(MockRelease::new("mock-forward")))
.build()
.unwrap();
assert_eq!(updater.current_version(), "1.0.0");
assert_eq!(updater.target(), "test-target");
assert_eq!(updater.target_version(), None);
assert_eq!(updater.bin_name(), "mock-forward");
assert_eq!(
updater.bin_install_path(),
std::env::temp_dir().join("mock-forward")
);
assert_eq!(updater.bin_path_in_archive(), "mock-forward");
assert!(!updater.show_download_progress());
assert!(!updater.show_output());
assert!(updater.no_confirm());
assert_eq!(updater.progress_template(), "");
assert_eq!(updater.progress_chars(), "");
assert_eq!(updater.auth_token(), None);
assert!(updater.get_latest_release().is_ok());
assert!(updater.get_latest_releases("1.0.0").is_ok());
assert!(updater.get_release_version("1.0.0").is_ok());
}
}