use { crate::{ config::{Config, ExplicitRelease}, stop_process::stop_process, update_manifest::{SignedUpdateManifest, UpdateManifest}, }, chrono::{Local, TimeZone}, console::{style, Emoji}, crossbeam_channel::unbounded, indicatif::{ProgressBar, ProgressStyle}, serde::{Deserialize, Serialize}, solana_config_program::{config_instruction, get_config_data, ConfigState}, solana_rpc_client::rpc_client::RpcClient, solana_sdk::{ hash::{Hash, Hasher}, message::Message, pubkey::Pubkey, signature::{read_keypair_file, Keypair, Signable, Signer}, transaction::Transaction, }, std::{ fs::{self, File}, io::{self, BufReader, Read}, path::{Path, PathBuf}, time::{Duration, Instant, SystemTime}, }, tempfile::TempDir, url::Url, }; #[derive(Deserialize, Debug)] pub struct ReleaseVersion { pub target: String, pub commit: String, channel: String, } static TRUCK: Emoji = Emoji("🚚 ", ""); static LOOKING_GLASS: Emoji = Emoji("🔍 ", ""); static BULLET: Emoji = Emoji("â€ĸ ", "* "); static SPARKLE: Emoji = Emoji("✨ ", ""); static WRAPPED_PRESENT: Emoji = Emoji("🎁 ", ""); static PACKAGE: Emoji = Emoji("đŸ“Ļ ", ""); static INFORMATION: Emoji = Emoji("ℹī¸ ", ""); static RECYCLING: 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}") .expect("ProgresStyle::template direct input to be correct"), ); progress_bar.enable_steady_tick(Duration::from_millis(100)); progress_bar } /// Pretty print a "name value" fn println_name_value(name: &str, value: &str) { println!("{} {}", style(name).bold(), value); } /// Downloads a file at `url` to a temporary location. If `expected_sha256` is /// Some(_), produce an error if the SHA256 of the file contents doesn't match. /// /// Returns a tuple consisting of: /// * TempDir - drop this value to clean up the temporary location /// * PathBuf - path to the downloaded file (within `TempDir`) /// * String - SHA256 of the release /// fn download_to_temp( url: &str, expected_sha256: Option<&Hash>, ) -> Result<(TempDir, PathBuf, Hash), Box> { fn sha256_file_digest>(path: P) -> Result> { let input = File::open(path)?; let mut reader = BufReader::new(input); let mut hasher = Hasher::default(); let mut buffer = [0; 1024]; loop { let count = reader.read(&mut buffer)?; if count == 0 { break; } hasher.hash(&buffer[..count]); } Ok(hasher.result()) } let url = Url::parse(url).map_err(|err| format!("Unable to parse {}: {}", url, err))?; let temp_dir = TempDir::new()?; let temp_file = temp_dir.path().join("download"); let client = reqwest::blocking::Client::builder() .connect_timeout(Duration::from_secs(30)) .timeout(None) .build()?; let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(format!("{}Downloading...", TRUCK)); let response = client.get(url.as_str()).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( "{spinner:.green}{wide_msg} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})", ) .expect("ProgresStyle::template direct input to be correct") .progress_chars("=> "), ); progress_bar.set_message(format!("{}Downloading", TRUCK)); 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))?; if expected_sha256.is_some() && expected_sha256 != Some(&temp_file_sha256) { return Err(io::Error::new(io::ErrorKind::Other, "Incorrect hash").into()); } source.progress_bar.finish_and_clear(); 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, tar::Archive}; let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(format!("{}Extracting...", PACKAGE)); if extract_dir.exists() { let _ = fs::remove_dir_all(extract_dir); } let tmp_extract_dir = extract_dir.with_file_name("tmp-extract"); if tmp_extract_dir.exists() { let _ = fs::remove_dir_all(&tmp_extract_dir); } fs::create_dir_all(&tmp_extract_dir)?; let tar_bz2 = File::open(archive)?; let tar = BzDecoder::new(BufReader::new(tar_bz2)); let mut release = Archive::new(tar); release.unpack(&tmp_extract_dir)?; fs::rename(&tmp_extract_dir, extract_dir)?; progress_bar.finish_and_clear(); Ok(()) } fn load_release_version(version_yml: &Path) -> Result { let file = File::open(version_yml) .map_err(|err| format!("Unable to open {:?}: {:?}", version_yml, err))?; let version: ReleaseVersion = serde_yaml::from_reader(file) .map_err(|err| format!("Unable to parse {:?}: {:?}", version_yml, err))?; Ok(version) } /// Reads the supported TARGET triple for the given release fn load_release_target(release_dir: &Path) -> Result { let mut version_yml = PathBuf::from(release_dir); version_yml.push("solana-release"); version_yml.push("version.yml"); let version = load_release_version(&version_yml)?; 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 recent_blockhash = rpc_client.get_latest_blockhash()?; let lamports = rpc_client .get_minimum_balance_for_rent_exemption(SignedUpdateManifest::max_space() as usize)?; let instructions = config_instruction::create_account::( &from_keypair.pubkey(), &update_manifest_keypair.pubkey(), lamports, vec![], // additional keys ); let message = Message::new(&instructions, Some(&from_keypair.pubkey())); let signers = [from_keypair, update_manifest_keypair]; let transaction = Transaction::new(&signers, message, recent_blockhash); rpc_client.send_and_confirm_transaction(&transaction)?; } 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 recent_blockhash = rpc_client.get_latest_blockhash()?; let signers = [from_keypair, update_manifest_keypair]; let instruction = config_instruction::store::( &update_manifest_keypair.pubkey(), true, // update_manifest_keypair is signer vec![], // additional keys update_manifest, ); let message = Message::new(&[instruction], Some(&from_keypair.pubkey())); let transaction = Transaction::new(&signers, message, recent_blockhash); rpc_client.send_and_confirm_transaction(&transaction)?; 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 config_data = get_config_data(&data) .map_err(|err| format!("Unable to get at config_data to update manifest: {}", err))?; let signed_update_manifest = SignedUpdateManifest::deserialize(update_manifest_pubkey, config_data) .map_err(|err| format!("Unable to deserialize update manifest: {}", err))?; Ok(signed_update_manifest.manifest) } /// Bug the user if active_release_bin_dir is not in their PATH fn check_env_path_for_bin_dir(config: &Config) { use std::env; let bin_dir = config .active_release_bin_dir() .canonicalize() .unwrap_or_default(); let found = match env::var_os("PATH") { Some(paths) => env::split_paths(&paths).any(|path| { if let Ok(path) = path.canonicalize() { if path == 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.active_release_bin_dir().to_str().unwrap() ); } } /// Encodes a UTF-8 string as a null-terminated UCS-2 string in bytes #[cfg(windows)] pub fn string_to_winreg_bytes(s: &str) -> Vec { use std::{ffi::OsString, os::windows::ffi::OsStrExt}; let v: Vec<_> = OsString::from(format!("{}\x00", s)).encode_wide().collect(); unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 2).to_vec() } } // This is used to decode the value of HKCU\Environment\PATH. If that // key is not Unicode (or not REG_SZ | REG_EXPAND_SZ) then this // returns null. The winreg library itself does a lossy unicode // conversion. #[cfg(windows)] pub fn string_from_winreg_value(val: &winreg::RegValue) -> Option { use {std::slice, winreg::enums::RegType}; match val.vtype { RegType::REG_SZ | RegType::REG_EXPAND_SZ => { // Copied from winreg let words = unsafe { slice::from_raw_parts(val.bytes.as_ptr() as *const u16, val.bytes.len() / 2) }; let mut s = if let Ok(s) = String::from_utf16(words) { s } else { return None; }; while s.ends_with('\u{0}') { s.pop(); } Some(s) } _ => None, } } // Get the windows PATH variable out of the registry as a String. If // this returns None then the PATH variable is not Unicode and we // should not mess with it. #[cfg(windows)] fn get_windows_path_var() -> Result, String> { use winreg::{ enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE}, RegKey, }; let root = RegKey::predef(HKEY_CURRENT_USER); let environment = root .open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE) .map_err(|err| format!("Unable to open HKEY_CURRENT_USER\\Environment: {}", err))?; let reg_value = environment.get_raw_value("PATH"); match reg_value { Ok(val) => { if let Some(s) = string_from_winreg_value(&val) { Ok(Some(s)) } else { println!("the registry key HKEY_CURRENT_USER\\Environment\\PATH does not contain valid Unicode. Not modifying the PATH variable"); Ok(None) } } Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(Some(String::new())), Err(e) => Err(e.to_string()), } } #[cfg(windows)] fn add_to_path(new_path: &str) -> bool { use { std::ptr, winapi::{ shared::minwindef::*, um::winuser::{ SendMessageTimeoutA, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE, }, }, winreg::{ enums::{RegType, HKEY_CURRENT_USER, KEY_READ, KEY_WRITE}, RegKey, RegValue, }, }; let old_path = if let Some(s) = get_windows_path_var().unwrap_or_else(|err| panic!("Unable to get PATH: {}", err)) { s } else { return false; }; if !old_path.contains(new_path) { let mut new_path = new_path.to_string(); if !old_path.is_empty() { new_path.push(';'); new_path.push_str(&old_path); } let root = RegKey::predef(HKEY_CURRENT_USER); let environment = root .open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE) .unwrap_or_else(|err| panic!("Unable to open HKEY_CURRENT_USER\\Environment: {}", err)); let reg_value = RegValue { bytes: string_to_winreg_bytes(&new_path), vtype: RegType::REG_EXPAND_SZ, }; environment .set_raw_value("PATH", ®_value) .unwrap_or_else(|err| { panic!("Unable set HKEY_CURRENT_USER\\Environment\\PATH: {}", err) }); // Tell other processes to update their environment unsafe { SendMessageTimeoutA( HWND_BROADCAST, WM_SETTINGCHANGE, 0_usize, "Environment\0".as_ptr() as LPARAM, SMTO_ABORTIFHUNG, 5000, ptr::null_mut(), ); } } println!( "\n{}\n {}\n\n{}", style("The HKEY_CURRENT_USER/Environment/PATH registry key has been modified to include:").bold(), new_path, style("Future applications will automatically have the correct environment, but you may need to restart your current shell.").bold() ); true } #[cfg(unix)] fn add_to_path(new_path: &str) -> bool { let shell_export_string = format!("\nexport PATH=\"{}:$PATH\"", new_path); let mut modified_rcfiles = false; // Look for sh, bash, and zsh rc files let mut rcfiles = vec![dirs_next::home_dir().map(|p| p.join(".profile"))]; if let Ok(shell) = std::env::var("SHELL") { if shell.contains("zsh") { let zdotdir = std::env::var("ZDOTDIR") .ok() .map(PathBuf::from) .or_else(dirs_next::home_dir); let zprofile = zdotdir.map(|p| p.join(".zprofile")); rcfiles.push(zprofile); } } if let Some(bash_profile) = dirs_next::home_dir().map(|p| p.join(".bash_profile")) { // Only update .bash_profile if it exists because creating .bash_profile // will cause .profile to not be read if bash_profile.exists() { rcfiles.push(Some(bash_profile)); } } let rcfiles = rcfiles.into_iter().filter_map(|f| f.filter(|f| f.exists())); // For each rc file, append a PATH entry if not already present for rcfile in rcfiles { if !rcfile.exists() { continue; } fn read_file(path: &Path) -> io::Result { let mut file = fs::OpenOptions::new().read(true).open(path)?; let mut contents = String::new(); io::Read::read_to_string(&mut file, &mut contents)?; Ok(contents) } match read_file(&rcfile) { Err(err) => { println!("Unable to read {:?}: {}", rcfile, err); } Ok(contents) => { if !contents.contains(&shell_export_string) { println!( "Adding {} to {}", style(&shell_export_string).italic(), style(rcfile.to_str().unwrap()).bold() ); fn append_file(dest: &Path, line: &str) -> io::Result<()> { use std::io::Write; let mut dest_file = fs::OpenOptions::new() .write(true) .append(true) .create(true) .open(dest)?; writeln!(&mut dest_file, "{}", line)?; dest_file.sync_data()?; Ok(()) } append_file(&rcfile, &shell_export_string).unwrap_or_else(|err| { format!("Unable to append to {:?}: {}", rcfile, err); }); modified_rcfiles = true; } } } } if modified_rcfiles { println!( "\n{}\n {}\n", style("Close and reopen your terminal to apply the PATH changes or run the following in your existing shell:").bold().blue(), shell_export_string ); } modified_rcfiles } pub fn init( config_file: &str, data_dir: &str, json_rpc_url: &str, update_manifest_pubkey: &Pubkey, no_modify_path: bool, explicit_release: Option, ) -> Result<(), String> { let config = { // Write new config file only if different, so that running |solana-install init| // repeatedly doesn't unnecessarily re-download let mut current_config = Config::load(config_file).unwrap_or_default(); current_config.current_update_manifest = None; let config = Config::new( data_dir, json_rpc_url, update_manifest_pubkey, explicit_release, ); if current_config != config { config.save(config_file)?; } config }; init_or_update(config_file, true, false)?; let path_modified = if !no_modify_path { add_to_path(config.active_release_bin_dir().to_str().unwrap()) } else { false }; if !path_modified && !no_modify_path { check_env_path_for_bin_dir(&config); } Ok(()) } fn github_release_download_url(release_semver: &str) -> String { format!( "https://github.com/solana-labs/solana/releases/download/v{}/solana-release-{}.tar.bz2", release_semver, crate::build_env::TARGET ) } fn release_channel_download_url(release_channel: &str) -> String { format!( "https://release.solana.com/{}/solana-release-{}.tar.bz2", release_channel, crate::build_env::TARGET ) } fn release_channel_version_url(release_channel: &str) -> String { format!( "https://release.solana.com/{}/solana-release-{}.yml", release_channel, crate::build_env::TARGET ) } 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.to_string()); println_name_value( &format!("{}download URL:", BULLET), &update_manifest.download_url, ); } pub fn info(config_file: &str, local_info_only: bool, eval: bool) -> Result<(), String> { let config = Config::load(config_file)?; if eval { println!( "SOLANA_INSTALL_ACTIVE_RELEASE={}", &config.active_release_dir().to_str().unwrap_or("") ); config .explicit_release .map(|er| match er { ExplicitRelease::Semver(semver) => semver, ExplicitRelease::Channel(channel) => channel, }) .and_then(|channel| { println!("SOLANA_INSTALL_ACTIVE_CHANNEL={}", channel,); Option::::None }); return Ok(()); } println_name_value("Configuration:", config_file); println_name_value( "Active release directory:", config.active_release_dir().to_str().unwrap_or("?"), ); fn print_release_version(config: &Config) { if let Ok(release_version) = load_release_version(&config.active_release_dir().join("version.yml")) { println_name_value( &format!("{}Release commit:", BULLET), &release_version.commit[0..7], ); } } if let Some(explicit_release) = &config.explicit_release { match explicit_release { ExplicitRelease::Semver(release_semver) => { println_name_value(&format!("{}Release version:", BULLET), release_semver); println_name_value( &format!("{}Release URL:", BULLET), &github_release_download_url(release_semver), ); } ExplicitRelease::Channel(release_channel) => { println_name_value(&format!("{}Release channel:", BULLET), release_channel); println_name_value( &format!("{}Release URL:", BULLET), &release_channel_download_url(release_channel), ); } } print_release_version(&config); } else { println_name_value("JSON RPC URL:", &config.json_rpc_url); println_name_value( "Update manifest pubkey:", &config.update_manifest_pubkey.to_string(), ); match config.current_update_manifest { Some(ref update_manifest) => { println_name_value("Installed version:", ""); print_release_version(&config); print_update_manifest(update_manifest); } None => { println_name_value("Installed version:", "None"); } } } if local_info_only { Ok(()) } else { update(config_file, true).map(|_| ()) } } 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_file(from_keypair_file) .map_err(|err| format!("Unable to read {}: {}", from_keypair_file, err))?; let update_manifest_keypair = read_keypair_file(update_manifest_keypair_file) .map_err(|err| format!("Unable to read {}: {}", update_manifest_keypair_file, err))?; println_name_value("JSON RPC URL:", json_rpc_url); println_name_value( "Update manifest pubkey:", &update_manifest_keypair.pubkey().to_string(), ); // Confirm the `json_rpc_url` is good and that `from_keypair` is a valid account let rpc_client = RpcClient::new(json_rpc_url.to_string()); let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(format!("{}Checking cluster...", LOOKING_GLASS)); let balance = rpc_client .get_balance(&from_keypair.pubkey()) .map_err(|err| { format!( "Unable to get the account balance of {}: {}", from_keypair_file, err ) })?; progress_bar.finish_and_clear(); if balance == 0 { return Err(format!("{} account balance is empty", from_keypair_file)); } // Download the release let (temp_dir, temp_archive, temp_archive_sha256) = download_to_temp(download_url, None) .map_err(|err| format!("Unable to download {}: {}", download_url, err))?; if let Ok(update_manifest) = get_update_manifest(&rpc_client, &update_manifest_keypair.pubkey()) { if temp_archive_sha256 == update_manifest.download_sha256 { println!( " {}{}", INFORMATION, style("Update is already deployed").bold() ); return Ok(()); } } // 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("Update target:", &release_target); 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 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(()) } #[cfg(windows)] fn symlink_dir, Q: AsRef>(src: P, dst: Q) -> std::io::Result<()> { std::os::windows::fs::symlink_dir(src, dst) } #[cfg(not(windows))] fn symlink_dir, Q: AsRef>(src: P, dst: Q) -> std::io::Result<()> { std::os::unix::fs::symlink(src, dst) } pub fn gc(config_file: &str) -> Result<(), String> { let config = Config::load(config_file)?; let entries = fs::read_dir(&config.releases_dir) .map_err(|err| format!("Unable to read {}: {}", config.releases_dir.display(), err))?; let mut releases = entries .filter_map(|entry| entry.ok()) .filter_map(|entry| { entry .metadata() .ok() .map(|metadata| (entry.path(), metadata)) }) .filter_map(|(release_path, metadata)| { if metadata.is_dir() { Some((release_path, metadata)) } else { None } }) .filter_map(|(release_path, metadata)| { metadata .modified() .ok() .map(|modified_time| (release_path, modified_time)) }) .collect::>(); releases.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); // order by newest releases const MAX_CACHE_LEN: usize = 5; if releases.len() > MAX_CACHE_LEN { let old_releases = releases.split_off(MAX_CACHE_LEN); if !old_releases.is_empty() { let progress_bar = new_spinner_progress_bar(); progress_bar.set_length(old_releases.len() as u64); progress_bar.set_style( ProgressStyle::default_bar() .template("{spinner:.green}{wide_msg} [{bar:40.cyan/blue}] {pos}/{len} ({eta})") .expect("ProgresStyle::template direct input to be correct") .progress_chars("=> "), ); progress_bar.set_message(format!("{}Removing old releases", RECYCLING)); for (release, _modified_type) in old_releases { progress_bar.inc(1); let _ = fs::remove_dir_all(release); } progress_bar.finish_and_clear(); } } Ok(()) } #[derive(Debug, Deserialize, Serialize)] pub struct GithubRelease { pub tag_name: String, pub prerelease: bool, } #[derive(Debug, Deserialize, Serialize)] pub struct GithubReleases(Vec); fn semver_of(string: &str) -> Result { if string.starts_with('v') { semver::Version::parse(string.split_at(1).1) } else { semver::Version::parse(string) } .map_err(|err| err.to_string()) } fn check_for_newer_github_release( version_filter: Option, prerelease_allowed: bool, ) -> reqwest::Result> { let mut page = 1; const PER_PAGE: usize = 100; let client = reqwest::blocking::Client::builder() .user_agent("solana-install") .build()?; let mut all_releases = vec![]; let mut releases = vec![]; while page == 1 || releases.len() == PER_PAGE { let url = reqwest::Url::parse_with_params( "https://api.github.com/repos/solana-labs/solana/releases", &[ ("per_page", &format!("{}", PER_PAGE)), ("page", &format!("{}", page)), ], ) .unwrap(); let request = client.get(url).build()?; let response = client.execute(request)?; releases = response .json::()? .0 .into_iter() .filter_map( |GithubRelease { tag_name, prerelease, }| { if let Ok(version) = semver_of(&tag_name) { if (prerelease_allowed || !prerelease) && version_filter .as_ref() .map_or(true, |version_filter| version_filter.matches(&version)) { return Some(version); } } None }, ) .collect::>(); all_releases.extend_from_slice(&releases); page += 1; } all_releases.sort(); Ok(all_releases.pop().map(|r| r.to_string())) } pub enum SemverUpdateType { Fixed, Patch, _Minor, } pub fn update(config_file: &str, check_only: bool) -> Result { init_or_update(config_file, false, check_only) } pub fn init_or_update(config_file: &str, is_init: bool, check_only: bool) -> Result { let mut config = Config::load(config_file)?; let semver_update_type = if is_init { SemverUpdateType::Fixed } else { SemverUpdateType::Patch }; let (updated_version, download_url_and_sha256, release_dir) = if let Some(explicit_release) = &config.explicit_release { match explicit_release { ExplicitRelease::Semver(current_release_semver) => { let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(format!("{}Checking for updates...", LOOKING_GLASS)); let github_release = check_for_newer_github_release( semver::VersionReq::parse(&format!( "{}{}", match semver_update_type { SemverUpdateType::Fixed => "=", SemverUpdateType::Patch => "~", SemverUpdateType::_Minor => "^", }, current_release_semver )) .ok(), is_init, ) .map_err(|err| err.to_string())?; progress_bar.finish_and_clear(); match github_release { None => { return Err(format!("Unknown release: {}", current_release_semver)); } Some(release_semver) => { if release_semver == *current_release_semver { if let Ok(active_release_version) = load_release_version( &config.active_release_dir().join("version.yml"), ) { if format!("v{}", current_release_semver) == active_release_version.channel { println!( "Install is up to date. {} is the latest compatible release", release_semver ); return Ok(false); } } } config.explicit_release = Some(ExplicitRelease::Semver(release_semver.clone())); let release_dir = config.release_dir(&release_semver); let download_url_and_sha256 = if release_dir.exists() { // Release already present in the cache None } else { Some((github_release_download_url(&release_semver), None)) }; (release_semver, download_url_and_sha256, release_dir) } } } ExplicitRelease::Channel(release_channel) => { let version_url = release_channel_version_url(release_channel); let (_temp_dir, temp_file, _temp_archive_sha256) = download_to_temp(&version_url, None) .map_err(|err| format!("Unable to download {}: {}", version_url, err))?; let update_release_version = load_release_version(&temp_file)?; let release_id = format!("{}-{}", release_channel, update_release_version.commit); let release_dir = config.release_dir(&release_id); let current_release_version_yml = release_dir.join("solana-release").join("version.yml"); let download_url = release_channel_download_url(release_channel); if !current_release_version_yml.exists() { ( format!( "{} commit {}", release_channel, &update_release_version.commit[0..7] ), Some((download_url, None)), release_dir, ) } else { let current_release_version = load_release_version(¤t_release_version_yml)?; if update_release_version.commit == current_release_version.commit { if let Ok(active_release_version) = load_release_version(&config.active_release_dir().join("version.yml")) { if current_release_version.commit == active_release_version.commit { // Same version, no update required println!( "Install is up to date. {} is the latest commit for {}", &active_release_version.commit[0..7], release_channel ); return Ok(false); } } // Release already present in the cache ( format!( "{} commit {}", release_channel, &update_release_version.commit[0..7] ), None, release_dir, ) } else { ( format!( "{} (from {})", &update_release_version.commit[0..7], ¤t_release_version.commit[0..7], ), Some((download_url, None)), release_dir, ) } } } } } 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 update_manifest = get_update_manifest(&rpc_client, &config.update_manifest_pubkey)?; progress_bar.finish_and_clear(); if Some(&update_manifest) == config.current_update_manifest.as_ref() { println!("Install is up to date"); return Ok(false); } println!("\n{}", style("An update is available:").bold()); print_update_manifest(&update_manifest); if timestamp_secs() < crate::build_env::BUILD_SECONDS_SINCE_UNIX_EPOCH .parse::() .unwrap() { return Err("Unable to update as system time seems unreliable".to_string()); } if let Some(ref current_update_manifest) = config.current_update_manifest { if update_manifest.timestamp_secs < current_update_manifest.timestamp_secs { return Err("Unable to update to an older version".to_string()); } } config.current_update_manifest = Some(update_manifest.clone()); let release_dir = config.release_dir(&update_manifest.download_sha256.to_string()); let download_url = update_manifest.download_url; let archive_sha256 = Some(update_manifest.download_sha256); ( "latest manifest".to_string(), Some((download_url, archive_sha256)), release_dir, ) }; if check_only { println!( " {}{}", WRAPPED_PRESENT, style(format!("Update available: {}", updated_version)).bold() ); return Ok(true); } if let Some((download_url, archive_sha256)) = download_url_and_sha256 { let (_temp_dir, temp_archive, _temp_archive_sha256) = download_to_temp(&download_url, archive_sha256.as_ref()) .map_err(|err| format!("Unable to download {}: {}", download_url, err))?; 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 { return Err(format!("Incompatible update target: {}", release_target)); } // Trigger an update to the modification time for `release_dir` { let path = &release_dir.join(".touch"); let _ = fs::OpenOptions::new().create(true).write(true).open(path); let _ = fs::remove_file(path); } let _ = fs::remove_dir_all(config.active_release_dir()); symlink_dir( release_dir.join("solana-release"), config.active_release_dir(), ) .map_err(|err| { format!( "Unable to symlink {:?} to {:?}: {}", release_dir, config.active_release_dir(), err ) })?; config.save(config_file)?; gc(config_file)?; if is_init { println!( " {}{}", SPARKLE, style(format!("{} initialized", updated_version)).bold() ); } else { println!( " {}{}", SPARKLE, style(format!("Update successful to {}", updated_version)).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 mut full_program_path = config.active_release_bin_dir().join(program_name); if cfg!(windows) && full_program_path.extension().is_none() { full_program_path.set_extension("exe"); } if !full_program_path.exists() { return Err(format!( "{} does not exist", full_program_path.to_str().unwrap() )); } let mut child_option: Option = None; let mut now = Instant::now(); let (signal_sender, signal_receiver) = unbounded(); ctrlc::set_handler(move || { let _ = signal_sender.send(()); }) .expect("Error setting Ctrl-C handler"); 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 config.explicit_release.is_none() && now.elapsed().as_secs() > config.update_poll_secs { match update(config_file, false) { Ok(true) => { // Update successful, kill current process so it will be restart if let Some(ref mut child) = child_option { stop_process(child).unwrap_or_else(|err| { eprintln!("Failed to stop child: {:?}", err); }); } } Ok(false) => {} // No update available Err(err) => { eprintln!("Failed to apply update: {:?}", err); } }; now = Instant::now(); } if let Ok(()) = signal_receiver.recv_timeout(Duration::from_secs(1)) { // Handle SIGTERM... if let Some(ref mut child) = child_option { stop_process(child).unwrap_or_else(|err| { eprintln!("Failed to stop child: {:?}", err); }); } std::process::exit(0); } } }