wip JS bindings

This commit is contained in:
Caelan Sayler
2024-08-13 09:12:28 +01:00
committed by Caelan
parent 086eb0a980
commit 31ab5f138c
9 changed files with 278 additions and 67 deletions

24
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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();

View File

@@ -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).

View File

@@ -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" }

View File

@@ -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: ts_rs::TS>(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<RefCell<UpdateManagerWrapper>>;
fn js_new_update_manager(mut cx: FunctionContext) -> JsResult<BoxedUpdateManager> {
let arg_source = cx.argument::<JsString>(0)?.value(&mut cx);
let arg_options = cx.argument::<JsString>(1)?.value(&mut cx);
fn get_js_options(mut cx: FunctionContext, obj: &Handle<JsObject>) -> JsResult<UpdateOptions> {
let allow_downgrade = obj.get(&mut cx, "allowDowngrade")?;
let mut options: Option<UpdateOptions> = None;
if !arg_options.is_empty() {
let new_opt = serde_json::from_str::<UpdateOptions>(&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<JsBox<UpdateManagerWrapper>> {
let url = cx.argument::<JsString>(0)?.value(&mut cx);
let options: Option<UpdateOptions> = None;
let obj = cx.argument::<JsObject>(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<JsString> {
let this = cx.this::<BoxedUpdateManager>()?;
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<JsString> {
Ok(cx.string("hello node"))
fn js_check_for_updates_async(mut cx: FunctionContext) -> JsResult<JsPromise> {
let this = cx.this::<BoxedUpdateManager>()?;
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(())
}

View File

@@ -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<string | null>;
function js_download_update_async(um: UpdateManagerOpaque, update: string, progress: (perc: number) => void, ignoreDeltas: boolean): Promise<void>;
function js_wait_then_apply_update_async(um: UpdateManagerOpaque, update?: string): Promise<void>;
}
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<UpdateInfo | null> {
let json: Promise<string> = 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<void> {
return addon.js_download_update_async.call(this.opaque, JSON.stringify(update), progress, ignoreDeltas);
}
waitExitThenApplyUpdateAsync(update: UpdateInfo): Promise<void> {
return addon.js_wait_then_apply_update_async.call(this.opaque, JSON.stringify(update));
}
}

View File

@@ -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 }

View File

@@ -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<VelopackAsset> 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<String>,
source: Rc<Box<dyn UpdateSource + 'a>>,
source: Box<dyn UpdateSource>,
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<T: UpdateSource + 'a>(source: T, options: Option<UpdateOptions>) -> Result<UpdateManager::<'a>, Error> {
pub fn new<T: UpdateSource>(source: T, options: Option<UpdateOptions>) -> Result<UpdateManager, Error> {
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(),
})
}
@@ -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<Result<UpdateCheck, Error>>
where T: 'static,
where
T: 'static,
{
let self_clone = self.clone();
async_std::task::spawn_blocking(move || self_clone.check_for_updates())
@@ -263,8 +273,7 @@ impl<'a> UpdateManager<'a> {
/// 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<Sender<i16>>) -> Result<(), Error>
{
pub fn download_updates(&self, update: &UpdateInfo, progress: Option<Sender<i16>>) -> Result<(), Error> {
let name = &update.TargetFullRelease.FileName;
let packages_dir = &self.paths.packages_dir;
fs::create_dir_all(packages_dir)?;
@@ -324,8 +333,7 @@ impl<'a> UpdateManager<'a> {
/// 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<Sender<i16>>) -> JoinHandle<Result<(), Error>>
{
pub fn download_updates_async(&self, update: &UpdateInfo, progress: Option<Sender<i16>>) -> JoinHandle<Result<(), Error>> {
let self_clone = self.clone();
let update_clone = update.clone();
async_std::task::spawn_blocking(move || self_clone.download_updates(&update_clone, progress))

View File

@@ -14,6 +14,54 @@ pub trait UpdateSource: Send + Sync {
fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result<VelopackAssetFeed, Error>;
/// Download the specified VelopackAsset to the provided local file path.
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error>;
/// Clone the source to create a new lifetime.
fn clone_boxed(&self) -> Box<dyn UpdateSource>;
}
impl Clone for Box<dyn UpdateSource> {
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<dyn UpdateSource>,
}
impl AutoSource {
/// Create a new AutoSource with the specified input string.
pub fn new(input: &str) -> AutoSource {
let source: Box<dyn UpdateSource> = 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<VelopackAssetFeed, Error> {
self.source.get_release_feed(channel, app)
}
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
self.source.download_release_entry(asset, local_file, progress_sender)
}
fn clone_boxed(&self) -> Box<dyn UpdateSource> {
self.source.clone_boxed()
}
}
#[derive(Clone)]
@@ -59,6 +107,10 @@ impl UpdateSource for HttpSource {
})?;
Ok(())
}
fn clone_boxed(&self) -> Box<dyn UpdateSource> {
Box::new(self.clone())
}
}
#[derive(Clone)]
@@ -99,4 +151,8 @@ impl UpdateSource for FileSource {
}
Ok(())
}
fn clone_boxed(&self) -> Box<dyn UpdateSource> {
Box::new(self.clone())
}
}