From 36ccfd2c7a588cd6a554b7a75b76b713f34afeb4 Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Thu, 6 Apr 2023 21:12:27 -0700 Subject: [PATCH] updater/ has moved to its own repository --- updater/.gitignore | 17 - updater/BUILDING_ENGINE.md | 122 ----- updater/Cargo.toml | 2 - updater/README.md | 16 - updater/UPDATING_ENGINE.md | 30 -- updater/cbindgen.toml | 20 - updater/cli/Cargo.toml | 10 - updater/cli/src/main.rs | 66 --- updater/dart_bindings/.gitignore | 7 - updater/dart_bindings/README.md | 6 - updater/dart_bindings/analysis_options.yaml | 30 -- updater/dart_bindings/lib/src/bindings.dart | 116 ----- updater/dart_bindings/lib/updater.dart | 108 ----- updater/dart_bindings/pubspec.yaml | 16 - updater/dart_cli/.gitignore | 4 - updater/dart_cli/README.md | 1 - updater/dart_cli/analysis_options.yaml | 30 -- updater/dart_cli/bin/dart_cli.dart | 137 ------ updater/dart_cli/pubspec.yaml | 18 - updater/library/.gitignore | 14 - updater/library/Cargo.toml | 50 --- updater/library/README.md | 175 -------- updater/library/include/updater.h | 101 ----- updater/library/src/c_api.rs | 146 ------ updater/library/src/cache.rs | 362 --------------- updater/library/src/config.rs | 114 ----- updater/library/src/lib.rs | 24 - updater/library/src/logging.rs | 18 - updater/library/src/network.rs | 124 ------ updater/library/src/updater.rs | 464 -------------------- updater/library/src/yaml.rs | 20 - updater/patch/Cargo.toml | 15 - updater/patch/README.md | 13 - updater/patch/src/main.rs | 52 --- 34 files changed, 2448 deletions(-) delete mode 100644 updater/.gitignore delete mode 100644 updater/BUILDING_ENGINE.md delete mode 100644 updater/Cargo.toml delete mode 100644 updater/README.md delete mode 100644 updater/UPDATING_ENGINE.md delete mode 100644 updater/cbindgen.toml delete mode 100644 updater/cli/Cargo.toml delete mode 100644 updater/cli/src/main.rs delete mode 100644 updater/dart_bindings/.gitignore delete mode 100644 updater/dart_bindings/README.md delete mode 100644 updater/dart_bindings/analysis_options.yaml delete mode 100644 updater/dart_bindings/lib/src/bindings.dart delete mode 100644 updater/dart_bindings/lib/updater.dart delete mode 100644 updater/dart_bindings/pubspec.yaml delete mode 100644 updater/dart_cli/.gitignore delete mode 100644 updater/dart_cli/README.md delete mode 100644 updater/dart_cli/analysis_options.yaml delete mode 100644 updater/dart_cli/bin/dart_cli.dart delete mode 100644 updater/dart_cli/pubspec.yaml delete mode 100644 updater/library/.gitignore delete mode 100644 updater/library/Cargo.toml delete mode 100644 updater/library/README.md delete mode 100644 updater/library/include/updater.h delete mode 100644 updater/library/src/c_api.rs delete mode 100644 updater/library/src/cache.rs delete mode 100644 updater/library/src/config.rs delete mode 100644 updater/library/src/lib.rs delete mode 100644 updater/library/src/logging.rs delete mode 100644 updater/library/src/network.rs delete mode 100644 updater/library/src/updater.rs delete mode 100644 updater/library/src/yaml.rs delete mode 100644 updater/patch/Cargo.toml delete mode 100644 updater/patch/README.md delete mode 100644 updater/patch/src/main.rs diff --git a/updater/.gitignore b/updater/.gitignore deleted file mode 100644 index 1aaec948..00000000 --- a/updater/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - -# Cache directory from updater cli. -updater_cache \ No newline at end of file diff --git a/updater/BUILDING_ENGINE.md b/updater/BUILDING_ENGINE.md deleted file mode 100644 index 73bbe829..00000000 --- a/updater/BUILDING_ENGINE.md +++ /dev/null @@ -1,122 +0,0 @@ -# Building the Shorebird Flutter Engine - -Shorebird uses a modified version of the Flutter engine. Normally -when you use Shorebird, you would use the pre-built engine binaries -that we provide. However, if you want to build the engine yourself, -this document describes how to do that. - -The primary modification Shorebird makes to the stock Flutter engine -is adding support for the updater library. The updater library is -written in Rust and is used to update the code running in the Flutter -app. The updater library is built as a static library and is linked -into the Flutter engine during build time. - -## Building the Updater Library - -### Installing Rust - -The updater library is written in Rust. You can install Rust using -rustup. See https://rustup.rs/ for details. - -## Building for Android - -Rust Android tooling *mostly* works out of the box, but needs a bunch -of configuration to get it to work. - -The best way I found was to install: -https://github.com/bbqsrc/cargo-ndk - -``` -rustup install beta -cargo +beta install cargo-ndk -rustup +beta target add \ - aarch64-linux-android \ - armv7-linux-androideabi \ - x86_64-linux-android \ - i686-linux-android -``` - -If others know of better instructions, please send us a PR! - -Once you have cargo-ndk installed, you can build the updater library with the -beta toolchain you installed and the ndk command: - -``` -cargo +beta ndk --target aarch64-linux-android build --release -``` - -### Setting up to build the Flutter Engine: - -https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment -https://github.com/flutter/flutter/wiki/Compiling-the-engine - -The .gclient file I recommend is: -``` -solutions = [ - { - "managed": False, - "name": "src/flutter", - "url": "git@github.com:shorebirdtech/engine.git", - "custom_deps": {}, - "deps_file": "DEPS", - "safesync_url": "", - }, -] -``` -(We should probably just check that in somewhere.) - -Once you have that set up and `gclient sync` has run, you will need -to switch your flutter checkout to the `codepush` branch: - -``` -cd src/flutter -git checkout codepush -``` - -And then `gclient sync` again. - -### Symlink in the Rust binaries - -Currently you need to symlink in the results of the rust build into the engine/src directory: - -``` -cd flutter -mkdir updater -cd updater -ln -s $SRC/shorebird/updater/library/include/updater.h -mkdir android_aarch64 -cd android_aarch64 -ln -s $SRC/shorebird/updater/target/aarch64-linux-android/release/libupdater.a -``` - -## Building Flutter Engine - -``` -./flutter/tools/gn --android --android-cpu arm64 --runtime-mode=release -ninja -C out/android_release_arm64 -``` - -The linking step for android_release_arm64 is _much_ longer than other platforms -we may need to use unopt or debug builds for faster iteration. - -I also add `&& say "done"` to the end of the ninja command so I know when it's -done (because it takes minutes). - - -## Running with your local engine - -The `shorebird` tools don't yet support local engines, so you need to use -`flutter run` directly. -https://github.com/shorebirdtech/shorebird/issues/42 - -Here is a script: -``` -#! /bin/sh -x - -LOCAL_ENGINE_SRC_PATH=/path/to/local/flutter/engine -LOCAL_ENGINE=android_release_arm64 -flutter build apk --release --no-tree-shake-icons --local-engine-src-path $LOCAL_ENGINE_SRC_PATH --local-engine=$LOCAL_ENGINE - ``` - -Only need to build with your custom engine once. Once the app is installed on -the phone then you can `shorebird publish` to it as normal. diff --git a/updater/Cargo.toml b/updater/Cargo.toml deleted file mode 100644 index ee7db420..00000000 --- a/updater/Cargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[workspace] -members = ["cli", "library", "patch"] \ No newline at end of file diff --git a/updater/README.md b/updater/README.md deleted file mode 100644 index 656ae33d..00000000 --- a/updater/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Updater library - -This is the C/Rust side of the Shorebird code push system. This is built -in Rust with a C API for easy calling from other languages, most notably -for linking into libflutter.so. - -See cli/README.md for more documentation on the library. - -## Parts -* cli: Test the updater library via the Rust API (for development). -* dart_cli: Test ffi wrapping of updater library. -* library: The rust library that does the actual update work. -* dart_bindings: The Dart bindings for the updater library. - -All of the interesting code is in the `library` directory. There is also -a README.md in that directory explaining the design. diff --git a/updater/UPDATING_ENGINE.md b/updater/UPDATING_ENGINE.md deleted file mode 100644 index ee25e813..00000000 --- a/updater/UPDATING_ENGINE.md +++ /dev/null @@ -1,30 +0,0 @@ -Shorebird currently maintains patches to the Flutter Engine to integrate the Shorebird updater library. - -We have changes to both flutter/engine and flutter/buildroot. We need to make sure those changes are applied to the correct version of each, to be compatible with a given `flutter/flutter` version. - -* flutter/flutter version is whatever is the latest `stable` in flutter/flutter. -* From that you can get the `flutter/engine` version in `bin/internal/engine.version` in flutter/flutter. -* From that you can get the `flutter/buildroot` version in `DEPS` in `flutter/engine` - -Working from a `flutter/engine` checkout can be confusing because the layout is: -src/ <- `flutter/buildroot` -src/flutter <- `flutter/engine` - -`gclient` uses the file in `src/flutter/DEPS` to control the whole checkout, as directed by your `.gclient` file in the parent directory of `src`. - -Here are the steps to update those forks every time Flutter releases: - -1. You need the release name / git id for the Flutter release. e.g. 3.7.7 -1. Fetch flutter at that tag -1. Look at engine version https://github.com/flutter/flutter/blob/3.7.7/bin/internal/engine.version -See that is 1837b5be5f0f1376a1ccf383950e83a80177fb4e. -1. Rebase our engine patches onto that engine version. https://github.com/shorebirdtech/engine/tree/stable_codepush -If the engine is tagged correctly (it isn't always) it's as simple as: -`git rebase --onto 3.7.7 3.7.6 -1. Should then also look at DEPS file in engine: https://github.com/flutter/engine/blob/3.7.7/DEPS -Where we are looking for the buildroot hash: https://github.com/flutter/engine/blob/3.7.7/DEPS#L239 -1. We now need to rebase the buildroot (src/) changes onto the buildroot version in that DEPS. https://github.com/shorebirdtech/buildroot/tree/stable_codepush -e.g. `git rebase --onto 8747bce41d0dc6d9dc45c4d1b46d2100bb9ee688 93f7f85422a8604bdc44ef76c3f105ead65e8c1c` -Where as that should be `--onto new_base_revision previous_base_revision`. The base revision is the git id *before* we started making changes. -1. Once you've successfully rebased the buildroot, you then need to change the buildroot id in our fork of the engine (so that when others use `gclient sync` it pulls our modified buildroot rather than an unmodified one. -1. Finally, update the [Shorebird CLI flutterEngineRevision](https://github.com/shorebirdtech/shorebird/blob/main/packages/shorebird_cli/lib/src/flutter_engine_revision.dart#L3) to the new Flutter engine revision diff --git a/updater/cbindgen.toml b/updater/cbindgen.toml deleted file mode 100644 index aa67c5a4..00000000 --- a/updater/cbindgen.toml +++ /dev/null @@ -1,20 +0,0 @@ -# See https://github.com/eqrion/cbindgen/blob/master/docs.md#cbindgentoml -# for detailed documentation of every option here. -language = "C" -include_guard = "updater_h" -autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" -cpp_compat = true -line_length = 80 - -# I don't know if these are required to export the shorebird_ symbols -# since I've hit multiple levels of export trouble in libflutter.so. -# But I'm leaving them here for now. -after_includes = """ -#ifdef _WIN32 -#define SHOREBIRD_EXPORT __declspec(dllexport) -#else -#define SHOREBIRD_EXPORT __attribute__((visibility("default"))) -#endif -""" -[fn] -prefix = "SHOREBIRD_EXPORT" \ No newline at end of file diff --git a/updater/cli/Cargo.toml b/updater/cli/Cargo.toml deleted file mode 100644 index dff817fc..00000000 --- a/updater/cli/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "cli" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -clap = { version = "4.1.6", features = ["derive"] } -updater = { path = "../library" } \ No newline at end of file diff --git a/updater/cli/src/main.rs b/updater/cli/src/main.rs deleted file mode 100644 index 967fc349..00000000 --- a/updater/cli/src/main.rs +++ /dev/null @@ -1,66 +0,0 @@ -extern crate updater; - -use clap::{Parser, Subcommand}; - -#[derive(Parser)] -#[command(author, version, about, long_about = None, arg_required_else_help=true)] -struct Cli { - #[command(subcommand)] - command: Option, -} - -#[derive(Subcommand)] -enum Commands { - Check {}, - Current {}, - Update {}, -} - -fn main() { - let cli = Cli::parse(); - - let config = updater::AppConfig { - cache_dir: "updater_cache".to_owned(), - release_version: "0.1.0".to_owned(), - original_libapp_paths: vec!["libapp.so".to_owned()], - vm_path: "libflutter.so".to_owned(), - }; - let yaml_str = " -app_id: demo -channel: stable -base_url: http://localhost:8000 -"; - updater::init(config, yaml_str).expect("init failed"); - - // You can check for the existence of subcommands, and if found use their - // matches just as you would the top level cmd - match &cli.command { - Some(Commands::Check {}) => { - let needs_update = updater::check_for_update(); - println!("Checking for update..."); - if needs_update { - println!("Update needed."); - } else { - println!("No update needed."); - } - } - Some(Commands::Current {}) => { - let version = updater::active_patch(); - println!("Current version info:"); - match version { - Some(v) => { - println!("path: {:?}", v.path); - println!("number: {:?}", v.number); - } - None => { - println!("None"); - } - } - } - Some(Commands::Update {}) => { - let status = updater::update(); - println!("Update: {}", status); - } - None => {} - } -} diff --git a/updater/dart_bindings/.gitignore b/updater/dart_bindings/.gitignore deleted file mode 100644 index 3cceda55..00000000 --- a/updater/dart_bindings/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# https://dart.dev/guides/libraries/private-files -# Created by `dart pub` -.dart_tool/ - -# Avoid committing pubspec.lock for library packages; see -# https://dart.dev/guides/libraries/private-files#pubspeclock. -pubspec.lock diff --git a/updater/dart_bindings/README.md b/updater/dart_bindings/README.md deleted file mode 100644 index c0e9f834..00000000 --- a/updater/dart_bindings/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# dart_bindings - -Dart bindings for the updater library. - -This presumably eventually either gets published to pub.dev, or more likely -ends up as dart:shorebird or something included with the Shorebird SDK. \ No newline at end of file diff --git a/updater/dart_bindings/analysis_options.yaml b/updater/dart_bindings/analysis_options.yaml deleted file mode 100644 index dee8927a..00000000 --- a/updater/dart_bindings/analysis_options.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# This file configures the static analysis results for your project (errors, -# warnings, and lints). -# -# This enables the 'recommended' set of lints from `package:lints`. -# This set helps identify many issues that may lead to problems when running -# or consuming Dart code, and enforces writing Dart using a single, idiomatic -# style and format. -# -# If you want a smaller set of lints you can change this to specify -# 'package:lints/core.yaml'. These are just the most critical lints -# (the recommended set includes the core lints). -# The core lints are also what is used by pub.dev for scoring packages. - -include: package:lints/recommended.yaml - -# Uncomment the following section to specify additional rules. - -# linter: -# rules: -# - camel_case_types - -# analyzer: -# exclude: -# - path/to/excluded/files/** - -# For more information about the core and recommended set of lints, see -# https://dart.dev/go/core-lints - -# For additional information about configuring this file, see -# https://dart.dev/guides/language/analysis-options diff --git a/updater/dart_bindings/lib/src/bindings.dart b/updater/dart_bindings/lib/src/bindings.dart deleted file mode 100644 index 4622135b..00000000 --- a/updater/dart_bindings/lib/src/bindings.dart +++ /dev/null @@ -1,116 +0,0 @@ -// This entire file could be easily autogenerated. -// Probably https://pub.dev/packages/ffigen would work. - -import 'dart:ffi' as ffi; - -import 'package:ffi/ffi.dart'; - -// This must be kept in sync with the C struct in updater.h. -// Including *in the same order* as the C struct. -class AppParameters extends ffi.Struct { - external ffi.Pointer channel; - // ignore: non_constant_identifier_names - external ffi.Pointer app_id; - // ignore: non_constant_identifier_names - external ffi.Pointer base_version; - // ignore: non_constant_identifier_names - external ffi.Pointer update_url; - // ignore: non_constant_identifier_names - external ffi.Pointer> original_libapp_paths; - @ffi.Int8() - // ignore: non_constant_identifier_names - external int original_libapp_paths_size; - // ignore: non_constant_identifier_names - external ffi.Pointer vm_path; - // ignore: non_constant_identifier_names - external ffi.Pointer cache_dir; - - static ffi.Pointer allocate({ - required String appId, - required String version, - required String channel, - required String? updateUrl, - required List libappPaths, - required String libflutterPath, - required String cacheDir, - }) { - var config = calloc(); - config.ref.app_id = appId.toNativeUtf8(); - config.ref.base_version = version.toNativeUtf8(); - config.ref.channel = channel.toNativeUtf8(); - if (updateUrl != null) { - config.ref.update_url = updateUrl.toNativeUtf8(); - } - config.ref.original_libapp_paths = calloc>( - libappPaths.length, - ); - - for (var i = 0; i < libappPaths.length; i++) { - config.ref.original_libapp_paths[i] = libappPaths[i].toNativeUtf8(); - } - config.ref.vm_path = libflutterPath.toNativeUtf8(); - config.ref.cache_dir = cacheDir.toNativeUtf8(); - return config; - } - - static void free(ffi.Pointer config) { - calloc.free(config.ref.app_id); - calloc.free(config.ref.base_version); - calloc.free(config.ref.channel); - calloc.free(config.ref.update_url); - // Free all paths in original_libapp_path. - for (var i = 0; i < config.ref.original_libapp_paths_size; i++) { - calloc.free(config.ref.original_libapp_paths[i]); - } - calloc.free(config.ref.original_libapp_paths); - calloc.free(config.ref.vm_path); - calloc.free(config.ref.cache_dir); - calloc.free(config); - } -} - -typedef _GetBoolFunc = ffi.Bool Function(); -typedef GetBool = bool Function(); - -typedef _GetStringFunc = ffi.Pointer Function(); -typedef GetString = ffi.Pointer Function(); - -typedef _GetVoidFunc = ffi.Void Function(); -typedef GetVoid = void Function(); - -typedef _SBInitFunc = ffi.Void Function(ffi.Pointer config); -typedef SBInit = void Function(ffi.Pointer config); - -typedef _FreeStringFunc = ffi.Void Function(ffi.Pointer str); -typedef FreeString = void Function(ffi.Pointer str); - -class UpdaterBindings { - final ffi.DynamicLibrary library; - - late SBInit init; - late GetBool checkForUpdate; - late GetString activeVersion; - late GetString activePath; - late FreeString freeString; - late GetVoid update; - - UpdaterBindings(this.library) { - // None of these call back into Dart, so they're all safely "isLeaf: true". - init = library.lookupFunction<_SBInitFunc, SBInit>('shorebird_init', - isLeaf: true); - activeVersion = library.lookupFunction<_GetStringFunc, GetString>( - 'shorebird_active_version', - isLeaf: true); - activePath = library.lookupFunction<_GetStringFunc, GetString>( - 'shorebird_active_path', - isLeaf: true); - freeString = library.lookupFunction<_FreeStringFunc, FreeString>( - 'shorebird_free_string', - isLeaf: true); - checkForUpdate = library.lookupFunction<_GetBoolFunc, GetBool>( - 'shorebird_check_for_update', - isLeaf: true); - update = library.lookupFunction<_GetVoidFunc, GetVoid>('shorebird_update', - isLeaf: true); - } -} diff --git a/updater/dart_bindings/lib/updater.dart b/updater/dart_bindings/lib/updater.dart deleted file mode 100644 index bbcf9a73..00000000 --- a/updater/dart_bindings/lib/updater.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:ffi' as ffi; -import 'dart:io'; - -import 'package:ffi/ffi.dart'; -import 'package:path/path.dart' as path; - -import 'src/bindings.dart'; - -class Updater { - Updater(); - - static UpdaterBindings? _bindings; - - static ffi.DynamicLibrary _loadLibraryInDirectory( - {required String directory, required String name}) { - if (Platform.isMacOS) { - return ffi.DynamicLibrary.open(path.join(directory, 'lib$name.dylib')); - } - if (Platform.isWindows) { - return ffi.DynamicLibrary.open(path.join(directory, '$name.dll')); - } - // Assume everything else follows the Linux pattern. - return ffi.DynamicLibrary.open(path.join(directory, 'lib$name.so')); - } - - static loadLibrary({required String name, required String directory}) { - if (_bindings != null) { - throw Exception('Library already loaded.'); - } - final updater = _loadLibraryInDirectory(directory: directory, name: name); - _bindings = UpdaterBindings(updater); - } - - static loadFlutterLibrary() { - if (_bindings != null) { - throw Exception('Library already loaded.'); - } - final updater = ffi.DynamicLibrary.process(); - _bindings = UpdaterBindings(updater); - } - - static UpdaterBindings get bindings { - if (_bindings == null) { - throw Exception('Must call loadLibrary() first.'); - } - return _bindings!; - } - - // This is only used when called from a Dart command line. - // Shorebird will have initialized the library already for you when - // inside a Flutter app. - static void initUpdaterLibrary({ - required String appId, - required String version, - required String channel, - required String? updateUrl, - required List baseLibraryPaths, - required String vmPath, - required String cacheDir, - }) { - var config = AppParameters.allocate( - appId: appId, - version: version, - channel: channel, - updateUrl: updateUrl, - libappPaths: baseLibraryPaths, - libflutterPath: vmPath, - cacheDir: cacheDir, - ); - try { - bindings.init(config); - } finally { - AppParameters.free(config); - } - } - - bool checkForUpdate() { - return bindings.checkForUpdate(); - } - - void update() { - return bindings.update(); - } - - String? _returnsMaybeString(ffi.Pointer Function() f) { - ffi.Pointer cString = ffi.Pointer.fromAddress(0); - cString = f(); - if (cString.address == 0) { - return null; - } - try { - return cString.toDartString(); - } finally { - // Using finally for two reasons: - // 1. it runs after the return (saving us a local) - // 2. it runs even if toDartString throws (which it shouldn't) - bindings.freeString(cString); - } - } - - String? activeVersion() { - return _returnsMaybeString(bindings.activeVersion); - } - - String? activePath() { - return _returnsMaybeString(bindings.activePath); - } -} diff --git a/updater/dart_bindings/pubspec.yaml b/updater/dart_bindings/pubspec.yaml deleted file mode 100644 index 9d59b22f..00000000 --- a/updater/dart_bindings/pubspec.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: dart_bindings -description: dart bindings for Shorebird updater library -version: 1.0.0 - -environment: - sdk: '>=2.19.0 <4.0.0' - -# Add regular dependencies here. -dependencies: - ffi: ^2.0.1 - # path version must match that depended on by Flutter SDK. - path: ^1.8.2 - -dev_dependencies: - lints: ^2.0.0 - test: ^1.21.0 diff --git a/updater/dart_cli/.gitignore b/updater/dart_cli/.gitignore deleted file mode 100644 index 82cc80fb..00000000 --- a/updater/dart_cli/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# https://dart.dev/guides/libraries/private-files -# Created by `dart pub` -.dart_tool/ -pubspec.lock diff --git a/updater/dart_cli/README.md b/updater/dart_cli/README.md deleted file mode 100644 index 7fb92493..00000000 --- a/updater/dart_cli/README.md +++ /dev/null @@ -1 +0,0 @@ -Command line application to test ffi wrapping of updater library. \ No newline at end of file diff --git a/updater/dart_cli/analysis_options.yaml b/updater/dart_cli/analysis_options.yaml deleted file mode 100644 index dee8927a..00000000 --- a/updater/dart_cli/analysis_options.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# This file configures the static analysis results for your project (errors, -# warnings, and lints). -# -# This enables the 'recommended' set of lints from `package:lints`. -# This set helps identify many issues that may lead to problems when running -# or consuming Dart code, and enforces writing Dart using a single, idiomatic -# style and format. -# -# If you want a smaller set of lints you can change this to specify -# 'package:lints/core.yaml'. These are just the most critical lints -# (the recommended set includes the core lints). -# The core lints are also what is used by pub.dev for scoring packages. - -include: package:lints/recommended.yaml - -# Uncomment the following section to specify additional rules. - -# linter: -# rules: -# - camel_case_types - -# analyzer: -# exclude: -# - path/to/excluded/files/** - -# For more information about the core and recommended set of lints, see -# https://dart.dev/go/core-lints - -# For additional information about configuring this file, see -# https://dart.dev/guides/language/analysis-options diff --git a/updater/dart_cli/bin/dart_cli.dart b/updater/dart_cli/bin/dart_cli.dart deleted file mode 100644 index 32575423..00000000 --- a/updater/dart_cli/bin/dart_cli.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:dart_bindings/updater.dart'; -import 'package:path/path.dart' as path; - -void main(List args) async { - var directory = path.join(Directory.current.path, 'target', 'debug'); - Updater.loadLibrary(directory: directory, name: "updater"); - - Updater.initUpdaterLibrary( - appId: 'demo', - version: '1.0.0', - channel: 'stable', - updateUrl: null, - baseLibraryPaths: ['libapp.so'], - vmPath: Platform.executable, - cacheDir: 'updater_cache', - ); - - var updater = Updater(); - final runner = CommandRunner('updater', 'Updater CLI') - ..addCommand(CheckForUpdate(updater)) - ..addCommand(PrintVersion(updater)) - ..addCommand(PrintPath(updater)) - ..addCommand(Update(updater)) - ..addCommand(Run(updater)); - await runner.run(args); -} - -class CheckForUpdate extends Command { - final Updater updater; - CheckForUpdate(this.updater); - - @override - final name = 'check'; - - @override - final description = 'Check for an update.'; - - @override - void run() { - var result = updater.checkForUpdate(); - if (result) { - print('Update available'); - } else { - print('No update available'); - } - } -} - -class PrintVersion extends Command { - final Updater updater; - PrintVersion(this.updater); - - @override - final name = 'version'; - - @override - final description = 'Print current installed version.'; - - @override - void run() { - print(updater.activeVersion()); - } -} - -class PrintPath extends Command { - final Updater updater; - PrintPath(this.updater); - - @override - final name = 'path'; - - @override - final description = 'Print current installed path.'; - - @override - void run() { - print(updater.activePath()); - } -} - -class Update extends Command { - final Updater updater; - Update(this.updater); - - @override - final name = 'update'; - - @override - final description = 'Update to the latest version.'; - - @override - void run() { - updater.update(); - } -} - -class Run extends Command { - final Updater updater; - Run(this.updater) { - argParser.addFlag('update', abbr: 'u', help: 'Update before running.'); - } - - @override - final name = 'run'; - - @override - final description = 'Run the active version.'; - - @override - void run() async { - // This is a basic demo of what this might look like. - // Real callers wouldn't likely do this from Dart as there is no need - // to have two copies of the Dart VM running. - - if (argResults!['update']) { - updater.update(); - } - - var path = updater.activePath(); - if (path == null) { - print('No active version (should run the bundled version)'); - return; - } - // Should this run update first? - print('Running $path'); - // Is there a portable way to just "exec" and replace the current process? - var process = await Process.start(Platform.executable, ['run', path]); - process.stdout.transform(utf8.decoder).forEach(stdout.write); - process.stderr.transform(utf8.decoder).forEach(stderr.write); - // TODO: Handle stdin. - exit(await process.exitCode); - } -} diff --git a/updater/dart_cli/pubspec.yaml b/updater/dart_cli/pubspec.yaml deleted file mode 100644 index 2b61d238..00000000 --- a/updater/dart_cli/pubspec.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: dart_cli -description: A sample command-line application. -version: 1.0.0 -# repository: https://github.com/my_org/my_repo -publish_to: 'none' - -environment: - sdk: ">=2.19.0 <4.0.0" - -dependencies: - args: ^2.4.0 - dart_bindings: - path: ../dart_bindings - path: ^1.8.3 - -dev_dependencies: - lints: ^2.0.0 - test: ^1.21.0 diff --git a/updater/library/.gitignore b/updater/library/.gitignore deleted file mode 100644 index ada8be92..00000000 --- a/updater/library/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb \ No newline at end of file diff --git a/updater/library/Cargo.toml b/updater/library/Cargo.toml deleted file mode 100644 index a3d00b23..00000000 --- a/updater/library/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -[package] -name = "updater" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -# "lib" is used by the "cli" target for testing from Rust -# "cdylib" is used by the "dart_cli" target for testing from Dart -# "staticlib" is used by the engine build for linking into libflutter.so -crate-type = ["lib", "cdylib", "staticlib"] - -[dependencies] -# Used for exposing C API -libc = "0.2.98" -# Used for networking. -reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] } -# Json serialization/de-serialization. -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.93" -# Used for error handling for now. -anyhow = {version = "1.0.69", features = ["backtrace"]} -# For error!(), info!(), etc macros. `print` will not show up on Android. -log = "0.4.14" -# For implementing thread-local-storage of ResolvedConfig object. -once_cell = "1.17.1" -# For reading shorebird.yaml -serde_yaml = "0.9.19" -# For inflating compressed patch files. -bipatch = "1.0.0" -# comde is a wrapper around several compression libraries. -# We only use zstd and could depend on it directly instead. -comde = {version = "0.2.3", default-features = false, features = ["zstandard"]} -# Pipe is a simple in-memory pipe implementation, there might be a std way too? -pipe = "0.4.0" -# For computing hashes of patch files for validation. -sha2 = "0.10.6" -# For decoding the hex-encoded hashes in Patch network responses. -hex = "0.4.3" - -[target.'cfg(target_os = "android")'.dependencies] -# For logging to Android logcat. -android_logger = "0.13.0" -# Send panics to log (instead of stderr), thus logcat on Android. -log-panics = { version = "2", features = ["with-backtrace"]} - - -[dev-dependencies] -tempdir = "0.3.7" diff --git a/updater/library/README.md b/updater/library/README.md deleted file mode 100644 index 00c29da6..00000000 --- a/updater/library/README.md +++ /dev/null @@ -1,175 +0,0 @@ -# Shorebird CodePush Updater - -The rust library that does the actual update work. - -## Design - -The updater library is built in Rust for safety (and modernity). It's built -as a C-compatible library, so it can be used from any language. - -The library is thread-safe, as it needs to be called both from the flutter_main -thread (during initialization) and then later from the Dart/UI thread -(from application Dart code) in Flutter. - -The overarching principle with the Updater is "first, do no harm". The updater -should "fail open", terms of continuing to work with the currently installed -or active version of the application even when the network is unavailable. - -The updater also needs to handle error cases conservatively, such as partial -downloads from a server, or malformed responses (e.g. a proxy interfering) -and not crash the application or leave the application in a broken state. - -Every time the updater runs it needs to verify that the currently installed -patch is compatible with the currently installed base version. If it is not, -it should refuse to return paths to incompatible patches. - -The updater also needs to regularly verify that the current state directory -is in a consistent state. If it is not, it should invalidate any installed -patches and return to a clean state. - -Not all of the above is implemented yet, but such is the intent. - -## Architecture - -The updater is split into separate layers. The top layer is the C-compatible -API, which is used by all consumers of the updater. The C-compatible API -is a thin wrapper around the Rust API, which is the main implementation but -only used directly for testing (see the `cli` directory). - -Thread safety is handled by a global configuration object that is locked -when accessed. It's possible I've missed cases where this is not sufficient, -and there could be thread safety issues in the library. - -* src/c_api.rs - C-compatible API -* src/lib.rs - Rust API (and crate root) -* src/update.rs - Core updater logic -* src/config.rs - In memory configuration and thread locking -* src/cache.rs - On-disk state management -* src/logging.rs - Logging configuration (for platforms that need it) -* src/network.rs - Logic dealing with network requests and updater server - -## Rust -We use normal rust idioms (e.g. Result) inside the library and then bridge those -to C via an explicit stable C API (explicit enums, null pointers for optional -arguments, etc). The reason for this is that it lets the Rust code feel natural -and also gives us maximum flexibility in the future for exposing more in the C -API without having to refactor the internals of the library. - -https://docs.rust-embedded.org/book/interoperability/rust-with-c.html -are docs on how to use Rust from C (what we're doing). - -https://github.com/RubberDuckEng/safe_wren has an example of building in Rust -and exposing it with a C api. - -## Integration - -The updater library is built as a static library, and is linked into the -libflutter.so as part of a custom build of Flutter. We also link libflutter.so -with the correct flags such that updater symbols are exposed to Dart. - -The `dart_bindings` directory contains the Dart bindings for the updater -library. - -## Building for Android - -The best way I found was to install: -https://github.com/bbqsrc/cargo-ndk - -``` -cargo install cargo-ndk -rustup target add \ - aarch64-linux-android \ - armv7-linux-androideabi \ - x86_64-linux-android \ - i686-linux-android -cargo ndk -t armeabi-v7a -t arm64-v8a build --release -``` - -When building to include with libflutter.so, you need to build with the same -version of the ndk as Flutter is using: - -You'll need to have a Flutter engine checkout already setup and synced. -As part of `gclient sync` the Flutter engine repo will pull down a copy of the -ndk into `src/third_party/android_tools/ndk`. - -Then you can set the NDK_HOME environment variable to point to that directory. -e.g.: -``` -NDK_HOME=$HOME/Documents/GitHub/engine/src/third_party/android_tools/ndk -``` - -Then you can build the updater library as above. If you don't want to change -your NDK_HOME, you can also set the environment variable for just the one call: -``` -NDK_HOME=$HOME/Documents/GitHub/engine/src/third_party/android_tools/ndk cargo ndk -t armeabi-v7a -t arm64-v8a build --release -``` - -## Development - -Uses cbindgen to generate the header file. - -It isn't currently wired into the build process, so you'll need to run it -manually if you change the API. -https://github.com/shorebirdtech/shorebird/issues/121 - -``` -cargo install cbindgen -cbindgen --config cbindgen.toml --crate updater --output library/include/updater.h -``` - -## Imagined Architecture (not all implemented) - -### Assumptions (not all enforced yet) -* Updater library is never allowed to crash, except on bad parameters from C. -* Network and Disk are untrusted. -* Running code is trusted. -* Store-installed bundle is trusted (e.g. APK). -* Updates are signed by a trusted key. -* Updates must be applied in order. -* Updates are applied in a single transaction. - -### Update State Machine -* Server is authoritative, regarding current update/patch state. Client can - cache state in memory. Not written to disk. -* Patches are downloaded to a temporary location on disk. -* Update State Machine: - * `ready`: Just woke up, ready to check for updates. - * `checking`: Checking for updates. - * `update_available`: Update or rollback is available. - * `no_update_available`: No update is available. - * `downloading`: Downloading an update. - * `downloaded`: Downloaded an update. -* Client keeps on disk: - * cache of patches in "slots" - * cache of in-progress download state. - * Last booted patch (may not have been successful). - * Last successful patch (never rolled back from unless becomes invalid). -* Boot State Machine: - * `ready`: Just woke up, ready to boot. - * `booting`: Booting a patch. - * `booted`: Patch is booted, we will not go back from here. - -### Slot State Machine -* Patches are cached on disk in "slots". -* There is a currently active slot (the one that is booted). -* Patches are identified by base revision + patch number. -* A given slot is: - * `empty`: No update is installed. - * `pending`: An update is installed but has not been validated. - * `valid`: An update is installed and has been validated. -* Validation is a temporary state. Patches/slots are revalidated on boot. - -### Trust model -* Network and Disk are untrusted. -* Running software (including apk service) is trusted. -* Patch contents are signed, public key is included in the APK. - -## TODO: -* Add an async API. -* Write tests for state management. -* Make state management/filesystem management atomic (and tested). -* Support validating patches/slots (hashes, signatures, etc). - -## Later-stage update system design docs -* https://theupdateframework.io/ -* https://fuchsia.dev/fuchsia-src/concepts/packages/software_update_system diff --git a/updater/library/include/updater.h b/updater/library/include/updater.h deleted file mode 100644 index d258c7fc..00000000 --- a/updater/library/include/updater.h +++ /dev/null @@ -1,101 +0,0 @@ -#ifndef updater_h -#define updater_h - -/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ - -#include -#include -#include -#include -#ifdef _WIN32 -#define SHOREBIRD_EXPORT __declspec(dllexport) -#else -#define SHOREBIRD_EXPORT __attribute__((visibility("default"))) -#endif - - -/** - * Struct containing configuration parameters for the updater. - * Passed to all updater functions. - * NOTE: If this struct is changed all language bindings must be updated. - */ -typedef struct AppParameters { - /** - * release_version, required. Named version of the app, off of which updates - * are based. Can be either a version number or a hash. - */ - const char *release_version; - /** - * Array of paths to the original aot library, required. For Flutter apps - * these are the paths to the bundled libapp.so. May be used for compression downloaded artifacts. - */ - const char *const *original_libapp_paths; - /** - * Length of the original_libapp_paths array. - */ - int original_libapp_paths_size; - /** - * Path to the app's libflutter.so, required. May be used for ensuring - * downloaded artifacts are compatible with the Flutter/Dart versions - * used by the app. For Flutter apps this should be the path to the - * bundled libflutter.so. For Dart apps this should be the path to the - * dart executable. - */ - const char *vm_path; - /** - * Path to cache_dir where the updater will store downloaded artifacts. - */ - const char *cache_dir; -} AppParameters; - -#ifdef __cplusplus -extern "C" { -#endif // __cplusplus - -/** - * Configures updater. First parameter is a struct containing configuration - * from the running app. Second parameter is a YAML string containing - * configuration compiled into the app. - */ -SHOREBIRD_EXPORT -void shorebird_init(const struct AppParameters *c_params, - const char *c_yaml); - -/** - * Return the active patch number, or NULL if there is no active patch. - */ -SHOREBIRD_EXPORT char *shorebird_active_patch_number(void); - -/** - * Return the path to the active patch for the app, or NULL if there is no - * active patch. - */ -SHOREBIRD_EXPORT char *shorebird_active_path(void); - -/** - * Free a string returned by the updater library. - */ -SHOREBIRD_EXPORT void shorebird_free_string(char *c_string); - -/** - * Check for an update. Returns true if an update is available. - */ -SHOREBIRD_EXPORT bool shorebird_check_for_update(void); - -/** - * Synchronously download an update if one is available. - */ -SHOREBIRD_EXPORT void shorebird_update(void); - -/** - * Report that the app failed to launch. This will cause the updater to - * attempt to roll back to the previous version if this version has not - * been launched successfully before. - */ -SHOREBIRD_EXPORT void shorebird_report_failed_launch(void); - -#ifdef __cplusplus -} // extern "C" -#endif // __cplusplus - -#endif /* updater_h */ diff --git a/updater/library/src/c_api.rs b/updater/library/src/c_api.rs deleted file mode 100644 index e9084071..00000000 --- a/updater/library/src/c_api.rs +++ /dev/null @@ -1,146 +0,0 @@ -// This file handles translating the updater library's types into C types. - -// Currently manually prefixing all functions with "shorebird_" to avoid -// name collisions with other libraries. -// cbindgen:prefix-with-name could do this for us. - -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; - -use crate::updater; - -/// Struct containing configuration parameters for the updater. -/// Passed to all updater functions. -/// NOTE: If this struct is changed all language bindings must be updated. -#[repr(C)] -pub struct AppParameters { - /// release_version, required. Named version of the app, off of which updates - /// are based. Can be either a version number or a hash. - pub release_version: *const libc::c_char, - - /// Array of paths to the original aot library, required. For Flutter apps - /// these are the paths to the bundled libapp.so. May be used for compression downloaded artifacts. - pub original_libapp_paths: *const *const libc::c_char, - - /// Length of the original_libapp_paths array. - pub original_libapp_paths_size: libc::c_int, - - /// Path to the app's libflutter.so, required. May be used for ensuring - /// downloaded artifacts are compatible with the Flutter/Dart versions - /// used by the app. For Flutter apps this should be the path to the - /// bundled libflutter.so. For Dart apps this should be the path to the - /// dart executable. - pub vm_path: *const libc::c_char, - - /// Path to cache_dir where the updater will store downloaded artifacts. - pub cache_dir: *const libc::c_char, -} - -fn to_rust(c_string: *const libc::c_char) -> String { - unsafe { CStr::from_ptr(c_string).to_str().unwrap() }.to_string() -} - -fn to_rust_vector(c_array: *const *const libc::c_char, size: libc::c_int) -> Vec { - let mut result = Vec::new(); - for i in 0..size { - let c_string = unsafe { *c_array.offset(i as isize) }; - result.push(to_rust(c_string)); - } - result -} - -fn app_config_from_c(c_params: *const AppParameters) -> updater::AppConfig { - let c_params_ref = unsafe { &*c_params }; - - updater::AppConfig { - cache_dir: to_rust(c_params_ref.cache_dir), - release_version: to_rust(c_params_ref.release_version), - original_libapp_paths: to_rust_vector( - c_params_ref.original_libapp_paths, - c_params_ref.original_libapp_paths_size, - ), - vm_path: to_rust(c_params_ref.vm_path), - } -} - -/// Configures updater. First parameter is a struct containing configuration -/// from the running app. Second parameter is a YAML string containing -/// configuration compiled into the app. -#[no_mangle] -pub extern "C" fn shorebird_init(c_params: *const AppParameters, c_yaml: *const libc::c_char) { - let config = app_config_from_c(c_params); - - let yaml_string = to_rust(c_yaml); - let result = updater::init(config, &yaml_string); - match result { - Ok(_) => {} - Err(e) => { - error!("Error initializing updater: {:?}", e); - } - } -} - -/// Return the active patch number, or NULL if there is no active patch. -#[no_mangle] -pub extern "C" fn shorebird_active_patch_number() -> *mut c_char { - let patch = updater::active_patch(); - match patch { - Some(v) => { - let c_patch = CString::new(v.number.to_string()).unwrap(); - c_patch.into_raw() - } - None => std::ptr::null_mut(), - } -} - -/// Return the path to the active patch for the app, or NULL if there is no -/// active patch. -#[no_mangle] -// rename to shorebird_patch_path -pub extern "C" fn shorebird_active_path() -> *mut c_char { - let version = updater::active_patch(); - match version { - Some(v) => { - let c_version = CString::new(v.path).unwrap(); - c_version.into_raw() - } - None => std::ptr::null_mut(), - } -} - -/// Free a string returned by the updater library. -#[no_mangle] -pub extern "C" fn shorebird_free_string(c_string: *mut c_char) { - unsafe { - if c_string.is_null() { - return; - } - drop(CString::from_raw(c_string)); - } -} - -/// Check for an update. Returns true if an update is available. -#[no_mangle] -pub extern "C" fn shorebird_check_for_update() -> bool { - return updater::check_for_update(); -} - -/// Synchronously download an update if one is available. -#[no_mangle] -pub extern "C" fn shorebird_update() { - updater::update(); -} - -/// Report that the app failed to launch. This will cause the updater to -/// attempt to roll back to the previous version if this version has not -/// been launched successfully before. -#[no_mangle] -pub extern "C" fn shorebird_report_failed_launch() { - let result = updater::report_failed_launch(); - match result { - Ok(_) => {} - Err(e) => { - error!("Error recording launch failure: {:?}", e); - } - } -} diff --git a/updater/library/src/cache.rs b/updater/library/src/cache.rs deleted file mode 100644 index e1ac6580..00000000 --- a/updater/library/src/cache.rs +++ /dev/null @@ -1,362 +0,0 @@ -// This file deals with the cache / state management for the updater. - -use std::fs::File; -use std::io::{BufReader, BufWriter}; -use std::path::{Path, PathBuf}; - -use anyhow::Ok; -use serde::{Deserialize, Serialize}; - -use crate::updater::UpdateError; - -#[derive(PartialEq, Debug)] -pub struct PatchInfo { - pub path: String, - pub number: usize, -} - -#[derive(Deserialize, Serialize, Default, Clone, Debug)] -struct Slot { - /// Path to the slot directory. - path: String, - /// Patch number for the patch in this slot. - patch_number: usize, -} - -impl Slot { - fn to_patch_info(&self) -> PatchInfo { - PatchInfo { - path: self.path.clone(), - number: self.patch_number, - } - } -} - -// This struct is public, as callers can have a handle to it, but modifying -// anything inside should be done via the functions below. -#[derive(Deserialize, Serialize)] -pub struct UpdaterState { - /// Where this writes to disk. - cache_dir: String, - /// The release version this cache corresponds to. - /// If this does not match the release version we're booting from we will - /// clear the cache. - release_version: String, - /// The patch number of the patch that was last downloaded. - latest_downloaded_patch: Option, - /// List of patches that failed to boot. We will never attempt these again. - failed_patches: Vec, - /// List of patches that successfully booted. We will never rollback past - /// one of these for this device. - successful_patches: Vec, - /// Currently selected slot. - current_slot_index: Option, - /// List of slots. - slots: Vec, - // Add file path or FD so modifying functions can save it to disk? -} - -impl UpdaterState { - fn new(cache_dir: String, release_version: String) -> Self { - Self { - cache_dir, - release_version, - current_slot_index: None, - latest_downloaded_patch: None, - failed_patches: Vec::new(), - successful_patches: Vec::new(), - slots: Vec::new(), - } - } -} - -impl UpdaterState { - pub fn is_known_good_patch(&self, patch: &PatchInfo) -> bool { - self.successful_patches.iter().any(|v| v == &patch.number) - } - - pub fn is_known_bad_patch(&self, patch: &PatchInfo) -> bool { - self.failed_patches.iter().any(|v| v == &patch.number) - } - - pub fn mark_patch_as_bad(&mut self, patch: &PatchInfo) { - if self.is_known_good_patch(patch) { - warn!("Tried to report failed launch for a known good patch. Ignoring."); - return; - } - - if self.is_known_bad_patch(patch) { - return; - } - info!("Marking patch {} as bad", patch.number); - self.failed_patches.push(patch.number.clone()); - } - - pub fn mark_patch_as_good(&mut self, patch: &PatchInfo) { - if self.is_known_bad_patch(patch) { - warn!("Tried to report successful launch for a known bad patch. Ignoring."); - return; - } - - if self.is_known_good_patch(patch) { - return; - } - self.successful_patches.push(patch.number.clone()); - } - - fn load(cache_dir: &str) -> anyhow::Result { - // Load UpdaterState from disk - let path = Path::new(cache_dir).join("state.json"); - let file = File::open(path)?; - let reader = BufReader::new(file); - // TODO: Now that we depend on serde_yaml for shorebird.yaml - // we could use yaml here instead of json. - let state = serde_json::from_reader(reader)?; - Ok(state) - } - - pub fn load_or_new_on_error(cache_dir: &str, release_version: &str) -> Self { - let loaded = Self::load(cache_dir).unwrap_or_else(|e| { - warn!("Failed to load updater state: {}", e); - Self::new(cache_dir.to_owned(), release_version.to_owned()) - }); - if loaded.release_version != release_version { - warn!("Release version changed, clearing updater state"); - Self::new(cache_dir.to_owned(), release_version.to_owned()) - } else { - loaded - } - } - - pub fn save(&self) -> anyhow::Result<()> { - // Save UpdaterState to disk - std::fs::create_dir_all(&self.cache_dir)?; - let path = Path::new(&self.cache_dir).join("state.json"); - let file = File::create(path)?; - let writer = BufWriter::new(file); - serde_json::to_writer_pretty(writer, self)?; - Ok(()) - } - - /// This is NOT the current booted path (we don't keep that in memory yet). - /// This is the patch that is selected in the state.json, which may or may - /// not be the one that is booted, but will be the one used next boot. - pub fn current_patch(&self) -> Option { - if self.slots.is_empty() { - return None; - } - if let Some(slot_index) = self.current_slot_index { - if slot_index >= self.slots.len() { - return None; - } - let slot = &self.slots[slot_index]; - // Otherwise return the version info from the current slot. - return Some(slot.to_patch_info()); - } - None - } - - fn validate_slot(&self, slot: &Slot) -> bool { - // Check if the patch is known bad. - if self.is_known_bad_patch(&slot.to_patch_info()) { - return false; - } - if PathBuf::from(&slot.path).exists() { - return true; - } - // TODO: This should also check if the hash matches? - // let hash = compute_hash(&PathBuf::from(&slot.path)); - // if let Ok(hash) = hash { - // if hash == slot.hash { - // return true; - // } - // error!("Hash mismatch for slot: {:?}", slot); - // } - false - } - - fn latest_bootable_slot(&self) -> Option { - // Find the latest slot that has a patch that is not bad. - // Sort the slots by patch number, then return the highest - // patch number that is not bad. - let mut slots = self.slots.clone(); - slots.sort_by(|a, b| a.patch_number.cmp(&b.patch_number)); - slots.reverse(); - for slot in slots { - if self.validate_slot(&slot) { - return Some(slot.patch_number); - } - } - None - } - - pub fn activate_latest_bootable_patch(&mut self) -> Result<(), UpdateError> { - self.set_current_slot(self.latest_bootable_slot()); - self.save().map_err(|_| UpdateError::FailedToSaveState) - } - - fn available_slot(&self) -> usize { - // Assume we only use two slots and pick the one that's not current. - if self.slots.is_empty() { - return 0; - } - if let Some(slot_index) = self.current_slot_index { - if slot_index == 0 { - return 1; - } - } - return 0; - } - - fn clear_slot(&mut self, index: usize) { - if self.slots.len() < index + 1 { - return; - } - self.slots[index] = Slot::default(); - } - - fn set_slot(&mut self, index: usize, slot: Slot) { - info!("Setting slot {} to {:?}", index, slot); - if self.slots.len() < index + 1 { - // Make sure we're not filling with empty slots. - assert!(self.slots.len() == index); - self.slots.resize(index + 1, Slot::default()); - } - // Set the given slot to the given version. - self.slots[index] = slot - } - - fn slot_dir(&self, index: usize) -> String { - Path::new(&self.cache_dir) - .join(format!("slot_{}", index)) - .to_str() - .unwrap() - .to_owned() - } - - pub fn install_patch(&mut self, patch: PatchInfo) -> anyhow::Result<()> { - let slot_index = self.available_slot(); - let slot_dir_string = self.slot_dir(slot_index); - let slot_dir = PathBuf::from(&slot_dir_string); - - // Clear the slot. - self.clear_slot(slot_index); // Invalidate the slot. - self.save()?; - if slot_dir.exists() { - std::fs::remove_dir_all(&slot_dir)?; - } - std::fs::create_dir_all(&slot_dir)?; - - if self.is_known_bad_patch(&patch) { - return Err(UpdateError::InvalidArgument( - "patch".to_owned(), - format!("Refusing to install known bad patch: {:?}", patch), - ) - .into()); - } - - // Move the artifact into the slot. - let artifact_path = slot_dir.join("dlc.vmcode"); - std::fs::rename(&patch.path, &artifact_path)?; - - // Update the state to include the new slot. - self.set_slot( - slot_index, - Slot { - path: artifact_path.to_str().unwrap().to_owned(), - patch_number: patch.number, - }, - ); - self.set_current_slot(Some(slot_index)); - - if (self.latest_downloaded_patch.is_none()) - || (self.latest_downloaded_patch.unwrap() < patch.number) - { - self.latest_downloaded_patch = Some(patch.number); - } else { - warn!( - "Installed patch {} but latest downloaded patch is {:?}", - patch.number, self.latest_downloaded_patch - ); - } - self.save()?; - Ok(()) - } - - pub fn set_current_slot(&mut self, maybe_index: Option) { - self.current_slot_index = maybe_index; - } - pub fn latest_patch_number(&self) -> Option { - self.latest_downloaded_patch - } -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - use tempdir::TempDir; - - use crate::cache::{PatchInfo, UpdaterState}; - - fn test_state(tmp_dir: &TempDir) -> UpdaterState { - let cache_dir = tmp_dir.path().to_str().unwrap().to_string(); - UpdaterState::new(cache_dir, "1.0.0".to_string()) - } - - fn fake_patch(tmp_dir: &TempDir, number: usize) -> super::PatchInfo { - let path = PathBuf::from(tmp_dir.path()).join(format!("patch_{}", number)); - std::fs::write(&path, "fake patch").unwrap(); - PatchInfo { - number, - path: path.to_str().unwrap().to_owned(), - } - } - - #[test] - fn current_patch_does_not_crash() { - let tmp_dir = TempDir::new("example").unwrap(); - let mut state = test_state(&tmp_dir); - assert_eq!(state.current_patch(), None); - state.current_slot_index = Some(3); - assert_eq!(state.current_patch(), None); - state.slots.push(super::Slot::default()); - // This used to crash, where index was bad, but slots were not empty. - assert_eq!(state.current_patch(), None); - } - - #[test] - fn release_version_changed() { - let tmp_dir = TempDir::new("example").unwrap(); - let mut state = test_state(&tmp_dir); - state.latest_downloaded_patch = Some(1); - state.save().unwrap(); - let loaded = UpdaterState::load_or_new_on_error(&state.cache_dir, &state.release_version); - assert_eq!(loaded.latest_downloaded_patch, Some(1)); - - let loaded_after_version_change = - UpdaterState::load_or_new_on_error(&state.cache_dir, "1.0.1"); - assert_eq!(loaded_after_version_change.latest_downloaded_patch, None); - } - - #[test] - fn latest_downloaded_patch() { - let tmp_dir = TempDir::new("example").unwrap(); - let mut state = test_state(&tmp_dir); - assert_eq!(state.latest_downloaded_patch, None); - state.install_patch(fake_patch(&tmp_dir, 1)).unwrap(); - assert_eq!(state.latest_downloaded_patch, Some(1)); - state.install_patch(fake_patch(&tmp_dir, 2)).unwrap(); - assert_eq!(state.latest_downloaded_patch, Some(2)); - state.install_patch(fake_patch(&tmp_dir, 1)).unwrap(); - assert_eq!(state.latest_downloaded_patch, Some(2)); - } - - #[test] - fn do_not_install_known_bad_patch() { - let tmp_dir = TempDir::new("example").unwrap(); - let mut state = test_state(&tmp_dir); - let bad_patch = fake_patch(&tmp_dir, 1); - state.mark_patch_as_bad(&bad_patch); - assert!(state.install_patch(bad_patch).is_err()); - } -} diff --git a/updater/library/src/config.rs b/updater/library/src/config.rs deleted file mode 100644 index 6e99d425..00000000 --- a/updater/library/src/config.rs +++ /dev/null @@ -1,114 +0,0 @@ -// This file handles the global config for the updater library. - -use std::sync::Mutex; - -use crate::updater::AppConfig; -use crate::yaml::YamlConfig; -use once_cell::sync::OnceCell; - -// cbindgen looks for const, ignore these so it doesn't warn about them. - -/// cbindgen:ignore -const DEFAULT_BASE_URL: &'static str = "https://api.shorebird.dev"; -/// cbindgen:ignore -const DEFAULT_CHANNEL: &'static str = "stable"; - -fn global_config() -> &'static Mutex { - static INSTANCE: OnceCell> = OnceCell::new(); - INSTANCE.get_or_init(|| Mutex::new(ResolvedConfig::empty())) -} - -pub fn with_config(f: F) -> R -where - F: FnOnce(&ResolvedConfig) -> R, -{ - let lock = global_config() - .lock() - .expect("Failed to acquire updater lock."); - - if !lock.is_initialized { - panic!("Must call shorebird_init() before using the updater."); - } - return f(&lock); -} - -#[derive(Debug)] -pub struct ResolvedConfig { - is_initialized: bool, - pub cache_dir: String, - pub download_dir: String, - pub channel: String, - pub app_id: String, - pub release_version: String, - pub original_libapp_paths: Vec, - pub vm_path: String, - pub base_url: String, -} - -impl ResolvedConfig { - pub fn empty() -> Self { - Self { - is_initialized: false, - cache_dir: String::new(), - download_dir: String::new(), - channel: String::new(), - app_id: String::new(), - release_version: String::new(), - original_libapp_paths: Vec::new(), - vm_path: String::new(), - base_url: String::new(), - } - } -} - -pub fn set_config(config: AppConfig, yaml: YamlConfig) { - // If there is no base_url, use the default. - // If there is no channel, use the default. - let mut lock = global_config() - .lock() - .expect("Failed to acquire updater lock."); - lock.base_url = yaml - .base_url - .as_deref() - .unwrap_or(DEFAULT_BASE_URL) - .to_owned(); - lock.channel = yaml - .channel - .as_deref() - .unwrap_or(DEFAULT_CHANNEL) - .to_owned(); - lock.cache_dir = config.cache_dir.to_string(); - let mut cache_path = std::path::PathBuf::from(config.cache_dir); - cache_path.push("downloads"); - lock.download_dir = cache_path.to_str().unwrap().to_string(); - lock.app_id = yaml.app_id.to_string(); - lock.release_version = config.release_version.to_string(); - lock.original_libapp_paths = config.original_libapp_paths; - lock.vm_path = config.vm_path.to_string(); - lock.is_initialized = true; - info!("Updater configured with: {:?}", lock); -} - -pub fn current_arch() -> &'static str { - #[cfg(target_arch = "x86")] - static ARCH: &str = "x86"; - #[cfg(target_arch = "x86_64")] - static ARCH: &str = "x86_64"; - #[cfg(target_arch = "aarch64")] - static ARCH: &str = "aarch64"; - #[cfg(target_arch = "arm")] - static ARCH: &str = "arm"; - return ARCH; -} - -pub fn current_platform() -> &'static str { - #[cfg(target_os = "macos")] - static PLATFORM: &str = "macos"; - #[cfg(target_os = "linux")] - static PLATFORM: &str = "linux"; - #[cfg(target_os = "windows")] - static PLATFORM: &str = "windows"; - #[cfg(target_os = "android")] - static PLATFORM: &str = "android"; - return PLATFORM; -} diff --git a/updater/library/src/lib.rs b/updater/library/src/lib.rs deleted file mode 100644 index cf271799..00000000 --- a/updater/library/src/lib.rs +++ /dev/null @@ -1,24 +0,0 @@ -// This is a required file for rust libraries which declares what files are -// part of the library and what interfaces are public from the library. - -// Declare that the c_api.rs file exists and is a public sub-namespace. -// C doesn't care about the namespaces, but Rust does. -pub mod c_api; - -// Declare other .rs file/module exists, but make them public. -mod cache; -mod config; -mod logging; -mod network; -mod updater; -mod yaml; - -// Take all public items from the updater namespace and make them public. -pub use self::updater::*; - -// Exposes error!(), info!(), etc macros. -#[macro_use] -extern crate log; - -#[cfg(test)] -extern crate tempdir; diff --git a/updater/library/src/logging.rs b/updater/library/src/logging.rs deleted file mode 100644 index b4cc6d81..00000000 --- a/updater/library/src/logging.rs +++ /dev/null @@ -1,18 +0,0 @@ -#[cfg(target_os = "android")] -pub fn init_logging() { - log_panics::init(); - - android_logger::init_once( - android_logger::Config::default() - // `flutter` tool ignores non-flutter tagged logs. - .with_tag("flutter") - .with_max_level(log::LevelFilter::Debug), - ); - debug!("Logging initialized"); -} - -#[cfg(not(target_os = "android"))] -pub fn init_logging() { - // Nothing to do on non-Android platforms. - // Eventually iOS/MacOS may need something here. -} diff --git a/updater/library/src/network.rs b/updater/library/src/network.rs deleted file mode 100644 index b4be605e..00000000 --- a/updater/library/src/network.rs +++ /dev/null @@ -1,124 +0,0 @@ -// This file's job is to deal with the update_server and network side -// of the updater library. - -use serde::{Deserialize, Serialize}; -use std::fs::File; -use std::io::Write; -use std::path::Path; -use std::string::ToString; - -use crate::cache::UpdaterState; -use crate::config::{current_arch, current_platform, ResolvedConfig}; - -fn patches_check_url(base_url: &str) -> String { - return format!("{}/api/v1/patches/check", base_url); -} - -#[derive(Debug, Deserialize)] -pub struct Patch { - /// The patch number. Starts at 1 for each new release and increases - /// monotonically. - pub number: usize, - /// The hex-encoded sha256 hash of the final uncompressed patch file. - /// Legacy: originally "#" before we implemented hash checks (remove). - pub hash: String, - /// The URL to download the patch file from. - pub download_url: String, -} - -#[derive(Debug, Serialize)] -pub struct PatchCheckRequest { - /// The Shorebird app_id built into the shorebird.yaml in the app. - pub app_id: String, - /// The Shorebird channel built into the shorebird.yaml in the app. - pub channel: String, - /// The release version from AndroidManifest.xml, Info.plist in the app. - pub release_version: String, - /// The latest patch number that the client has downloaded. - /// Not necessarily the one it's running (if some have been marked bad). - /// We could rename this to be more clear. - #[serde(skip_serializing_if = "Option::is_none")] - pub patch_number: Option, - /// Platform (e.g. "android", "ios", "windows", "macos", "linux"). - pub platform: String, - /// Architecture we're running (e.g. "aarch64", "x86", "x86_64"). - pub arch: String, -} - -#[derive(Debug, Deserialize)] -pub struct PatchCheckResponse { - pub patch_available: bool, - #[serde(default)] - pub patch: Option, -} - -pub fn send_patch_check_request( - config: &ResolvedConfig, - state: &UpdaterState, -) -> anyhow::Result { - let latest_patch_number = state.latest_patch_number(); - - // Send the request to the server. - let client = reqwest::blocking::Client::new(); - let req = PatchCheckRequest { - app_id: config.app_id.clone(), - channel: config.channel.clone(), - release_version: config.release_version.clone(), - patch_number: latest_patch_number, - platform: current_platform().to_string(), - arch: current_arch().to_string(), - }; - info!("Sending patch check request: {:?}", req); - let response = client - .post(&patches_check_url(&config.base_url)) - .json(&req) - .send()? - .json()?; - - info!("Patch check response: {:?}", response); - return Ok(response); -} - -pub fn download_to_path(url: &str, path: &Path) -> anyhow::Result<()> { - // Download the file at the given url to the given path. - let client = reqwest::blocking::Client::new(); - let response = client.get(url).send()?; - let mut bytes = response.bytes()?; - - // Ensure the download directory exists. - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - - let mut file = File::create(path)?; - file.write_all(&mut bytes)?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use crate::network::PatchCheckResponse; - - #[test] - fn check_patch_request_response_deserialization() { - let data = r###" - { - "patch_available": true, - "patch": { - "number": 1, - "download_url": "https://storage.googleapis.com/patch_artifacts/17a28ec1-00cf-452d-bdf9-dbb9acb78600/dlc.vmcode", - "hash": "#" - } - }"###; - - let response: PatchCheckResponse = serde_json::from_str(data).unwrap(); - - assert!(response.patch_available == true); - assert!(response.patch.is_some()); - - let patch = response.patch.unwrap(); - assert_eq!(patch.number, 1); - assert_eq!(patch.download_url, "https://storage.googleapis.com/patch_artifacts/17a28ec1-00cf-452d-bdf9-dbb9acb78600/dlc.vmcode"); - assert_eq!(patch.hash, "#"); - } -} diff --git a/updater/library/src/updater.rs b/updater/library/src/updater.rs deleted file mode 100644 index b8d0851a..00000000 --- a/updater/library/src/updater.rs +++ /dev/null @@ -1,464 +0,0 @@ -// This file's job is to be the Rust API for the updater. - -use std::fmt::{Display, Formatter}; - -use crate::cache::{PatchInfo, UpdaterState}; -use crate::config::{set_config, with_config, ResolvedConfig}; -use crate::logging::init_logging; -use crate::network::{download_to_path, send_patch_check_request}; -use crate::yaml::YamlConfig; -use std::path::{Path, PathBuf}; - -pub enum UpdateStatus { - NoUpdate, - UpdateAvailable, - UpdateDownloaded, - UpdateInstalled, - UpdateHadError, -} - -impl Display for UpdateStatus { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - UpdateStatus::NoUpdate => write!(f, "No update"), - UpdateStatus::UpdateAvailable => write!(f, "Update available"), - UpdateStatus::UpdateDownloaded => write!(f, "Update downloaded"), - UpdateStatus::UpdateInstalled => write!(f, "Update installed"), - UpdateStatus::UpdateHadError => write!(f, "Update had error"), - } - } -} - -#[derive(Debug, PartialEq)] -pub enum UpdateError { - InvalidArgument(String, String), - InvalidState(String), - BadServerResponse, - FailedToSaveState, -} - -impl std::error::Error for UpdateError {} - -impl Display for UpdateError { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - match self { - UpdateError::InvalidArgument(name, value) => { - write!(f, "Invalid Argument: {} -> {}", name, value) - } - UpdateError::InvalidState(msg) => write!(f, "Invalid State: {}", msg), - UpdateError::FailedToSaveState => write!(f, "Failed to save state"), - UpdateError::BadServerResponse => write!(f, "Bad server response"), - } - } -} - -// AppConfig is the rust API. ResolvedConfig is the internal storage. -// However rusty api would probably used &str instead of String, -// but making &str from CStr* is a bit of a pain. -pub struct AppConfig { - pub cache_dir: String, - pub release_version: String, - pub original_libapp_paths: Vec, - pub vm_path: String, -} - -/// Initialize the updater library. -/// Takes a AppConfig struct and a yaml string. -/// The yaml string is the contents of the shorebird.yaml file. -/// The AppConfig struct is information about the running app and where -/// the updater should keep its cache. -pub fn init(app_config: AppConfig, yaml: &str) -> Result<(), UpdateError> { - init_logging(); - let config = YamlConfig::from_yaml(&yaml) - .map_err(|err| UpdateError::InvalidArgument("yaml".to_string(), err.to_string()))?; - set_config(app_config, config); - Ok(()) -} - -fn check_for_update_internal(config: &ResolvedConfig) -> bool { - // Load UpdaterState from disk - // If there is no state, make an empty state. - let state = UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version); - // Send info from app + current slot to server. - let response_result = send_patch_check_request(&config, &state); - match response_result { - Err(err) => { - error!("Failed update check: {err}"); - return false; - } - Ok(response) => { - return response.patch_available; - } - } -} - -/// Synchronously checks for an update and returns true if an update is available. -pub fn check_for_update() -> bool { - return with_config(check_for_update_internal); -} - -fn check_hash(path: &Path, expected_string: &str) -> anyhow::Result { - let result = hex::decode(expected_string); - // Remove this legacy behavior. - if result.is_err() { - warn!("Failed to decode hash from server, allowing: {expected_string}"); - return Ok(true); - } - let expected = result.unwrap(); - - use sha2::{Digest, Sha256}; - use std::{fs, io}; - // Based on guidance from: - // https://github.com/RustCrypto/hashes#hashing-readable-objects - - let mut file = fs::File::open(&path)?; - let mut hasher = Sha256::new(); - io::copy(&mut file, &mut hasher)?; - // Check that the length from copy is the same as the file size? - let hash = hasher.finalize(); - let hash_matches = hash.as_slice() == expected; - if !hash_matches { - warn!( - "Hash mismatch: {:?}, expected: {}, got: {:?}", - path, - expected_string, - hex::encode(hash) - ); - } else { - info!("Hash match: {:?}", path); - } - return Ok(hash_matches); -} - -fn update_internal(config: &ResolvedConfig) -> anyhow::Result { - // Load the state from disk. - let mut state = UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version); - // Check for update. - let response = send_patch_check_request(&config, &state)?; - if !response.patch_available { - return Ok(UpdateStatus::NoUpdate); - } - - let patch = response.patch.ok_or(UpdateError::BadServerResponse)?; - - let download_dir = PathBuf::from(&config.cache_dir); - let download_path = download_dir.join(patch.number.to_string()); - download_to_path(&patch.download_url, &download_path)?; - - let base_path = get_base_path(&config.original_libapp_paths)?; - let output_path = download_dir.join(format!("{}.full", patch.number.to_string())); - inflate(&download_path, &base_path, &output_path)?; - - // Check the hash before moving into place. - let hash_ok = check_hash(&output_path, &patch.hash)?; - if !hash_ok { - return Err(UpdateError::InvalidState("Hash mismatch".to_string()).into()); - } - // Move/state update should be "atomic". - // Consider supporting allowing the system to download for us (e.g. iOS). - - let patch_info = PatchInfo { - path: output_path.to_str().unwrap().to_string(), - number: patch.number, - }; - state.install_patch(patch_info)?; - info!("Patch {} successfully installed.", patch.number); - - // Set the state to "restart required". - return Ok(UpdateStatus::UpdateInstalled); -} - -fn get_base_path(original_lib_app_paths: &Vec) -> anyhow::Result { - // Iterate through the paths and find the first one that exists. - for path in original_lib_app_paths { - let path = PathBuf::from(path); - match path.try_exists() { - Ok(true) => { - return Ok(path); - } - Ok(false) => { - info!("File does not exist: {:?}", path); - continue; - } - Err(err) => { - info!("Failed to check for file: {:?}, err: {err}", path); - continue; - } - } - } - return Err(UpdateError::InvalidState("No base file found".to_string()).into()); -} - -fn inflate(patch_path: &Path, base_path: &Path, output_path: &Path) -> anyhow::Result<()> { - info!("Patch is compressed, inflating..."); - use anyhow::Context; - use comde::de::Decompressor; - use comde::zstd::ZstdDecompressor; - use std::fs::File; - use std::io::{BufReader, BufWriter}; - - // Open all our files first for error clarity. Otherwise we might see - // PipeReader/Writer errors instead of file open errors. - info!("Reading base file: {:?}", base_path); - let base_r = - File::open(base_path).context(format!("Failed to open base file: {:?}", base_path))?; - - info!("Reading patch file: {:?}", patch_path); - let compressed_patch_r = BufReader::new( - File::open(patch_path).context(format!("Failed to open patch file: {:?}", patch_path))?, - ); - let output_file_w = File::create(&output_path)?; - - // Set up a pipe to connect the writing from the decompression thread - // to the reading of the decompressed patch data on this thread. - let (patch_r, patch_w) = pipe::pipe(); - - let decompress = ZstdDecompressor::new(); - // Spawn a thread to run the decompression in parallel to the patching. - // decompress.copy will block on the pipe being full (I think) and then - // when it returns the thread will exit. - std::thread::spawn(move || { - let result = decompress.copy(compressed_patch_r, patch_w); - // If this thread fails, undoubtedly the main thread will fail too. - // Most important is to not crash. - if let Err(err) = result { - error!("Decompression thread failed: {err}"); - } - }); - - // Do the patch, using the uncompressed patch data from the pipe. - let mut fresh_r = bipatch::Reader::new(patch_r, base_r)?; - - // Write out the resulting patched file to the new location. - let mut output_w = BufWriter::new(output_file_w); - std::io::copy(&mut fresh_r, &mut output_w)?; - Ok(()) -} - -/// Reads the current patch from the cache and returns it. -pub fn active_patch() -> Option { - return with_config(|config| { - let state = UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version); - return state.current_patch(); - }); -} - -pub fn report_failed_launch() -> Result<(), UpdateError> { - info!("Reporting failed launch."); - with_config(|config| { - let mut state = - UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version); - - let patch = state - .current_patch() - .ok_or(UpdateError::InvalidState("No current patch".to_string()))?; - state.mark_patch_as_bad(&patch); - state.activate_latest_bootable_patch() - }) -} - -pub fn report_successful_launch() -> Result<(), UpdateError> { - with_config(|config| { - let mut state = - UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version); - - let patch = state - .current_patch() - .ok_or(UpdateError::InvalidState("No current patch".to_string()))?; - state.mark_patch_as_good(&patch); - state.save().map_err(|_| UpdateError::FailedToSaveState) - }) -} - -/// Synchronously checks for an update and downloads and installs it if available. -pub fn update() -> UpdateStatus { - return with_config(|config| { - let result = update_internal(&config); - match result { - Err(err) => { - error!("Problem updating: {err}"); - error!("{}", err.backtrace()); - return UpdateStatus::UpdateHadError; - } - Ok(status) => status, - } - }); -} - -#[cfg(test)] -mod tests { - use tempdir::TempDir; - - fn init_for_testing(tmp_dir: &TempDir) { - let cache_dir = tmp_dir.path().to_str().unwrap().to_string(); - crate::init( - crate::AppConfig { - cache_dir: cache_dir.clone(), - release_version: "1.0.0".to_string(), - original_libapp_paths: vec!["original_libapp_path".to_string()], - vm_path: "vm_path".to_string(), - }, - "app_id: 1234", - ) - .unwrap(); - } - - #[test] - fn init_missing_yaml() { - let tmp_dir = TempDir::new("example").unwrap(); - let cache_dir = tmp_dir.path().to_str().unwrap().to_string(); - assert_eq!( - crate::init( - crate::AppConfig { - cache_dir: cache_dir.clone(), - release_version: "1.0.0".to_string(), - original_libapp_paths: vec!["original_libapp_path".to_string()], - vm_path: "vm_path".to_string(), - }, - "", - ), - Err(crate::UpdateError::InvalidArgument( - "yaml".to_string(), - "missing field `app_id`".to_string() - )) - ); - } - - #[test] - fn report_launch_result_with_no_current_patch() { - let tmp_dir = TempDir::new("example").unwrap(); - init_for_testing(&tmp_dir); - assert_eq!( - crate::report_failed_launch(), - Err(crate::UpdateError::InvalidState( - "No current patch".to_string() - )) - ); - assert_eq!( - crate::report_successful_launch(), - Err(crate::UpdateError::InvalidState( - "No current patch".to_string() - )) - ); - } - - #[test] - fn ignore_version_after_marked_bad() { - let tmp_dir = TempDir::new("example").unwrap(); - init_for_testing(&tmp_dir); - - use crate::cache::{PatchInfo, UpdaterState}; - use crate::config::with_config; - - // Install a fake patch. - with_config(|config| { - let download_dir = std::path::PathBuf::from(&config.download_dir); - let artifact_path = download_dir.join("1"); - println!("artifact_path: {:?}", artifact_path); - std::fs::create_dir_all(&download_dir).unwrap(); - std::fs::write(&artifact_path, "hello").unwrap(); - - let mut state = - UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version); - state - .install_patch(PatchInfo { - path: artifact_path.to_str().unwrap().to_string(), - number: 1, - }) - .expect("move failed"); - state.save().expect("save failed"); - }); - assert!(crate::active_patch().is_some()); - // pretend we booted from it - crate::report_successful_launch().unwrap(); - assert!(crate::active_patch().is_some()); - // mark it bad. - crate::report_failed_launch().unwrap(); - // Technically might need to "reload" - // ask for current patch (should get none). - assert!(crate::active_patch().is_none()); - } - - #[test] - fn hash_matches() { - let tmp_dir = TempDir::new("example").unwrap(); - - let input_path = tmp_dir.path().join("input"); - std::fs::write(&input_path, "hello world").unwrap(); - - let expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"; - assert!(super::check_hash(&input_path, expected).unwrap()); - - // modify hash to not match - let expected = "a94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"; - assert_eq!(super::check_hash(&input_path, expected).unwrap(), false); - - // invalid hashes should not match either - // Except for now they do (legacy behavior). - let expected = "foo"; - assert_eq!(super::check_hash(&input_path, expected).unwrap(), true); - - // Remove this case when legacy clients are gone. - let expected = "#"; - assert!(super::check_hash(&input_path, expected).unwrap()); - } - - #[test] - fn inflate_missing_files() { - let tmp_dir = TempDir::new("example").unwrap(); - let missing_file = tmp_dir.path().join("missing_file"); - let existing_file = tmp_dir.path().join("existing_file"); - std::fs::write(&existing_file, "hello world").unwrap(); - - let mut result = super::inflate(&missing_file, &missing_file, &missing_file); - let mut error = result.unwrap_err(); - assert!(format!("{}", error).starts_with("Failed to open base file:")); - - result = super::inflate(&missing_file, &existing_file, &missing_file); - error = result.unwrap_err(); - assert!(format!("{}", error).starts_with("Failed to open patch file:")); - } - - #[test] - fn get_base_path_uses_correct_path() { - let tmp_dir = TempDir::new("example").unwrap(); - let missing_file = tmp_dir.path().join("missing_file"); - let existing_file = tmp_dir.path().join("existing_file"); - std::fs::write(&existing_file, "hello world").unwrap(); - - // Should use first file since it exists. - let mut base_paths = vec![ - existing_file.as_path().to_str().unwrap().to_string(), - missing_file.as_path().to_str().unwrap().to_string(), - ]; - - let mut result = super::get_base_path(&base_paths); - - assert_eq!(result.unwrap(), existing_file.as_path()); - - // Should skip first file since it is missing. - base_paths = vec![ - missing_file.as_path().to_str().unwrap().to_string(), - existing_file.as_path().to_str().unwrap().to_string(), - ]; - - result = super::get_base_path(&base_paths); - - assert_eq!(result.unwrap(), existing_file.as_path()); - - // Should error since all files are missing. - base_paths = vec![ - missing_file.as_path().to_str().unwrap().to_string(), - missing_file.as_path().to_str().unwrap().to_string(), - ]; - - result = super::get_base_path(&base_paths); - - let error = result.unwrap_err(); - - assert_eq!( - format!("{}", error), - "Invalid State: No base file found".to_string() - ); - } -} diff --git a/updater/library/src/yaml.rs b/updater/library/src/yaml.rs deleted file mode 100644 index c00fbaae..00000000 --- a/updater/library/src/yaml.rs +++ /dev/null @@ -1,20 +0,0 @@ -use serde::Deserialize; - -/// Struct for parsing shorebird.yaml. -#[derive(Deserialize)] -pub struct YamlConfig { - /// App ID. Required. Generated by Shorebird and included - /// in your app to identify which app/channel/version triple to update. - pub app_id: String, - /// Update channel name. Defaults to "stable" if not set. - pub channel: Option, - /// Update URL. Defaults to the default update URL if not set. - pub base_url: Option, -} - -impl YamlConfig { - /// Read in shorebird.yaml from a string. - pub fn from_yaml(yaml: &str) -> Result { - serde_yaml::from_str(yaml) - } -} diff --git a/updater/patch/Cargo.toml b/updater/patch/Cargo.toml deleted file mode 100644 index 62295a43..00000000 --- a/updater/patch/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "patch" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# Compression and decompression of patch files. -bidiff = "1.0.0" -# Pipe is a simple in-memory pipe implementation, there might be a std way too? -pipe = "0.4.0" -# comde is a wrapper around several compression libraries. -# We only use zstd and could depend on it directly instead. -comde = {version = "0.2.3", default-features = false, features = ["zstandard"]} diff --git a/updater/patch/README.md b/updater/patch/README.md deleted file mode 100644 index 394c6ffd..00000000 --- a/updater/patch/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# patch command line tool - -This is the tool used by the `shorebird` command line to compute the patch -file for uploading to the server. - -This currently uses the rust `bidiff` crate to compute the patch file. -and could just use the `bic` command line tool included in that crate. However -we're explicitly writing our own command line to allow us to change the -underlying compression without affecting the `shorebird` command line callers. - -## Usage - - patch diff --git a/updater/patch/src/main.rs b/updater/patch/src/main.rs deleted file mode 100644 index bf58a5e3..00000000 --- a/updater/patch/src/main.rs +++ /dev/null @@ -1,52 +0,0 @@ -use bidiff::DiffParams; -use std::{ - fs::{self, File}, - io::{BufWriter, Write}, - time::Instant, -}; - -use comde::com::Compressor; -use comde::zstd::ZstdCompressor; - -// Originally inspired from example in: -// https://github.com/divvun/bidiff/blob/main/crates/bic/src/main.rs -// and then hacked down to just service our needs. - -// comde is just a wrapper around various compression/decompression libraries. -// and we could just depend on the zstd crate directly if we end up using -// zstd long term. - -fn main() { - let mut args = std::env::args(); - args.next(); // skip program name - let older = args.next().expect("path to base file"); - let newer = args.next().expect("path to new file"); - let patch = args.next().expect("path to output file"); - - let start = Instant::now(); - - let older_contents = fs::read(older).expect("read base file"); - let newer_contents = fs::read(newer).expect("read new file"); - - let (mut patch_r, mut patch_w) = pipe::pipe(); - let diff_params = DiffParams::new(1, None).unwrap(); - std::thread::spawn(move || { - bidiff::simple_diff_with_params( - &older_contents[..], - &newer_contents[..], - &mut patch_w, - &diff_params, - ) - .unwrap(); - }); - - let compressor = ZstdCompressor::new(); - - let mut compatch_w = BufWriter::new(File::create(patch).expect("create patch file")); - compressor - .compress(&mut compatch_w, &mut patch_r) - .expect("compress patch"); - compatch_w.flush().expect("flush patch"); - - println!("Completed in {:?}", start.elapsed()); -}