Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an automatic node self-update feature #1703

Open
wants to merge 2 commits into
base: testnet2-2022
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"

Expand Down
40 changes: 40 additions & 0 deletions snarkos/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<N: Network, E: Environment>(server: Server<N, E>) {
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<mpsc::Sender<Vec<u8>>>) {
match verbosity {
0 => std::env::set_var("RUST_LOG", "info"),
Expand Down
148 changes: 136 additions & 12 deletions snarkos/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,146 @@
// You should have received a copy of the GNU General Public License
// along with the snarkOS library. If not, see <https://www.gnu.org/licenses/>.

#[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<String> {
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<Self> {
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<bool> {
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::<HashMap<String, serde_json::Value>>()
.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::<Vec<_>>())
.collect::<Vec<String>>();

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<String, UpdaterError> {
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()?;

Expand All @@ -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)
Expand All @@ -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<String, UpdaterError> {
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()?;

Expand Down