feat: Add support for diff patches (#163)

Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
Eric Seidel
2023-03-24 12:59:35 -07:00
committed by GitHub
parent a25b5d0ec1
commit 0ba204e5c0
6 changed files with 89 additions and 17 deletions
+13 -2
View File
@@ -19,17 +19,28 @@ reqwest = { version = "0.11", default-features = false, features = ["blocking",
# Json serialization/de-serialization.
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.93"
# Used for error handling.
# 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"
uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"]}
# For validating hashes of downloaded patch files.
sha2 = "0.10.6"
# 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"
[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"]}
+9 -4
View File
@@ -59,13 +59,13 @@ void shorebird_init(const struct AppParameters *c_params,
const char *c_yaml);
/**
* Return the active version of the app, or NULL if there is no active version.
* Return the active patch number, or NULL if there is no active patch.
*/
SHOREBIRD_EXPORT char *shorebird_active_version(void);
SHOREBIRD_EXPORT char *shorebird_active_patch_number(void);
/**
* Return the path to the active version of the app, or NULL if there is no
* active version.
* 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);
@@ -84,6 +84,11 @@ SHOREBIRD_EXPORT bool shorebird_check_for_update(void);
*/
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
+4 -5
View File
@@ -66,10 +66,9 @@ pub extern "C" fn shorebird_init(c_params: *const AppParameters, c_yaml: *const
}
}
/// Return the active version of the app, or NULL if there is no active version.
// TODO: This should probably be renamed to `shorebird_active_patch_number`.
/// Return the active patch number, or NULL if there is no active patch.
#[no_mangle]
pub extern "C" fn shorebird_active_version() -> *mut c_char {
pub extern "C" fn shorebird_active_patch_number() -> *mut c_char {
let patch = updater::active_patch();
match patch {
Some(v) => {
@@ -80,8 +79,8 @@ pub extern "C" fn shorebird_active_version() -> *mut c_char {
}
}
/// Return the path to the active version of the app, or NULL if there is no
/// active version.
/// 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 {
+2 -2
View File
@@ -266,7 +266,7 @@ impl UpdaterState {
.into());
}
// Move the patch into the slot.
// Move the artifact into the slot.
let artifact_path = slot_dir.join("dlc.vmcode");
std::fs::rename(&patch.path, &artifact_path)?;
@@ -363,7 +363,7 @@ mod tests {
}
#[test]
fn dont_install_known_bad_patch() {
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);
+10 -2
View File
@@ -4,7 +4,7 @@
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::path::Path;
use std::string::ToString;
use crate::cache::UpdaterState;
@@ -16,9 +16,17 @@ fn patches_check_url(base_url: &str) -> String {
#[derive(Debug, Deserialize)]
pub struct Patch {
/// The patch number. Starts at 1 for each new release and increases
/// monotonically.
pub number: usize,
/// The hash of the final uncompressed patch file.
pub hash: String,
/// The URL to download the patch file from.
pub download_url: String,
/// Whether the artifact is a diff (modern) or full (legacy) artifact.
/// Will eventually be removed once we no longer support legacy artifacts.
#[serde(default)]
pub is_diff: bool,
}
#[derive(Debug, Serialize)]
@@ -74,7 +82,7 @@ pub fn send_patch_check_request(
return Ok(response);
}
pub fn download_to_path(url: &str, path: &PathBuf) -> anyhow::Result<()> {
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()?;
+51 -2
View File
@@ -7,7 +7,7 @@ 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::PathBuf;
use std::path::{Path, PathBuf};
pub enum UpdateStatus {
NoUpdate,
@@ -109,8 +109,17 @@ fn update_internal(config: &ResolvedConfig) -> anyhow::Result<UpdateStatus> {
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());
let mut download_path = download_dir.join(patch.number.to_string());
download_to_path(&patch.download_url, &download_path)?;
// Inflate the patch from a diff if needed.
if patch.is_diff {
let base_path = PathBuf::from(&config.original_libapp_path);
let output_path = download_dir.join(format!("{}.full", patch.number.to_string()));
inflate(&download_path, &base_path, &output_path)?;
download_path = output_path;
}
// Check the hash before moving into place.
// Move/state update should be "atomic".
// Consider supporting allowing the system to download for us (e.g. iOS).
@@ -125,6 +134,46 @@ fn update_internal(config: &ResolvedConfig) -> anyhow::Result<UpdateStatus> {
return Ok(UpdateStatus::UpdateInstalled);
}
fn inflate(patch_path: &Path, base_path: &Path, output_path: &Path) -> anyhow::Result<()> {
info!("Patch is compressed, inflating...");
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)?;
let compressed_patch_r = BufReader::new(File::open(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<PatchInfo> {
return with_config(|config| {