use crate::config::Config; use crate::update_manifest::{SignedUpdateManifest, UpdateManifest}; use chrono::{Local, TimeZone}; use console::{style, Emoji}; use indicatif::{ProgressBar, ProgressStyle}; use ring::digest::{Context, Digest, SHA256}; use solana_client::rpc_client::RpcClient; use solana_config_api::ConfigInstruction; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil, Signable}; use solana_sdk::transaction::Transaction; use std::fs::{self, File}; use std::io::{self, BufReader, Read}; use std::path::{Path, PathBuf}; use std::thread::sleep; use std::time::SystemTime; use std::time::{Duration, Instant}; use tempdir::TempDir; use url::Url; static TRUCK: Emoji = Emoji("🚚 ", ""); static LOOKING_GLASS: Emoji = Emoji("🔍 ", ""); static BULLET: Emoji = Emoji("• ", "* "); static SPARKLE: Emoji = Emoji("✨ ", ""); static PACKAGE: Emoji = Emoji("📦 ", ""); /// Creates a new process bar for processing that will take an unknown amount of time fn new_spinner_progress_bar() -> ProgressBar { let progress_bar = ProgressBar::new(42); progress_bar .set_style(ProgressStyle::default_spinner().template("{spinner:.green} {wide_msg}")); progress_bar.enable_steady_tick(100); progress_bar } /// Pretty print a "name value" fn println_name_value(name: &str, value: &str) { println!("{} {}", style(name).bold(), value); } /// Downloads the release archive at `url` to a temporary location. If `expected_sha256` is /// Some(_), produce an error if the release SHA256 doesn't match. /// /// Returns a tuple consisting of: /// * TempDir - drop this value to clean up the temporary location /// * PathBuf - path to the downloaded release (within `TempDir`) /// * String - SHA256 of the release /// fn download_to_temp_archive( url: &str, expected_sha256: Option<&str>, ) -> Result<(TempDir, PathBuf, String), Box> { fn sha256_digest(mut reader: R) -> Result> { let mut context = Context::new(&SHA256); let mut buffer = [0; 1024]; loop { let count = reader.read(&mut buffer)?; if count == 0 { break; } context.update(&buffer[..count]); } Ok(context.finish()) } fn sha256_file_digest>(path: P) -> Result> { let input = File::open(path)?; let reader = BufReader::new(input); sha256_digest(reader) } let url = Url::parse(url).map_err(|err| format!("Unable to parse {}: {}", url, err))?; let temp_dir = TempDir::new(clap::crate_name!())?; let temp_file = temp_dir.path().join("release.tar.bz2"); let client = reqwest::Client::new(); let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(&format!("{}Downloading...", TRUCK)); let response = client.get(url).send()?; let download_size = { response .headers() .get(reqwest::header::CONTENT_LENGTH) .and_then(|content_length| content_length.to_str().ok()) .and_then(|content_length| content_length.parse().ok()) .unwrap_or(0) }; progress_bar.set_length(download_size); progress_bar.set_style( ProgressStyle::default_bar() .template(&format!( "{}{}{}", "{spinner:.green} ", TRUCK, "Downloading [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})" )) .progress_chars("=> "), ); struct DownloadProgress { progress_bar: ProgressBar, response: R, } impl Read for DownloadProgress { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.response.read(buf).map(|n| { self.progress_bar.inc(n as u64); n }) } } let mut source = DownloadProgress { progress_bar, response, }; let mut file = File::create(&temp_file)?; std::io::copy(&mut source, &mut file)?; let temp_file_sha256 = sha256_file_digest(&temp_file) .map_err(|err| format!("Unable to hash {:?}: {}", temp_file, err))?; let temp_file_sha256 = bs58::encode(temp_file_sha256).into_string(); if expected_sha256.is_some() && expected_sha256 != Some(&temp_file_sha256) { Err(io::Error::new(io::ErrorKind::Other, "Incorrect hash"))?; } Ok((temp_dir, temp_file, temp_file_sha256)) } /// Extracts the release archive into the specified directory fn extract_release_archive( archive: &Path, extract_dir: &Path, ) -> Result<(), Box> { use bzip2::bufread::BzDecoder; use tar::Archive; let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(&format!("{}Extracting...", PACKAGE)); let _ = fs::remove_dir_all(extract_dir); fs::create_dir_all(extract_dir)?; let tar_bz2 = File::open(archive)?; let tar = BzDecoder::new(BufReader::new(tar_bz2)); let mut release = Archive::new(tar); release.unpack(extract_dir)?; progress_bar.finish_and_clear(); Ok(()) } /// Reads the supported TARGET triple for the given release fn load_release_target(release_dir: &Path) -> Result> { use serde_derive::Deserialize; #[derive(Deserialize, Debug)] pub struct ReleaseVersion { pub target: String, pub commit: String, channel: String, } let mut version_yml = PathBuf::from(release_dir); version_yml.push("solana-release"); version_yml.push("version.yml"); let file = File::open(&version_yml)?; let version: ReleaseVersion = serde_yaml::from_reader(file)?; Ok(version.target) } /// Time in seconds since the UNIX_EPOCH fn timestamp_secs() -> u64 { SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs() } /// Create an empty update manifest for the given `update_manifest_keypair` if it doesn't already /// exist on the cluster fn new_update_manifest( rpc_client: &RpcClient, from_keypair: &Keypair, update_manifest_keypair: &Keypair, ) -> Result<(), Box> { if rpc_client .get_account_data(&update_manifest_keypair.pubkey()) .is_err() { let recect_blockhash = rpc_client.get_recent_blockhash()?; let new_account = ConfigInstruction::new_account::( &from_keypair.pubkey(), &update_manifest_keypair.pubkey(), 1, // lamports ); let mut transaction = Transaction::new(vec![new_account]); transaction.sign(&[from_keypair], recect_blockhash); rpc_client.send_and_confirm_transaction(&mut transaction, from_keypair)?; } Ok(()) } /// Update the update manifest on the cluster with new content fn store_update_manifest( rpc_client: &RpcClient, from_keypair: &Keypair, update_manifest_keypair: &Keypair, update_manifest: &SignedUpdateManifest, ) -> Result<(), Box> { let recect_blockhash = rpc_client.get_recent_blockhash()?; let new_store = ConfigInstruction::new_store::( &from_keypair.pubkey(), &update_manifest_keypair.pubkey(), update_manifest, ); let mut transaction = Transaction::new(vec![new_store]); transaction.sign(&[from_keypair, update_manifest_keypair], recect_blockhash); rpc_client.send_and_confirm_transaction(&mut transaction, from_keypair)?; Ok(()) } /// Read the current contents of the update manifest from the cluster fn get_update_manifest( rpc_client: &RpcClient, update_manifest_pubkey: &Pubkey, ) -> Result { let data = rpc_client .get_account_data(update_manifest_pubkey) .map_err(|err| format!("Unable to fetch update manifest: {}", err))?; let signed_update_manifest = SignedUpdateManifest::deserialize(update_manifest_pubkey, &data) .map_err(|err| format!("Unable to deserialize update manifest: {}", err))?; Ok(signed_update_manifest.manifest) } /// Bug the user if bin_dir is not in their PATH fn check_env_path_for_bin_dir(config: &Config) { use std::env; let found = match env::var_os("PATH") { Some(paths) => env::split_paths(&paths).any(|path| { if let Ok(path) = path.canonicalize() { if path == *config.bin_dir() { return true; } } false }), None => false, }; if !found { println!( "\nPlease update your PATH environment variable to include the solana programs:\n PATH=\"{}:$PATH\"\n", config.bin_dir().to_str().unwrap() ); } } pub fn init( config_file: &str, data_dir: &str, json_rpc_url: &str, update_manifest_pubkey: &Pubkey, ) -> Result<(), String> { let config = Config::new(data_dir, json_rpc_url, update_manifest_pubkey); config.save(config_file)?; update(config_file)?; Ok(()) } pub fn info(config_file: &str, local_info_only: bool) -> Result, String> { let config = Config::load(config_file)?; println_name_value("JSON RPC URL:", &config.json_rpc_url); println_name_value( "Update manifest pubkey:", &config.update_manifest_pubkey.to_string(), ); fn print_update_manifest(update_manifest: &UpdateManifest) { let when = Local.timestamp(update_manifest.timestamp_secs as i64, 0); println_name_value( &format!("{}release date", BULLET), &when.format("%c").to_string(), ); println_name_value( &format!("{}download URL", BULLET), &update_manifest.download_url, ); } match config.current_update_manifest { Some(ref update_manifest) => { println_name_value("Installed version:", ""); print_update_manifest(&update_manifest); } None => { println_name_value("Installed version:", "None"); } } if local_info_only { Ok(None) } else { let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(&format!("{}Checking for updates...", LOOKING_GLASS)); let rpc_client = RpcClient::new(config.json_rpc_url.clone()); let manifest = get_update_manifest(&rpc_client, &config.update_manifest_pubkey)?; progress_bar.finish_and_clear(); if Some(&manifest) == config.current_update_manifest.as_ref() { println!("\n{}", style("Installation is up to date").italic()); Ok(None) } else { println!("\n{}", style("An update is available:").bold()); print_update_manifest(&manifest); Ok(Some(manifest)) } } } pub fn deploy( json_rpc_url: &str, from_keypair_file: &str, download_url: &str, update_manifest_keypair_file: &str, ) -> Result<(), String> { let from_keypair = read_keypair(from_keypair_file) .map_err(|err| format!("Unable to read {}: {}", from_keypair_file, err))?; let update_manifest_keypair = read_keypair(update_manifest_keypair_file) .map_err(|err| format!("Unable to read {}: {}", update_manifest_keypair_file, err))?; // Download the release let (temp_dir, temp_archive, temp_archive_sha256) = download_to_temp_archive(download_url, None) .map_err(|err| format!("Unable to download {}: {}", download_url, err))?; // Extract it and load the release version metadata let temp_release_dir = temp_dir.path().join("archive"); extract_release_archive(&temp_archive, &temp_release_dir).map_err(|err| { format!( "Unable to extract {:?} into {:?}: {}", temp_archive, temp_release_dir, err ) })?; let release_target = load_release_target(&temp_release_dir).map_err(|err| { format!( "Unable to load release target from {:?}: {}", temp_release_dir, err ) })?; println_name_value("JSON RPC URL:", json_rpc_url); println_name_value("Update target:", &release_target); println_name_value( "Update manifest pubkey:", &update_manifest_keypair.pubkey().to_string(), ); let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(&format!("{}Deploying update...", PACKAGE)); // Construct an update manifest for the release let mut update_manifest = SignedUpdateManifest { account_pubkey: update_manifest_keypair.pubkey(), ..SignedUpdateManifest::default() }; update_manifest.manifest.timestamp_secs = timestamp_secs(); update_manifest.manifest.download_url = download_url.to_string(); update_manifest.manifest.download_sha256 = temp_archive_sha256; update_manifest.sign(&update_manifest_keypair); assert!(update_manifest.verify()); // Store the new update manifest on the cluster let rpc_client = RpcClient::new(json_rpc_url.to_string()); new_update_manifest(&rpc_client, &from_keypair, &update_manifest_keypair) .map_err(|err| format!("Unable to create update manifest: {}", err))?; store_update_manifest( &rpc_client, &from_keypair, &update_manifest_keypair, &update_manifest, ) .map_err(|err| format!("Unable to store update manifest: {:?}", err))?; progress_bar.finish_and_clear(); println!(" {}{}", SPARKLE, style("Deployment successful").bold()); Ok(()) } pub fn update(config_file: &str) -> Result { let update_manifest = info(config_file, false)?; if update_manifest.is_none() { return Ok(false); } let update_manifest = update_manifest.unwrap(); if timestamp_secs() < u64::from_str_radix(crate::build_env::BUILD_SECONDS_SINCE_UNIX_EPOCH, 10).unwrap() { Err("Unable to update as system time seems unreliable".to_string())? } let mut config = Config::load(config_file)?; if let Some(ref current_update_manifest) = config.current_update_manifest { if update_manifest.timestamp_secs < current_update_manifest.timestamp_secs { Err("Unable to update to an older version".to_string())? } } let (_temp_dir, temp_archive, _temp_archive_sha256) = download_to_temp_archive( &update_manifest.download_url, Some(&update_manifest.download_sha256), ) .map_err(|err| { format!( "Unable to download {}: {}", update_manifest.download_url, err ) })?; let release_dir = config.release_dir(&update_manifest.download_sha256); extract_release_archive(&temp_archive, &release_dir).map_err(|err| { format!( "Unable to extract {:?} to {:?}: {}", temp_archive, release_dir, err ) })?; let release_target = load_release_target(&release_dir).map_err(|err| { format!( "Unable to load release target from {:?}: {}", release_dir, err ) })?; if release_target != crate::build_env::TARGET { Err(format!("Incompatible update target: {}", release_target))?; } let _ = fs::remove_dir_all(config.bin_dir()); std::os::unix::fs::symlink( release_dir.join("solana-release").join("bin"), config.bin_dir(), ) .map_err(|err| { format!( "Unable to symlink {:?} to {:?}: {}", release_dir, config.bin_dir(), err ) })?; config.current_update_manifest = Some(update_manifest); config.save(config_file)?; check_env_path_for_bin_dir(&config); println!(" {}{}", SPARKLE, style("Update successful").bold()); Ok(true) } pub fn run( config_file: &str, program_name: &str, program_arguments: Vec<&str>, ) -> Result<(), String> { let config = Config::load(config_file)?; let full_program_path = config.bin_dir().join(program_name); if !full_program_path.exists() { Err(format!( "{} does not exist", full_program_path.to_str().unwrap() ))?; } let mut child_option: Option = None; let mut now = Instant::now(); loop { child_option = match child_option { Some(mut child) => match child.try_wait() { Ok(Some(status)) => { println_name_value( &format!("{} exited with:", program_name), &status.to_string(), ); None } Ok(None) => Some(child), Err(err) => { eprintln!("Error attempting to wait for program to exit: {}", err); None } }, None => { match std::process::Command::new(&full_program_path) .args(&program_arguments) .spawn() { Ok(child) => Some(child), Err(err) => { eprintln!("Failed to spawn {}: {:?}", program_name, err); None } } } }; if now.elapsed().as_secs() > config.update_poll_secs { match update(config_file) { Ok(true) => { // Update successful, kill current process so it will be restart if let Some(ref mut child) = child_option { println!("Killing program: {:?}", child.kill()); } } Ok(false) => {} // No update available Err(err) => { eprintln!("Failed to apply update: {:?}", err); } }; now = Instant::now(); } sleep(Duration::from_secs(1)); } }