OXYGENHUB
◆ v0.5.2 oxygenflow / crates / flow / src / update.rs
24.8 KiB · Rust · last changed Runner builds fetched through the swarm (refs #84); the WASM tier as a cargo feature, off for Haiku (refs #91) 7 days ago source blame history raw
85275e1e Andy McCall ↶ 1
//! Runner updates, the runner's side (docs/jetstream.md §8.2, #85): check
2
//! a new `flow` build, stage it beside the running one, swap it in keeping
3
//! the old one as `flow.prev`, and have the running runner leave with
4
//! notice so its unit starts the new build. The new build proves itself by
5
//! reaching the hub; three failed starts, or ten minutes without the hub,
6
//! put `flow.prev` back.
7
8
use anyhow::{bail, Context, Result};
9
use serde::{Deserialize, Serialize};
10
use std::path::{Path, PathBuf};
11
use std::time::Duration;
12
13
/// The staged build, beside the installed one.
14
pub const NEW: &str = "flow.new";
15
/// The build an update replaced, kept to go back to.
16
pub const PREV: &str = "flow.prev";
17
/// In the runner's home: the update not yet proven.
18
const PENDING: &str = "update.json";
19
/// In the runner's home: every update and rollback, a JSON line each.
20
const LOG: &str = "updates.log";
21
/// In the runner's home: the running runner's process id.
22
const PID: &str = "runner.pid";
494a1b9a Andy McCall ↶ 23
/// In the runner's home: a new build is in place, leave for it. How a
24
/// runner is told on Windows, which has no SIGUSR1.
25
const REQUEST: &str = "update.request";
85275e1e Andy McCall ↶ 26
/// Starts of an unproven build after which the previous one comes back.
27
pub const MAX_STARTS: u32 = 3;
28
/// How long a new build has to reach the hub.
29
pub const PROVE_WITHIN: Duration = Duration::from_secs(10 * 60);
30
31
/// An update swapped in and not yet proven.
32
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33
pub struct Pending {
34
    pub from: String,
35
    pub to: String,
36
    /// The installed binary the update replaced.
37
    pub exe: PathBuf,
38
    pub at_ms: u64,
39
    /// How long the running runner had to finish what it held.
40
    pub notice_secs: u64,
41
    /// Starts of the new build so far.
42
    pub starts: u32,
43
}
44
45
/// What [`install`] did, or would do.
46
#[derive(Debug, PartialEq, Eq)]
47
pub struct Installed {
48
    pub from: String,
49
    pub to: String,
50
    pub exe: PathBuf,
51
    /// The running runner told to leave, by process id.
52
    pub told: Option<u32>,
53
}
54
55
/// The version a `flow --version` line names: `flow 0.3.1 (build)` → `0.3.1`.
56
pub fn parse_version(line: &str) -> Option<String> {
57
    let v = line.trim().strip_prefix("flow ")?.split_whitespace().next()?;
58
    v.split('.').all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit())).then(|| v.to_string())
59
}
60
61
/// Whether version `a` is older than `b`, part by part.
62
pub fn older(a: &str, b: &str) -> bool {
63
    let parts = |v: &str| v.split('.').map(|p| p.parse::<u64>().unwrap_or(0)).collect::<Vec<_>>();
64
    parts(a) < parts(b)
65
}
66
494a1b9a Andy McCall ↶ 67
/// `exe`'s sibling named `stem`, with `exe`'s extension (`flow.prev.exe`
68
/// beside `flow.exe` on Windows, `flow.prev` beside `flow` elsewhere).
69
pub fn beside(exe: &Path, stem: &str) -> PathBuf {
70
    match exe.extension() {
71
        Some(e) => exe.with_file_name(format!("{stem}.{}", e.to_string_lossy())),
72
        None => exe.with_file_name(stem),
73
    }
74
}
75
76
/// The PE machine number of this build's architecture (Windows).
77
fn pe_machine_here() -> Option<u16> {
78
    Some(match std::env::consts::ARCH {
79
        "x86_64" => 0x8664,
80
        "aarch64" => 0xAA64,
81
        _ => return None,
82
    })
83
}
84
85
/// Refuse a file that is not a PE executable for this machine (Windows).
86
pub fn check_pe(bytes: &[u8]) -> Result<()> {
87
    if bytes.len() < 0x40 || &bytes[..2] != b"MZ" {
88
        bail!("not a Windows executable");
89
    }
90
    let at = u32::from_le_bytes([bytes[0x3c], bytes[0x3d], bytes[0x3e], bytes[0x3f]]) as usize;
91
    if bytes.len() < at + 6 || &bytes[at..at + 4] != b"PE\0\0" {
92
        bail!("not a Windows executable (no PE header in its first {} bytes)", bytes.len());
93
    }
94
    let machine = u16::from_le_bytes([bytes[at + 4], bytes[at + 5]]);
95
    match pe_machine_here() {
96
        Some(m) if m == machine => Ok(()),
97
        Some(_) => bail!("a build for another machine (PE machine {machine:#x}); this runner is {}", std::env::consts::ARCH),
98
        None => bail!("updates are not supported on {}", std::env::consts::ARCH),
99
    }
100
}
101
85275e1e Andy McCall ↶ 102
/// The ELF machine number of this build's architecture.
103
fn machine_here() -> Option<u16> {
104
    Some(match std::env::consts::ARCH {
105
        "x86_64" => 62,
106
        "aarch64" => 183,
107
        "riscv64" => 243,
108
        _ => return None,
109
    })
110
}
111
112
/// Refuse a file that is not a 64-bit little-endian ELF executable for this
113
/// machine: a build for another target would not start, and the rollback
114
/// would only notice after three tries.
115
pub fn check_target(bytes: &[u8]) -> Result<()> {
116
    if bytes.len() < 20 || &bytes[..4] != b"\x7fELF" {
117
        bail!("not an ELF executable");
118
    }
119
    if bytes[4] != 2 || bytes[5] != 1 {
120
        bail!("not a 64-bit little-endian build");
121
    }
122
    let machine = u16::from_le_bytes([bytes[18], bytes[19]]);
123
    match machine_here() {
124
        Some(m) if m == machine => Ok(()),
125
        Some(_) => bail!("a build for another machine (ELF machine {machine}); this runner is {}", std::env::consts::ARCH),
126
        None => bail!("updates are not supported on {}", std::env::consts::ARCH),
127
    }
128
}
129
130
/// Run `file args…` with a time limit, returning its standard output.
131
fn run(file: &Path, args: &[&str], limit: Duration) -> Result<String> {
132
    let mut child = std::process::Command::new(file)
133
        .args(args)
134
        .stdin(std::process::Stdio::null())
135
        .stdout(std::process::Stdio::piped())
136
        .stderr(std::process::Stdio::piped())
137
        .spawn()
138
        .with_context(|| format!("starting {}", file.display()))?;
139
    let start = std::time::Instant::now();
140
    loop {
141
        if child.try_wait()?.is_some() {
142
            break;
143
        }
144
        if start.elapsed() > limit {
145
            let _ = child.kill();
146
            let _ = child.wait();
147
            bail!("`{} {}` did not finish within {} s", file.display(), args.join(" "), limit.as_secs());
148
        }
149
        std::thread::sleep(Duration::from_millis(50));
150
    }
151
    let out = child.wait_with_output()?;
152
    if !out.status.success() {
153
        bail!("`{} {}` failed ({}): {}", file.display(), args.join(" "), out.status, String::from_utf8_lossy(&out.stderr).trim());
154
    }
155
    Ok(String::from_utf8_lossy(&out.stdout).into_owned())
156
}
157
21a3ea4a Andy McCall ↶ 158
/// Copy `from` to `to` if it is `size` bytes with BLAKE3 `blake3`, as the
159
/// hub attests a build: a part file renamed into place, so `to` is either
160
/// the whole checked build or absent.
161
pub fn copy_checked(from: &Path, to: &Path, size: u64, blake3: &str) -> Result<()> {
162
    let bytes = std::fs::read(from).with_context(|| format!("reading {}", from.display()))?;
163
    let got = blake3::hash(&bytes).to_hex().to_string();
164
    if bytes.len() as u64 != size || got != blake3 {
165
        bail!("{} is not the build the hub attests ({} bytes, BLAKE3 {}…)", from.display(), bytes.len(), &got[..12]);
166
    }
167
    let part = to.with_extension("partial");
168
    std::fs::write(&part, &bytes).with_context(|| format!("writing {}", part.display()))?;
169
    std::fs::rename(&part, to)?;
170
    Ok(())
171
}
172
85275e1e Andy McCall ↶ 173
/// Check a build as the runner would run it: this machine's target, it
174
/// starts and names its version, and it finds the tiers this home offers.
175
/// Returns its version.
176
pub fn check(file: &Path, home: &Path) -> Result<String> {
494a1b9a Andy McCall ↶ 177
    let mut head = vec![0u8; 4096];
85275e1e Andy McCall ↶ 178
    let n = std::io::Read::read(&mut std::fs::File::open(file).with_context(|| format!("opening {}", file.display()))?, &mut head)?;
494a1b9a Andy McCall ↶ 179
    if cfg!(windows) {
180
        check_pe(&head[..n])?;
181
    } else {
182
        check_target(&head[..n])?;
183
    }
85275e1e Andy McCall ↶ 184
    let limit = Duration::from_secs(30);
185
    let line = run(file, &["--version"], limit)?;
186
    let version = parse_version(&line).with_context(|| format!("{} does not say it is flow: {:?}", file.display(), line.trim()))?;
187
    let home_s = home.to_string_lossy();
188
    let tiers = run(file, &["--home", &home_s, "tiers"], limit)?;
189
    if !tiers.lines().any(|l| l.ends_with(": available")) {
190
        bail!("flow {version} finds no tier it can run here:\n{}", tiers.trim());
191
    }
192
    Ok(version)
193
}
194
195
fn now_ms() -> u64 {
196
    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0)
197
}
198
199
fn log(home: &Path, event: &str, from: &str, to: &str, why: &str) {
200
    use std::io::Write;
201
    let line = serde_json::json!({"at_ms": now_ms(), "event": event, "from": from, "to": to, "why": why});
202
    if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(home.join(LOG)) {
203
        let _ = writeln!(f, "{line}");
204
    }
205
}
206
207
/// The update not yet proven, if any.
208
pub fn pending(home: &Path) -> Option<Pending> {
209
    serde_json::from_slice(&std::fs::read(home.join(PENDING)).ok()?).ok()
210
}
211
212
fn save(home: &Path, p: &Pending) -> Result<()> {
213
    let tmp = home.join(format!("{PENDING}.tmp"));
214
    std::fs::write(&tmp, serde_json::to_vec_pretty(p)?)?;
215
    std::fs::rename(&tmp, home.join(PENDING))?;
216
    Ok(())
494a1b9a Andy McCall ↶ 217
}
218
219
/// Whether `p` is the program running now.
220
fn is_self(p: &Path) -> bool {
221
    let canon = |p: &Path| std::fs::canonicalize(p).ok();
222
    std::env::current_exe().ok().and_then(|me| canon(&me)).is_some_and(|me| Some(me) == canon(p))
223
}
224
225
/// Rename program `from` to `to`. On Windows a running program cannot
226
/// rename its own file (other programs can), so a copy of it, run as
227
/// `flow __rename`, does it then.
228
fn rename_exe(from: &Path, to: &Path) -> Result<()> {
229
    if cfg!(windows) && is_self(from) {
230
        let helper = beside(from, "flow.rename");
231
        std::fs::copy(from, &helper).with_context(|| format!("copying {} to rename it", from.display()))?;
232
        let out = std::process::Command::new(&helper).arg("__rename").arg(from).arg(to).output();
233
        let _ = std::fs::remove_file(&helper);
234
        let out = out.with_context(|| format!("starting {}", helper.display()))?;
235
        if !out.status.success() {
236
            bail!("{}", String::from_utf8_lossy(&out.stderr).trim());
237
        }
238
        return Ok(());
239
    }
240
    Ok(crate::portable::rename(from, to)?)
85275e1e Andy McCall ↶ 241
}
242
243
/// Put `staged` in place of `exe`, keeping `exe` as [`PREV`] beside it.
244
/// `exe` is never missing: it is copied aside, then replaced in one rename.
494a1b9a Andy McCall ↶ 245
/// On Windows, where a running program cannot be replaced but can be
246
/// renamed, `exe` is renamed aside and `staged` into its place.
85275e1e Andy McCall ↶ 247
pub fn swap(exe: &Path, staged: &Path) -> Result<()> {
494a1b9a Andy McCall ↶ 248
    let prev = beside(exe, PREV);
249
    if cfg!(windows) {
250
        let _ = std::fs::remove_file(&prev);
251
        rename_exe(exe, &prev).with_context(|| format!("keeping {} as {}", exe.display(), prev.display()))?;
252
        if let Err(e) = crate::portable::rename(staged, exe) {
253
            let _ = crate::portable::rename(&prev, exe);
254
            return Err(e).with_context(|| format!("putting {} in place of {}", staged.display(), exe.display()));
255
        }
256
        return Ok(());
257
    }
85275e1e Andy McCall ↶ 258
    let _ = std::fs::remove_file(&prev);
259
    if std::fs::hard_link(exe, &prev).is_err() {
260
        std::fs::copy(exe, &prev).with_context(|| format!("keeping {} as {}", exe.display(), prev.display()))?;
261
    }
262
    std::fs::rename(staged, exe).with_context(|| format!("putting {} in place of {}", staged.display(), exe.display()))?;
263
    Ok(())
264
}
265
266
/// Put [`PREV`] back in place of `exe`.
267
pub fn restore(exe: &Path) -> Result<()> {
494a1b9a Andy McCall ↶ 268
    let prev = beside(exe, PREV);
85275e1e Andy McCall ↶ 269
    if !prev.is_file() {
270
        bail!("{} is not there to go back to", prev.display());
494a1b9a Andy McCall ↶ 271
    }
272
    if cfg!(windows) {
273
        // The failed build may be the one running: rename it aside first.
274
        let failed = beside(exe, "flow.failed");
275
        let _ = std::fs::remove_file(&failed);
276
        rename_exe(exe, &failed).with_context(|| format!("moving {} aside", exe.display()))?;
277
        return crate::portable::rename(&prev, exe).with_context(|| format!("putting {} back", prev.display()));
85275e1e Andy McCall ↶ 278
    }
279
    std::fs::rename(&prev, exe).with_context(|| format!("putting {} back", prev.display()))?;
280
    Ok(())
281
}
282
283
/// Check `file`, stage it beside `exe`, and unless `dry_run` swap it in and
284
/// tell the running runner (if any) to leave within `notice`, finishing
285
/// what fits and handing back the rest, so its unit starts the new build.
286
pub fn install(home: &crate::home::Home, exe: &Path, file: &Path, notice: Duration, dry_run: bool, allow_older: bool) -> Result<Installed> {
287
    if home.config.runner.as_ref().and_then(|r| r.update.as_ref()).is_some_and(|u| !u.allow) {
288
        bail!("flow.toml says [runner.update] allow = false: this runner takes no updates");
289
    }
290
    if let Some(p) = pending(&home.root) {
291
        bail!("the update from {} to {} is not proven yet; wait for it (or for its rollback)", p.from, p.to);
292
    }
293
    let from = env!("CARGO_PKG_VERSION").to_string();
494a1b9a Andy McCall ↶ 294
    let staged = beside(exe, NEW);
85275e1e Andy McCall ↶ 295
    let _ = std::fs::remove_file(&staged);
296
    std::fs::copy(file, &staged).with_context(|| format!("staging {} as {}", file.display(), staged.display()))?;
3554afef Andy McCall ↶ 297
    crate::portable::set_mode(&staged, 0o755)?;
85275e1e Andy McCall ↶ 298
    let checked = check(&staged, &home.root).and_then(|to| {
299
        if older(&to, &from) && !allow_older {
300
            bail!("flow {to} is older than this runner's {from} (--allow-older to go back)");
301
        }
302
        Ok(to)
303
    });
304
    let to = match checked {
305
        Ok(v) => v,
306
        Err(e) => {
307
            let _ = std::fs::remove_file(&staged);
308
            return Err(e);
309
        }
310
    };
311
    if dry_run {
312
        let _ = std::fs::remove_file(&staged);
313
        return Ok(Installed { from, to, exe: exe.to_path_buf(), told: None });
494a1b9a Andy McCall ↶ 314
    }
315
    if let Err(e) = swap(exe, &staged) {
316
        let _ = std::fs::remove_file(&staged);
317
        return Err(e);
85275e1e Andy McCall ↶ 318
    }
319
    let p =
320
        Pending { from: from.clone(), to: to.clone(), exe: exe.to_path_buf(), at_ms: now_ms(), notice_secs: notice.as_secs(), starts: 0 };
321
    save(&home.root, &p)?;
322
    log(&home.root, "installed", &from, &to, "flow runner update --from");
494a1b9a Andy McCall ↶ 323
    let told = running(&home.root).filter(|pid| cfg!(not(unix)) || tell(*pid));
85275e1e Andy McCall ↶ 324
    Ok(Installed { from, to, exe: exe.to_path_buf(), told })
3554afef Andy McCall ↶ 325
}
326
327
/// Ask runner `pid` to leave for the new build (SIGUSR1).
328
#[cfg(unix)]
329
fn tell(pid: u32) -> bool {
330
    unsafe { libc::kill(pid as libc::pid_t, libc::SIGUSR1) == 0 }
331
}
332
494a1b9a Andy McCall ↶ 333
/// On Windows: a request in the runner's home, which its loop reads.
3554afef Andy McCall ↶ 334
#[cfg(not(unix))]
335
fn tell(_pid: u32) -> bool {
336
    false
85275e1e Andy McCall ↶ 337
}
338
4dd392d1 Andy McCall ↶ 339
/// In the runner's home: drain (claim nothing more, finish what it holds,
340
/// then stop), as SIGTERM asks on Linux: `flow runner drain`, for Windows.
341
const DRAIN: &str = "drain.request";
342
343
/// Ask the runner of `home` to drain; whether one is running to ask.
344
pub fn ask_to_drain(home: &Path) -> std::io::Result<bool> {
345
    std::fs::write(home.join(DRAIN), "")?;
346
    Ok(home.join(PID).is_file())
347
}
348
349
/// Whether the runner has been asked, by file, to drain (once).
350
pub fn drain_requested(home: &Path) -> bool {
351
    std::fs::remove_file(home.join(DRAIN)).is_ok()
352
}
353
494a1b9a Andy McCall ↶ 354
/// Whether the runner has been asked, by file, to leave for a new build
355
/// (and forget the asking).
356
pub fn requested(home: &Path) -> bool {
357
    std::fs::remove_file(home.join(REQUEST)).is_ok()
358
}
359
85275e1e Andy McCall ↶ 360
/// The running runner's process id, from its pid file, if that process is
361
/// still a `flow runner`: a stale file's pid may be another program's now,
362
/// and SIGUSR1 would end it. Builds before #85 write no pid file (and do
363
/// not listen for SIGUSR1), so they are never told.
494a1b9a Andy McCall ↶ 364
#[cfg(unix)]
85275e1e Andy McCall ↶ 365
fn running(home: &Path) -> Option<u32> {
366
    let pid: u32 = std::fs::read_to_string(home.join(PID)).ok()?.trim().parse().ok()?;
367
    let cmdline = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
368
    is_runner(&cmdline).then_some(pid)
494a1b9a Andy McCall ↶ 369
}
370
371
/// On Windows: a runner of this home has a pid file, and is asked by a
372
/// request file it reads (a stale one is cleared when a runner starts).
373
#[cfg(not(unix))]
374
fn running(home: &Path) -> Option<u32> {
375
    let pid: u32 = std::fs::read_to_string(home.join(PID)).ok()?.trim().parse().ok()?;
376
    std::fs::write(home.join(REQUEST), pid.to_string()).ok()?;
377
    Some(pid)
85275e1e Andy McCall ↶ 378
}
379
380
/// Whether a NUL-separated command line is `flow … runner` with no subcommand.
494a1b9a Andy McCall ↶ 381
#[cfg_attr(not(unix), allow(dead_code))]
85275e1e Andy McCall ↶ 382
fn is_runner(cmdline: &[u8]) -> bool {
383
    let args: Vec<&[u8]> = cmdline.split(|b| *b == 0).filter(|a| !a.is_empty()).collect();
384
    let Some((argv0, rest)) = args.split_first() else { return false };
385
    let name = argv0.rsplit(|b| *b == b'/').next().unwrap_or(argv0);
386
    name.starts_with(b"flow") && rest.last() == Some(&&b"runner"[..])
387
}
388
389
/// Note this process as the home's runner, for `flow runner update` to tell.
390
pub fn note_runner(home: &Path) {
494a1b9a Andy McCall ↶ 391
    let _ = std::fs::remove_file(home.join(REQUEST));
4dd392d1 Andy McCall ↶ 392
    let _ = std::fs::remove_file(home.join(DRAIN));
85275e1e Andy McCall ↶ 393
    let _ = std::fs::write(home.join(PID), std::process::id().to_string());
394
}
395
396
/// The runner stopped: forget its pid.
397
pub fn forget_runner(home: &Path) {
398
    if std::fs::read_to_string(home.join(PID)).is_ok_and(|p| p.trim() == std::process::id().to_string()) {
399
        let _ = std::fs::remove_file(home.join(PID));
400
    }
401
}
402
403
/// What a runner starting up should do about an update.
404
#[derive(Debug, PartialEq, Eq)]
405
pub enum Start {
406
    /// No update waits to be proven.
407
    Settled,
408
    /// The new build is on trial: prove it within [`PROVE_WITHIN`].
409
    Trial(Pending),
410
    /// It failed too often; the previous build is back and should be run.
411
    RolledBack(Pending),
412
}
413
414
/// Count this start of an unproven build; after [`MAX_STARTS`], put the
415
/// previous build back.
416
pub fn on_start(home: &Path) -> Result<Start> {
417
    let Some(mut p) = pending(home) else { return Ok(Start::Settled) };
418
    p.starts += 1;
419
    if p.starts > MAX_STARTS {
420
        roll_back(home, &p, &format!("{MAX_STARTS} starts without reaching the hub"))?;
421
        return Ok(Start::RolledBack(p));
422
    }
423
    save(home, &p)?;
424
    Ok(Start::Trial(p))
425
}
426
cedd1234 Andy McCall ↶ 427
/// In the runner's home: what to tell the hub once it answers.
428
const NOTE: &str = "update.note";
429
430
/// Keep `note` to tell the hub (a proof, a rollback) once it answers.
431
fn note(home: &Path, note: &str) {
432
    let _ = std::fs::write(home.join(NOTE), note);
433
}
434
435
/// What there is to tell the hub, once.
436
pub fn take_note(home: &Path) -> Option<String> {
437
    let n = std::fs::read_to_string(home.join(NOTE)).ok()?;
438
    let _ = std::fs::remove_file(home.join(NOTE));
439
    Some(n).filter(|n| !n.trim().is_empty())
440
}
441
85275e1e Andy McCall ↶ 442
/// The new build reached the hub: it stays.
443
pub fn proven(home: &Path) {
444
    if let Some(p) = pending(home) {
445
        let _ = std::fs::remove_file(home.join(PENDING));
446
        log(home, "proven", &p.from, &p.to, "reached the hub");
cedd1234 Andy McCall ↶ 447
        note(home, &format!("runs flow {} (was {}); proven", p.to, p.from));
494a1b9a Andy McCall ↶ 448
        eprintln!("flow runner: flow {} proven (it reached the hub); flow {} stays as {}", p.to, p.from, beside(&p.exe, PREV).display());
85275e1e Andy McCall ↶ 449
    }
450
}
451
452
/// Put the previous build back and forget the update.
453
pub fn roll_back(home: &Path, p: &Pending, why: &str) -> Result<()> {
454
    restore(&p.exe)?;
455
    let _ = std::fs::remove_file(home.join(PENDING));
456
    log(home, "rolled back", &p.to, &p.from, why);
cedd1234 Andy McCall ↶ 457
    note(home, &format!("rolled back from flow {} to {}: {why}", p.to, p.from));
85275e1e Andy McCall ↶ 458
    eprintln!("flow runner: rolled back from flow {} to {} ({why})", p.to, p.from);
459
    Ok(())
460
}
461
462
#[cfg(test)]
463
mod tests {
21a3ea4a Andy McCall ↶ 464
465
    #[test]
466
    fn a_build_from_the_swarm_is_copied_only_when_it_is_the_one_attested() {
467
        let dir = tempfile::tempdir().unwrap();
468
        let (from, to) = (dir.path().join("cas.ext4"), dir.path().join("flow-0.5.2"));
469
        std::fs::write(&from, b"flow build").unwrap();
470
        let hash = blake3::hash(b"flow build").to_hex().to_string();
471
        assert!(copy_checked(&from, &to, 9, &hash).is_err(), "the wrong size");
472
        assert!(copy_checked(&from, &to, 10, &"0".repeat(64)).is_err(), "the wrong hash");
473
        assert!(!to.exists() && !to.with_extension("partial").exists(), "nothing left behind");
474
        copy_checked(&from, &to, 10, &hash).unwrap();
475
        assert_eq!(std::fs::read(&to).unwrap(), b"flow build");
476
    }
85275e1e Andy McCall ↶ 477
    use super::*;
478
479
    #[test]
480
    fn versions_are_read_and_compared() {
481
        assert_eq!(parse_version("flow 0.3.1 (local build)\n").as_deref(), Some("0.3.1"));
482
        assert_eq!(parse_version("flow 0.10.0"), Some("0.10.0".into()));
483
        assert_eq!(parse_version("oxy 0.6.0"), None);
484
        assert_eq!(parse_version("flow banana"), None);
485
        assert!(older("0.3.1", "0.10.0"), "by number, not by text");
486
        assert!(!older("0.3.1", "0.3.1"));
487
        assert!(older("0.3", "0.3.1"));
488
    }
489
490
    #[test]
491
    fn only_a_build_for_this_machine_passes() {
492
        let me = std::fs::read(std::env::current_exe().unwrap()).unwrap();
493
        check_target(&me[..64]).unwrap();
494
        assert!(check_target(b"#!/bin/sh\necho flow 9.9.9\n").unwrap_err().to_string().contains("not an ELF"));
495
        let mut other = me[..64].to_vec();
496
        other[18..20].copy_from_slice(&(if std::env::consts::ARCH == "riscv64" { 62u16 } else { 243u16 }).to_le_bytes());
497
        assert!(check_target(&other).unwrap_err().to_string().contains("another machine"));
498
    }
499
500
    #[test]
501
    fn a_swap_keeps_the_old_build_and_three_failed_starts_bring_it_back() {
502
        let dir = tempfile::tempdir().unwrap();
503
        let (bin, home) = (dir.path().join("bin"), dir.path().join("home"));
504
        std::fs::create_dir_all(&bin).unwrap();
505
        std::fs::create_dir_all(&home).unwrap();
506
        let exe = bin.join("flow");
507
        std::fs::write(&exe, "old").unwrap();
508
        std::fs::write(bin.join(NEW), "new").unwrap();
509
        swap(&exe, &bin.join(NEW)).unwrap();
510
        assert_eq!(std::fs::read_to_string(&exe).unwrap(), "new");
511
        assert_eq!(std::fs::read_to_string(bin.join(PREV)).unwrap(), "old");
512
        assert!(!bin.join(NEW).exists());
513
514
        assert_eq!(on_start(&home).unwrap(), Start::Settled, "no update, nothing to do");
515
        let p = Pending { from: "0.3.1".into(), to: "0.3.2".into(), exe: exe.clone(), at_ms: 1, notice_secs: 600, starts: 0 };
516
        save(&home, &p).unwrap();
517
        for n in 1..=MAX_STARTS {
518
            assert_eq!(on_start(&home).unwrap(), Start::Trial(Pending { starts: n, ..p.clone() }));
519
        }
520
        assert!(matches!(on_start(&home).unwrap(), Start::RolledBack(_)));
521
        assert_eq!(std::fs::read_to_string(&exe).unwrap(), "old", "the previous build is back");
522
        assert!(pending(&home).is_none());
523
        let log = std::fs::read_to_string(home.join(LOG)).unwrap();
524
        assert!(log.contains("rolled back") && log.contains("0.3.2"), "{log}");
494a1b9a Andy McCall ↶ 525
    }
526
527
    #[test]
528
    fn a_windows_build_is_checked_as_pe_and_named_beside_with_its_extension() {
529
        let mut pe = vec![0u8; 0x100];
530
        pe[..2].copy_from_slice(b"MZ");
531
        pe[0x3c..0x40].copy_from_slice(&0x80u32.to_le_bytes());
532
        pe[0x80..0x84].copy_from_slice(b"PE\0\0");
533
        pe[0x84..0x86].copy_from_slice(&0x8664u16.to_le_bytes());
534
        if std::env::consts::ARCH == "x86_64" {
535
            check_pe(&pe).unwrap();
536
        }
537
        pe[0x84..0x86].copy_from_slice(&0x01c4u16.to_le_bytes());
538
        assert!(check_pe(&pe).is_err(), "another machine");
539
        assert!(check_pe(b"\x7fELF and more bytes than enough for a header, really, quite a few more")
540
            .unwrap_err()
541
            .to_string()
542
            .contains("not a Windows"));
543
        assert_eq!(beside(Path::new("C:/flow/flow.exe"), PREV), Path::new("C:/flow/flow.prev.exe"));
544
        assert_eq!(beside(Path::new("/usr/local/bin/flow"), NEW), Path::new("/usr/local/bin/flow.new"));
4dd392d1 Andy McCall ↶ 545
    }
546
547
    #[test]
548
    fn a_drain_is_asked_by_file_once() {
549
        let dir = tempfile::tempdir().unwrap();
550
        assert!(!ask_to_drain(dir.path()).unwrap(), "no runner running");
551
        assert!(drain_requested(dir.path()) && !drain_requested(dir.path()));
552
        note_runner(dir.path());
553
        assert!(ask_to_drain(dir.path()).unwrap());
554
        note_runner(dir.path());
555
        assert!(!drain_requested(dir.path()), "a starting runner clears a stale request");
494a1b9a Andy McCall ↶ 556
    }
557
558
    #[test]
559
    fn a_request_file_asks_once() {
560
        let dir = tempfile::tempdir().unwrap();
561
        assert!(!requested(dir.path()));
562
        std::fs::write(dir.path().join(REQUEST), "1").unwrap();
563
        assert!(requested(dir.path()) && !requested(dir.path()), "read once");
564
        std::fs::write(dir.path().join(REQUEST), "1").unwrap();
565
        note_runner(dir.path());
566
        assert!(!requested(dir.path()), "a starting runner clears a stale request");
85275e1e Andy McCall ↶ 567
    }
568
569
    #[test]
570
    fn only_a_running_runner_is_told() {
571
        assert!(is_runner(b"/usr/local/bin/flow\0runner\0"));
572
        assert!(is_runner(b"flow\0--home\0/srv/flow\0runner\0"));
573
        assert!(!is_runner(b"flow\0runner\0update\0--from\0x\0"), "not the update command itself");
574
        assert!(!is_runner(b"/usr/bin/vim\0runner\0"));
575
        assert!(!is_runner(b""));
576
        let dir = tempfile::tempdir().unwrap();
577
        std::fs::write(dir.path().join(PID), std::process::id().to_string()).unwrap();
578
        assert_eq!(running(dir.path()), None, "this test is not a flow runner");
579
    }
580
581
    #[test]
582
    fn a_proven_build_stays() {
583
        let dir = tempfile::tempdir().unwrap();
584
        let exe = dir.path().join("flow");
585
        std::fs::write(&exe, "new").unwrap();
586
        let p = Pending { from: "0.3.1".into(), to: "0.3.2".into(), exe: exe.clone(), at_ms: 1, notice_secs: 0, starts: 1 };
587
        save(dir.path(), &p).unwrap();
588
        proven(dir.path());
589
        assert!(pending(dir.path()).is_none());
590
        assert_eq!(on_start(dir.path()).unwrap(), Start::Settled);
591
        assert!(std::fs::read_to_string(dir.path().join(LOG)).unwrap().contains("proven"));
592
    }
593
}