Skip to content

Commit

Permalink
Merge pull request #28440 from ProvableHQ/feat/cli-new-version-notifi…
Browse files Browse the repository at this point in the history
…cation

[Feature] CLI Update Notification.
  • Loading branch information
d0cd authored Nov 15, 2024
2 parents 1e31a90 + bc848ef commit 166a3c3
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 9 deletions.
6 changes: 6 additions & 0 deletions leo/cli/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ pub fn run_with_args(cli: CLI) -> Result<()> {
})?;
}

// Check for updates. If not forced, it checks once per day.
if let Ok(true) = updater::Updater::check_for_updates(false) {
let _ = updater::Updater::print_cli();
}

// Get custom root folder and create context for it.
// If not specified, default context will be created in cwd.
let context = handle_error(Context::new(cli.path, cli.home, false));
Expand All @@ -143,6 +148,7 @@ pub fn run_with_args(cli: CLI) -> Result<()> {
Commands::Update { command } => command.try_execute(context),
}
}

#[cfg(test)]
mod tests {
use crate::cli::{
Expand Down
152 changes: 143 additions & 9 deletions leo/cli/helpers/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,28 @@

use leo_errors::{CliError, Result};

use std::fmt::Write as _;
use aleo_std;

use colored::Colorize;
use self_update::{Status, backends::github, version::bump_is_greater};
use std::{
fmt::Write as _,
fs,
path::{Path, PathBuf},
time::{Duration, SystemTime, UNIX_EPOCH},
};

pub struct Updater;

// TODO Add logic for users to easily select release versions.
impl Updater {
const LEO_BIN_NAME: &'static str = "leo";
const LEO_CACHE_LAST_CHECK_FILE: &'static str = "leo_cache_last_update_check";
const LEO_CACHE_VERSION_FILE: &'static str = "leo_cache_latest_version";
const LEO_REPO_NAME: &'static str = "leo";
const LEO_REPO_OWNER: &'static str = "AleoHQ";
// 24 hours
const LEO_UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);

/// Show all available releases for `leo`.
pub fn show_available_releases() -> Result<String> {
Expand Down Expand Up @@ -85,15 +95,139 @@ impl Updater {
}
}

/// Display the CLI message, if the Leo configuration allows.
pub fn print_cli() {
// If the auto update configuration is off, notify the user to update leo.
if let Ok(latest_version) = Self::update_available() {
let mut message = "🟢 A new version is available! Run".bold().green().to_string();
message += &" `leo update` ".bold().white();
message += &format!("to update to v{latest_version}.").bold().green();
/// Read the latest version from the version file.
pub fn read_latest_version() -> Result<Option<String>, CliError> {
let version_file_path = Self::get_version_file_path();
match fs::read_to_string(version_file_path) {
Ok(version) => Ok(Some(version.trim().to_string())),
Err(_) => Ok(None),
}
}

/// Generate the CLI message if a new version is available.
pub fn get_cli_string() -> Result<Option<String>, CliError> {
if let Some(latest_version) = Self::read_latest_version()? {
let colorized_message = format!(
"\n🟢 {} {} {}",
"A new version is available! Run".bold().green(),
"`leo update`".bold().white(),
format!("to update to v{}.", latest_version).bold().green()
);
Ok(Some(colorized_message))
} else {
Ok(None)
}
}

tracing::info!("\n{}\n", message);
/// Display the CLI message if a new version is available.
pub fn print_cli() -> Result<(), CliError> {
if let Some(message) = Self::get_cli_string()? {
println!("{}", message);
}
Ok(())
}

/// Check for updates, respecting the update interval. (Currently once per day.)
/// If a new version is found, write it to a cache file and alert in every call.
pub fn check_for_updates(force: bool) -> Result<bool, CliError> {
// Get the cache directory and relevant file paths.
let cache_dir = Self::get_cache_dir();
let last_check_file = cache_dir.join(Self::LEO_CACHE_LAST_CHECK_FILE);
let version_file = Self::get_version_file_path();

// Determine if we should check for updates.
let should_check = force || Self::should_check_for_updates(&last_check_file)?;

if should_check {
match Self::update_available() {
Ok(latest_version) => {
// A new version is available
Self::update_check_files(&cache_dir, &last_check_file, &version_file, &latest_version)?;
Ok(true)
}
Err(_) => {
// No new version available or error occurred
// We'll treat both cases as "no update" for simplicity
Self::update_check_files(&cache_dir, &last_check_file, &version_file, env!("CARGO_PKG_VERSION"))?;
Ok(false)
}
}
} else if version_file.exists() {
if let Ok(stored_version) = fs::read_to_string(&version_file) {
let current_version = env!("CARGO_PKG_VERSION");
Ok(bump_is_greater(current_version, stored_version.trim()).map_err(CliError::self_update_error)?)
} else {
// If we can't read the file, assume no update is available
Ok(false)
}
} else {
Ok(false)
}
}

/// Updates the check files with the latest version information and timestamp.
///
/// This function creates the cache directory if it doesn't exist, writes the current time
/// to the last check file, and writes the latest version to the version file.
fn update_check_files(
cache_dir: &Path,
last_check_file: &Path,
version_file: &Path,
latest_version: &str,
) -> Result<(), CliError> {
// Recursively create the cache directory and all of its parent components if they are missing.
fs::create_dir_all(cache_dir).map_err(CliError::cli_io_error)?;

// Get the current time.
let current_time = Self::get_current_time()?;

// Write the current time to the last check file.
fs::write(last_check_file, current_time.to_string()).map_err(CliError::cli_io_error)?;

// Write the latest version to the version file.
fs::write(version_file, latest_version).map_err(CliError::cli_io_error)?;

Ok(())
}

/// Determines if an update check should be performed based on the last check time.
///
/// This function reads the last check timestamp from a file and compares it with
/// the current time to decide if enough time has passed for a new check.
fn should_check_for_updates(last_check_file: &Path) -> Result<bool, CliError> {
match fs::read_to_string(last_check_file) {
Ok(contents) => {
// Parse the last check timestamp from the file.
let last_check = contents
.parse::<u64>()
.map_err(|e| CliError::cli_runtime_error(format!("Failed to parse last check time: {}", e)))?;

// Get the current time.
let current_time = Self::get_current_time()?;

// Check if enough time has passed since the last check.
Ok(current_time.saturating_sub(last_check) > Self::LEO_UPDATE_CHECK_INTERVAL.as_secs())
}
// If we can't read the file, assume we should check
Err(_) => Ok(true),
}
}

/// Gets the current system time as seconds since the Unix epoch.
fn get_current_time() -> Result<u64, CliError> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| CliError::cli_runtime_error(format!("System time error: {}", e)))
.map(|duration| duration.as_secs())
}

/// Get the path to the file storing the latest version information.
fn get_version_file_path() -> PathBuf {
Self::get_cache_dir().join(Self::LEO_CACHE_VERSION_FILE)
}

/// Get the cache directory for Leo.
fn get_cache_dir() -> PathBuf {
aleo_std::aleo_dir().join("leo")
}
}

0 comments on commit 166a3c3

Please sign in to comment.