A Windows runner updates itself 494a1b9a8ac2
PE check; flow.exe renamed aside and flow.new.exe into place with MoveFileExW (retrying while a scanner holds the file); a copy of flow renames its own running file (flow __rename); the runner is told by an update.request file and exits 75 having left, so a Task Scheduler task restarting on failure starts the new build. Tried in the Windows VM: install, leave, exit 75, restart, proven. Spec 11.5.3. refs #83, #85
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-26 03:20 UTC
- Signature
- ◈ sealed by Andy McCall
- Hash
494a1b9a8ac268416c2fb9b53cf0277143685bdcacd64a00643abcec506b549c- Depends on
- 28cc603f737d 3007d8f011fa 3554afefb6aa 5cf1e8aefbb6 6f66e397539c 731413202557 85275e1ef3b7 a083cc802892 c3db750ed96e cedd12342afa e0918b887850 ea56c56eaf90
- Depended on by
- 8aa45fa6ece5 f4dfd742b30d 4dd392d16454
- In bookmarks
- main
- In tags
- v0.4.0 v0.5.0 v0.5.1 v0.5.2 v0.5.3
- Edits
- 38 atoms, 0 tree operations, 8 files
Browse the tree at this change
modified CHANGELOG.md +1 −0
| … | 3 unchanged lines |
||
| 4 | 4 | ||
| 5 | 5 | ## Unreleased |
|
| 6 | 6 | ||
| 7 | + | - **A Windows runner updates itself (#83, #85):** `flow runner update` on Windows checks a PE build, renames `flow.exe` to `flow.prev.exe` and `flow.new.exe` into its place with `MoveFileExW` (retrying while a virus scanner holds a new file; a running program cannot rename its own file, so a copy of it, `flow __rename`, does), tells the running runner through an `update.request` file in its home (no SIGUSR1 there), and the runner leaves and exits 75 so a Task Scheduler task set to restart on failure starts the new build, which proves itself by reaching the hub. Tried in the Windows VM: install, leave, exit 75, restart, proven. A rollback runs the restored build and ends with it. Spec §11.5.3. |
|
| 7 | 8 | - **`flow` builds for Windows (#83, step 2 begun):** `cargo build --target x86_64-pc-windows-gnu` gives a `flow.exe` that offers the WASM tier: the syscall tier, the guest helpers and the egress proxy are Linux's only (a Windows build says the syscall tier "runs only on Linux"), cache pruning by reads stays on Linux, and file modes, links and sizes go through a small `portable` module (modes are not kept on Windows). Its claims report free disk (`GetDiskFreeSpaceExW`); load and kernel are not measured there yet, and a Windows runner does not update itself. `dist` builds `flow-windows-x86_64.exe` too. Nothing changes on Linux or Android. Tried in the Windows VM on Earth: `flow.exe tiers` offers wasm, a local `flow run` of a 120-step WASM job passes, and registered as runner `windows-1` (label `windows`) it ran andymccall/windows-runner-demo's job from the hub (run #1). Helpers start from the running binary's path on Windows (`/proc/self/exe` on Linux and Android). |
|
| 8 | 9 | - **The syscall tier's loopback reaches TCP (#73):** with `network "loopback"`, a job may open IPv4 and IPv6 sockets, which in its own network namespace reach only its `lo`, so a server it starts on 127.0.0.1 answers it (a conformance suite over an emulator's HTTP control port, which skipped every test on narvi-1). With `network "isolated"` sockets other than `AF_UNIX` are still refused. |
|
| 9 | 10 | - **Runner builds in releases (oxygenhub#322):** a `dist` job (`scripts/dist.sh`, on a change to `Cargo.toml` or the script) builds `flow` stripped for Linux x86_64, aarch64 and riscv64 (glibc 2.28 and later) as `flow-linux-ARCH`, named by the architecture a runner reports, with `SHA256SUMS`: the files a release carries for runners that update themselves. |
|
| … | 100 unchanged lines |
modified README.md +4 −2
| … | 60 unchanged lines |
||
| 61 | 61 | for a runner of your own repositories). A pipeline sends it work with a |
|
| 62 | 62 | `runtime "wasm"` sandbox and `runs-on "windows"`. To keep it running, a |
|
| 63 | 63 | Task Scheduler task at start-up with that command (restarting on failure) |
|
| 64 | - | does. What a Windows runner does not do yet: report load and kernel, update |
|
| 65 | - | itself, or drain when Windows stops it (oxygenflow#83). |
|
| 64 | + | does; set it to restart on failure, as a runner that leaves for a new build |
|
| 65 | + | exits 75 (`flow runner update`, spec §11.5.3). What a Windows runner does |
|
| 66 | + | not do yet: report load and kernel, or drain when Windows stops it |
|
| 67 | + | (oxygenflow#83). |
|
| 66 | 68 | ||
| 67 | 69 | ## Commands |
|
| 68 | 70 | ||
| … | 71 unchanged lines |
modified crates/flow/Cargo.toml +2 −1
| … | 33 unchanged lines |
||
| 34 | 34 | wasmtime = { version = "=49.0.4", default-features = false, features = ["cranelift", "runtime", "std", "parallel-compilation"] } |
|
| 35 | 35 | wasmtime-wasi = { version = "=49.0.4", default-features = false, features = ["p1"] } |
|
| 36 | 36 | ||
| 37 | - | # A Windows runner's free disk (oxygenflow#83); already in the tree through wasmtime. |
|
| 37 | + | # A Windows runner's free disk and renaming a running flow.exe (oxygenflow#83); |
|
| 38 | + | # already in the tree through wasmtime. |
|
| 38 | 39 | [target.'cfg(windows)'.dependencies] |
|
| 39 | 40 | windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem"] } |
|
| 40 | 41 | ||
| … | 4 unchanged lines |
modified crates/flow/src/main.rs +15 −1
| … | 127 unchanged lines |
||
| 128 | 128 | /// Check that this machine can run the syscall tier |
|
| 129 | 129 | #[command(name = "__sandbox-probe", hide = true)] |
|
| 130 | 130 | SandboxProbe, |
|
| 131 | + | /// Rename a program that cannot rename its own file (Windows; #83) |
|
| 132 | + | #[command(name = "__rename", hide = true)] |
|
| 133 | + | Rename { from: PathBuf, to: PathBuf }, |
|
| 131 | 134 | /// The WASM tier's helper, started by the runner for each job |
|
| 132 | 135 | #[command(name = "__wasm", hide = true)] |
|
| 133 | 136 | Wasm { spec: PathBuf }, |
|
| … | 46 unchanged lines |
||
| 180 | 183 | match cli.cmd { |
|
| 181 | 184 | Cmd::Sandbox { spec } => std::process::exit(syscall::sandbox_main(&spec)), |
|
| 182 | 185 | Cmd::SandboxProbe => std::process::exit(syscall::probe_main()), |
|
| 186 | + | Cmd::Rename { from, to } => { |
|
| 187 | + | flow::portable::rename(&from, &to).with_context(|| format!("renaming {} to {}", from.display(), to.display()))?; |
|
| 188 | + | Ok(()) |
|
| 189 | + | } |
|
| 183 | 190 | Cmd::Wasm { spec } => std::process::exit(wasm::wasm_main(&spec)), |
|
| 184 | 191 | Cmd::Check { file } => check(&file.unwrap_or_else(|| PathBuf::from(PIPELINE_PATH))), |
|
| 185 | 192 | Cmd::Hash { file } => { |
|
| … | 129 unchanged lines |
||
| 315 | 322 | let err = std::process::Command::new(&p.exe).args(std::env::args_os().skip(1)).exec(); |
|
| 316 | 323 | bail!("starting flow {} again after the rollback: {err}", p.from); |
|
| 317 | 324 | } |
|
| 325 | + | // Windows has no exec: run the restored build and end with it. |
|
| 318 | 326 | #[cfg(not(unix))] |
|
| 319 | - | bail!("rolled back to flow {}; start the runner again", p.from); |
|
| 327 | + | { |
|
| 328 | + | let status = std::process::Command::new(&p.exe) |
|
| 329 | + | .args(std::env::args_os().skip(1)) |
|
| 330 | + | .status() |
|
| 331 | + | .with_context(|| format!("starting flow {} again after the rollback", p.from))?; |
|
| 332 | + | std::process::exit(status.code().unwrap_or(1)); |
|
| 333 | + | } |
|
| 320 | 334 | } |
|
| 321 | 335 | // The runner's oxy state (identity, user config) stays in its home. |
|
| 322 | 336 | if std::env::var_os("OXY_HOME").is_none() { |
|
| … | 295 unchanged lines |
modified crates/flow/src/portable.rs +32 −0
| … | 103 unchanged lines |
||
| 104 | 104 | std::os::windows::fs::symlink_file(target, link) |
|
| 105 | 105 | } |
|
| 106 | 106 | } |
|
| 107 | + | ||
| 108 | + | /// Rename `from` to `to`, which must not exist. On Windows through |
|
| 109 | + | /// `MoveFileExW`, which renames a program that is running, trying again |
|
| 110 | + | /// while another program (a virus scanner) holds the file. |
|
| 111 | + | pub fn rename(from: &Path, to: &Path) -> io::Result<()> { |
|
| 112 | + | #[cfg(windows)] |
|
| 113 | + | { |
|
| 114 | + | use std::os::windows::ffi::OsStrExt; |
|
| 115 | + | let wide = |p: &Path| p.as_os_str().encode_wide().chain(Some(0)).collect::<Vec<u16>>(); |
|
| 116 | + | let (f, t) = (wide(from), wide(to)); |
|
| 117 | + | // A virus scanner holds a new program for a while after it is |
|
| 118 | + | // written (a sharing violation, 32, or access denied, 5): try again |
|
| 119 | + | // for half a minute. |
|
| 120 | + | let mut tries = 0; |
|
| 121 | + | loop { |
|
| 122 | + | // SAFETY: both are NUL-terminated wide paths. |
|
| 123 | + | if unsafe { windows_sys::Win32::Storage::FileSystem::MoveFileExW(f.as_ptr(), t.as_ptr(), 0) } != 0 { |
|
| 124 | + | return Ok(()); |
|
| 125 | + | } |
|
| 126 | + | let e = io::Error::last_os_error(); |
|
| 127 | + | tries += 1; |
|
| 128 | + | if tries >= 30 || !matches!(e.raw_os_error(), Some(32) | Some(5)) { |
|
| 129 | + | return Err(e); |
|
| 130 | + | } |
|
| 131 | + | std::thread::sleep(std::time::Duration::from_secs(1)); |
|
| 132 | + | } |
|
| 133 | + | } |
|
| 134 | + | #[cfg(not(windows))] |
|
| 135 | + | { |
|
| 136 | + | std::fs::rename(from, to) |
|
| 137 | + | } |
|
| 138 | + | } |
modified crates/flow/src/runner.rs +12 −1
| … | 682 unchanged lines |
||
| 683 | 683 | } |
|
| 684 | 684 | let served = serve_until_drained(home); |
|
| 685 | 685 | crate::update::forget_runner(&home.root); |
|
| 686 | + | // Left for a new build: on Windows, end as a failure, so a Task |
|
| 687 | + | // Scheduler task set to restart on failure starts it (systemd's |
|
| 688 | + | // Restart=always starts it after a clean exit). |
|
| 689 | + | if cfg!(windows) && served.is_ok() && LEFT_FOR_UPDATE.load(std::sync::atomic::Ordering::SeqCst) { |
|
| 690 | + | eprintln!("flow runner: left for the new build; exiting 75 so it is started"); |
|
| 691 | + | std::process::exit(75); |
|
| 692 | + | } |
|
| 686 | 693 | served |
|
| 687 | 694 | } |
|
| 695 | + | ||
| 696 | + | /// Set once the runner leaves for a new build. |
|
| 697 | + | static LEFT_FOR_UPDATE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); |
|
| 688 | 698 | ||
| 689 | 699 | /// Set by SIGUSR1: a new build is in place; leave for it. |
|
| 690 | 700 | static UPDATE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); |
|
| … | 124 unchanged lines |
||
| 815 | 825 | let mut update_asked: Option<Instant> = None; |
|
| 816 | 826 | let mut update_said = String::new(); |
|
| 817 | 827 | loop { |
|
| 818 | - | if UPDATE.swap(false, std::sync::atomic::Ordering::SeqCst) { |
|
| 828 | + | if UPDATE.swap(false, std::sync::atomic::Ordering::SeqCst) || crate::update::requested(&home.root) { |
|
| 819 | 829 | let p = crate::update::pending(&home.root); |
|
| 820 | 830 | let notice = Duration::from_secs(p.as_ref().map_or(0, |p| p.notice_secs)); |
|
| 821 | 831 | let to = p.map_or_else(|| "a new build".to_string(), |p| format!("flow {}", p.to)); |
|
| 822 | 832 | eprintln!("flow runner {name}: updating to {to}: leaving within {} s", notice.as_secs()); |
|
| 833 | + | LEFT_FOR_UPDATE.store(true, std::sync::atomic::Ordering::SeqCst); |
|
| 823 | 834 | leave_within(&format!("updating to {to}"), notice); |
|
| 824 | 835 | } |
|
| 825 | 836 | // Draining (oxygenflow#50): told to stop, claim nothing more and |
|
| … | 1421 unchanged lines |
modified crates/flow/src/update.rs +145 −11
| … | 19 unchanged lines |
||
| 20 | 20 | const LOG: &str = "updates.log"; |
|
| 21 | 21 | /// In the runner's home: the running runner's process id. |
|
| 22 | 22 | const PID: &str = "runner.pid"; |
|
| 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"; |
|
| 23 | 26 | /// Starts of an unproven build after which the previous one comes back. |
|
| 24 | 27 | pub const MAX_STARTS: u32 = 3; |
|
| 25 | 28 | /// How long a new build has to reach the hub. |
|
| … | 35 unchanged lines |
||
| 61 | 64 | parts(a) < parts(b) |
|
| 62 | 65 | } |
|
| 63 | 66 | ||
| 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 | + | ||
| 64 | 102 | /// The ELF machine number of this build's architecture. |
|
| 65 | 103 | fn machine_here() -> Option<u16> { |
|
| 66 | 104 | Some(match std::env::consts::ARCH { |
|
| … | 54 unchanged lines |
||
| 121 | 159 | /// starts and names its version, and it finds the tiers this home offers. |
|
| 122 | 160 | /// Returns its version. |
|
| 123 | 161 | pub fn check(file: &Path, home: &Path) -> Result<String> { |
|
| 124 | - | let mut head = vec![0u8; 64]; |
|
| 162 | + | let mut head = vec![0u8; 4096]; |
|
| 125 | 163 | let n = std::io::Read::read(&mut std::fs::File::open(file).with_context(|| format!("opening {}", file.display()))?, &mut head)?; |
|
| 126 | - | check_target(&head[..n])?; |
|
| 164 | + | if cfg!(windows) { |
|
| 165 | + | check_pe(&head[..n])?; |
|
| 166 | + | } else { |
|
| 167 | + | check_target(&head[..n])?; |
|
| 168 | + | } |
|
| 127 | 169 | let limit = Duration::from_secs(30); |
|
| 128 | 170 | let line = run(file, &["--version"], limit)?; |
|
| 129 | 171 | let version = parse_version(&line).with_context(|| format!("{} does not say it is flow: {:?}", file.display(), line.trim()))?; |
|
| … | 27 unchanged lines |
||
| 157 | 199 | std::fs::write(&tmp, serde_json::to_vec_pretty(p)?)?; |
|
| 158 | 200 | std::fs::rename(&tmp, home.join(PENDING))?; |
|
| 159 | 201 | Ok(()) |
|
| 202 | + | } |
|
| 203 | + | ||
| 204 | + | /// Whether `p` is the program running now. |
|
| 205 | + | fn is_self(p: &Path) -> bool { |
|
| 206 | + | let canon = |p: &Path| std::fs::canonicalize(p).ok(); |
|
| 207 | + | std::env::current_exe().ok().and_then(|me| canon(&me)).is_some_and(|me| Some(me) == canon(p)) |
|
| 208 | + | } |
|
| 209 | + | ||
| 210 | + | /// Rename program `from` to `to`. On Windows a running program cannot |
|
| 211 | + | /// rename its own file (other programs can), so a copy of it, run as |
|
| 212 | + | /// `flow __rename`, does it then. |
|
| 213 | + | fn rename_exe(from: &Path, to: &Path) -> Result<()> { |
|
| 214 | + | if cfg!(windows) && is_self(from) { |
|
| 215 | + | let helper = beside(from, "flow.rename"); |
|
| 216 | + | std::fs::copy(from, &helper).with_context(|| format!("copying {} to rename it", from.display()))?; |
|
| 217 | + | let out = std::process::Command::new(&helper).arg("__rename").arg(from).arg(to).output(); |
|
| 218 | + | let _ = std::fs::remove_file(&helper); |
|
| 219 | + | let out = out.with_context(|| format!("starting {}", helper.display()))?; |
|
| 220 | + | if !out.status.success() { |
|
| 221 | + | bail!("{}", String::from_utf8_lossy(&out.stderr).trim()); |
|
| 222 | + | } |
|
| 223 | + | return Ok(()); |
|
| 224 | + | } |
|
| 225 | + | Ok(crate::portable::rename(from, to)?) |
|
| 160 | 226 | } |
|
| 161 | 227 | ||
| 162 | 228 | /// Put `staged` in place of `exe`, keeping `exe` as [`PREV`] beside it. |
|
| 163 | 229 | /// `exe` is never missing: it is copied aside, then replaced in one rename. |
|
| 230 | + | /// On Windows, where a running program cannot be replaced but can be |
|
| 231 | + | /// renamed, `exe` is renamed aside and `staged` into its place. |
|
| 164 | 232 | pub fn swap(exe: &Path, staged: &Path) -> Result<()> { |
|
| 165 | - | let prev = exe.with_file_name(PREV); |
|
| 233 | + | let prev = beside(exe, PREV); |
|
| 234 | + | if cfg!(windows) { |
|
| 235 | + | let _ = std::fs::remove_file(&prev); |
|
| 236 | + | rename_exe(exe, &prev).with_context(|| format!("keeping {} as {}", exe.display(), prev.display()))?; |
|
| 237 | + | if let Err(e) = crate::portable::rename(staged, exe) { |
|
| 238 | + | let _ = crate::portable::rename(&prev, exe); |
|
| 239 | + | return Err(e).with_context(|| format!("putting {} in place of {}", staged.display(), exe.display())); |
|
| 240 | + | } |
|
| 241 | + | return Ok(()); |
|
| 242 | + | } |
|
| 166 | 243 | let _ = std::fs::remove_file(&prev); |
|
| 167 | 244 | if std::fs::hard_link(exe, &prev).is_err() { |
|
| 168 | 245 | std::fs::copy(exe, &prev).with_context(|| format!("keeping {} as {}", exe.display(), prev.display()))?; |
|
| … | 4 unchanged lines |
||
| 173 | 250 | ||
| 174 | 251 | /// Put [`PREV`] back in place of `exe`. |
|
| 175 | 252 | pub fn restore(exe: &Path) -> Result<()> { |
|
| 176 | - | let prev = exe.with_file_name(PREV); |
|
| 253 | + | let prev = beside(exe, PREV); |
|
| 177 | 254 | if !prev.is_file() { |
|
| 178 | 255 | bail!("{} is not there to go back to", prev.display()); |
|
| 256 | + | } |
|
| 257 | + | if cfg!(windows) { |
|
| 258 | + | // The failed build may be the one running: rename it aside first. |
|
| 259 | + | let failed = beside(exe, "flow.failed"); |
|
| 260 | + | let _ = std::fs::remove_file(&failed); |
|
| 261 | + | rename_exe(exe, &failed).with_context(|| format!("moving {} aside", exe.display()))?; |
|
| 262 | + | return crate::portable::rename(&prev, exe).with_context(|| format!("putting {} back", prev.display())); |
|
| 179 | 263 | } |
|
| 180 | 264 | std::fs::rename(&prev, exe).with_context(|| format!("putting {} back", prev.display()))?; |
|
| 181 | 265 | Ok(()) |
|
| … | 3 unchanged lines |
||
| 185 | 269 | /// tell the running runner (if any) to leave within `notice`, finishing |
|
| 186 | 270 | /// what fits and handing back the rest, so its unit starts the new build. |
|
| 187 | 271 | pub fn install(home: &crate::home::Home, exe: &Path, file: &Path, notice: Duration, dry_run: bool, allow_older: bool) -> Result<Installed> { |
|
| 188 | - | if !cfg!(unix) { |
|
| 189 | - | bail!("runners update themselves on Linux only, so far"); |
|
| 190 | - | } |
|
| 191 | 272 | if home.config.runner.as_ref().and_then(|r| r.update.as_ref()).is_some_and(|u| !u.allow) { |
|
| 192 | 273 | bail!("flow.toml says [runner.update] allow = false: this runner takes no updates"); |
|
| 193 | 274 | } |
|
| … | 1 unchanged lines |
||
| 195 | 276 | bail!("the update from {} to {} is not proven yet; wait for it (or for its rollback)", p.from, p.to); |
|
| 196 | 277 | } |
|
| 197 | 278 | let from = env!("CARGO_PKG_VERSION").to_string(); |
|
| 198 | - | let staged = exe.with_file_name(NEW); |
|
| 279 | + | let staged = beside(exe, NEW); |
|
| 199 | 280 | let _ = std::fs::remove_file(&staged); |
|
| 200 | 281 | std::fs::copy(file, &staged).with_context(|| format!("staging {} as {}", file.display(), staged.display()))?; |
|
| 201 | 282 | crate::portable::set_mode(&staged, 0o755)?; |
|
| … | 13 unchanged lines |
||
| 215 | 296 | if dry_run { |
|
| 216 | 297 | let _ = std::fs::remove_file(&staged); |
|
| 217 | 298 | return Ok(Installed { from, to, exe: exe.to_path_buf(), told: None }); |
|
| 299 | + | } |
|
| 300 | + | if let Err(e) = swap(exe, &staged) { |
|
| 301 | + | let _ = std::fs::remove_file(&staged); |
|
| 302 | + | return Err(e); |
|
| 218 | 303 | } |
|
| 219 | - | swap(exe, &staged)?; |
|
| 220 | 304 | let p = |
|
| 221 | 305 | Pending { from: from.clone(), to: to.clone(), exe: exe.to_path_buf(), at_ms: now_ms(), notice_secs: notice.as_secs(), starts: 0 }; |
|
| 222 | 306 | save(&home.root, &p)?; |
|
| 223 | 307 | log(&home.root, "installed", &from, &to, "flow runner update --from"); |
|
| 224 | - | let told = running(&home.root).filter(|pid| tell(*pid)); |
|
| 308 | + | let told = running(&home.root).filter(|pid| cfg!(not(unix)) || tell(*pid)); |
|
| 225 | 309 | Ok(Installed { from, to, exe: exe.to_path_buf(), told }) |
|
| 226 | 310 | } |
|
| 227 | 311 | ||
| … | 3 unchanged lines |
||
| 231 | 315 | unsafe { libc::kill(pid as libc::pid_t, libc::SIGUSR1) == 0 } |
|
| 232 | 316 | } |
|
| 233 | 317 | ||
| 318 | + | /// On Windows: a request in the runner's home, which its loop reads. |
|
| 234 | 319 | #[cfg(not(unix))] |
|
| 235 | 320 | fn tell(_pid: u32) -> bool { |
|
| 236 | 321 | false |
|
| 237 | 322 | } |
|
| 238 | 323 | ||
| 324 | + | /// Whether the runner has been asked, by file, to leave for a new build |
|
| 325 | + | /// (and forget the asking). |
|
| 326 | + | pub fn requested(home: &Path) -> bool { |
|
| 327 | + | std::fs::remove_file(home.join(REQUEST)).is_ok() |
|
| 328 | + | } |
|
| 329 | + | ||
| 239 | 330 | /// The running runner's process id, from its pid file, if that process is |
|
| 240 | 331 | /// still a `flow runner`: a stale file's pid may be another program's now, |
|
| 241 | 332 | /// and SIGUSR1 would end it. Builds before #85 write no pid file (and do |
|
| 242 | 333 | /// not listen for SIGUSR1), so they are never told. |
|
| 334 | + | #[cfg(unix)] |
|
| 243 | 335 | fn running(home: &Path) -> Option<u32> { |
|
| 244 | 336 | let pid: u32 = std::fs::read_to_string(home.join(PID)).ok()?.trim().parse().ok()?; |
|
| 245 | 337 | let cmdline = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?; |
|
| 246 | 338 | is_runner(&cmdline).then_some(pid) |
|
| 339 | + | } |
|
| 340 | + | ||
| 341 | + | /// On Windows: a runner of this home has a pid file, and is asked by a |
|
| 342 | + | /// request file it reads (a stale one is cleared when a runner starts). |
|
| 343 | + | #[cfg(not(unix))] |
|
| 344 | + | fn running(home: &Path) -> Option<u32> { |
|
| 345 | + | let pid: u32 = std::fs::read_to_string(home.join(PID)).ok()?.trim().parse().ok()?; |
|
| 346 | + | std::fs::write(home.join(REQUEST), pid.to_string()).ok()?; |
|
| 347 | + | Some(pid) |
|
| 247 | 348 | } |
|
| 248 | 349 | ||
| 249 | 350 | /// Whether a NUL-separated command line is `flow … runner` with no subcommand. |
|
| 351 | + | #[cfg_attr(not(unix), allow(dead_code))] |
|
| 250 | 352 | fn is_runner(cmdline: &[u8]) -> bool { |
|
| 251 | 353 | let args: Vec<&[u8]> = cmdline.split(|b| *b == 0).filter(|a| !a.is_empty()).collect(); |
|
| 252 | 354 | let Some((argv0, rest)) = args.split_first() else { return false }; |
|
| … | 3 unchanged lines |
||
| 256 | 358 | ||
| 257 | 359 | /// Note this process as the home's runner, for `flow runner update` to tell. |
|
| 258 | 360 | pub fn note_runner(home: &Path) { |
|
| 361 | + | let _ = std::fs::remove_file(home.join(REQUEST)); |
|
| 259 | 362 | let _ = std::fs::write(home.join(PID), std::process::id().to_string()); |
|
| 260 | 363 | } |
|
| 261 | 364 | ||
| … | 49 unchanged lines |
||
| 311 | 414 | let _ = std::fs::remove_file(home.join(PENDING)); |
|
| 312 | 415 | log(home, "proven", &p.from, &p.to, "reached the hub"); |
|
| 313 | 416 | note(home, &format!("runs flow {} (was {}); proven", p.to, p.from)); |
|
| 314 | - | eprintln!("flow runner: flow {} proven (it reached the hub); flow {} stays as {PREV}", p.to, p.from); |
|
| 417 | + | eprintln!("flow runner: flow {} proven (it reached the hub); flow {} stays as {}", p.to, p.from, beside(&p.exe, PREV).display()); |
|
| 315 | 418 | } |
|
| 316 | 419 | } |
|
| 317 | 420 | ||
| … | 57 unchanged lines |
||
| 375 | 478 | assert!(pending(&home).is_none()); |
|
| 376 | 479 | let log = std::fs::read_to_string(home.join(LOG)).unwrap(); |
|
| 377 | 480 | assert!(log.contains("rolled back") && log.contains("0.3.2"), "{log}"); |
|
| 481 | + | } |
|
| 482 | + | ||
| 483 | + | #[test] |
|
| 484 | + | fn a_windows_build_is_checked_as_pe_and_named_beside_with_its_extension() { |
|
| 485 | + | let mut pe = vec![0u8; 0x100]; |
|
| 486 | + | pe[..2].copy_from_slice(b"MZ"); |
|
| 487 | + | pe[0x3c..0x40].copy_from_slice(&0x80u32.to_le_bytes()); |
|
| 488 | + | pe[0x80..0x84].copy_from_slice(b"PE\0\0"); |
|
| 489 | + | pe[0x84..0x86].copy_from_slice(&0x8664u16.to_le_bytes()); |
|
| 490 | + | if std::env::consts::ARCH == "x86_64" { |
|
| 491 | + | check_pe(&pe).unwrap(); |
|
| 492 | + | } |
|
| 493 | + | pe[0x84..0x86].copy_from_slice(&0x01c4u16.to_le_bytes()); |
|
| 494 | + | assert!(check_pe(&pe).is_err(), "another machine"); |
|
| 495 | + | assert!(check_pe(b"\x7fELF and more bytes than enough for a header, really, quite a few more") |
|
| 496 | + | .unwrap_err() |
|
| 497 | + | .to_string() |
|
| 498 | + | .contains("not a Windows")); |
|
| 499 | + | assert_eq!(beside(Path::new("C:/flow/flow.exe"), PREV), Path::new("C:/flow/flow.prev.exe")); |
|
| 500 | + | assert_eq!(beside(Path::new("/usr/local/bin/flow"), NEW), Path::new("/usr/local/bin/flow.new")); |
|
| 501 | + | } |
|
| 502 | + | ||
| 503 | + | #[test] |
|
| 504 | + | fn a_request_file_asks_once() { |
|
| 505 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 506 | + | assert!(!requested(dir.path())); |
|
| 507 | + | std::fs::write(dir.path().join(REQUEST), "1").unwrap(); |
|
| 508 | + | assert!(requested(dir.path()) && !requested(dir.path()), "read once"); |
|
| 509 | + | std::fs::write(dir.path().join(REQUEST), "1").unwrap(); |
|
| 510 | + | note_runner(dir.path()); |
|
| 511 | + | assert!(!requested(dir.path()), "a starting runner clears a stale request"); |
|
| 378 | 512 | } |
|
| 379 | 513 | ||
| 380 | 514 | #[test] |
|
| … | 24 unchanged lines |
modified docs/spec.md +9 −0
| … | 882 unchanged lines |
||
| 883 | 883 | on the fourth start without that answer, or after ten minutes running |
|
| 884 | 884 | without it, `flow.prev` is put back (and on a start, run in its place). |
|
| 885 | 885 | `updates.log` in the home records every install, proof and rollback. |
|
| 886 | + | 6. **On Windows** (oxygenflow#83) the same steps run with Windows' means: |
|
| 887 | + | the build is checked as a PE executable for this machine; `flow.exe` |
|
| 888 | + | is renamed to `flow.prev.exe` and `flow.new.exe` into its place (a |
|
| 889 | + | running program can be renamed but not replaced, and cannot rename |
|
| 890 | + | its own file, so a copy of it, `flow __rename`, does that); the runner |
|
| 891 | + | is told by an `update.request` file in its home, which it reads in its |
|
| 892 | + | loop, there being no SIGUSR1; and having left, it exits 75, so a Task |
|
| 893 | + | Scheduler task set to restart on failure starts the new build. A |
|
| 894 | + | rollback runs the restored build in its place and ends with it. |
|
| 886 | 895 | ||
| 887 | 896 | ### 11.6 Conformance |
|
| 888 | 897 | ||
| … | 11 unchanged lines |