From a48d05fac779e53e92fe0e8b5668adf120f319e4 Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Thu, 20 Oct 2022 16:15:21 +0200 Subject: [PATCH] feat(cli): check for updates in background (#15974) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartek IwaƄczuk Co-authored-by: Divy Srivastava --- cli/args/flags.rs | 2 + cli/main.rs | 24 +++++++ cli/tools/upgrade.rs | 166 ++++++++++++++++++++++++++++++++++++++----- cli/version.rs | 8 +++ 4 files changed, 181 insertions(+), 19 deletions(-) diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 5e5d80c4a1..b497376615 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -506,6 +506,8 @@ static ENV_VARIABLES_HELP: &str = r#"ENVIRONMENT VARIABLES: (defaults to $HOME/.deno/bin) DENO_NO_PROMPT Set to disable permission prompts on access (alternative to passing --no-prompt on invocation) + DENO_NO_UPDATE_CHECK Set to disable checking if newer Deno version is + available DENO_WEBGPU_TRACE Directory to use for wgpu traces DENO_JOBS Number of parallel workers used for the --parallel flag with the test subcommand. Defaults to number diff --git a/cli/main.rs b/cli/main.rs index f0cac4f384..5faf9c4014 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -807,6 +807,29 @@ async fn run_with_watch(flags: Flags, script: String) -> Result { Ok(0) } +fn check_for_upgrades(cache_dir: PathBuf) { + // Run a background task that checks for available upgrades. If an earlier + // run of this background task found a new version of Deno, the new version + // number (or commit hash for canary builds) is returned. + let maybe_upgrade_version = tools::upgrade::check_for_upgrades(cache_dir); + + // Print a message if an update is available, unless: + // * stderr is not a tty + // * we're already running the 'deno upgrade' command. + if let Some(upgrade_version) = maybe_upgrade_version { + if atty::is(atty::Stream::Stderr) { + eprint!( + "{} ", + colors::green(format!("Deno {upgrade_version} has been released.")) + ); + eprintln!( + "{}", + colors::italic_gray("Run `deno upgrade` to install it.") + ); + } + } +} + async fn run_command( flags: Flags, run_flags: RunFlags, @@ -824,6 +847,7 @@ async fn run_command( // map specified and bare specifier is used on the command line - this should // probably call `ProcState::resolve` instead let ps = ProcState::build(flags).await?; + check_for_upgrades(ps.dir.root.clone()); let main_module = if NpmPackageReference::from_str(&run_flags.script).is_ok() { ModuleSpecifier::parse(&run_flags.script)? diff --git a/cli/tools/upgrade.rs b/cli/tools/upgrade.rs index bd1fe0c311..0e828925f3 100644 --- a/cli/tools/upgrade.rs +++ b/cli/tools/upgrade.rs @@ -3,6 +3,7 @@ //! This module provides feature to upgrade deno executable use crate::args::UpgradeFlags; +use crate::version; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::futures::StreamExt; @@ -15,12 +16,81 @@ use std::io::Write; use std::path::Path; use std::path::PathBuf; use std::process::Command; +use std::time::Duration; static ARCHIVE_NAME: Lazy = Lazy::new(|| format!("deno-{}.zip", env!("TARGET"))); const RELEASE_URL: &str = "https://github.com/denoland/deno/releases"; +// How often query server for new version. In hours. +const UPGRADE_CHECK_INTERVAL: i64 = 24; +const UPGRADE_CHECK_FILE_NAME: &str = "latest.txt"; + +const UPGRADE_CHECK_FETCH_DELAY: Duration = Duration::from_millis(500); + +pub fn check_for_upgrades(cache_dir: PathBuf) -> Option { + if env::var("DENO_NO_UPDATE_CHECK").is_ok() { + return None; + } + + let p = cache_dir.join(UPGRADE_CHECK_FILE_NAME); + let content = match std::fs::read_to_string(&p) { + Ok(file) => file, + Err(_) => "".to_string(), + }; + + let (last_checked, latest_version) = parse_upgrade_check_file(content); + + if latest_version.is_none() || latest_version.as_ref().unwrap().is_empty() { + let last_checked_dt = last_checked.and_then(|last_checked| { + chrono::DateTime::parse_from_rfc3339(&last_checked) + .map(|dt| dt.with_timezone(&chrono::Utc)) + .ok() + }); + + let should_check = match last_checked_dt { + Some(last_checked_dt) => { + let last_check_age = + chrono::Utc::now().signed_duration_since(last_checked_dt); + last_check_age > chrono::Duration::hours(UPGRADE_CHECK_INTERVAL) + } + None => true, + }; + + if should_check { + tokio::spawn(async move { + // Sleep for a small amount of time to not unnecessarily impact startup + // time. + tokio::time::sleep(UPGRADE_CHECK_FETCH_DELAY).await; + + // Fetch latest version or commit hash from server. + let client = match build_http_client(None) { + Ok(client) => client, + Err(_) => return, + }; + let latest_version = match if version::is_canary() { + get_latest_canary_version(&client).await + } else { + get_latest_release_version(&client).await + } { + Ok(latest_version) => latest_version, + Err(_) => return, + }; + + let contents = + serialize_upgrade_check_file(chrono::Utc::now(), latest_version); + let _ = + std::fs::write(cache_dir.join(UPGRADE_CHECK_FILE_NAME), contents); + }); + } + } + + // Return `Some(version)` if a new version is available, `None` otherwise. + latest_version + .filter(|v| v != version::release_version_or_canary_commit_hash()) +} + pub async fn upgrade(upgrade_flags: UpgradeFlags) -> Result<(), AnyError> { let old_exe_path = std::env::current_exe()?; let permissions = fs::metadata(&old_exe_path)?.permissions(); @@ -29,17 +99,7 @@ pub async fn upgrade(upgrade_flags: UpgradeFlags) -> Result<(), AnyError> { bail!("You do not have write permission to {:?}", old_exe_path); } - let mut client_builder = Client::builder(); - - // If we have been provided a CA Certificate, add it into the HTTP client - let ca_file = upgrade_flags.ca_file.or_else(|| env::var("DENO_CERT").ok()); - if let Some(ca_file) = ca_file { - let buf = std::fs::read(ca_file)?; - let cert = reqwest::Certificate::from_pem(&buf)?; - client_builder = client_builder.add_root_certificate(cert); - } - - let client = client_builder.build()?; + let client = build_http_client(upgrade_flags.ca_file)?; let install_version = match upgrade_flags.version { Some(passed_version) => { @@ -73,8 +133,10 @@ pub async fn upgrade(upgrade_flags: UpgradeFlags) -> Result<(), AnyError> { } None => { let latest_version = if upgrade_flags.canary { + println!("Looking up latest canary version"); get_latest_canary_version(&client).await? } else { + println!("Looking up latest version"); get_latest_release_version(&client).await? }; @@ -140,31 +202,44 @@ pub async fn upgrade(upgrade_flags: UpgradeFlags) -> Result<(), AnyError> { Ok(()) } +fn build_http_client( + ca_file: Option, +) -> Result { + let mut client_builder = + Client::builder().user_agent(version::get_user_agent()); + + // If we have been provided a CA Certificate, add it into the HTTP client + let ca_file = ca_file.or_else(|| env::var("DENO_CERT").ok()); + if let Some(ca_file) = ca_file { + let buf = std::fs::read(ca_file)?; + let cert = reqwest::Certificate::from_pem(&buf)?; + client_builder = client_builder.add_root_certificate(cert); + } + + let client = client_builder.build()?; + + Ok(client) +} + async fn get_latest_release_version( client: &Client, ) -> Result { - println!("Looking up latest version"); - let res = client - .get(&format!("{}/latest", RELEASE_URL)) + .get("https://dl.deno.land/release-latest.txt") .send() .await?; - let version = res.url().path_segments().unwrap().last().unwrap(); - + let version = res.text().await?.trim().to_string(); Ok(version.replace('v', "")) } async fn get_latest_canary_version( client: &Client, ) -> Result { - println!("Looking up latest version"); - let res = client .get("https://dl.deno.land/canary-latest.txt") .send() .await?; let version = res.text().await?.trim().to_string(); - Ok(version) } @@ -311,3 +386,56 @@ fn check_exe(exe_path: &Path) -> Result<(), AnyError> { assert!(output.status.success()); Ok(()) } + +fn parse_upgrade_check_file( + content: String, +) -> (Option, Option) { + let (mut last_checked, mut latest_version) = (None, None); + let split_content = content.split('!').collect::>(); + + if split_content.len() == 2 { + last_checked = Some(split_content[0].to_owned()); + + if !split_content[1].is_empty() { + latest_version = Some(split_content[1].to_owned()); + } + } + + (last_checked, latest_version) +} + +#[test] +fn test_parse_upgrade_check_file() { + let (last_checked, latest_version) = + parse_upgrade_check_file("2020-01-01T00:00:00+00:00!1.2.3".to_string()); + assert_eq!(last_checked, Some("2020-01-01T00:00:00+00:00".to_string())); + assert_eq!(latest_version, Some("1.2.3".to_string())); + + let (last_checked, latest_version) = + parse_upgrade_check_file("2020-01-01T00:00:00+00:00!".to_string()); + assert_eq!(last_checked, Some("2020-01-01T00:00:00+00:00".to_string())); + assert_eq!(latest_version, None); + + let (last_checked, latest_version) = + parse_upgrade_check_file("2020-01-01T00:00:00+00:00".to_string()); + assert_eq!(last_checked, None); + assert_eq!(latest_version, None); +} + +fn serialize_upgrade_check_file( + dt: chrono::DateTime, + version: String, +) -> String { + format!("{}!{}", dt.to_rfc3339(), version) +} + +#[test] +fn test_serialize_upgrade_check_file() { + let s = serialize_upgrade_check_file( + chrono::DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z") + .unwrap() + .with_timezone(&chrono::Utc), + "1.2.3".to_string(), + ); + assert_eq!(s, "2020-01-01T00:00:00+00:00!1.2.3"); +} diff --git a/cli/version.rs b/cli/version.rs index 00301c85de..1a96eb2346 100644 --- a/cli/version.rs +++ b/cli/version.rs @@ -14,6 +14,14 @@ pub fn is_canary() -> bool { option_env!("DENO_CANARY").is_some() } +pub fn release_version_or_canary_commit_hash() -> &'static str { + if is_canary() { + GIT_COMMIT_HASH + } else { + env!("CARGO_PKG_VERSION") + } +} + pub fn get_user_agent() -> String { format!("Deno/{}", deno()) }