From e33ba6d0812e94d8bd517f480552989d19667f17 Mon Sep 17 00:00:00 2001 From: Caelan Date: Fri, 4 Oct 2024 13:16:46 -0600 Subject: [PATCH] add more rust/node functions --- Cargo.lock | 84 +++++++++++++++++++++ Cargo.toml | 2 + samples/NodeJSElectron/src/index.ts | 9 +-- src/bins/Cargo.toml | 2 +- src/bins/src/commands/apply.rs | 4 +- src/bins/src/commands/start_windows_impl.rs | 10 +-- src/bins/src/shared/util_common.rs | 26 ------- src/lib-nodejs/src/index.ts | 57 ++++++++------ src/lib-nodejs/velopack_nodeffi/src/lib.rs | 40 ++++++++-- src/lib-rust/Cargo.toml | 2 + src/lib-rust/src/bundle.rs | 6 ++ src/lib-rust/src/locator.rs | 24 ++++++ src/lib-rust/src/manager.rs | 46 +++++++++-- src/lib-rust/src/util.rs | 22 +++++- 14 files changed, 256 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab40b645..b61c62d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,6 +269,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "blocking" version = "1.6.1" @@ -474,6 +483,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -489,6 +507,16 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cvt" version = "0.1.2" @@ -545,6 +573,16 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "2.0.2" @@ -807,6 +845,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1677,12 +1725,34 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + [[package]] name = "sha1_smol" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1963,6 +2033,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2044,6 +2120,8 @@ dependencies = [ "semver", "serde", "serde_json", + "sha1", + "sha2", "thiserror", "ts-rs", "ureq", @@ -2114,6 +2192,12 @@ dependencies = [ "velopack", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "versions" version = "5.0.1" diff --git a/Cargo.toml b/Cargo.toml index 7774632b..9c3e9e63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,8 @@ file-rotate = "0.7" simple-stopwatch = "0.1" enum-flags = "0.3" remove_dir_all = { git = "https://github.com/caesay/remove_dir_all.git", features = ["log"] } +sha1 = "0.10" +sha2 = "0.10" sha1_smol = "1.0" time = "0.3" os_info = "3.8" diff --git a/samples/NodeJSElectron/src/index.ts b/samples/NodeJSElectron/src/index.ts index e78c9b50..e940e6b2 100644 --- a/samples/NodeJSElectron/src/index.ts +++ b/samples/NodeJSElectron/src/index.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain } from 'electron'; +import { app, BrowserWindow } from 'electron'; import { VelopackApp } from "velopack"; import { initializeUpdates } from "./update" // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack @@ -13,7 +13,7 @@ VelopackApp.build() .setLogger((lvl, msg) => console.log(`Velopack [${lvl}] ${msg}`)) .run(); -// configure IPC listener for update messages +// Configure IPC listener for Velopack update messages initializeUpdates(); const createWindow = (): void => { @@ -48,11 +48,6 @@ app.on('window-all-closed', () => { } }); -// Respond to quit messages from the render -ipcMain.on("exit-request", () => { - app.quit(); -}); - app.on('activate', () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. diff --git a/src/bins/Cargo.toml b/src/bins/Cargo.toml index 9f705cbb..6a9ddce6 100644 --- a/src/bins/Cargo.toml +++ b/src/bins/Cargo.toml @@ -52,7 +52,6 @@ strum.workspace = true derivative.workspace = true glob.workspace = true remove_dir_all.workspace = true -sha1_smol.workspace = true time.workspace = true os_info.workspace = true bitflags.workspace = true @@ -111,6 +110,7 @@ filelocksmith.workspace = true tempfile.workspace = true ntest.workspace = true pretty_assertions.workspace = true +sha1_smol.workspace = true [build-dependencies] semver.workspace = true diff --git a/src/bins/src/commands/apply.rs b/src/bins/src/commands/apply.rs index 391dee40..7132aff2 100644 --- a/src/bins/src/commands/apply.rs +++ b/src/bins/src/commands/apply.rs @@ -1,5 +1,5 @@ use crate::shared::{self, OperationWait}; -use velopack::{locator::VelopackLocator, constants}; +use velopack::{locator, locator::VelopackLocator, constants}; use anyhow::{bail, Result}; use std::path::PathBuf; @@ -21,7 +21,7 @@ pub fn apply<'a>( shared::operation_wait(wait); let packages_dir = locator.get_packages_dir(); - let package = package.cloned().or_else(|| shared::find_latest_full_package(&packages_dir).map(|x| x.0)); + let package = package.cloned().or_else(|| locator::find_latest_full_package(&packages_dir).map(|x| x.0)); match package { Some(package) => { diff --git a/src/bins/src/commands/start_windows_impl.rs b/src/bins/src/commands/start_windows_impl.rs index f9a7fee5..d914f7b1 100644 --- a/src/bins/src/commands/start_windows_impl.rs +++ b/src/bins/src/commands/start_windows_impl.rs @@ -12,7 +12,7 @@ use std::{ process::Command as Process, }; use velopack::{bundle::Manifest, constants}; -use velopack::locator::{auto_locate_app_manifest, create_config_from_root_dir, LocationContext, VelopackLocator}; +use velopack::locator::{self, LocationContext, VelopackLocator}; use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow; enum LocatorResult @@ -62,7 +62,7 @@ impl LocatorResult { } fn legacy_locator() -> Result { - let locator = auto_locate_app_manifest(LocationContext::IAmUpdateExe); + let locator = locator::auto_locate_app_manifest(LocationContext::IAmUpdateExe); match locator { Ok(locator) => Ok(LocatorResult::Normal(locator)), Err(e) => { @@ -70,7 +70,7 @@ fn legacy_locator() -> Result { let my_exe = std::env::current_exe()?; let parent_dir = my_exe.parent().expect("Unable to determine parent directory"); let packages_dir = parent_dir.join("packages"); - if let Some((path, manifest)) = shared::find_latest_full_package(&packages_dir) { + if let Some((path, manifest)) = locator::find_latest_full_package(&packages_dir) { info!("Found full package to read: {}", path.to_string_lossy()); Ok(LocatorResult::Legacy(parent_dir.to_path_buf(), manifest)) } else { @@ -148,9 +148,9 @@ fn try_legacy_migration(root_dir: &PathBuf, manifest: &Manifest) -> Result Option<(PathBuf, Manifest)> { - let packages_dir = packages_dir.to_string_lossy(); - - info!("Attempting to auto-detect package in: {}", packages_dir); - let mut package: Option<(PathBuf, Manifest)> = None; - - if let Ok(paths) = glob::glob(format!("{}/*.nupkg", packages_dir).as_str()) { - for path in paths { - if let Ok(path) = path { - trace!("Checking package: '{}'", path.to_string_lossy()); - if let Ok(mut bun) = load_bundle_from_file(&path) { - if let Ok(mani) = bun.read_manifest() { - if package.is_none() || mani.version > package.clone().unwrap().1.version { - info!("Found {}: '{}'", mani.version, path.to_string_lossy()); - package = Some((path, mani)); - } - } - } - } - } - } - package -} - pub fn retry_io(op: F) -> Result where F: Fn() -> Result, diff --git a/src/lib-nodejs/src/index.ts b/src/lib-nodejs/src/index.ts index bceb91c2..cdc5a10f 100644 --- a/src/lib-nodejs/src/index.ts +++ b/src/lib-nodejs/src/index.ts @@ -15,10 +15,11 @@ declare module "./load" { function js_get_current_version(um: UpdateManagerOpaque): string; - // function js_get_app_id(um: UpdateManagerOpaque): string; - // function js_is_portable(um: UpdateManagerOpaque): boolean; - // function js_is_installed(um: UpdateManagerOpaque): boolean; - // function js_is_update_pending_restart(um: UpdateManagerOpaque): boolean; + function js_get_app_id(um: UpdateManagerOpaque): string; + + function js_is_portable(um: UpdateManagerOpaque): boolean; + + function js_update_pending_restart(um: UpdateManagerOpaque): UpdateInfo | null; function js_check_for_updates_async( um: UpdateManagerOpaque, @@ -146,14 +147,6 @@ export class VelopackApp { return this; } - /** - * Set a custom logger callback to receive log messages from Velopack. The default behavior is to log to console.log. - */ - setLogger(callback: (loglevel: LogLevel, msg: string) => void): VelopackApp { - addon.js_set_logger_callback(callback); - return this; - } - /** * Runs the Velopack startup logic. This should be the first thing to run in your app. * In some circumstances it may terminate/restart the process to perform tasks. @@ -194,21 +187,28 @@ export class UpdateManager { return addon.js_get_current_version(this.opaque); } - // getAppId(): string { - // return addon.js_get_app_id.call(this.opaque); - // } + /** + * Returns the currently installed app id. + */ + getAppId(): string { + return addon.js_get_app_id.call(this.opaque); + } - // isInstalled(): boolean { - // return addon.js_is_installed.call(this.opaque); - // } + /** + * Returns whether the app is in portable mode. On Windows this can be true or false. + * On MacOS and Linux this will always be true. + */ + isPortable(): boolean { + return addon.js_is_portable.call(this.opaque); + } - // isPortable(): boolean { - // return addon.js_is_portable.call(this.opaque); - // } - - // isUpdatePendingRestart(): boolean { - // return addon.js_is_update_pending_restart.call(this.opaque); - // } + /** + * Returns an UpdateInfo object if there is an update downloaded which still needs to be applied. + * You can pass the UpdateInfo object to waitExitThenApplyUpdate to apply the update. + */ + getUpdatePendingRestart(): UpdateInfo | null { + return addon.js_update_pending_restart.call(this.opaque); + } /** * Checks for updates, returning None if there are none available. If there are updates available, this method will return an @@ -271,3 +271,10 @@ export class UpdateManager { ); } } + +/** + * Set a custom logger callback to receive log messages from Velopack. The default behavior is to log to console.log. + */ +export function setVelopackLogger(callback: (loglevel: LogLevel, msg: string) => void) { + addon.js_set_logger_callback(callback); +} \ No newline at end of file diff --git a/src/lib-nodejs/velopack_nodeffi/src/lib.rs b/src/lib-nodejs/velopack_nodeffi/src/lib.rs index 6f558a6b..1be19889 100644 --- a/src/lib-nodejs/velopack_nodeffi/src/lib.rs +++ b/src/lib-nodejs/velopack_nodeffi/src/lib.rs @@ -68,10 +68,41 @@ fn js_new_update_manager(mut cx: FunctionContext) -> JsResult JsResult { let mgr_boxed = cx.argument::(0)?; let mgr_ref = &mgr_boxed.borrow().manager; - let version = mgr_ref.current_version().or_else(|e| cx.throw_error(e.to_string()))?; + let version = mgr_ref.get_current_version(); Ok(cx.string(version)) } +fn js_get_app_id(mut cx: FunctionContext) -> JsResult { + let mgr_boxed = cx.argument::(0)?; + let mgr_ref = &mgr_boxed.borrow().manager; + let id = mgr_ref.get_app_id(); + Ok(cx.string(id)) +} + +fn js_is_portable(mut cx: FunctionContext) -> JsResult { + let mgr_boxed = cx.argument::(0)?; + let mgr_ref = &mgr_boxed.borrow().manager; + let is_portable = mgr_ref.get_is_portable(); + Ok(cx.boolean(is_portable)) +} + +fn js_update_pending_restart(mut cx: FunctionContext) -> JsResult { + let mgr_boxed = cx.argument::(0)?; + let mgr_ref = &mgr_boxed.borrow().manager; + let pending_restart = mgr_ref.get_update_pending_restart(); + + if let Some(asset) = pending_restart { + let json = serde_json::to_string(&asset); + match json { + Ok(json) => Ok(cx.string(json).upcast()), + Err(e) => cx.throw_error(e.to_string()), + } + } else { + let nil = cx.null().upcast(); + Ok(nil) + } +} + fn js_check_for_updates_async(mut cx: FunctionContext) -> JsResult { let mgr_boxed = cx.argument::(0)?; let mgr_ref = &mgr_boxed.borrow().manager; @@ -254,10 +285,9 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("js_new_update_manager", js_new_update_manager)?; cx.export_function("js_get_current_version", js_get_current_version)?; - // cx.export_function("js_get_app_id", js_get_app_id)?; - // cx.export_function("js_is_portable", js_is_portable)?; - // cx.export_function("js_is_installed", js_is_installed)?; - // cx.export_function("js_is_update_pending_restart", js_is_update_pending_restart)?; + cx.export_function("js_get_app_id", js_get_app_id)?; + cx.export_function("js_is_portable", js_is_portable)?; + cx.export_function("js_update_pending_restart", js_update_pending_restart)?; cx.export_function("js_check_for_updates_async", js_check_for_updates_async)?; cx.export_function("js_download_update_async", js_download_update_async)?; cx.export_function("js_wait_exit_then_apply_update", js_wait_exit_then_apply_update)?; diff --git a/src/lib-rust/Cargo.toml b/src/lib-rust/Cargo.toml index 60835188..747eb9ba 100644 --- a/src/lib-rust/Cargo.toml +++ b/src/lib-rust/Cargo.toml @@ -44,6 +44,8 @@ normpath.workspace = true bitflags.workspace = true rand.workspace = true native-tls.workspace = true +sha1.workspace = true +sha2.workspace = true # typescript ts-rs = { workspace = true, optional = true } diff --git a/src/lib-rust/src/bundle.rs b/src/lib-rust/src/bundle.rs index 42943cc9..ae98700f 100644 --- a/src/lib-rust/src/bundle.rs +++ b/src/lib-rust/src/bundle.rs @@ -355,6 +355,8 @@ pub struct Manifest { pub channel: String, pub shortcut_locations: String, pub shortcut_amuid: String, + pub release_notes: String, + pub release_notes_html: String, } /// Parse manifest object from an XML string. @@ -399,6 +401,10 @@ pub fn read_manifest_from_string(xml: &str) -> Result { obj.shortcut_locations = text; } else if el_name == "shortcutAmuid" { obj.shortcut_amuid = text; + } else if el_name == "releaseNotes" { + obj.release_notes = text; + } else if el_name == "releaseNotesHtml" { + obj.release_notes_html = text; } } Ok(XmlEvent::EndElement { .. }) => { diff --git a/src/lib-rust/src/locator.rs b/src/lib-rust/src/locator.rs index cfb42888..ce4f5618 100644 --- a/src/lib-rust/src/locator.rs +++ b/src/lib-rust/src/locator.rs @@ -482,3 +482,27 @@ fn read_current_manifest(nuspec_path: &PathBuf) -> Result { Err(Error::MissingNuspec) } +/// Returns the path and manifest of the latest full package in the given directory. +pub fn find_latest_full_package(packages_dir: &PathBuf) -> Option<(PathBuf, Manifest)> { + let packages_dir = packages_dir.to_string_lossy(); + + info!("Attempting to auto-detect package in: {}", packages_dir); + let mut package: Option<(PathBuf, Manifest)> = None; + + if let Ok(paths) = glob::glob(format!("{}/*.nupkg", packages_dir).as_str()) { + for path in paths { + if let Ok(path) = path { + trace!("Checking package: '{}'", path.to_string_lossy()); + if let Ok(mut bun) = bundle::load_bundle_from_file(&path) { + if let Ok(mani) = bun.read_manifest() { + if package.is_none() || mani.version > package.clone().unwrap().1.version { + info!("Found {}: '{}'", mani.version, path.to_string_lossy()); + package = Some((path, mani)); + } + } + } + } + } + } + package +} diff --git a/src/lib-rust/src/manager.rs b/src/lib-rust/src/manager.rs index 3418d1b2..b54099ba 100644 --- a/src/lib-rust/src/manager.rs +++ b/src/lib-rust/src/manager.rs @@ -14,11 +14,11 @@ use semver::Version; use serde::{Deserialize, Serialize}; use crate::{ - locator::{self, VelopackLocatorConfig}, + locator::{self, VelopackLocatorConfig, LocationContext, VelopackLocator}, sources::UpdateSource, Error, + util, }; -use crate::locator::{auto_locate_app_manifest, LocationContext, VelopackLocator}; #[allow(non_snake_case)] #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -140,7 +140,7 @@ impl UpdateManager { let manifest = config.load_manifest()?; VelopackLocator::new(config.clone(), manifest) } else { - auto_locate_app_manifest(LocationContext::FromCurrentExe)? + locator::auto_locate_app_manifest(LocationContext::FromCurrentExe)? }; Ok(UpdateManager { options: options.unwrap_or_default(), @@ -160,8 +160,42 @@ impl UpdateManager { } /// The currently installed app version. - pub fn current_version(&self) -> Result { - Ok(self.locator.get_manifest_version_full_string()) + pub fn get_current_version(&self) -> String { + self.locator.get_manifest_version_full_string() + } + + /// The currently installed app id. + pub fn get_app_id(&self) -> String { + self.locator.get_manifest_id() + } + + /// Check if the app is in portable mode. This can be true or false on Windows. + /// On Linux and MacOS, this will always return true. + pub fn get_is_portable(&self) -> bool { + self.locator.get_is_portable() + } + + /// Returns None if there is no local package waiting to be applied. Returns a VelopackAsset + /// if there is an update downloaded which has not yet been applied. In that case, the + /// VelopackAsset can be applied by calling apply_updates_and_restart or wait_exit_then_apply_updates. + pub fn get_update_pending_restart(&self) -> Option { + let packages_dir = self.locator.get_packages_dir(); + if let Some((path, manifest)) = locator::find_latest_full_package(&packages_dir) { + if manifest.version > self.locator.get_manifest_version() { + return Some(VelopackAsset { + PackageId: manifest.id, + Version: manifest.version.to_string(), + Type: "Full".to_string(), + FileName: path.file_name().unwrap().to_string_lossy().to_string(), + SHA1: util::calculate_file_sha1(&path).unwrap_or_default(), + SHA256: util::calculate_file_sha256(&path).unwrap_or_default(), + Size: path.metadata().map(|m| m.len()).unwrap_or(0), + NotesMarkdown: manifest.release_notes, + NotesHtml: manifest.release_notes_html, + }); + } + } + None } /// Get a list of available remote releases from the package source. @@ -252,7 +286,7 @@ impl UpdateManager { pub fn download_updates(&self, update: &UpdateInfo, progress: Option>) -> Result<(), Error> { let name = &update.TargetFullRelease.FileName; let packages_dir = &self.locator.get_packages_dir(); - + fs::create_dir_all(packages_dir)?; let target_file = packages_dir.join(name); diff --git a/src/lib-rust/src/util.rs b/src/lib-rust/src/util.rs index 15db759d..17601e04 100644 --- a/src/lib-rust/src/util.rs +++ b/src/lib-rust/src/util.rs @@ -1,6 +1,10 @@ +use crate::Error; +use rand::distributions::{Alphanumeric, DistString}; +use sha2::Digest; +use std::fs::File; +use std::path::Path; use std::thread; use std::time::Duration; -use rand::distributions::{Alphanumeric, DistString}; pub fn retry_io(op: F) -> Result where @@ -36,4 +40,20 @@ where pub fn random_string(len: usize) -> String { Alphanumeric.sample_string(&mut rand::thread_rng(), len) +} + +pub fn calculate_file_sha256>(file: P) -> Result { + let mut file = File::open(file)?; + let mut sha256 = sha2::Sha256::new(); + std::io::copy(&mut file, &mut sha256)?; + let hash = sha256.finalize(); + Ok(format!("{:x}", hash)) +} + +pub fn calculate_file_sha1>(file: P) -> Result { + let mut file = File::open(file)?; + let mut sha1o = sha1::Sha1::new(); + std::io::copy(&mut file, &mut sha1o)?; + let hash = sha1o.finalize(); + Ok(format!("{:x}", hash)) } \ No newline at end of file