mirror of
https://github.com/denoland/deno.git
synced 2024-10-29 08:58:01 -04:00
4b6168d5e3
<!-- Before submitting a PR, please read http://deno.land/manual/contributing 1. Give the PR a descriptive title. Examples of good title: - fix(std/http): Fix race condition in server - docs(console): Update docstrings - feat(doc): Handle nested reexports Examples of bad title: - fix #7123 - update docs - fix bugs 2. Ensure there is a related issue and it is referenced in the PR text. 3. Ensure there are tests that cover the changes. 4. Ensure `cargo test` passes. 5. Ensure `./tools/format.js` passes without changing files. 6. Ensure `./tools/lint.js` passes. -->
526 lines
15 KiB
Rust
526 lines
15 KiB
Rust
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
|
|
|
//! This module provides feature to upgrade deno executable
|
|
|
|
use crate::args::UpgradeFlags;
|
|
use crate::colors;
|
|
use crate::version;
|
|
|
|
use deno_core::anyhow::bail;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::futures::StreamExt;
|
|
use deno_runtime::deno_fetch::reqwest;
|
|
use deno_runtime::deno_fetch::reqwest::Client;
|
|
use once_cell::sync::Lazy;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
use std::time::Duration;
|
|
|
|
static ARCHIVE_NAME: Lazy<String> =
|
|
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) {
|
|
if env::var("DENO_NO_UPDATE_CHECK").is_ok() {
|
|
return;
|
|
}
|
|
|
|
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 maybe_file = CheckVersionFile::parse(content);
|
|
|
|
let should_check = match &maybe_file {
|
|
Some(file) => {
|
|
let last_check_age =
|
|
chrono::Utc::now().signed_duration_since(file.last_checked);
|
|
last_check_age > chrono::Duration::hours(UPGRADE_CHECK_INTERVAL)
|
|
}
|
|
None => true,
|
|
};
|
|
|
|
if should_check {
|
|
let cache_dir = cache_dir.clone();
|
|
tokio::spawn(async {
|
|
// 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 file = CheckVersionFile {
|
|
last_prompt: chrono::Utc::now(),
|
|
last_checked: chrono::Utc::now(),
|
|
latest_version,
|
|
};
|
|
file.save(cache_dir);
|
|
});
|
|
}
|
|
|
|
// Return `Some(version)` if a new version is available, `None` otherwise.
|
|
let new_version_available = maybe_file
|
|
.as_ref()
|
|
.map(|f| f.latest_version.to_string())
|
|
.filter(|latest_version| {
|
|
latest_version != version::release_version_or_canary_commit_hash()
|
|
});
|
|
|
|
let should_prompt = match &maybe_file {
|
|
Some(file) => {
|
|
let last_prompt_age =
|
|
chrono::Utc::now().signed_duration_since(file.last_prompt);
|
|
last_prompt_age > chrono::Duration::hours(UPGRADE_CHECK_INTERVAL)
|
|
}
|
|
None => true,
|
|
};
|
|
|
|
// Print a message if an update is available, unless:
|
|
// * stderr is not a tty
|
|
// * we're already running the 'deno upgrade' command.
|
|
if should_prompt {
|
|
if let Some(upgrade_version) = new_version_available {
|
|
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.")
|
|
);
|
|
|
|
if let Some(file) = maybe_file {
|
|
file.with_last_prompt(chrono::Utc::now()).save(cache_dir);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
if permissions.readonly() {
|
|
bail!("You do not have write permission to {:?}", old_exe_path);
|
|
}
|
|
|
|
let client = build_http_client(upgrade_flags.ca_file)?;
|
|
|
|
let install_version = match upgrade_flags.version {
|
|
Some(passed_version) => {
|
|
if upgrade_flags.canary
|
|
&& !regex::Regex::new("^[0-9a-f]{40}$")?.is_match(&passed_version)
|
|
{
|
|
bail!("Invalid commit hash passed");
|
|
} else if !upgrade_flags.canary
|
|
&& semver::Version::parse(&passed_version).is_err()
|
|
{
|
|
bail!("Invalid semver passed");
|
|
}
|
|
|
|
let current_is_passed = if upgrade_flags.canary {
|
|
crate::version::GIT_COMMIT_HASH == passed_version
|
|
} else if !crate::version::is_canary() {
|
|
crate::version::deno() == passed_version
|
|
} else {
|
|
false
|
|
};
|
|
|
|
if !upgrade_flags.force
|
|
&& upgrade_flags.output.is_none()
|
|
&& current_is_passed
|
|
{
|
|
println!("Version {} is already installed", crate::version::deno());
|
|
return Ok(());
|
|
} else {
|
|
passed_version
|
|
}
|
|
}
|
|
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?
|
|
};
|
|
|
|
let current_is_most_recent = if upgrade_flags.canary {
|
|
let latest_hash = latest_version.clone();
|
|
crate::version::GIT_COMMIT_HASH == latest_hash
|
|
} else if !crate::version::is_canary() {
|
|
let current = semver::Version::parse(&crate::version::deno()).unwrap();
|
|
let latest = semver::Version::parse(&latest_version).unwrap();
|
|
current >= latest
|
|
} else {
|
|
false
|
|
};
|
|
|
|
if !upgrade_flags.force
|
|
&& upgrade_flags.output.is_none()
|
|
&& current_is_most_recent
|
|
{
|
|
println!(
|
|
"Local deno version {} is the most recent release",
|
|
crate::version::deno()
|
|
);
|
|
return Ok(());
|
|
} else {
|
|
println!("Found latest version {}", latest_version);
|
|
latest_version
|
|
}
|
|
}
|
|
};
|
|
|
|
let download_url = if upgrade_flags.canary {
|
|
format!(
|
|
"https://dl.deno.land/canary/{}/{}",
|
|
install_version, *ARCHIVE_NAME
|
|
)
|
|
} else {
|
|
format!(
|
|
"{}/download/v{}/{}",
|
|
RELEASE_URL, install_version, *ARCHIVE_NAME
|
|
)
|
|
};
|
|
|
|
let archive_data = download_package(client, &download_url).await?;
|
|
|
|
println!("Deno is upgrading to version {}", &install_version);
|
|
|
|
let new_exe_path = unpack(archive_data, cfg!(windows))?;
|
|
fs::set_permissions(&new_exe_path, permissions)?;
|
|
check_exe(&new_exe_path)?;
|
|
|
|
if !upgrade_flags.dry_run {
|
|
match upgrade_flags.output {
|
|
Some(path) => {
|
|
fs::rename(&new_exe_path, &path)
|
|
.or_else(|_| fs::copy(&new_exe_path, &path).map(|_| ()))?;
|
|
}
|
|
None => replace_exe(&new_exe_path, &old_exe_path)?,
|
|
}
|
|
}
|
|
|
|
println!("Upgraded successfully");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn build_http_client(
|
|
ca_file: Option<String>,
|
|
) -> Result<reqwest::Client, AnyError> {
|
|
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<String, AnyError> {
|
|
let res = client
|
|
.get("https://dl.deno.land/release-latest.txt")
|
|
.send()
|
|
.await?;
|
|
let version = res.text().await?.trim().to_string();
|
|
Ok(version.replace('v', ""))
|
|
}
|
|
|
|
async fn get_latest_canary_version(
|
|
client: &Client,
|
|
) -> Result<String, AnyError> {
|
|
let res = client
|
|
.get("https://dl.deno.land/canary-latest.txt")
|
|
.send()
|
|
.await?;
|
|
let version = res.text().await?.trim().to_string();
|
|
Ok(version)
|
|
}
|
|
|
|
async fn download_package(
|
|
client: Client,
|
|
download_url: &str,
|
|
) -> Result<Vec<u8>, AnyError> {
|
|
println!("Checking {}", &download_url);
|
|
|
|
let res = client.get(download_url).send().await?;
|
|
|
|
if res.status().is_success() {
|
|
let total_size = res.content_length().unwrap() as f64;
|
|
let mut current_size = 0.0;
|
|
let mut data = Vec::with_capacity(total_size as usize);
|
|
let mut stream = res.bytes_stream();
|
|
let mut skip_print = 0;
|
|
let is_tty = atty::is(atty::Stream::Stdout);
|
|
const MEBIBYTE: f64 = 1024.0 * 1024.0;
|
|
while let Some(item) = stream.next().await {
|
|
let bytes = item?;
|
|
current_size += bytes.len() as f64;
|
|
data.extend_from_slice(&bytes);
|
|
if skip_print == 0 {
|
|
if is_tty {
|
|
print!("\u{001b}[1G\u{001b}[2K");
|
|
}
|
|
print!(
|
|
"{:>4.1} MiB / {:.1} MiB ({:^5.1}%)",
|
|
current_size / MEBIBYTE,
|
|
total_size / MEBIBYTE,
|
|
(current_size / total_size) * 100.0,
|
|
);
|
|
std::io::stdout().flush()?;
|
|
skip_print = 10;
|
|
} else {
|
|
skip_print -= 1;
|
|
}
|
|
}
|
|
if is_tty {
|
|
print!("\u{001b}[1G\u{001b}[2K");
|
|
}
|
|
println!(
|
|
"{:.1} MiB / {:.1} MiB (100.0%)",
|
|
current_size / MEBIBYTE,
|
|
total_size / MEBIBYTE
|
|
);
|
|
|
|
Ok(data)
|
|
} else {
|
|
println!("Download could not be found, aborting");
|
|
std::process::exit(1)
|
|
}
|
|
}
|
|
|
|
pub fn unpack(
|
|
archive_data: Vec<u8>,
|
|
is_windows: bool,
|
|
) -> Result<PathBuf, std::io::Error> {
|
|
const EXE_NAME: &str = "deno";
|
|
// We use into_path so that the tempdir is not automatically deleted. This is
|
|
// useful for debugging upgrade, but also so this function can return a path
|
|
// to the newly uncompressed file without fear of the tempdir being deleted.
|
|
let temp_dir = secure_tempfile::TempDir::new()?.into_path();
|
|
let exe_ext = if is_windows { "exe" } else { "" };
|
|
let archive_path = temp_dir.join(EXE_NAME).with_extension("zip");
|
|
let exe_path = temp_dir.join(EXE_NAME).with_extension(exe_ext);
|
|
assert!(!exe_path.exists());
|
|
|
|
let archive_ext = Path::new(&*ARCHIVE_NAME)
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.unwrap();
|
|
let unpack_status = match archive_ext {
|
|
"zip" if cfg!(windows) => {
|
|
fs::write(&archive_path, &archive_data)?;
|
|
Command::new("powershell.exe")
|
|
.arg("-NoLogo")
|
|
.arg("-NoProfile")
|
|
.arg("-NonInteractive")
|
|
.arg("-Command")
|
|
.arg(
|
|
"& {
|
|
param($Path, $DestinationPath)
|
|
trap { $host.ui.WriteErrorLine($_.Exception); exit 1 }
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
[System.IO.Compression.ZipFile]::ExtractToDirectory(
|
|
$Path,
|
|
$DestinationPath
|
|
);
|
|
}",
|
|
)
|
|
.arg("-Path")
|
|
.arg(format!("'{}'", &archive_path.to_str().unwrap()))
|
|
.arg("-DestinationPath")
|
|
.arg(format!("'{}'", &temp_dir.to_str().unwrap()))
|
|
.spawn()?
|
|
.wait()?
|
|
}
|
|
"zip" => {
|
|
fs::write(&archive_path, &archive_data)?;
|
|
Command::new("unzip")
|
|
.current_dir(&temp_dir)
|
|
.arg(archive_path)
|
|
.spawn()
|
|
.map_err(|err| {
|
|
if err.kind() == std::io::ErrorKind::NotFound {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::NotFound,
|
|
"`unzip` was not found on your PATH, please install `unzip`",
|
|
)
|
|
} else {
|
|
err
|
|
}
|
|
})?
|
|
.wait()?
|
|
}
|
|
ext => panic!("Unsupported archive type: '{}'", ext),
|
|
};
|
|
assert!(unpack_status.success());
|
|
assert!(exe_path.exists());
|
|
Ok(exe_path)
|
|
}
|
|
|
|
fn replace_exe(new: &Path, old: &Path) -> Result<(), std::io::Error> {
|
|
if cfg!(windows) {
|
|
// On windows you cannot replace the currently running executable.
|
|
// so first we rename it to deno.old.exe
|
|
fs::rename(old, old.with_extension("old.exe"))?;
|
|
} else {
|
|
fs::remove_file(old)?;
|
|
}
|
|
// Windows cannot rename files across device boundaries, so if rename fails,
|
|
// we try again with copy.
|
|
fs::rename(new, old).or_else(|_| fs::copy(new, old).map(|_| ()))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn check_exe(exe_path: &Path) -> Result<(), AnyError> {
|
|
let output = Command::new(exe_path)
|
|
.arg("-V")
|
|
.stderr(std::process::Stdio::inherit())
|
|
.output()?;
|
|
assert!(output.status.success());
|
|
Ok(())
|
|
}
|
|
|
|
struct CheckVersionFile {
|
|
pub last_prompt: chrono::DateTime<chrono::Utc>,
|
|
pub last_checked: chrono::DateTime<chrono::Utc>,
|
|
pub latest_version: String,
|
|
}
|
|
|
|
impl CheckVersionFile {
|
|
pub fn parse(content: String) -> Option<Self> {
|
|
let split_content = content.split('!').collect::<Vec<_>>();
|
|
|
|
if split_content.len() != 3 {
|
|
return None;
|
|
}
|
|
|
|
let latest_version = split_content[2].trim().to_owned();
|
|
if latest_version.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let last_prompt = chrono::DateTime::parse_from_rfc3339(split_content[0])
|
|
.map(|dt| dt.with_timezone(&chrono::Utc))
|
|
.ok()?;
|
|
let last_checked = chrono::DateTime::parse_from_rfc3339(split_content[1])
|
|
.map(|dt| dt.with_timezone(&chrono::Utc))
|
|
.ok()?;
|
|
|
|
Some(CheckVersionFile {
|
|
last_prompt,
|
|
last_checked,
|
|
latest_version,
|
|
})
|
|
}
|
|
|
|
fn serialize(&self) -> String {
|
|
format!(
|
|
"{}!{}!{}",
|
|
self.last_prompt.to_rfc3339(),
|
|
self.last_checked.to_rfc3339(),
|
|
self.latest_version
|
|
)
|
|
}
|
|
|
|
fn with_last_prompt(self, dt: chrono::DateTime<chrono::Utc>) -> Self {
|
|
Self {
|
|
last_prompt: dt,
|
|
..self
|
|
}
|
|
}
|
|
|
|
fn save(&self, cache_dir: PathBuf) {
|
|
let _ =
|
|
std::fs::write(cache_dir.join(UPGRADE_CHECK_FILE_NAME), self.serialize());
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_upgrade_check_file() {
|
|
let file = CheckVersionFile::parse(
|
|
"2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3".to_string(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
file.last_prompt.to_rfc3339(),
|
|
"2020-01-01T00:00:00+00:00".to_string()
|
|
);
|
|
assert_eq!(
|
|
file.last_checked.to_rfc3339(),
|
|
"2020-01-01T00:00:00+00:00".to_string()
|
|
);
|
|
assert_eq!(file.latest_version, "1.2.3".to_string());
|
|
|
|
let result =
|
|
CheckVersionFile::parse("2020-01-01T00:00:00+00:00!".to_string());
|
|
assert!(result.is_none());
|
|
|
|
let result = CheckVersionFile::parse("garbage!test".to_string());
|
|
assert!(result.is_none());
|
|
|
|
let result = CheckVersionFile::parse("test".to_string());
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_serialize_upgrade_check_file() {
|
|
let file = CheckVersionFile {
|
|
last_prompt: chrono::DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
|
|
.unwrap()
|
|
.with_timezone(&chrono::Utc),
|
|
last_checked: chrono::DateTime::parse_from_rfc3339(
|
|
"2020-01-01T00:00:00Z",
|
|
)
|
|
.unwrap()
|
|
.with_timezone(&chrono::Utc),
|
|
latest_version: "1.2.3".to_string(),
|
|
};
|
|
assert_eq!(
|
|
file.serialize(),
|
|
"2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3"
|
|
);
|
|
}
|
|
}
|