use crate::{ config::{Config, ExplicitRelease}, stop_process::stop_process, update_manifest::{SignedUpdateManifest, UpdateManifest}, }; use chrono::{Local, TimeZone}; use console::{style, Emoji}; use indicatif::{ProgressBar, ProgressStyle}; use serde_derive::Deserialize; use solana_client::rpc_client::RpcClient; use solana_config_program::{config_instruction, get_config_data, ConfigState}; use solana_sdk::{ hash::{Hash, Hasher}, message::Message, pubkey::Pubkey, signature::{read_keypair_file, Keypair, Signable, Signer}, transaction::Transaction, }; use std::{ fs::{self, File}, io::{self, BufReader, Read}, path::{Path, PathBuf}, sync::mpsc, time::{Duration, Instant, SystemTime}, }; use tempdir::TempDir; use 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 PACKAGE: Emoji = Emoji("đŸ“Ļ ", ""); static INFORMATION: 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 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(clap::crate_name!())?; let temp_file = temp_dir.path().join("download"); let client = reqwest::blocking::Client::new(); 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(&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))?; 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; 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(()) } 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, _fee_calculator) = rpc_client.get_recent_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, _fee_calculator) = rpc_client.get_recent_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; use std::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; use 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}; use winreg::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"); return 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) -> Result { use std::ptr; use winapi::shared::minwindef::*; use winapi::um::winuser::{ SendMessageTimeoutA, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE, }; use winreg::enums::{RegType, HKEY_CURRENT_USER, KEY_READ, KEY_WRITE}; use winreg::{RegKey, RegValue}; let old_path = if let Some(s) = get_windows_path_var()? { s } else { return Ok(false); }; if !old_path.contains(&new_path) { let mut new_path = new_path.to_string(); if !old_path.is_empty() { new_path.push_str(";"); 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) .map_err(|err| format!("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) .map_err(|err| format!("Unable set HKEY_CURRENT_USER\\Environment\\PATH: {}", err))?; // Tell other processes to update their environment unsafe { SendMessageTimeoutA( HWND_BROADCAST, WM_SETTINGCHANGE, 0 as WPARAM, "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() ); Ok(true) } #[cfg(unix)] fn add_to_path(new_path: &str) -> Result { let shell_export_string = format!(r#"export PATH="{}:$PATH""#, new_path); let mut modified_rcfiles = false; // Look for sh, bash, and zsh rc files let mut rcfiles = vec![dirs::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::home_dir); let zprofile = zdotdir.map(|p| p.join(".zprofile")); rcfiles.push(zprofile); } } if let Some(bash_profile) = dirs::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 ); } Ok(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 }; update(config_file)?; 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!( "http://release.solana.com/{}/solana-release-{}.tar.bz2", release_channel, crate::build_env::TARGET ) } fn release_channel_version_url(release_channel: &str) -> String { format!( "http://release.solana.com/{}/solana-release-{}.yml", release_channel, crate::build_env::TARGET ) } pub fn info(config_file: &str, local_info_only: bool) -> Result, String> { let config = Config::load(config_file)?; println_name_value("Configuration:", &config_file); println_name_value( "Active release directory:", &config.active_release_dir().to_str().unwrap_or("?"), ); 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), ); } } return Ok(None); } 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.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_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 update(config_file: &str) -> Result { let mut config = Config::load(config_file)?; let update_manifest = info(config_file, false)?; let release_dir = if let Some(explicit_release) = &config.explicit_release { let (download_url, release_dir) = match explicit_release { ExplicitRelease::Semver(release_semver) => { let download_url = github_release_download_url(release_semver); let release_dir = config.release_dir(&release_semver); let download_url = if release_dir.join(".ok").exists() { // If this release_semver has already been successfully downloaded, no update // needed println!("{} is present, no download required.", release_semver); None } else { Some(download_url) }; (download_url, release_dir) } ExplicitRelease::Channel(release_channel) => { let release_dir = config.release_dir(&release_channel); let current_release_version_yml = release_dir.join("solana-release").join("version.yml"); let download_url = Some(release_channel_download_url(release_channel)); if !current_release_version_yml.exists() { (download_url, release_dir) } else { 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 current_release_version = load_release_version(¤t_release_version_yml)?; if update_release_version.commit == current_release_version.commit { // Same commit, no update required println!( "Latest {} build is already present, no download required.", release_channel ); (None, release_dir) } else { (download_url, release_dir) } } } }; if let Some(download_url) = download_url { let (_temp_dir, temp_archive, _temp_archive_sha256) = download_to_temp(&download_url, None) .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 _ = fs::create_dir_all(release_dir.join(".ok")); } release_dir } else { 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() { 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()); } } let release_dir = config.release_dir(&update_manifest.download_sha256.to_string()); let (_temp_dir, temp_archive, _temp_archive_sha256) = download_to_temp( &update_manifest.download_url, Some(&update_manifest.download_sha256), ) .map_err(|err| { format!( "Unable to download {}: {}", update_manifest.download_url, err ) })?; extract_release_archive(&temp_archive, &release_dir).map_err(|err| { format!( "Unable to extract {:?} to {:?}: {}", temp_archive, release_dir, err ) })?; config.current_update_manifest = Some(update_manifest); release_dir }; 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)); } 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)?; 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 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) = mpsc::channel(); 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) { 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); } } }