diff --git a/Cargo.lock b/Cargo.lock index 935b08fb87..43386ac256 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2368,6 +2368,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-util", "tower-service", "url", "wasm-bindgen", @@ -2765,8 +2766,10 @@ dependencies = [ "num_cpus", "rand", "rayon", + "reqwest", "rusty-hook", "self_update", + "serde_json", "snarkos", "snarkos-environment", "snarkos-metrics", diff --git a/Cargo.toml b/Cargo.toml index 21730a5820..6c3c586180 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ prometheus = [ "snarkos-metrics/prometheus", "snarkos-network/prometheus" ] rpc = [ "snarkos-rpc" ] task-metrics = [ "snarkos-environment/task-metrics" ] test = [ "snarkos-metrics/test", "snarkos-network/test" ] +auto-update = [ "reqwest", "serde_json", "tokio/process" ] [dependencies.aleo-std] version = "0.1.14" @@ -96,6 +97,15 @@ version = "0.8.0" version = "3.2" features = [ "derive" ] +[dependencies.reqwest] +version = "0.11" +features = [ "json", "stream" ] +optional = true + +[dependencies.serde_json] +version = "1" +optional = true + [dependencies.thiserror] version = "1.0" diff --git a/snarkos/node.rs b/snarkos/node.rs index 1e4d2d0d26..c342cef48a 100644 --- a/snarkos/node.rs +++ b/snarkos/node.rs @@ -212,14 +212,54 @@ impl Node { } } + #[cfg(feature = "auto-update")] + initialize_auto_updater(server).await; + // Note: Do not move this. The pending await must be here otherwise // other snarkOS commands will not exit. + #[cfg(not(feature = "auto-update"))] std::future::pending::<()>().await; Ok(()) } } +#[cfg(feature = "auto-update")] +async fn initialize_auto_updater(server: Server) { + let mut auto_updater = crate::AutoUpdater::new().await.expect("Auto-updater failed"); + + loop { + match auto_updater.check_for_updates().await { + Ok(true) => { + warn!("[auto-updater]: You are using an outdated version of the snarkOS client! Automatically updating..."); + if let Err(e) = auto_updater.update_local_repo().await { + error!("[auto-updater]: Couldn't automatically update the local snarkOS repo: {}", e); + continue; + } + + server.shut_down().await; + + if let Err(e) = auto_updater.rebuild_local_repo().await { + error!("[auto-updater]: Couldn't automatically rebuild the local snarkOS repo: {}", e); + } + + if let Err(e) = auto_updater.restart() { + error!("[auto-updater]: Couldn't automatically restart the node: {}", e); + } + + break; + } + Ok(false) => { + debug!("[auto-updater]: You are using an up-to-date version of the snarkOS client."); + } + Err(e) => { + error!("[auto-updater]: Couldn't check the snarkOS repo for updates: {}", e); + } + } + tokio::time::sleep(std::time::Duration::from_secs(crate::AUTO_UPDATE_INTERVAL_SECS)).await; + } +} + pub fn initialize_logger(verbosity: u8, log_sender: Option>>) { match verbosity { 0 => std::env::set_var("RUST_LOG", "info"), diff --git a/snarkos/updater.rs b/snarkos/updater.rs index 2147b427cd..aabeb61741 100644 --- a/snarkos/updater.rs +++ b/snarkos/updater.rs @@ -14,22 +14,146 @@ // You should have received a copy of the GNU General Public License // along with the snarkOS library. If not, see . +#[cfg(feature = "auto-update")] +use anyhow::anyhow; use colored::Colorize; use self_update::{backends::github, version::bump_is_greater, Status}; use std::fmt::Write; +#[cfg(feature = "auto-update")] +use std::{collections::HashMap, str}; +#[cfg(feature = "auto-update")] +use tokio::process::Command; + +const SNARKOS_BIN_NAME: &str = "snarkos"; +const SNARKOS_REPO_NAME: &str = "snarkOS"; +const SNARKOS_REPO_OWNER: &str = "AleoHQ"; + +#[cfg(feature = "auto-update")] +const SNARKOS_CURRENT_BRANCH: &str = "testnet3"; +#[cfg(feature = "auto-update")] +pub(crate) const AUTO_UPDATE_INTERVAL_SECS: u64 = 60 * 15; // 15 minutes + +#[cfg(feature = "auto-update")] +pub struct AutoUpdater { + reqwest_client: reqwest::Client, + pub latest_sha: String, +} pub struct Updater; -impl Updater { - const SNARKOS_BIN_NAME: &'static str = "snarkos"; - const SNARKOS_REPO_NAME: &'static str = "snarkOS"; - const SNARKOS_REPO_OWNER: &'static str = "AleoHQ"; +#[cfg(feature = "auto-update")] +impl AutoUpdater { + pub async fn get_build_sha() -> anyhow::Result { + let local_repo_path = env!("CARGO_MANIFEST_DIR"); + let local_repo_sha_bytes = Command::new("git") + .args(&["rev-parse", "HEAD"]) + .current_dir(local_repo_path) + .output() + .await? + .stdout; + let local_repo_sha = str::from_utf8(&local_repo_sha_bytes)?.trim_end(); + + debug!("[auto-updater]: The local repo SHA is {}", local_repo_sha); + + Ok(local_repo_sha.into()) + } + + pub async fn new() -> anyhow::Result { + let reqwest_client = reqwest::Client::builder().user_agent("curl").build()?; + let latest_sha = Self::get_build_sha().await?; + + Ok(Self { + reqwest_client, + latest_sha, + }) + } + + pub async fn check_for_updates(&mut self) -> anyhow::Result { + let check_endpoint = format!( + "https://api.github.com/repos/{}/{}/commits/{}", + SNARKOS_REPO_OWNER, SNARKOS_REPO_NAME, SNARKOS_CURRENT_BRANCH + ); + let req = self.reqwest_client.get(check_endpoint).build()?; + let remote_repo_sha = self + .reqwest_client + .execute(req) + .await? + .json::>() + .await? + .remove("sha") + .and_then(|v| v.as_str().map(|s| s.to_owned())) + .ok_or_else(|| anyhow!("GitHub's API didn't return the latest SHA"))?; + + debug!("[auto-updater]: The remote repo SHA is {}", remote_repo_sha); + + if self.latest_sha != remote_repo_sha { + self.latest_sha = remote_repo_sha; + Ok(true) + } else { + Ok(false) + } + } + + pub async fn update_local_repo(&self) -> anyhow::Result<()> { + let local_repo_path = env!("CARGO_MANIFEST_DIR"); + let repo_addr = format!("https://github.com/{}/{}", SNARKOS_REPO_OWNER, SNARKOS_REPO_NAME); + + Command::new("git") + .args(&["pull", &repo_addr, SNARKOS_CURRENT_BRANCH]) + .current_dir(local_repo_path) + .output() + .await?; + + debug!("[auto-updater]: Updated the local repo"); + + Ok(()) + } + pub async fn rebuild_local_repo(&self) -> anyhow::Result<()> { + let local_repo_path = env!("CARGO_MANIFEST_DIR"); + + info!("[auto-updater]: Rebuilding the local repo..."); + + Command::new("cargo") + .args(&["build", "--release"]) + .current_dir(local_repo_path) + .output() + .await?; + + info!("[auto-updater]: Rebuilt the local repo; your snarkOS client is now up to date"); + + Ok(()) + } + + #[cfg(target_family = "unix")] + pub fn restart(&self) -> anyhow::Result<()> { + use std::{env, os::unix::process::CommandExt, path::PathBuf}; + + let mut binary_path: PathBuf = env!("CARGO_MANIFEST_DIR").parse().unwrap(); + binary_path.push("target"); + binary_path.push("release"); + + let original_args = env::args() + .skip(1) + .flat_map(|arg| arg.split('=').map(|s| s.to_owned()).collect::>()) + .collect::>(); + + std::process::Command::new("cargo") + .args(&["run", "--release", "--"]) + .args(&original_args) + .current_dir(binary_path) + .exec(); + + Ok(()) + } +} + +impl Updater { /// Show all available releases for `snarkos`. pub fn show_available_releases() -> Result { let releases = github::ReleaseList::configure() - .repo_owner(Self::SNARKOS_REPO_OWNER) - .repo_name(Self::SNARKOS_REPO_NAME) + .repo_owner(SNARKOS_REPO_OWNER) + .repo_name(SNARKOS_REPO_NAME) .build()? .fetch()?; @@ -45,9 +169,9 @@ impl Updater { let mut update_builder = github::Update::configure(); update_builder - .repo_owner(Self::SNARKOS_REPO_OWNER) - .repo_name(Self::SNARKOS_REPO_NAME) - .bin_name(Self::SNARKOS_BIN_NAME) + .repo_owner(SNARKOS_REPO_OWNER) + .repo_name(SNARKOS_REPO_NAME) + .bin_name(SNARKOS_BIN_NAME) .current_version(env!("CARGO_PKG_VERSION")) .show_download_progress(show_output) .no_confirm(true) @@ -64,9 +188,9 @@ impl Updater { /// Check if there is an available update for `aleo` and return the newest release. pub fn update_available() -> Result { let updater = github::Update::configure() - .repo_owner(Self::SNARKOS_REPO_OWNER) - .repo_name(Self::SNARKOS_REPO_NAME) - .bin_name(Self::SNARKOS_BIN_NAME) + .repo_owner(SNARKOS_REPO_OWNER) + .repo_name(SNARKOS_REPO_NAME) + .bin_name(SNARKOS_BIN_NAME) .current_version(env!("CARGO_PKG_VERSION")) .build()?;