use std::cell::RefCell;
use std::collections::BTreeMap;
use std::error::Error;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
use leptoskit_codepush::Bucket;
use leptoskit_codepush::CodePushClient;
use leptoskit_codepush::CodePushError;
use leptoskit_codepush::FileTransport;
use leptoskit_codepush::ObjectKey;
use leptoskit_codepush::PullReport;
use leptoskit_codepush::ReleaseSigningKey;
use leptoskit_codepush::ReleaseStore;
use leptoskit_codepush::StaticObject;
use leptoskit_codepush::Transport;
use leptoskit_codepush::TrustedKeys;
use leptoskit_codepush::bundle_directory_as;
use leptoskit_codepush::installed::InstalledBundles;
use leptoskit_codepush::installed::RuntimeContract;
use leptoskit_codepush::installed::UpdateConfiguration;
use leptoskit_codepush::installed::UpdateStatus;
use leptoskit_codepush::publish_channel;
use leptoskit_codepush::pull_channel;
use semver::Version;
const APP: &str = "lighthouse";
const CHANNEL: &str = "production";
type Outcome = Result<(), Box<dyn Error>>;
fn main() -> Outcome {
run(&mut std::io::stdout())
}
fn run(out: &mut impl Write) -> Outcome {
let work = tempfile::tempdir()?;
let work = work.path();
let publisher = ReleaseSigningKey::from_seed(&[0x1b; 32]);
let trusted = TrustedKeys::new([publisher.public_key()])?;
let store = ReleaseStore::open(work.join("registry"))?;
let server = format!("file://{}", store.root().display());
let baseline = bundle(work, Version::new(1, 0, 0), 0, &module("store build", 0))?;
let mut device = InstalledBundles::open(
&work.join("device"),
&baseline,
configuration(&server, &publisher),
)?;
let launch = device.launch()?;
device.confirm(&launch.metadata.receipt)?;
writeln!(
out,
"1. launched the store build {}+{}",
launch.metadata.version, launch.metadata.build
)?;
release(
&store,
&publisher,
work,
&Version::new(1, 0, 1),
1,
&module("lantern", 0),
)?;
let staged = device.sync_with(&mut FileTransport)?;
ensure(staged.status == UpdateStatus::Staged, "1.0.1 is staged")?;
let trial = device.launch()?;
ensure(trial.metadata.candidate, "1.0.1 launches as a trial")?;
device.confirm(&trial.metadata.receipt)?;
writeln!(
out,
"2. staged, launched, and confirmed {}+{} on the device",
trial.metadata.version, trial.metadata.build
)?;
let previous = store.latest(APP, CHANNEL)?.ok_or("the store holds 1.0.1")?;
release(
&store,
&publisher,
work,
&Version::new(1, 0, 2),
2,
&module("lantern", 1),
)?;
let fetched = Rc::new(RefCell::new(Fetched::default()));
let mut desktop = CodePushClient::new(
Counting::new(FileTransport, &fetched),
server.clone(),
APP,
CHANNEL,
Version::new(1, 0, 1),
1,
previous.container.clone(),
trusted.clone(),
);
let synced = desktop.sync(&work.join("desktop"))?;
ensure(synced.used_delta, "1.0.2 arrives as a delta")?;
let downloaded = fetched.borrow().bytes;
writeln!(
out,
"3. a desktop client moved to 1.0.2 fetching {} KiB instead of a {} KiB container",
downloaded / 1024,
previous.container.len() / 1024
)?;
ensure(
downloaded * 4 < previous.container.len(),
"the delta is a fraction of the container",
)?;
let next = device.sync_with(&mut FileTransport)?;
ensure(next.status == UpdateStatus::Staged, "1.0.2 is staged")?;
let trial = device.launch()?;
device.confirm(&trial.metadata.receipt)?;
release(
&store,
&publisher,
work,
&Version::new(1, 0, 3),
3,
&module("broken", 1),
)?;
device.sync_with(&mut FileTransport)?;
let trial = device.launch()?;
ensure(trial.metadata.build == 3, "1.0.3 launches as a trial")?;
device.reject(&trial.metadata.receipt)?;
let fallback = device.launch()?;
ensure(
fallback.metadata.build == 2,
"the device falls back to 1.0.2",
)?;
device.confirm(&fallback.metadata.receipt)?;
let checked = Rc::new(RefCell::new(Fetched::default()));
let after = device.sync_with(&mut Counting::new(FileTransport, &checked))?;
ensure(after.status == UpdateStatus::Current, "1.0.3 is refused")?;
let checked = checked.borrow();
ensure(
checked.urls.iter().all(|url| url.ends_with("/index.json")),
"only the channel index is fetched",
)?;
writeln!(
out,
"4. rejected the crashing 1.0.3, fell back to {}+{}, and checked again with {} request",
fallback.metadata.version,
fallback.metadata.build,
checked.urls.len()
)?;
let mut bucket = MemoryBucket::default();
let mirrored = publish_channel(&store, APP, CHANNEL, &mut bucket)?;
let runner = ReleaseStore::open(work.join("runner"))?;
let pulled = pull_channel(&runner, APP, CHANNEL, &mut bucket, &trusted)?;
ensure(
matches!(
pulled,
PullReport::Pulled {
build: 3,
imported: true,
..
}
),
"the runner holds the newest release",
)?;
writeln!(
out,
"5. published {} objects to a bucket and pulled {}+{} into a fresh store",
mirrored.written, mirrored.version, mirrored.build
)?;
Ok(())
}
fn contract() -> RuntimeContract {
RuntimeContract {
abi: 1,
shell: "lighthouse-shell-v1".to_owned(),
plugins: Vec::new(),
}
}
fn configuration(server: &str, publisher: &ReleaseSigningKey) -> UpdateConfiguration {
UpdateConfiguration {
enabled: true,
app: APP.to_owned(),
channel: CHANNEL.to_owned(),
server: server.to_owned(),
public_keys: vec![publisher.public_key().to_hex()],
runtime: contract(),
allow_http: false,
provenance: None,
}
}
fn module(marker: &str, edits: usize) -> Vec<u8> {
const CODE: &[u8] = b"\0asm local.get i32.add call br_if";
let mut state = 0x2545_f491_4f6c_dd1d_u64;
let mut wasm = b"\0asm\x01\0\0\0".to_vec();
wasm.extend_from_slice(marker.as_bytes());
wasm.extend((0..1024 * 1024).map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
CODE[(state % CODE.len() as u64) as usize]
}));
let middle = wasm.len() / 2;
for edit in 0..edits {
wasm[middle + edit * 4096..][..12].copy_from_slice(b"patched code");
}
wasm
}
fn bundle(
work: &Path,
version: Version,
build: u64,
wasm: &[u8],
) -> Result<Vec<u8>, Box<dyn Error>> {
let source = source_dir(work, &version, build, wasm)?;
let output = work.join(format!("{version}+{build}.lkb"));
bundle_directory_as(&source, &output, APP, CHANNEL, version, build)?;
Ok(fs::read(output)?)
}
fn release(
store: &ReleaseStore,
publisher: &ReleaseSigningKey,
work: &Path,
version: &Version,
build: u64,
wasm: &[u8],
) -> Outcome {
let source = source_dir(work, version, build, wasm)?;
store.ingest_directory(publisher, APP, CHANNEL, version, build, &source, false)?;
Ok(())
}
fn source_dir(
work: &Path,
version: &Version,
build: u64,
wasm: &[u8],
) -> Result<PathBuf, Box<dyn Error>> {
let source = work.join(format!("source-{version}+{build}"));
fs::create_dir_all(&source)?;
fs::write(source.join("app.wasm"), wasm)?;
fs::write(
source.join("runtime.json"),
serde_json::to_vec(&contract())?,
)?;
Ok(source)
}
fn ensure(condition: bool, claim: &str) -> Outcome {
if condition {
Ok(())
} else {
Err(format!("expected: {claim}").into())
}
}
#[derive(Default)]
struct Fetched {
urls: Vec<String>,
bytes: usize,
}
struct Counting<T> {
inner: T,
fetched: Rc<RefCell<Fetched>>,
}
impl<T> Counting<T> {
fn new(inner: T, fetched: &Rc<RefCell<Fetched>>) -> Self {
Self {
inner,
fetched: Rc::clone(fetched),
}
}
}
impl<T: Transport> Transport for Counting<T> {
fn fetch(&mut self, url: &str) -> Result<Vec<u8>, CodePushError> {
let body = self.inner.fetch(url)?;
let mut fetched = self.fetched.borrow_mut();
fetched.urls.push(url.to_owned());
fetched.bytes += body.len();
Ok(body)
}
}
#[derive(Default)]
struct MemoryBucket {
objects: BTreeMap<String, Vec<u8>>,
}
impl Bucket for MemoryBucket {
fn get(&mut self, key: &ObjectKey) -> Result<Option<Vec<u8>>, CodePushError> {
Ok(self.objects.get(key.as_str()).cloned())
}
fn put(&mut self, object: &StaticObject) -> Result<(), CodePushError> {
self.objects
.insert(object.key.as_str().to_owned(), object.bytes.clone());
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn the_lifecycle_runs_from_store_build_to_mirror() {
let mut narration = Vec::new();
super::run(&mut narration).expect("every step holds");
let narration = String::from_utf8(narration).expect("utf-8");
assert_eq!(narration.lines().count(), 5, "{narration}");
}
}