diff --git a/Cargo.lock b/Cargo.lock index fbfb5cc0..a9719f1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2049,6 +2049,27 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +[[package]] +name = "ts-rs" +version = "9.0.1" +source = "git+https://github.com/Aleph-Alpha/ts-rs.git?branch=cli#7c9643e16038dd82e8ae2faf7914cdda6941b0a2" +dependencies = [ + "lazy_static", + "thiserror", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "9.0.1" +source = "git+https://github.com/Aleph-Alpha/ts-rs.git?branch=cli#7c9643e16038dd82e8ae2faf7914cdda6941b0a2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", + "termcolor", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2126,6 +2147,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "ts-rs", "ureq", "url", "xml", @@ -2193,6 +2215,8 @@ name = "velopack_nodeffi" version = "0.1.0" dependencies = [ "neon", + "serde_json", + "ts-rs", "velopack", ] diff --git a/src/bins/tests/commands.rs b/src/bins/tests/commands.rs index 60add9cf..e2263282 100644 --- a/src/bins/tests/commands.rs +++ b/src/bins/tests/commands.rs @@ -6,7 +6,6 @@ use std::{fs, path::Path, path::PathBuf}; use tempfile::tempdir; use velopack_bins::*; -use velopack_bins::logging::trace_logger; #[cfg(target_os = "windows")] use winsafe::{self as w, co}; @@ -14,7 +13,6 @@ use winsafe::{self as w, co}; #[test] pub fn test_install_apply_uninstall() { dialogs::set_silent(true); - trace_logger(); let fixtures = find_fixtures(); diff --git a/src/lib-node/README.md b/src/lib-node/README.md index 84f9705b..d3677a28 100644 --- a/src/lib-node/README.md +++ b/src/lib-node/README.md @@ -1,3 +1,10 @@ + + +installing ts tool: cargo install --git https://github.com/Aleph-Alpha/ts-rs.git --branch cli cargo-ts + + + + # veloz This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon). diff --git a/src/lib-node/crates/velopack_nodeffi/Cargo.toml b/src/lib-node/crates/velopack_nodeffi/Cargo.toml index 3c8527b5..42999147 100644 --- a/src/lib-node/crates/velopack_nodeffi/Cargo.toml +++ b/src/lib-node/crates/velopack_nodeffi/Cargo.toml @@ -12,4 +12,6 @@ crate-type = ["cdylib"] [dependencies] neon = "1" +serde_json = "1" +ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs.git", branch = "cli" } velopack = { path = "../../../lib-rust" } diff --git a/src/lib-node/crates/velopack_nodeffi/src/lib.rs b/src/lib-node/crates/velopack_nodeffi/src/lib.rs index e4372279..4a3556c9 100644 --- a/src/lib-node/crates/velopack_nodeffi/src/lib.rs +++ b/src/lib-node/crates/velopack_nodeffi/src/lib.rs @@ -1,49 +1,97 @@ use neon::prelude::*; -use velopack::*; +use std::cell::RefCell; +use std::thread; use velopack::sources::*; +use velopack::*; +#[derive(ts_rs::TS)] +#[ts(as = "T")] +pub struct Wrapper(T); -struct UpdateManagerWrapper<'a> { - manager: UpdateManager<'a>, +#[derive(ts_rs::TS)] +#[ts(export, export_to = "../../../bindings/")] +#[allow(dead_code)] +struct TsBindings { + pub t1: UpdateInfo, + pub t2: UpdateOptions, } -impl<'a> Finalize for UpdateManagerWrapper<'a> {} +struct UpdateManagerWrapper { + manager: UpdateManager, +} +impl Finalize for UpdateManagerWrapper {} +type BoxedUpdateManager = JsBox>; +fn js_new_update_manager(mut cx: FunctionContext) -> JsResult { + let arg_source = cx.argument::(0)?.value(&mut cx); + let arg_options = cx.argument::(1)?.value(&mut cx); -fn get_js_options(mut cx: FunctionContext, obj: &Handle) -> JsResult { - let allow_downgrade = obj.get(&mut cx, "allowDowngrade")?; + let mut options: Option = None; + if !arg_options.is_empty() { + let new_opt = serde_json::from_str::(&arg_options).or_else(|e| cx.throw_error(e.to_string()))?; + options = Some(new_opt); + } + + let source = AutoSource::new(&arg_source); + let manager = UpdateManager::new(source, options).or_else(|e| cx.throw_error(e.to_string()))?; + let wrapper = UpdateManagerWrapper { manager }; + Ok(cx.boxed(RefCell::new(wrapper))) } -fn js_new_from_http_source(mut cx: FunctionContext) -> JsResult> { - let url = cx.argument::(0)?.value(&mut cx); - - let options: Option = None; - - - let obj = cx.argument::(1)?; - - - - - - - - let source = HttpSource::new(&url); - let um = UpdateManager::new(source, options).map_err(|e| cx.throw_error(e.to_string()))?; - - let wrapper = UpdateManagerWrapper { manager: um }; - - - Ok(cx.boxed(wrapper)) +fn js_get_current_version(mut cx: FunctionContext) -> JsResult { + let this = cx.this::()?; + let version = this.borrow().manager.current_version().or_else(|e| cx.throw_error(e.to_string()))?; + Ok(cx.string(version)) } -fn hello(mut cx: FunctionContext) -> JsResult { - Ok(cx.string("hello node")) +fn js_check_for_updates_async(mut cx: FunctionContext) -> JsResult { + let this = cx.this::()?; + let (deferred, promise) = cx.promise(); + let channel = cx.channel(); + let manager = this.borrow().manager.clone(); + + thread::spawn(move || { + let result = manager.check_for_updates(); + channel.send(move |mut cx| { + match result { + Ok(res) => { + if let UpdateCheck::UpdateAvailable(upd) = &res { + let json = serde_json::to_string(&upd); + if let Err(e) = &json { + let err = cx.error(e.to_string()).unwrap(); + deferred.reject(&mut cx, err); + } else { + let val = cx.string(json.unwrap()); + deferred.resolve(&mut cx, val); + } + } else { + let nil = cx.null(); + deferred.resolve(&mut cx, nil); + } + } + Err(e) => { + let err = cx.error(e.to_string()).unwrap(); + deferred.reject(&mut cx, err); + } + }; + Ok(()) + }); + }); + + Ok(promise) } #[neon::main] fn main(mut cx: ModuleContext) -> NeonResult<()> { - cx.export_function("hello", hello)?; + 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_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_then_apply_update_async", js_wait_then_apply_update_async)?; Ok(()) } diff --git a/src/lib-node/src/index.cts b/src/lib-node/src/index.cts index 001e4208..52d5943a 100644 --- a/src/lib-node/src/index.cts +++ b/src/lib-node/src/index.cts @@ -1,19 +1,86 @@ -// This module is the CJS entry point for the library. - -// The Rust addon. import * as addon from './load.cjs'; -// Use this declaration to assign types to the addon's exports, -// which otherwise by default are `any`. +type UpdateManagerOpaque = {}; declare module "./load.cjs" { - function hello(): string; + function js_new_update_manager(urlOrPath: string, options?: string): UpdateManagerOpaque; + 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_check_for_updates_async(um: UpdateManagerOpaque): Promise; + function js_download_update_async(um: UpdateManagerOpaque, update: string, progress: (perc: number) => void, ignoreDeltas: boolean): Promise; + function js_wait_then_apply_update_async(um: UpdateManagerOpaque, update?: string): Promise; } -export type Greeting = { - message: string -}; - -export function greeting(): Greeting { - const message = addon.hello(); - return { message }; +export type UpdateOptions = { + AllowVersionDowngrade: boolean; + ExplicitChannel: string; } + +/** An individual Velopack asset, could refer to an asset on-disk or in a remote package feed. */ +export type VelopackAsset = { + FileName: string; + Version: string; + NotesHtml: string; + NotesMarkdown: string; + PackageId: string; + SHA1: string; + SHA256: string; + Size: number; + Type: "Full" | "Delta"; +} + +export type UpdateInfo = { + BaseRelease: VelopackAsset; + DeltasToTarget: VelopackAsset[]; + IsDowngrade: boolean; + TargetFullRelease: VelopackAsset; +} + +export class UpdateManager { + + private opaque: UpdateManagerOpaque; + + constructor(urlOrPath: string, options?: UpdateOptions) { + this.opaque = addon.js_new_update_manager(urlOrPath, options ? JSON.stringify(options) : ""); + } + + getCurrentVersion(): string { + return addon.js_get_current_version.call(this.opaque); + } + + // getAppId(): string { + // return addon.js_get_app_id.call(this.opaque); + // } + + // isInstalled(): boolean { + // return addon.js_is_installed.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); + // } + + checkForUpdatesAsync(): Promise { + let json: Promise = addon.js_check_for_updates_async.call(this.opaque); + return json.then((json) => { + if (json && json.length > 0) { + return JSON.parse(json); + } + return null; + }); + } + + downloadUpdateAsync(update: UpdateInfo, progress: (perc: number) => void, ignoreDeltas = false): Promise { + return addon.js_download_update_async.call(this.opaque, JSON.stringify(update), progress, ignoreDeltas); + } + + waitExitThenApplyUpdateAsync(update: UpdateInfo): Promise { + return addon.js_wait_then_apply_update_async.call(this.opaque, JSON.stringify(update)); + } +} \ No newline at end of file diff --git a/src/lib-rust/Cargo.toml b/src/lib-rust/Cargo.toml index b99eb263..51cd56d2 100644 --- a/src/lib-rust/Cargo.toml +++ b/src/lib-rust/Cargo.toml @@ -31,6 +31,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } zip = { version = "2.1", default-features = false, features = ["deflate"] } thiserror = "1.0" +ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs.git", branch = "cli" } # delta packages zstd = { version = "0.13", optional = true } diff --git a/src/lib-rust/src/manager.rs b/src/lib-rust/src/manager.rs index 1e457fa0..32b4852e 100644 --- a/src/lib-rust/src/manager.rs +++ b/src/lib-rust/src/manager.rs @@ -1,6 +1,10 @@ -use std::{fs, process::{exit, Command as Process}, rc::Rc, sync::mpsc::Sender}; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; +use std::{ + fs, + process::{exit, Command as Process}, + sync::mpsc::Sender, +}; #[cfg(feature = "async")] use async_std::channel::Sender; @@ -9,7 +13,11 @@ use async_std::task::JoinHandle; use semver::Version; use serde::{Deserialize, Serialize}; -use crate::{Error, locator::{self, VelopackLocator}, sources::UpdateSource}; +use crate::{ + locator::{self, VelopackLocator}, + sources::UpdateSource, + Error, +}; #[allow(non_snake_case)] #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -28,7 +36,7 @@ impl VelopackAssetFeed { } #[allow(non_snake_case)] -#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, ts_rs::TS)] #[serde(default)] /// An individual Velopack asset, could refer to an asset on-disk or in a remote package feed. pub struct VelopackAsset { @@ -53,7 +61,7 @@ pub struct VelopackAsset { } #[allow(non_snake_case)] -#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, ts_rs::TS)] #[serde(default)] /// Holds information about the current version and pending updates, such as how many there are, and access to release notes. pub struct UpdateInfo { @@ -71,8 +79,9 @@ impl AsRef for UpdateInfo { } } -#[derive(Clone)] #[allow(non_snake_case)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, ts_rs::TS)] +#[serde(default)] /// Options to customise the behaviour of UpdateManager. pub struct UpdateOptions { /// Allows UpdateManager to update to a version that's lower than the current version (i.e. downgrading). @@ -91,11 +100,11 @@ pub struct UpdateOptions { } /// Provides functionality for checking for updates, downloading updates, and applying updates to the current application. -pub struct UpdateManager<'a> -{ +#[derive(Clone)] +pub struct UpdateManager { allow_version_downgrade: bool, explicit_channel: Option, - source: Rc>, + source: Box, paths: VelopackLocator, } @@ -143,7 +152,7 @@ pub enum UpdateCheck { UpdateAvailable(UpdateInfo), } -impl<'a> UpdateManager<'a> { +impl UpdateManager { /// Create a new UpdateManager instance using the specified UpdateSource. /// This will return an error if the application is not yet installed. /// ## Example: @@ -153,12 +162,12 @@ impl<'a> UpdateManager<'a> { /// let source = sources::HttpSource::new("https://the.place/you-host/updates"); /// let um = UpdateManager::new(source, None); /// ``` - pub fn new(source: T, options: Option) -> Result, Error> { + pub fn new(source: T, options: Option) -> Result { Ok(UpdateManager { paths: locator::auto_locate()?, allow_version_downgrade: options.as_ref().map(|f| f.AllowVersionDowngrade).unwrap_or(false), explicit_channel: options.as_ref().map(|f| f.ExplicitChannel.clone()).unwrap_or(None), - source: Rc::new(Box::new(source)), + source: source.clone_boxed(), }) } @@ -185,8 +194,8 @@ impl<'a> UpdateManager<'a> { #[cfg(feature = "async")] /// Get a list of available remote releases from the package source. pub fn get_release_feed_async(&self) -> JoinHandle> - where - T: 'static, + where + T: 'static, { let self_clone = self.clone(); async_std::task::spawn_blocking(move || self_clone.get_release_feed()) @@ -251,7 +260,8 @@ impl<'a> UpdateManager<'a> { /// Checks for updates, returning None if there are none available. If there are updates available, this method will return an /// UpdateInfo object containing the latest available release, and any delta updates that can be applied if they are available. pub fn check_for_updates_async(&self) -> JoinHandle> - where T: 'static, + where + T: 'static, { let self_clone = self.clone(); async_std::task::spawn_blocking(move || self_clone.check_for_updates()) @@ -260,11 +270,10 @@ impl<'a> UpdateManager<'a> { /// Downloads the specified updates to the local app packages directory. Progress is reported back to the caller via an optional Sender. /// This function will acquire a global update lock so may fail if there is already another update operation in progress. /// - If the update contains delta packages and the delta feature is enabled - /// this method will attempt to unpack and prepare them. + /// this method will attempt to unpack and prepare them. /// - If there is no delta update available, or there is an error preparing delta - /// packages, this method will fall back to downloading the full version of the update. - pub fn download_updates(&self, update: &UpdateInfo, progress: Option>) -> Result<(), Error> - { + /// packages, this method will fall back to downloading the full version of the update. + pub fn download_updates(&self, update: &UpdateInfo, progress: Option>) -> Result<(), Error> { let name = &update.TargetFullRelease.FileName; let packages_dir = &self.paths.packages_dir; fs::create_dir_all(packages_dir)?; @@ -321,11 +330,10 @@ impl<'a> UpdateManager<'a> { /// Downloads the specified updates to the local app packages directory. Progress is reported back to the caller via an optional Sender. /// This function will acquire a global update lock so may fail if there is already another update operation in progress. /// - If the update contains delta packages and the delta feature is enabled - /// this method will attempt to unpack and prepare them. + /// this method will attempt to unpack and prepare them. /// - If there is no delta update available, or there is an error preparing delta - /// packages, this method will fall back to downloading the full version of the update. - pub fn download_updates_async(&self, update: &UpdateInfo, progress: Option>) -> JoinHandle> - { + /// packages, this method will fall back to downloading the full version of the update. + pub fn download_updates_async(&self, update: &UpdateInfo, progress: Option>) -> JoinHandle> { let self_clone = self.clone(); let update_clone = update.clone(); async_std::task::spawn_blocking(move || self_clone.download_updates(&update_clone, progress)) diff --git a/src/lib-rust/src/sources.rs b/src/lib-rust/src/sources.rs index 451efccf..5ad504b2 100644 --- a/src/lib-rust/src/sources.rs +++ b/src/lib-rust/src/sources.rs @@ -14,6 +14,54 @@ pub trait UpdateSource: Send + Sync { fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result; /// Download the specified VelopackAsset to the provided local file path. fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option>) -> Result<(), Error>; + /// Clone the source to create a new lifetime. + fn clone_boxed(&self) -> Box; +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_boxed() + } +} + +#[derive(Clone)] +/// Automatically delegates to the appropriate source based on the provided input string. If the input is a local path, +/// it will use a FileSource. If the input is a URL, it will use an HttpSource. +pub struct AutoSource { + source: Box, +} + +impl AutoSource { + /// Create a new AutoSource with the specified input string. + pub fn new(input: &str) -> AutoSource { + let source: Box = if Self::is_http_url(input) { + Box::new(HttpSource::new(input)) + } else { + Box::new(FileSource::new(input)) + }; + AutoSource { source } + } + + fn is_http_url(url: &str) -> bool { + match url::Url::parse(url) { + Ok(url) => url.scheme().eq_ignore_ascii_case("http") || url.scheme().eq_ignore_ascii_case("https"), + _ => false, + } + } +} + +impl UpdateSource for AutoSource { + fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result { + self.source.get_release_feed(channel, app) + } + + fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option>) -> Result<(), Error> { + self.source.download_release_entry(asset, local_file, progress_sender) + } + + fn clone_boxed(&self) -> Box { + self.source.clone_boxed() + } } #[derive(Clone)] @@ -59,6 +107,10 @@ impl UpdateSource for HttpSource { })?; Ok(()) } + + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } } #[derive(Clone)] @@ -99,4 +151,8 @@ impl UpdateSource for FileSource { } Ok(()) } + + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } }