From d0b38c55996ca499a69d8c2559744d72e640d622 Mon Sep 17 00:00:00 2001 From: ungaro Date: Tue, 10 Sep 2024 18:19:52 -0400 Subject: [PATCH 1/4] new version notification --- leo/cli/cli.rs | 49 +++++++++++++- leo/cli/helpers/updater.rs | 127 ++++++++++++++++++++++++++++++++++--- leo/cli/main.rs | 1 + 3 files changed, 166 insertions(+), 11 deletions(-) diff --git a/leo/cli/cli.rs b/leo/cli/cli.rs index f883589b36..493d284875 100644 --- a/leo/cli/cli.rs +++ b/leo/cli/cli.rs @@ -15,13 +15,50 @@ // along with the Leo library. If not, see . use crate::cli::{commands::*, context::*, helpers::*}; -use clap::Parser; +use clap::{Command as ClapCommand, Parser}; use leo_errors::Result; -use std::{path::PathBuf, process::exit}; +use self_update::version::bump_is_greater; +use std::{path::PathBuf, process::exit, sync::OnceLock}; + +static VERSION_UPDATE_VERSION_STRING: OnceLock = OnceLock::new(); +static HELP_UPDATE_VERSION_STRING: OnceLock = OnceLock::new(); + +fn get_help() -> &'static str { + HELP_UPDATE_VERSION_STRING.get_or_init(|| { + let current_version = env!("CARGO_PKG_VERSION"); + let mut help_output = String::new(); + + if let Ok(Some(latest_version)) = updater::Updater::read_latest_version() { + if let Ok(true) = bump_is_greater(current_version, &latest_version) { + if let Ok(Some(update_message)) = updater::Updater::get_cli_string() { + help_output.push_str(&update_message); + help_output.push('\n'); + } + } + } + help_output + }) +} + +fn get_version() -> &'static str { + VERSION_UPDATE_VERSION_STRING.get_or_init(|| { + let current_version = env!("CARGO_PKG_VERSION"); + let mut version_output = format!("{} \n", current_version); + + if let Ok(Some(latest_version)) = updater::Updater::read_latest_version() { + if let Ok(true) = bump_is_greater(current_version, &latest_version) { + if let Ok(Some(update_message)) = updater::Updater::get_cli_string() { + version_output.push_str(&update_message); + } + } + } + version_output + }) +} /// CLI Arguments entry point - includes global parameters and subcommands #[derive(Parser, Debug)] -#[clap(name = "leo", author = "The Leo Team ", version)] +#[clap(name = "leo", author = "The Leo Team ", version = get_version(), before_help = get_help())] pub struct CLI { #[clap(short, global = true, help = "Print additional information for debugging")] debug: bool, @@ -124,6 +161,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)); @@ -143,6 +185,7 @@ pub fn run_with_args(cli: CLI) -> Result<()> { Commands::Update { command } => command.try_execute(context), } } + #[cfg(test)] mod tests { use crate::cli::{ diff --git a/leo/cli/helpers/updater.rs b/leo/cli/helpers/updater.rs index f1284a4389..794c924439 100644 --- a/leo/cli/helpers/updater.rs +++ b/leo/cli/helpers/updater.rs @@ -19,15 +19,26 @@ use leo_errors::{CliError, Result}; use std::fmt::Write as _; use colored::Colorize; +use dirs; use self_update::{backends::github, version::bump_is_greater, Status}; +use std::{ + 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_LAST_CHECK_FILE: &'static str = "leo_last_update_check"; const LEO_REPO_NAME: &'static str = "leo"; const LEO_REPO_OWNER: &'static str = "AleoHQ"; + // const LEO_UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours + const LEO_UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(5); + // 24 hours + const LEO_VERSION_FILE: &'static str = "leo_latest_version"; /// Show all available releases for `leo`. pub fn show_available_releases() -> Result { @@ -85,15 +96,115 @@ 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, 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, 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) + } + } + + /// 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 { + // Get the cache directory and relevant file paths. + let cache_dir = Self::get_cache_dir()?; + let last_check_file = cache_dir.join(Self::LEO_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 { + // We're not checking for updates, so just return whether we have a stored version + Ok(version_file.exists()) + } + } + + fn update_check_files( + cache_dir: &Path, + last_check_file: &Path, + version_file: &Path, + latest_version: &str, + ) -> Result<(), CliError> { + fs::create_dir_all(cache_dir).map_err(CliError::cli_io_error)?; + + let current_time = Self::get_current_time()?; + + fs::write(last_check_file, ¤t_time.to_string()).map_err(CliError::cli_io_error)?; + fs::write(version_file, latest_version).map_err(CliError::cli_io_error)?; + + Ok(()) + } - tracing::info!("\n{}\n", message); + fn should_check_for_updates(last_check_file: &Path) -> Result { + match fs::read_to_string(last_check_file) { + Ok(contents) => { + let last_check = contents + .parse::() + .map_err(|e| CliError::cli_runtime_error(format!("Failed to parse last check time: {}", e)))?; + let current_time = Self::get_current_time()?; + + Ok(current_time.saturating_sub(last_check) > Self::LEO_UPDATE_CHECK_INTERVAL.as_secs()) + } + Err(_) => Ok(true), } } + + fn get_current_time() -> Result { + 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() -> Result { + Self::get_cache_dir().map(|dir| dir.join(Self::LEO_VERSION_FILE)) + } + + /// Get the cache directory for Leo. + fn get_cache_dir() -> Result { + dirs::cache_dir() + .ok_or_else(|| CliError::cli_runtime_error("Failed to get cache directory".to_string())) + .map(|dir| dir.join("leo")) + } } diff --git a/leo/cli/main.rs b/leo/cli/main.rs index 979446e56b..8ac63bbb5e 100644 --- a/leo/cli/main.rs +++ b/leo/cli/main.rs @@ -18,6 +18,7 @@ use leo_lang::cli::*; use leo_span::symbol::create_session_if_not_set_then; use clap::Parser; +use std::env; fn set_panic_hook() { #[cfg(not(debug_assertions))] From 85a61eaddbe1f8c2d8b2405ade019754976242be Mon Sep 17 00:00:00 2001 From: ungaro Date: Tue, 10 Sep 2024 21:02:00 -0400 Subject: [PATCH 2/4] formatting updates --- leo/cli/cli.rs | 24 ++++++++++++++++++++---- leo/cli/helpers/updater.rs | 30 ++++++++++++++++++++++++------ leo/cli/main.rs | 1 - 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/leo/cli/cli.rs b/leo/cli/cli.rs index 493d284875..4d4d89e9fd 100644 --- a/leo/cli/cli.rs +++ b/leo/cli/cli.rs @@ -15,7 +15,7 @@ // along with the Leo library. If not, see . use crate::cli::{commands::*, context::*, helpers::*}; -use clap::{Command as ClapCommand, Parser}; +use clap::Parser; use leo_errors::Result; use self_update::version::bump_is_greater; use std::{path::PathBuf, process::exit, sync::OnceLock}; @@ -23,14 +23,22 @@ use std::{path::PathBuf, process::exit, sync::OnceLock}; static VERSION_UPDATE_VERSION_STRING: OnceLock = OnceLock::new(); static HELP_UPDATE_VERSION_STRING: OnceLock = OnceLock::new(); -fn get_help() -> &'static str { +/// Generates a static string containing an update notification to be shown before the help message. +/// +/// OnceLock is used because we need a thread-safe way to lazily initialize a static string. +fn show_update_notification_before_help() -> &'static str { HELP_UPDATE_VERSION_STRING.get_or_init(|| { + // Get the current version of the package. let current_version = env!("CARGO_PKG_VERSION"); let mut help_output = String::new(); + // Attempt to read the latest version. if let Ok(Some(latest_version)) = updater::Updater::read_latest_version() { + // Check if the latest version is greater than the current version. if let Ok(true) = bump_is_greater(current_version, &latest_version) { + // If a newer version is available, get the update message. if let Ok(Some(update_message)) = updater::Updater::get_cli_string() { + // Append the update message to the help output. help_output.push_str(&update_message); help_output.push('\n'); } @@ -40,14 +48,22 @@ fn get_help() -> &'static str { }) } -fn get_version() -> &'static str { +/// Generates a static string containing the current version and an update notification if available. +/// +/// OnceLock is used because we need a thread-safe way to lazily initialize a static string. +fn show_version_with_update_notification() -> &'static str { VERSION_UPDATE_VERSION_STRING.get_or_init(|| { + // Get the current version of the package. let current_version = env!("CARGO_PKG_VERSION"); let mut version_output = format!("{} \n", current_version); + // Attempt to read the latest version. if let Ok(Some(latest_version)) = updater::Updater::read_latest_version() { + // Check if the latest version is greater than the current version. if let Ok(true) = bump_is_greater(current_version, &latest_version) { + // If a newer version is available, get the update message. if let Ok(Some(update_message)) = updater::Updater::get_cli_string() { + // Append the update message to the version output. version_output.push_str(&update_message); } } @@ -58,7 +74,7 @@ fn get_version() -> &'static str { /// CLI Arguments entry point - includes global parameters and subcommands #[derive(Parser, Debug)] -#[clap(name = "leo", author = "The Leo Team ", version = get_version(), before_help = get_help())] +#[clap(name = "leo", author = "The Leo Team ", version = show_version_with_update_notification(), before_help = show_update_notification_before_help())] pub struct CLI { #[clap(short, global = true, help = "Print additional information for debugging")] debug: bool, diff --git a/leo/cli/helpers/updater.rs b/leo/cli/helpers/updater.rs index 794c924439..4b738df29e 100644 --- a/leo/cli/helpers/updater.rs +++ b/leo/cli/helpers/updater.rs @@ -32,13 +32,12 @@ pub struct Updater; // TODO Add logic for users to easily select release versions. impl Updater { const LEO_BIN_NAME: &'static str = "leo"; - const LEO_LAST_CHECK_FILE: &'static str = "leo_last_update_check"; + 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"; - // const LEO_UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours - const LEO_UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(5); // 24 hours - const LEO_VERSION_FILE: &'static str = "leo_latest_version"; + const LEO_UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); /// Show all available releases for `leo`. pub fn show_available_releases() -> Result { @@ -133,7 +132,7 @@ impl Updater { pub fn check_for_updates(force: bool) -> Result { // Get the cache directory and relevant file paths. let cache_dir = Self::get_cache_dir()?; - let last_check_file = cache_dir.join(Self::LEO_LAST_CHECK_FILE); + 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. @@ -159,36 +158,55 @@ impl Updater { } } + /// 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, ¤t_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 { match fs::read_to_string(last_check_file) { Ok(contents) => { + // Parse the last check timestamp from the file. let last_check = contents .parse::() .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 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -198,7 +216,7 @@ impl Updater { /// Get the path to the file storing the latest version information. fn get_version_file_path() -> Result { - Self::get_cache_dir().map(|dir| dir.join(Self::LEO_VERSION_FILE)) + Self::get_cache_dir().map(|dir| dir.join(Self::LEO_CACHE_VERSION_FILE)) } /// Get the cache directory for Leo. diff --git a/leo/cli/main.rs b/leo/cli/main.rs index 8ac63bbb5e..979446e56b 100644 --- a/leo/cli/main.rs +++ b/leo/cli/main.rs @@ -18,7 +18,6 @@ use leo_lang::cli::*; use leo_span::symbol::create_session_if_not_set_then; use clap::Parser; -use std::env; fn set_panic_hook() { #[cfg(not(debug_assertions))] From 30f936ee1cf927565a8cd97e58b86d5492eb1aad Mon Sep 17 00:00:00 2001 From: ungaro Date: Wed, 11 Sep 2024 04:12:04 -0400 Subject: [PATCH 3/4] handle edge case --- leo/cli/helpers/updater.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/leo/cli/helpers/updater.rs b/leo/cli/helpers/updater.rs index 4b738df29e..b11df2eae6 100644 --- a/leo/cli/helpers/updater.rs +++ b/leo/cli/helpers/updater.rs @@ -153,8 +153,17 @@ impl Updater { } } } else { - // We're not checking for updates, so just return whether we have a stored version - Ok(version_file.exists()) + 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) + } } } From 836da499f5faa63d60891b03bbe5d1aaa8d39aee Mon Sep 17 00:00:00 2001 From: ungaro Date: Wed, 11 Sep 2024 13:45:02 -0400 Subject: [PATCH 4/4] satisfy clippy warnings --- leo/cli/helpers/updater.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/leo/cli/helpers/updater.rs b/leo/cli/helpers/updater.rs index b11df2eae6..fa32735bfe 100644 --- a/leo/cli/helpers/updater.rs +++ b/leo/cli/helpers/updater.rs @@ -152,18 +152,16 @@ impl Updater { 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 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) } } @@ -184,7 +182,7 @@ impl Updater { let current_time = Self::get_current_time()?; // Write the current time to the last check file. - fs::write(last_check_file, ¤t_time.to_string()).map_err(CliError::cli_io_error)?; + 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)?;