diff --git a/Cargo.lock b/Cargo.lock index b79569f..6462ed1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,6 +174,15 @@ version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "argon2" version = "0.5.3" @@ -617,6 +626,27 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cassowary" version = "0.3.0" @@ -814,6 +844,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1165,6 +1208,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "der" version = "0.7.9" @@ -1185,6 +1234,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "digest" version = "0.10.7" @@ -1217,6 +1277,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "ecoji" version = "1.0.0" @@ -1260,6 +1331,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -1345,6 +1422,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1811,6 +1898,20 @@ dependencies = [ "serde", ] +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "tokio", + "unicode-width", +] + [[package]] name = "inout" version = "0.1.3" @@ -1825,6 +1926,7 @@ name = "insanity-core" version = "1.2.6" dependencies = [ "bon", + "built", ] [[package]] @@ -1856,12 +1958,14 @@ dependencies = [ "dirs", "ed25519-dalek", "fern", + "indicatif", "insanity-core", "insanity-tui-adapter", "itertools", "log", "nnnoiseless", "opus", + "reqwest", "rubato", "rubato-audio-source", "send_safe", @@ -1869,11 +1973,14 @@ dependencies = [ "serde_json", "sha2", "sled", + "tempfile", "tokio", "tokio-util", + "tracing-subscriber", "uuid", "veq", "whoami", + "zip", ] [[package]] @@ -2048,6 +2155,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.22" @@ -2057,6 +2170,16 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "mach" version = "0.3.2" @@ -2378,6 +2501,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.36.4" @@ -2563,6 +2692,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2704,6 +2843,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" + [[package]] name = "powerfmt" version = "0.2.0" @@ -3399,6 +3544,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "siphasher" version = "0.2.3" @@ -4461,6 +4612,63 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "zip" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap 2.5.0", + "lzma-rs", + "memchr", + "pbkdf2", + "rand 0.8.5", + "sha1", + "thiserror", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] [[package]] name = "zstd" diff --git a/insanity-core/Cargo.toml b/insanity-core/Cargo.toml index 64236d6..efa5986 100644 --- a/insanity-core/Cargo.toml +++ b/insanity-core/Cargo.toml @@ -5,3 +5,6 @@ edition = "2021" [dependencies] bon = "2.1.1" + +[build-dependencies] +built = { version = "0.7.4", features = ["chrono", "git2"] } diff --git a/insanity-core/build.rs b/insanity-core/build.rs new file mode 100644 index 0000000..d8f91cb --- /dev/null +++ b/insanity-core/build.rs @@ -0,0 +1,3 @@ +fn main() { + built::write_built_file().expect("Failed to acquire build-time information"); +} diff --git a/insanity-core/src/lib.rs b/insanity-core/src/lib.rs index 059108f..8088f81 100644 --- a/insanity-core/src/lib.rs +++ b/insanity-core/src/lib.rs @@ -1,3 +1,7 @@ pub mod audio_source; -pub mod user_input_event; pub mod loudness; +pub mod user_input_event; + +pub mod built_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} diff --git a/insanity-native-tui-app/Cargo.toml b/insanity-native-tui-app/Cargo.toml index 816baa6..921de45 100644 --- a/insanity-native-tui-app/Cargo.toml +++ b/insanity-native-tui-app/Cargo.toml @@ -43,3 +43,8 @@ argon2 = "0.5.3" chacha20poly1305 = "0.10.1" blake3 = "1.5.4" ed25519-dalek = { version = "2.1.1", features = ["serde"] } +reqwest = { version = "0.12.7", features = ["native-tls-vendored", "json"] } +tracing-subscriber = "0.3.18" +tempfile = "3.12.0" +indicatif = { version = "0.17.8", features = ["tokio"] } +zip = "2.2.0" diff --git a/insanity-native-tui-app/src/lib.rs b/insanity-native-tui-app/src/lib.rs index 3aa2ba5..7b4eeb0 100644 --- a/insanity-native-tui-app/src/lib.rs +++ b/insanity-native-tui-app/src/lib.rs @@ -7,3 +7,4 @@ pub mod protocol; pub mod realtime_buffer; pub mod room_handler; pub mod server; +pub mod update; diff --git a/insanity-native-tui-app/src/main.rs b/insanity-native-tui-app/src/main.rs index 5922bf8..f9d3d03 100644 --- a/insanity-native-tui-app/src/main.rs +++ b/insanity-native-tui-app/src/main.rs @@ -1,8 +1,9 @@ use std::{path::PathBuf, str::FromStr, time::Duration}; use clap::{Parser, Subcommand}; +use insanity_core::built_info; use insanity_tui_adapter::AppEvent; -use insanity_tui_app::connection_manager::ConnectionManager; +use insanity_tui_app::{connection_manager::ConnectionManager, update}; use tokio_util::sync::CancellationToken; // Update this number if there is a breaking change. @@ -10,7 +11,7 @@ use tokio_util::sync::CancellationToken; static BREAKING_CHANGE_VERSION: &str = "1"; #[derive(Parser, Debug)] -#[clap(author = "Nicolas Chan ")] +#[clap(version = built_info::GIT_VERSION, author = "Nicolas Chan ")] struct Cli { #[command(subcommand)] command: Commands, @@ -19,7 +20,13 @@ struct Cli { #[derive(Subcommand, Debug)] enum Commands { Run(RunOptions), - Update, + Update { + #[clap(long, default_value = "false")] + dry_run: bool, + + #[clap(long, default_value = "false")] + force: bool, + }, } #[derive(Parser, Debug)] @@ -54,14 +61,10 @@ async fn main() -> anyhow::Result<()> { match opts.command { Commands::Run(run_opts) => run(run_opts).await, - Commands::Update => update().await, + Commands::Update { dry_run, force } => update::update(dry_run, force).await, } } -async fn update() -> anyhow::Result<()> { - todo!() -} - async fn run(opts: RunOptions) -> anyhow::Result<()> { let main_cancellation_token = CancellationToken::new(); diff --git a/insanity-native-tui-app/src/update.rs b/insanity-native-tui-app/src/update.rs new file mode 100644 index 0000000..2c8fa54 --- /dev/null +++ b/insanity-native-tui-app/src/update.rs @@ -0,0 +1,236 @@ +use std::os::unix::fs::PermissionsExt; + +use insanity_core::built_info; +use log::{info, warn}; +use tokio::{fs::create_dir_all, io::AsyncWriteExt}; + +pub async fn update(dry_run: bool, force: bool) -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + let current_version = format!("v{}", built_info::PKG_VERSION); + info!("Current version is: {}", current_version); + + let release_url = "https://api.github.com/repos/nicolaschan/insanity/releases/latest"; + let client = reqwest::Client::new(); + let random_user_agent_string = uuid::Uuid::new_v4().to_string(); + let latest_release_response = client + .get(release_url) + .header( + "User-Agent", + format!("insanity-updater-{}", random_user_agent_string), + ) + .send() + .await?; + + if !latest_release_response.status().is_success() { + return Err(anyhow::anyhow!( + "Failed to fetch latest release: {}", + latest_release_response.status() + )); + } + + let latest_release = latest_release_response.json::().await?; + + let new_version = latest_release["tag_name"] + .as_str() + .expect("tag_name is not a string"); + + info!("Found latest release version: {}", new_version,); + + if !force && new_version == current_version { + info!("Already up to date"); + return Ok(()); + } + + let assets = latest_release["assets"] + .as_array() + .expect("no assets in latest release"); + + info!("Found {} release assets", assets.len()); + + let current_platform = + get_current_platform().expect("This platform does not support updating through the cli"); + info!("Current platform: {:?}", current_platform); + + let current_platform_asset_name = current_platform.get_asset_name(); + + let asset_for_current_platform = assets.iter().find(|asset| { + asset["name"] + .as_str() + .expect("asset name is not a string") + .starts_with(¤t_platform_asset_name) + }); + + let asset_download_url = asset_for_current_platform.expect("no asset for current platform") + ["browser_download_url"] + .as_str() + .expect("asset download url is not a string"); + + info!( + "Found download URL for current platform: {}", + asset_download_url + ); + + let content_length_response = client.head(asset_download_url).send().await?; + + let content_length: u64 = content_length_response + .headers() + .get(reqwest::header::CONTENT_LENGTH) + .expect("no content length header") + .to_str() + .unwrap() + .parse()?; + + info!("Expected content length: {:?}", content_length); + + let temp_dir = tempfile::tempdir()?; + let temp_file_path = temp_dir.path().join(¤t_platform_asset_name); + + info!("Downloading to {}", temp_file_path.display()); + + let mut download_response = client.get(asset_download_url).send().await?; + let mut dest = tokio::fs::File::create(&temp_file_path).await?; + + let pb = indicatif::ProgressBar::new(content_length); + + while let Some(chunk) = download_response.chunk().await? { + dest.write_all(&chunk).await?; + pb.set_position(pb.position() + chunk.len() as u64); + } + + pb.finish(); + + let extract_path = temp_dir.path().join("extracted"); + let zip_file = std::fs::File::open(&temp_file_path)?; + let mut archive = zip::ZipArchive::new(zip_file)?; + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let outpath = extract_path.join(file.name()); + + if file.name().ends_with('/') { + create_dir_all(&outpath).await?; + } else { + if let Some(p) = outpath.parent() { + if !p.exists() { + create_dir_all(p).await?; + } + } + let mut outfile = std::fs::File::create(&outpath)?; + std::io::copy(&mut file, &mut outfile)?; + } + } + + info!("Extracted to {}", extract_path.display()); + + let new_exe_dir = extract_path.join(¤t_platform_asset_name); + let new_exe_dir_contents = std::fs::read_dir(&new_exe_dir)?; + + let new_exe_path = new_exe_dir_contents + .into_iter() + .find_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + info!("Checking path: {}", path.display()); + if path.is_file() + && path + .file_name() + .and_then(|f| f.to_str()) + .map(|f| f.starts_with("insanity")) + .unwrap_or(false) + { + Some(path) + } else { + None + } + }) + .expect("no exe file found in extracted directory"); + + info!("New executable path: {}", new_exe_path.display()); + + let current_exe = std::env::current_exe()?; + info!("Current executable: {}", current_exe.display()); + + // Move the new executable to the current executable's location + if !dry_run { + info!( + "Replacing {} with {}", + current_exe.display(), + new_exe_path.display(), + ); + tokio::fs::rename(&new_exe_path, ¤t_exe).await?; + } + + #[cfg(unix)] + { + let current_exe_file = tokio::fs::File::open(¤t_exe).await?; + let mut perms = current_exe_file.metadata().await?.permissions(); + perms.set_mode(perms.mode() | 0o111); // Add execute permission + match tokio::fs::set_permissions(¤t_exe, perms).await { + Ok(_) => info!("Set execute permission on {}", current_exe.display()), + Err(e) => warn!( + "Failed to set execute permission on {}: {}", + current_exe.display(), + e + ), + } + } + + info!( + "Updated version {} -> {} complete", + current_version, new_version + ); + Ok(()) +} + +#[allow(dead_code)] +#[derive(Debug)] +enum UpdatablePlatform { + LinuxGNU, + LinuxMusl, + WindowsMSVC, + WindowsMingw, + MacOSAppleSilicon, +} + +impl UpdatablePlatform { + fn get_asset_name(&self) -> String { + match self { + UpdatablePlatform::LinuxGNU => "insanity-linux-gnu", + UpdatablePlatform::LinuxMusl => "insanity-linux-musl", + UpdatablePlatform::WindowsMSVC => "insanity-windows-msvc", + UpdatablePlatform::WindowsMingw => "insanity-windows-mingw", + UpdatablePlatform::MacOSAppleSilicon => "insanity-macos-apple-silicon", + } + .to_string() + } +} + +#[allow(unreachable_code)] +fn get_current_platform() -> Option { + #[cfg(all(target_os = "linux", target_env = "gnu"))] + { + return Some(UpdatablePlatform::LinuxGNU); + } + + #[cfg(all(target_os = "linux", target_env = "musl"))] + { + return Some(UpdatablePlatform::LinuxMusl); + } + + #[cfg(all(target_os = "windows", target_env = "msvc"))] + { + return Some(UpdatablePlatform::WindowsMSVC); + } + + #[cfg(all(target_os = "windows", target_env = "gnu"))] + { + return Some(UpdatablePlatform::WindowsMingw); + } + + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + { + return Some(UpdatablePlatform::MacOSAppleSilicon); + } + + None +} diff --git a/insanity-tui-adapter/src/render.rs b/insanity-tui-adapter/src/render.rs index 8dbde63..f0a6a47 100644 --- a/insanity-tui-adapter/src/render.rs +++ b/insanity-tui-adapter/src/render.rs @@ -1,3 +1,4 @@ +use insanity_core::built_info; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, @@ -373,10 +374,6 @@ fn render_chat(f: &mut Frame, app: &App, area: Rect) { f.render_widget(editor_widget, chunks[1]); } -mod built_info { - include!(concat!(env!("OUT_DIR"), "/built.rs")); -} - fn render_settings(f: &mut Frame, app: &App, area: Rect) { let chunks = Layout::default() .direction(Direction::Vertical)