diff --git a/Cargo.lock b/Cargo.lock index 7fe1c822b7..22d0e50d33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1787,6 +1787,17 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "ethnum" version = "1.5.0" @@ -2504,6 +2515,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.5.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.10" @@ -3768,6 +3795,7 @@ dependencies = [ "umask", "unicode-segmentation", "unicode-width 0.2.0", + "update-informer", "ureq", "url", "uu_cp", @@ -6026,6 +6054,7 @@ checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", "h2 0.4.7", @@ -6034,11 +6063,13 @@ dependencies = [ "http-body-util", "hyper 1.5.1", "hyper-rustls 0.27.3", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -6052,6 +6083,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.1", "tokio-util", "tower-service", @@ -7346,6 +7378,16 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -7625,6 +7667,20 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "update-informer" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53813bf5d5f0d8430794f8cc48e99521cc9e298066958d16383ccb8b39d182a7" +dependencies = [ + "etcetera", + "reqwest", + "semver", + "serde", + "serde_json", + "ureq", +] + [[package]] name = "ureq" version = "2.12.1" diff --git a/Cargo.toml b/Cargo.toml index c4c2b03237..e9234036dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -162,6 +162,7 @@ tempfile = "3.15" titlecase = "3.0" toml = "0.8" trash = "5.2" +update-informer = { version = "1.2.0", default-features = false, features = ["github", "native-tls", "ureq"] } umask = "2.1" unicode-segmentation = "1.12" unicode-width = "0.2" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index bfbd091847..bd34f2f742 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -91,7 +91,8 @@ tabled = { workspace = true, features = ["ansi"], default-features = false } titlecase = { workspace = true } toml = { workspace = true, features = ["preserve_order"] } unicode-segmentation = { workspace = true } -ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json"] } +update-informer = { workspace = true, optional = true } +ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json", "native-tls"], optional = true } url = { workspace = true } uu_cp = { workspace = true, optional = true } uu_mkdir = { workspace = true, optional = true } @@ -142,7 +143,7 @@ os = [ # include other features "js", "network", - "nu-protocol/os", + "nu-protocol/os", "nu-utils/os", # os-dependant dependencies @@ -160,13 +161,13 @@ os = [ "which", ] -# The dependencies listed below need 'getrandom'. -# They work with JS (usually with wasm-bindgen) or regular OS support. +# The dependencies listed below need 'getrandom'. +# They work with JS (usually with wasm-bindgen) or regular OS support. # Hence they are also put under the 'os' feature to avoid repetition. js = [ - "getrandom", - "getrandom/js", - "rand", + "getrandom", + "getrandom/js", + "rand", "uuid", ] @@ -174,15 +175,16 @@ js = [ # interface requires openssl which is not easy to embed into wasm, # using rustls could solve this issue. network = [ - "multipart-rs", + "multipart-rs", "native-tls", - "ureq/native-tls", + "update-informer/native-tls", + "ureq", "uuid", ] plugin = [ "nu-parser/plugin", - "os", + "os", ] sqlite = ["rusqlite"] trash-support = ["trash"] diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index ca06a4c04d..7175f7ff1e 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -404,6 +404,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { HttpPut, HttpOptions, Port, + VersionCheck, } bind_command! { Url, diff --git a/crates/nu-command/src/network/mod.rs b/crates/nu-command/src/network/mod.rs index ea46fd3dc8..2366fde594 100644 --- a/crates/nu-command/src/network/mod.rs +++ b/crates/nu-command/src/network/mod.rs @@ -3,6 +3,8 @@ mod http; #[cfg(feature = "network")] mod port; mod url; +#[cfg(feature = "network")] +mod version_check; #[cfg(feature = "network")] pub use self::http::*; @@ -10,3 +12,6 @@ pub use self::url::*; #[cfg(feature = "network")] pub use port::SubCommand as Port; + +#[cfg(feature = "network")] +pub use version_check::VersionCheck; diff --git a/crates/nu-command/src/network/version_check.rs b/crates/nu-command/src/network/version_check.rs new file mode 100644 index 0000000000..180bde9fd9 --- /dev/null +++ b/crates/nu-command/src/network/version_check.rs @@ -0,0 +1,156 @@ +use nu_engine::command_prelude::*; +use serde::Deserialize; +use update_informer::{ + http_client::{GenericHttpClient, HttpClient}, + registry, Check, Package, Registry, Result as UpdateResult, +}; + +#[derive(Clone)] +pub struct VersionCheck; + +impl Command for VersionCheck { + fn name(&self) -> &str { + "version check" + } + + fn description(&self) -> &str { + "Checks to see if you have the latest version of nushell." + } + + fn extra_description(&self) -> &str { + "If you're running nushell nightly, `version check` will check to see if you are running the latest nightly version. If you are running the nushell release, `version check` will check to see if you're running the latest release version." + } + + fn signature(&self) -> Signature { + Signature::build("version check") + .category(Category::Platform) + .input_output_types(vec![(Type::Nothing, Type::String)]) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Check if you have the latest version of nushell", + example: "version check", + result: None, + }] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + let version_check = check_for_latest_nushell_version(); + Ok(version_check.into_pipeline_data()) + } +} + +pub struct NuShellNightly; + +impl Registry for NuShellNightly { + const NAME: &'static str = "nushell/nightly"; + + fn get_latest_version( + http_client: GenericHttpClient, + pkg: &Package, + ) -> UpdateResult> { + #[derive(Deserialize, Debug)] + struct Response { + tag_name: String, + } + + let url = format!("https://api.github.com/repos/{}/releases", pkg); + let versions = http_client + .add_header("Accept", "application/vnd.github.v3+json") + .add_header("User-Agent", "update-informer") + .get::>(&url)?; + + if let Some(v) = versions.first() { + // The nightly repo tags look like "0.101.1-nightly.4+23dc1b6" + // We want to return the "0.101.1-nightly.4" part because hustcer + // is changing the cargo.toml package.version to be that syntax + let up_through_plus = match v.tag_name.split('+').next() { + Some(v) => v, + None => &v.tag_name, + }; + return Ok(Some(up_through_plus.to_string())); + } + + Ok(None) + } +} + +struct NativeTlsHttpClient; + +impl HttpClient for NativeTlsHttpClient { + fn get( + url: &str, + timeout: std::time::Duration, + headers: update_informer::http_client::HeaderMap, + ) -> update_informer::Result { + let agent = ureq::AgentBuilder::new() + .tls_connector(std::sync::Arc::new(native_tls::TlsConnector::new()?)) + .build(); + + let mut req = agent.get(url).timeout(timeout); + + for (header, value) in headers { + req = req.set(header, value); + } + + let json = req.call()?.into_json()?; + + Ok(json) + } +} + +pub fn check_for_latest_nushell_version() -> Value { + let current_version = env!("CARGO_PKG_VERSION").to_string(); + + let mut rec = Record::new(); + + if current_version.contains("nightly") { + rec.push("channel", Value::test_string("nightly")); + + let nightly_pkg_name = "nushell/nightly"; + // The .interval() determines how long the cached check lives. Setting it to std::time::Duration::ZERO + // means that there is essentially no cache and it will check for a new version each time you run nushell. + // Since this is run on demand, there isn't really a need to cache the check. + let informer = + update_informer::new(NuShellNightly, nightly_pkg_name, current_version.clone()) + .http_client(NativeTlsHttpClient) + .interval(std::time::Duration::ZERO); + + if let Ok(Some(new_version)) = informer.check_version() { + rec.push("current", Value::test_bool(false)); + rec.push("latest", Value::test_string(format!("{}", new_version))); + Value::test_record(rec) + } else { + rec.push("current", Value::test_bool(true)); + rec.push("latest", Value::test_string(current_version.clone())); + Value::test_record(rec) + } + } else { + rec.push("channel", Value::test_string("release")); + + let normal_pkg_name = "nushell/nushell"; + // By default, this update request is cached for 24 hours so it won't check for a new version + // each time you run nushell. Since this is run on demand, there isn't really a need to cache the check which + // is why we set the interval to std::time::Duration::ZERO. + let informer = + update_informer::new(registry::GitHub, normal_pkg_name, current_version.clone()) + .interval(std::time::Duration::ZERO); + + if let Ok(Some(new_version)) = informer.check_version() { + rec.push("current", Value::test_bool(false)); + rec.push("latest", Value::test_string(format!("{}", new_version))); + Value::test_record(rec) + } else { + rec.push("current", Value::test_bool(true)); + rec.push("latest", Value::test_string(current_version.clone())); + Value::test_record(rec) + } + } +}