flow builds for Windows, offering the WASM tier 3554afefb6aa
The syscall tier, guest helpers and egress proxy are Linux's only; a Windows build has a stand-in that says the syscall tier runs only on Linux. Pruning by reads stays on Linux; modes, links and sizes go through a portable module; artifacts are read with each step checked. Free disk, load and kernel are not measured on Windows yet. refs #83
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-26 01:58 UTC
- Signature
- ◈ sealed by Andy McCall
- Hash
3554afefb6aadff470e78828d68179b027f263578059e715a80fc65f5b20561c- Depends on
- 077a65242359 2594278f49b2 28cc603f737d 337afb8f6043 47c1d64dec8a 5cf1e8aefbb6 62829d15ef09 85275e1ef3b7 8f211e20bea5 a67b97e9a717 b14aeaae683a c3db750ed96e cc462b56fbbb d60aac6c3f8a e0918b887850 ea56c56eaf90 ea81796bb757 ecce3719da80 f201e1bc5bcd f98a8f7881e0 fc10d9e80383
- Depended on by
- 788f57e4d266 f4dfd742b30d 494a1b9a8ac2 6f66e397539c e6949d87205b
- In bookmarks
- main
- In tags
- v0.4.0 v0.5.0 v0.5.1 v0.5.2 v0.5.3
- Edits
- 86 atoms, 3 tree operations, 18 files
Browse the tree at this change
added crates/flow/src/portable.rs +106 −0
| 1 | + | //! What differs between the systems a runner runs on (oxygenflow#83): file |
|
| 2 | + | //! modes, file identities and links are Unix's. On Windows, where a runner |
|
| 3 | + | //! offers the WASM tier only, modes are not kept (files read as 0644 and |
|
| 4 | + | //! directories as 0755), each file is its own, and sizes are lengths. |
|
| 5 | + | ||
| 6 | + | use std::fs::{Metadata, OpenOptions}; |
|
| 7 | + | use std::io; |
|
| 8 | + | use std::path::Path; |
|
| 9 | + | ||
| 10 | + | /// Set `p`'s permission bits; nothing on Windows. |
|
| 11 | + | pub fn set_mode(p: &Path, mode: u32) -> io::Result<()> { |
|
| 12 | + | #[cfg(unix)] |
|
| 13 | + | { |
|
| 14 | + | use std::os::unix::fs::PermissionsExt; |
|
| 15 | + | std::fs::set_permissions(p, std::fs::Permissions::from_mode(mode)) |
|
| 16 | + | } |
|
| 17 | + | #[cfg(not(unix))] |
|
| 18 | + | { |
|
| 19 | + | let _ = (p, mode); |
|
| 20 | + | Ok(()) |
|
| 21 | + | } |
|
| 22 | + | } |
|
| 23 | + | ||
| 24 | + | /// A file's permission bits (`0o7777` of its mode). |
|
| 25 | + | pub fn mode(md: &Metadata) -> u32 { |
|
| 26 | + | #[cfg(unix)] |
|
| 27 | + | { |
|
| 28 | + | use std::os::unix::fs::MetadataExt; |
|
| 29 | + | md.mode() & 0o7777 |
|
| 30 | + | } |
|
| 31 | + | #[cfg(not(unix))] |
|
| 32 | + | { |
|
| 33 | + | let base = if md.is_dir() { 0o755 } else { 0o644 }; |
|
| 34 | + | if md.permissions().readonly() { |
|
| 35 | + | base & !0o222 |
|
| 36 | + | } else { |
|
| 37 | + | base |
|
| 38 | + | } |
|
| 39 | + | } |
|
| 40 | + | } |
|
| 41 | + | ||
| 42 | + | /// Open a new file readable and writable by its owner only. |
|
| 43 | + | pub fn private(o: &mut OpenOptions) -> &mut OpenOptions { |
|
| 44 | + | #[cfg(unix)] |
|
| 45 | + | { |
|
| 46 | + | use std::os::unix::fs::OpenOptionsExt; |
|
| 47 | + | o.mode(0o600) |
|
| 48 | + | } |
|
| 49 | + | #[cfg(not(unix))] |
|
| 50 | + | { |
|
| 51 | + | o |
|
| 52 | + | } |
|
| 53 | + | } |
|
| 54 | + | ||
| 55 | + | /// Make `p` and its parents, the last only its owner's. |
|
| 56 | + | pub fn create_private_dir(p: &Path) -> io::Result<()> { |
|
| 57 | + | #[cfg(unix)] |
|
| 58 | + | { |
|
| 59 | + | std::os::unix::fs::DirBuilderExt::mode(std::fs::DirBuilder::new().recursive(true), 0o700).create(p) |
|
| 60 | + | } |
|
| 61 | + | #[cfg(not(unix))] |
|
| 62 | + | { |
|
| 63 | + | std::fs::create_dir_all(p) |
|
| 64 | + | } |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | /// The disk a file takes: its blocks on Unix, its length elsewhere. |
|
| 68 | + | pub fn disk_bytes(md: &Metadata) -> u64 { |
|
| 69 | + | #[cfg(unix)] |
|
| 70 | + | { |
|
| 71 | + | use std::os::unix::fs::MetadataExt; |
|
| 72 | + | md.blocks() * 512 |
|
| 73 | + | } |
|
| 74 | + | #[cfg(not(unix))] |
|
| 75 | + | { |
|
| 76 | + | md.len() |
|
| 77 | + | } |
|
| 78 | + | } |
|
| 79 | + | ||
| 80 | + | /// For a file with more than one name, what names it once (device, inode); |
|
| 81 | + | /// `None` for a file with one name, a directory, or anywhere but Unix. |
|
| 82 | + | pub fn shared_identity(md: &Metadata) -> Option<(u64, u64)> { |
|
| 83 | + | #[cfg(unix)] |
|
| 84 | + | { |
|
| 85 | + | use std::os::unix::fs::MetadataExt; |
|
| 86 | + | (md.nlink() > 1 && !md.is_dir()).then(|| (md.dev(), md.ino())) |
|
| 87 | + | } |
|
| 88 | + | #[cfg(not(unix))] |
|
| 89 | + | { |
|
| 90 | + | let _ = md; |
|
| 91 | + | None |
|
| 92 | + | } |
|
| 93 | + | } |
|
| 94 | + | ||
| 95 | + | /// Make `link` a symbolic link to `target` (a file link on Windows, which |
|
| 96 | + | /// needs Developer Mode or the right to make them). |
|
| 97 | + | pub fn symlink(target: &Path, link: &Path) -> io::Result<()> { |
|
| 98 | + | #[cfg(unix)] |
|
| 99 | + | { |
|
| 100 | + | std::os::unix::fs::symlink(target, link) |
|
| 101 | + | } |
|
| 102 | + | #[cfg(windows)] |
|
| 103 | + | { |
|
| 104 | + | std::os::windows::fs::symlink_file(target, link) |
|
| 105 | + | } |
|
| 106 | + | } |
added crates/flow/src/syscall_none.rs +45 −0
| 1 | + | //! The syscall tier where it cannot be: it is built from Linux's user |
|
| 2 | + | //! namespaces, Landlock, seccomp and cgroups, so a runner elsewhere (a |
|
| 3 | + | //! Windows runner, oxygenflow#83) never offers it, and these say why. |
|
| 4 | + | ||
| 5 | + | use crate::home::Home; |
|
| 6 | + | use crate::microvm::{Outcome, Sink}; |
|
| 7 | + | use anyhow::{bail, Result}; |
|
| 8 | + | use flow_core::api::SecretInput; |
|
| 9 | + | use flow_core::{Job, Sandbox}; |
|
| 10 | + | use std::path::Path; |
|
| 11 | + | use std::time::Duration; |
|
| 12 | + | ||
| 13 | + | pub use crate::tier::disk_dir; |
|
| 14 | + | pub(crate) use crate::tier::{lines, self_hash, FAILED}; |
|
| 15 | + | ||
| 16 | + | const WHY: &str = "the syscall tier runs only on Linux"; |
|
| 17 | + | ||
| 18 | + | pub fn available() -> std::result::Result<(), String> { |
|
| 19 | + | Err(WHY.into()) |
|
| 20 | + | } |
|
| 21 | + | ||
| 22 | + | pub fn probe_main() -> i32 { |
|
| 23 | + | eprintln!("{WHY}"); |
|
| 24 | + | 1 |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | pub fn sandbox_main(_spec_path: &Path) -> i32 { |
|
| 28 | + | eprintln!("{WHY}"); |
|
| 29 | + | 125 |
|
| 30 | + | } |
|
| 31 | + | ||
| 32 | + | #[allow(clippy::too_many_arguments)] |
|
| 33 | + | pub fn run( |
|
| 34 | + | _home: &Home, |
|
| 35 | + | _sandbox: &Sandbox, |
|
| 36 | + | _job: &Job, |
|
| 37 | + | _secrets: &[SecretInput], |
|
| 38 | + | _workspace: &Path, |
|
| 39 | + | _scratch: &Path, |
|
| 40 | + | _timeout: Duration, |
|
| 41 | + | _sink: &mut dyn Sink, |
|
| 42 | + | _name: &str, |
|
| 43 | + | ) -> Result<Outcome> { |
|
| 44 | + | bail!("{WHY}") |
|
| 45 | + | } |
added crates/flow/src/tier.rs +48 −0
| 1 | + | //! What the sandbox tiers share, on every system a runner runs on: where a |
|
| 2 | + | //! job's disks are kept, how a helper says a job never started, reading a |
|
| 3 | + | //! job's output by lines, and this binary's hash for receipts. |
|
| 4 | + | ||
| 5 | + | use std::io::{BufRead, BufReader, Read}; |
|
| 6 | + | use std::path::{Path, PathBuf}; |
|
| 7 | + | use std::sync::{mpsc, OnceLock}; |
|
| 8 | + | ||
| 9 | + | /// Where the `n`th `ephemeral://disk` mount of a job keeps its files. |
|
| 10 | + | pub fn disk_dir(scratch: &Path, n: usize) -> PathBuf { |
|
| 11 | + | scratch.join(format!("mount-{n}")) |
|
| 12 | + | } |
|
| 13 | + | ||
| 14 | + | /// How the helper says the job never started, on its stderr. |
|
| 15 | + | pub(crate) const FAILED: &str = "\u{1e}F "; |
|
| 16 | + | ||
| 17 | + | pub(crate) fn lines(r: impl Read, stdout: bool, tx: mpsc::Sender<(bool, Vec<u8>)>) { |
|
| 18 | + | let mut r = BufReader::new(r); |
|
| 19 | + | let mut buf = Vec::new(); |
|
| 20 | + | loop { |
|
| 21 | + | buf.clear(); |
|
| 22 | + | // A long line is cut at 16 KiB, as flow-init cuts it. |
|
| 23 | + | match (&mut r).take(16 * 1024).read_until(b'\n', &mut buf) { |
|
| 24 | + | Ok(0) | Err(_) => break, |
|
| 25 | + | Ok(_) => { |
|
| 26 | + | if buf.last() == Some(&b'\n') { |
|
| 27 | + | buf.pop(); |
|
| 28 | + | } |
|
| 29 | + | if tx.send((stdout, buf.clone())).is_err() { |
|
| 30 | + | break; |
|
| 31 | + | } |
|
| 32 | + | } |
|
| 33 | + | } |
|
| 34 | + | } |
|
| 35 | + | } |
|
| 36 | + | ||
| 37 | + | /// This binary's hash, which receipts name as the sandbox. |
|
| 38 | + | pub(crate) fn self_hash() -> &'static str { |
|
| 39 | + | static HASH: OnceLock<String> = OnceLock::new(); |
|
| 40 | + | HASH.get_or_init(|| { |
|
| 41 | + | let exe = if cfg!(target_os = "linux") || cfg!(target_os = "android") { |
|
| 42 | + | PathBuf::from("/proc/self/exe") |
|
| 43 | + | } else { |
|
| 44 | + | std::env::current_exe().unwrap_or_default() |
|
| 45 | + | }; |
|
| 46 | + | crate::home::hash_file(&exe).unwrap_or_else(|_| "unknown".into()) |
|
| 47 | + | }) |
|
| 48 | + | } |
modified CHANGELOG.md +1 −0
| … | 3 unchanged lines |
||
| 4 | 4 | ||
| 5 | 5 | ## Unreleased |
|
| 6 | 6 | ||
| 7 | + | - **`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). Free disk, load and kernel are not measured there yet, and a Windows runner does not update itself. Nothing changes on Linux or Android. |
|
| 7 | 8 | - **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. |
|
| 8 | 9 | - **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. |
|
| 9 | 10 | - **Runners ask the hub for their update (oxygenhub#322):** once the hub has answered a claim, and every 15 minutes, a runner asks `GET /api/v1/flow/runner/update`; when the hub offers a build, it downloads it (kept in the home's `updates/`), refuses it unless its size and BLAKE3 are those the hub attests, and installs it as `flow runner update` does, leaving within 600 s, or for a dry run checks it and says what would happen. What it did goes back to the hub once per outcome, and a proof or a rollback is told at the next start. `[runner.update] allow = false` refuses and says so; a hub without runner updates (404) is simply asked again later. |
|
| … | 99 unchanged lines |
modified crates/flow-init/src/prune.rs +37 −2
| … | 29 unchanged lines |
||
| 30 | 30 | //! only if an older lockfile comes back, and then cargo rebuilds it. |
|
| 31 | 31 | ||
| 32 | 32 | use serde::{Deserialize, Serialize}; |
|
| 33 | + | #[cfg(unix)] |
|
| 33 | 34 | use std::collections::{HashMap, HashSet}; |
|
| 35 | + | #[cfg(unix)] |
|
| 34 | 36 | use std::os::unix::fs::MetadataExt; |
|
| 35 | - | use std::path::{Path, PathBuf}; |
|
| 37 | + | use std::path::Path; |
|
| 38 | + | #[cfg(unix)] |
|
| 39 | + | use std::path::PathBuf; |
|
| 36 | 40 | use std::time::{Duration, SystemTime}; |
|
| 37 | 41 | ||
| 38 | 42 | /// The share of a cache disk a kept cache may fill when no runner says |
|
| … | 45 unchanged lines |
||
| 84 | 88 | t |
|
| 85 | 89 | } |
|
| 86 | 90 | ||
| 91 | + | #[cfg(unix)] |
|
| 87 | 92 | /// Whether the file system holding `path` records reads (not `noatime`). |
|
| 88 | 93 | pub fn records_reads(path: &Path) -> bool { |
|
| 89 | 94 | let Ok(c) = std::ffi::CString::new(path.as_os_str().as_encoded_bytes()) else { return false }; |
|
| … | 4 unchanged lines |
||
| 94 | 99 | st.f_flag & libc::ST_NOATIME == 0 |
|
| 95 | 100 | } |
|
| 96 | 101 | ||
| 102 | + | #[cfg(unix)] |
|
| 97 | 103 | /// The blocks in use on the file system mounted at `path`, and its size |
|
| 98 | 104 | /// (as `df` counts them). |
|
| 99 | 105 | pub fn usage(path: &Path) -> std::io::Result<(u64, u64)> { |
|
| … | 6 unchanged lines |
||
| 106 | 112 | Ok(((st.f_blocks as u64).saturating_sub(st.f_bfree as u64) * unit, st.f_blocks as u64 * unit)) |
|
| 107 | 113 | } |
|
| 108 | 114 | ||
| 115 | + | #[cfg(unix)] |
|
| 109 | 116 | /// Set every regular file's atime under `root` to the epoch, its mtime |
|
| 110 | 117 | /// kept, so the job's first read of each is recorded even on `relatime` |
|
| 111 | 118 | /// (which records a read only of a file read no later than it changed). |
|
| … | 19 unchanged lines |
||
| 131 | 138 | } |
|
| 132 | 139 | } |
|
| 133 | 140 | ||
| 141 | + | #[cfg(unix)] |
|
| 134 | 142 | /// Each entry under `root` (not `root` itself) with its metadata, parents |
|
| 135 | 143 | /// before children; links are not followed, nor other file systems |
|
| 136 | 144 | /// entered. |
|
| … | 17 unchanged lines |
||
| 154 | 162 | Ok(()) |
|
| 155 | 163 | } |
|
| 156 | 164 | ||
| 165 | + | #[cfg(unix)] |
|
| 157 | 166 | /// The unit hashes in `rel`: a 16-digit lowercase hex word after a `-` |
|
| 158 | 167 | /// in any of its names, as cargo names a unit's files. |
|
| 159 | 168 | fn hashes(rel: &Path) -> impl Iterator<Item = String> + '_ { |
|
| … | 5 unchanged lines |
||
| 165 | 174 | }) |
|
| 166 | 175 | } |
|
| 167 | 176 | ||
| 177 | + | #[cfg(unix)] |
|
| 168 | 178 | fn nanos(s: i64, ns: i64) -> i128 { |
|
| 169 | 179 | s as i128 * 1_000_000_000 + ns as i128 |
|
| 170 | 180 | } |
|
| 171 | 181 | ||
| 182 | + | #[cfg(unix)] |
|
| 172 | 183 | /// One file, however many names it has. |
|
| 173 | 184 | struct File { |
|
| 174 | 185 | paths: Vec<PathBuf>, |
|
| … | 5 unchanged lines |
||
| 180 | 191 | used: bool, |
|
| 181 | 192 | } |
|
| 182 | 193 | ||
| 194 | + | #[cfg(unix)] |
|
| 183 | 195 | /// Prune the cache at `root`, a disk of `size` bytes with `before` in |
|
| 184 | 196 | /// use, down to `percent` of its size, removing only regular files this |
|
| 185 | 197 | /// run did not use (see the module's notes), those used longest ago |
|
| … | 75 unchanged lines |
||
| 261 | 273 | Ok(out) |
|
| 262 | 274 | } |
|
| 263 | 275 | ||
| 264 | - | #[cfg(test)] |
|
| 276 | + | // Elsewhere (a Windows runner, which has no microVM tier and so no cache |
|
| 277 | + | // disks to prune) nothing records reads, so nothing is pruned. |
|
| 278 | + | ||
| 279 | + | #[cfg(not(unix))] |
|
| 280 | + | pub fn records_reads(_path: &Path) -> bool { |
|
| 281 | + | false |
|
| 282 | + | } |
|
| 283 | + | ||
| 284 | + | #[cfg(not(unix))] |
|
| 285 | + | pub fn usage(_path: &Path) -> std::io::Result<(u64, u64)> { |
|
| 286 | + | Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "cache disks are pruned only on Linux")) |
|
| 287 | + | } |
|
| 288 | + | ||
| 289 | + | #[cfg(not(unix))] |
|
| 290 | + | pub fn forget_reads(_root: &Path) -> std::io::Result<u64> { |
|
| 291 | + | Ok(0) |
|
| 292 | + | } |
|
| 293 | + | ||
| 294 | + | #[cfg(not(unix))] |
|
| 295 | + | pub fn prune(_root: &Path, _started: SystemTime, _keep: &[String], _size: u64, _before: u64, _percent: u8) -> std::io::Result<Pruned> { |
|
| 296 | + | Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "cache disks are pruned only on Linux")) |
|
| 297 | + | } |
|
| 298 | + | ||
| 299 | + | #[cfg(all(test, unix))] |
|
| 265 | 300 | mod tests { |
|
| 266 | 301 | use super::*; |
|
| 267 | 302 | ||
| … | 98 unchanged lines |
modified crates/flow/src/cache.rs +12 −11
| … | 46 unchanged lines |
||
| 47 | 47 | use anyhow::{Context, Result}; |
|
| 48 | 48 | use flow_core::pipeline::Mount; |
|
| 49 | 49 | use flow_init::prune::Pruned; |
|
| 50 | - | use std::os::unix::fs::{MetadataExt, PermissionsExt}; |
|
| 51 | 50 | use std::path::{Path, PathBuf}; |
|
| 52 | 51 | use std::time::SystemTime; |
|
| 53 | 52 | ||
| … | 183 unchanged lines |
||
| 237 | 236 | seal(disk).with_context(|| format!("seal {}", disk.display()))?; |
|
| 238 | 237 | } |
|
| 239 | 238 | let mine = self.cache_dir(ctx, name).join(segment(ctx.bookmark)); |
|
| 240 | - | std::os::unix::fs::DirBuilderExt::mode(std::fs::DirBuilder::new().recursive(true), 0o700).create(&mine)?; |
|
| 241 | - | std::fs::set_permissions(&self.root, std::fs::Permissions::from_mode(0o700))?; |
|
| 239 | + | crate::portable::create_private_dir(&mine)?; |
|
| 240 | + | crate::portable::set_mode(&self.root, 0o700)?; |
|
| 242 | 241 | let dest = mine.join(format!("{key}.{}", ctx.form.ext())); |
|
| 243 | 242 | if ctx.form == Form::Dir { |
|
| 244 | 243 | // Another job of this slot kept one since: this one is newer. |
|
| … | 9 unchanged lines |
||
| 254 | 253 | let _ = std::fs::remove_file(&tmp); |
|
| 255 | 254 | { |
|
| 256 | 255 | use std::io::Write; |
|
| 257 | - | use std::os::unix::fs::OpenOptionsExt; |
|
| 258 | - | std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(&tmp)?.write_all(&serde_json::to_vec(sources)?)?; |
|
| 256 | + | crate::portable::private(std::fs::OpenOptions::new().write(true).create_new(true)) |
|
| 257 | + | .open(&tmp)? |
|
| 258 | + | .write_all(&serde_json::to_vec(sources)?)?; |
|
| 259 | 259 | } |
|
| 260 | 260 | std::fs::rename(&tmp, sidecar)?; |
|
| 261 | 261 | // One disk per key is enough; older keys of this bookmark go once |
|
| … | 66 unchanged lines |
||
| 328 | 328 | kept(&self.root, 4, &mut |p, form| { |
|
| 329 | 329 | if let Ok(md) = std::fs::symlink_metadata(p) { |
|
| 330 | 330 | let bytes = match form { |
|
| 331 | - | Form::Image => md.blocks() * 512, |
|
| 331 | + | Form::Image => crate::portable::disk_bytes(&md), |
|
| 332 | 332 | Form::Dir => tree_bytes(p), |
|
| 333 | 333 | }; |
|
| 334 | 334 | disks.push((md.modified().unwrap_or(SystemTime::UNIX_EPOCH), bytes, p.to_path_buf())); |
|
| … | 146 unchanged lines |
||
| 481 | 481 | fn tree_bytes(dir: &Path) -> u64 { |
|
| 482 | 482 | fn walk(p: &Path, seen: &mut std::collections::HashSet<(u64, u64)>) -> u64 { |
|
| 483 | 483 | let Ok(md) = std::fs::symlink_metadata(p) else { return 0 }; |
|
| 484 | - | if md.nlink() > 1 && !md.is_dir() && !seen.insert((md.dev(), md.ino())) { |
|
| 484 | + | if crate::portable::shared_identity(&md).is_some_and(|id| !seen.insert(id)) { |
|
| 485 | 485 | return 0; |
|
| 486 | 486 | } |
|
| 487 | - | let mut bytes = md.blocks() * 512; |
|
| 487 | + | let mut bytes = crate::portable::disk_bytes(&md); |
|
| 488 | 488 | if md.is_dir() { |
|
| 489 | 489 | for e in std::fs::read_dir(p).into_iter().flatten().flatten() { |
|
| 490 | 490 | bytes += walk(&e.path(), seen); |
|
| … | 14 unchanged lines |
||
| 505 | 505 | if md.file_type().is_symlink() { |
|
| 506 | 506 | return Ok(()); |
|
| 507 | 507 | } |
|
| 508 | - | let mode = md.mode() & 0o7777; |
|
| 508 | + | let mode = crate::portable::mode(&md); |
|
| 509 | 509 | let want = (mode & !0o7022) | if md.is_dir() { 0o700 } else { 0o400 }; |
|
| 510 | 510 | if want != mode { |
|
| 511 | - | std::fs::set_permissions(p, std::fs::Permissions::from_mode(want))?; |
|
| 511 | + | crate::portable::set_mode(p, want)?; |
|
| 512 | 512 | } |
|
| 513 | 513 | if md.is_dir() { |
|
| 514 | 514 | for e in std::fs::read_dir(p)? { |
|
| … | 43 unchanged lines |
||
| 558 | 558 | Ok(()) |
|
| 559 | 559 | } |
|
| 560 | 560 | ||
| 561 | - | #[cfg(test)] |
|
| 561 | + | #[cfg(all(test, unix))] |
|
| 562 | 562 | mod tests { |
|
| 563 | 563 | use super::*; |
|
| 564 | 564 | use flow_core::pipeline::MountSource; |
|
| 565 | + | use std::os::unix::fs::{MetadataExt, PermissionsExt}; |
|
| 565 | 566 | ||
| 566 | 567 | /// Pruning that never prunes: the cap is the whole disk. |
|
| 567 | 568 | fn keep_all() -> Prune<'static> { |
|
| … | 397 unchanged lines |
modified crates/flow/src/checkpoint.rs +2 −4
| … | 92 unchanged lines |
||
| 93 | 93 | } |
|
| 94 | 94 | ||
| 95 | 95 | fn entry(out: &mut Vec<u8>, kind: u8, path: &str, meta: &std::fs::Metadata, bytes: &[u8]) { |
|
| 96 | - | use std::os::unix::fs::PermissionsExt; |
|
| 97 | 96 | let since = meta.modified().ok().and_then(|t| t.duration_since(UNIX_EPOCH).ok()).unwrap_or_default(); |
|
| 98 | 97 | out.push(kind); |
|
| 99 | 98 | out.extend((path.len() as u32).to_le_bytes()); |
|
| 100 | 99 | out.extend(path.as_bytes()); |
|
| 101 | - | out.extend(meta.permissions().mode().to_le_bytes()); |
|
| 100 | + | out.extend(crate::portable::mode(meta).to_le_bytes()); |
|
| 102 | 101 | out.extend(since.as_secs().to_le_bytes()); |
|
| 103 | 102 | out.extend(since.subsec_nanos().to_le_bytes()); |
|
| 104 | 103 | out.extend((bytes.len() as u64).to_le_bytes()); |
|
| … | 3 unchanged lines |
||
| 108 | 107 | /// Restore a checkpoint: each entry at `place(checkpoint)` joined with the |
|
| 109 | 108 | /// rest of its path, for the job's own `checkpoints` only. |
|
| 110 | 109 | pub fn unpack(bytes: &[u8], checkpoints: &[String], place: impl Fn(&str) -> Option<PathBuf>) -> Result<Counts> { |
|
| 111 | - | use std::os::unix::fs::PermissionsExt; |
|
| 112 | 110 | let Some(mut rest) = bytes.strip_prefix(MAGIC) else { bail!("not a checkpoint") }; |
|
| 113 | 111 | let mut n = Counts::default(); |
|
| 114 | 112 | let take = |rest: &mut &[u8], k: usize| -> Result<Vec<u8>> { |
|
| … | 39 unchanged lines |
||
| 154 | 152 | } |
|
| 155 | 153 | std::fs::write(&at, &data)?; |
|
| 156 | 154 | let f = std::fs::File::options().write(true).open(&at)?; |
|
| 157 | - | f.set_permissions(std::fs::Permissions::from_mode(mode & 0o777))?; |
|
| 155 | + | crate::portable::set_mode(&at, mode & 0o777)?; |
|
| 158 | 156 | f.set_modified(SystemTime::UNIX_EPOCH + Duration::new(secs, nanos))?; |
|
| 159 | 157 | n.files += 1; |
|
| 160 | 158 | n.bytes += data.len() as u64; |
|
| … | 54 unchanged lines |
modified crates/flow/src/egress.rs +30 −3
| … | 6 unchanged lines |
||
| 7 | 7 | //! decision goes into the job log, and the bytes are relayed untouched (no |
|
| 8 | 8 | //! TLS is opened here). |
|
| 9 | 9 | ||
| 10 | + | #[cfg(unix)] |
|
| 10 | 11 | use std::io::{Read, Write}; |
|
| 11 | - | use std::net::{IpAddr, SocketAddr, TcpStream, ToSocketAddrs}; |
|
| 12 | + | use std::net::IpAddr; |
|
| 13 | + | #[cfg(unix)] |
|
| 14 | + | use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; |
|
| 15 | + | #[cfg(unix)] |
|
| 12 | 16 | use std::os::unix::net::{UnixListener, UnixStream}; |
|
| 13 | 17 | use std::path::Path; |
|
| 18 | + | #[cfg(unix)] |
|
| 14 | 19 | use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; |
|
| 15 | - | use std::sync::{mpsc, Arc}; |
|
| 20 | + | use std::sync::mpsc; |
|
| 21 | + | #[cfg(unix)] |
|
| 22 | + | use std::sync::Arc; |
|
| 23 | + | #[cfg(unix)] |
|
| 16 | 24 | use std::time::Duration; |
|
| 17 | 25 | ||
| 18 | 26 | /// The vsock port the guest's relay connects to on the host (CID 2). |
|
| … | 2 unchanged lines |
||
| 21 | 29 | pub const GUEST_PROXY: &str = "http://127.0.0.1:3128"; |
|
| 22 | 30 | /// The only port relayed: TLS. |
|
| 23 | 31 | const PORT: u16 = 443; |
|
| 32 | + | #[cfg(unix)] |
|
| 24 | 33 | /// Connections at once, per job. |
|
| 25 | 34 | const MAX_OPEN: usize = 64; |
|
| 35 | + | #[cfg(unix)] |
|
| 26 | 36 | const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); |
|
| 37 | + | #[cfg(unix)] |
|
| 27 | 38 | /// The longest request head read. |
|
| 28 | 39 | const MAX_HEAD: usize = 8 * 1024; |
|
| 29 | 40 | ||
| … | 74 unchanged lines |
||
| 104 | 115 | } |
|
| 105 | 116 | } |
|
| 106 | 117 | ||
| 118 | + | #[cfg(unix)] |
|
| 107 | 119 | /// The proxy for one job: listening until dropped. |
|
| 108 | 120 | pub struct Proxy { |
|
| 109 | 121 | stop: Arc<AtomicBool>, |
|
| 110 | 122 | path: std::path::PathBuf, |
|
| 111 | 123 | } |
|
| 112 | 124 | ||
| 125 | + | #[cfg(unix)] |
|
| 113 | 126 | impl Drop for Proxy { |
|
| 114 | 127 | fn drop(&mut self) { |
|
| 115 | 128 | self.stop.store(true, Ordering::SeqCst); |
|
| … | 3 unchanged lines |
||
| 119 | 132 | } |
|
| 120 | 133 | } |
|
| 121 | 134 | ||
| 135 | + | #[cfg(unix)] |
|
| 122 | 136 | /// Listen on `socket` (Firecracker's `uds_path` and `_3128`) for the |
|
| 123 | 137 | /// guest's connections; each decision is sent to `log` as a line. |
|
| 124 | 138 | pub fn start(socket: &Path, allow: Vec<String>, log: mpsc::Sender<String>) -> std::io::Result<Proxy> { |
|
| … | 25 unchanged lines |
||
| 150 | 164 | Ok(Proxy { stop, path: socket.to_path_buf() }) |
|
| 151 | 165 | } |
|
| 152 | 166 | ||
| 167 | + | #[cfg(unix)] |
|
| 153 | 168 | fn refuse(mut conn: UnixStream, why: &str) -> std::io::Result<()> { |
|
| 154 | 169 | let body = format!("{why}\n"); |
|
| 155 | 170 | write!(conn, "HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()) |
|
| 156 | 171 | } |
|
| 157 | 172 | ||
| 173 | + | #[cfg(unix)] |
|
| 158 | 174 | /// Read up to the blank line that ends a request head. |
|
| 159 | 175 | fn read_head(conn: &mut UnixStream) -> std::io::Result<String> { |
|
| 160 | 176 | let mut head = Vec::new(); |
|
| … | 7 unchanged lines |
||
| 168 | 184 | Ok(String::from_utf8_lossy(&head).into_owned()) |
|
| 169 | 185 | } |
|
| 170 | 186 | ||
| 187 | + | #[cfg(unix)] |
|
| 171 | 188 | fn serve(mut conn: UnixStream, allow: &[String], log: &mpsc::Sender<String>) { |
|
| 172 | 189 | let _ = conn.set_read_timeout(Some(Duration::from_secs(30))); |
|
| 173 | 190 | let head = match read_head(&mut conn) { |
|
| … | 41 unchanged lines |
||
| 215 | 232 | relay(conn, out); |
|
| 216 | 233 | } |
|
| 217 | 234 | ||
| 235 | + | #[cfg(unix)] |
|
| 218 | 236 | /// Copy both ways until either side closes. |
|
| 219 | 237 | fn relay(guest: UnixStream, out: TcpStream) { |
|
| 220 | 238 | let (Ok(mut guest_r), Ok(mut out_w)) = (guest.try_clone(), out.try_clone()) else { return }; |
|
| … | 7 unchanged lines |
||
| 228 | 246 | let _ = up.join(); |
|
| 229 | 247 | } |
|
| 230 | 248 | ||
| 231 | - | #[cfg(test)] |
|
| 249 | + | /// The egress proxy serves a microVM's vsock, which only Linux has. |
|
| 250 | + | #[cfg(not(unix))] |
|
| 251 | + | pub struct Proxy; |
|
| 252 | + | ||
| 253 | + | #[cfg(not(unix))] |
|
| 254 | + | pub fn start(_socket: &Path, _allow: Vec<String>, _log: mpsc::Sender<String>) -> std::io::Result<Proxy> { |
|
| 255 | + | Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "egress serves microVMs, which run only on Linux")) |
|
| 256 | + | } |
|
| 257 | + | ||
| 258 | + | #[cfg(all(test, unix))] |
|
| 232 | 259 | mod tests { |
|
| 233 | 260 | use super::*; |
|
| 234 | 261 | ||
| … | 67 unchanged lines |
modified crates/flow/src/facts.rs +24 −4
| … | 22 unchanged lines |
||
| 23 | 23 | ||
| 24 | 24 | /// The kernel's release, as uname says. Not read from /proc: an Android |
|
| 25 | 25 | /// app may not read it there, and is refused (and logged) every claim. |
|
| 26 | + | /// None on Windows (oxygenflow#83). |
|
| 27 | + | #[cfg(unix)] |
|
| 26 | 28 | pub fn kernel() -> Option<String> { |
|
| 27 | 29 | let mut u: libc::utsname = unsafe { std::mem::zeroed() }; |
|
| 28 | 30 | // SAFETY: `u` is a writable utsname. |
|
| … | 5 unchanged lines |
||
| 34 | 36 | Some(release.to_string_lossy().into_owned()) |
|
| 35 | 37 | } |
|
| 36 | 38 | ||
| 37 | - | /// Free space for an unprivileged writer on the file system holding `path`, in MiB. |
|
| 39 | + | #[cfg(not(unix))] |
|
| 40 | + | pub fn kernel() -> Option<String> { |
|
| 41 | + | None |
|
| 42 | + | } |
|
| 43 | + | ||
| 44 | + | /// Free space for an unprivileged writer on the file system holding `path`, |
|
| 45 | + | /// in MiB; not measured on Windows. |
|
| 46 | + | #[cfg(unix)] |
|
| 38 | 47 | pub fn free_disk_mib(path: &std::path::Path) -> Option<u64> { |
|
| 39 | 48 | use std::os::unix::ffi::OsStrExt; |
|
| 40 | 49 | let c = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?; |
|
| … | 3 unchanged lines |
||
| 44 | 53 | return None; |
|
| 45 | 54 | } |
|
| 46 | 55 | Some((s.f_bavail as u64).saturating_mul(s.f_frsize as u64) >> 20) |
|
| 56 | + | } |
|
| 57 | + | ||
| 58 | + | #[cfg(not(unix))] |
|
| 59 | + | pub fn free_disk_mib(_path: &std::path::Path) -> Option<u64> { |
|
| 60 | + | None |
|
| 47 | 61 | } |
|
| 48 | 62 | ||
| 49 | 63 | /// The disk the files under `dir` take (their blocks, not their sizes: a |
|
| 50 | 64 | /// job's disks are sparse), in MiB; links are not followed. |
|
| 51 | 65 | pub fn allocated_mib(dir: &std::path::Path) -> Option<u64> { |
|
| 52 | - | use std::os::unix::fs::MetadataExt; |
|
| 53 | 66 | let mut total: u64 = 0; |
|
| 54 | 67 | let mut stack = vec![dir.to_path_buf()]; |
|
| 55 | 68 | while let Some(d) = stack.pop() { |
|
| … | 2 unchanged lines |
||
| 58 | 71 | if m.is_dir() { |
|
| 59 | 72 | stack.push(e.path()); |
|
| 60 | 73 | } else { |
|
| 61 | - | total = total.saturating_add(m.blocks() * 512); |
|
| 74 | + | total = total.saturating_add(crate::portable::disk_bytes(&m)); |
|
| 62 | 75 | } |
|
| 63 | 76 | } |
|
| 64 | 77 | } |
|
| … | 8 unchanged lines |
||
| 73 | 86 | } |
|
| 74 | 87 | ||
| 75 | 88 | /// The 1-minute load average, times 100, from sysinfo (the kernel's own |
|
| 76 | - | /// numbers behind /proc/loadavg, which an Android app may not read). |
|
| 89 | + | /// numbers behind /proc/loadavg, which an Android app may not read); not |
|
| 90 | + | /// measured on Windows. |
|
| 91 | + | #[cfg(not(unix))] |
|
| 92 | + | pub fn load_x100() -> Option<u32> { |
|
| 93 | + | None |
|
| 94 | + | } |
|
| 95 | + | ||
| 96 | + | #[cfg(unix)] |
|
| 77 | 97 | pub fn load_x100() -> Option<u32> { |
|
| 78 | 98 | let mut i: libc::sysinfo = unsafe { std::mem::zeroed() }; |
|
| 79 | 99 | // SAFETY: `i` is a writable sysinfo. |
|
| … | 57 unchanged lines |
modified crates/flow/src/home.rs +1 −2
| … | 272 unchanged lines |
||
| 273 | 273 | let tmp = self.root.join("runner.key.tmp"); |
|
| 274 | 274 | { |
|
| 275 | 275 | use std::io::Write; |
|
| 276 | - | use std::os::unix::fs::OpenOptionsExt; |
|
| 277 | - | let mut f = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(&tmp)?; |
|
| 276 | + | let mut f = crate::portable::private(std::fs::OpenOptions::new().write(true).create(true).truncate(true)).open(&tmp)?; |
|
| 278 | 277 | f.write_all(&secret.encode(&identity.hash()))?; |
|
| 279 | 278 | f.sync_all()?; |
|
| 280 | 279 | } |
|
| … | 24 unchanged lines |
modified crates/flow/src/lib.rs +7 −0
| … | 8 unchanged lines |
||
| 9 | 9 | pub mod facts; |
|
| 10 | 10 | /// The job-starting code `flow-init` uses in a microVM, shared with the |
|
| 11 | 11 | /// syscall tier. |
|
| 12 | + | #[cfg(unix)] |
|
| 12 | 13 | #[path = "../../flow-init/src/job.rs"] |
|
| 13 | 14 | pub mod guest; |
|
| 14 | 15 | pub mod home; |
|
| … | 1 unchanged lines |
||
| 16 | 17 | pub mod metrics; |
|
| 17 | 18 | pub mod microvm; |
|
| 18 | 19 | pub mod outputs; |
|
| 20 | + | pub mod portable; |
|
| 19 | 21 | pub mod runner; |
|
| 20 | 22 | pub mod swarm; |
|
| 23 | + | #[cfg(unix)] |
|
| 21 | 24 | pub mod syscall; |
|
| 25 | + | #[cfg(not(unix))] |
|
| 26 | + | #[path = "syscall_none.rs"] |
|
| 27 | + | pub mod syscall; |
|
| 28 | + | pub mod tier; |
|
| 22 | 29 | pub mod update; |
|
| 23 | 30 | pub mod wait; |
|
| 24 | 31 | pub mod wasm; |
|
| … | 1 unchanged lines |
modified crates/flow/src/main.rs +8 −3
| … | 308 unchanged lines |
||
| 309 | 309 | // A new build on trial that keeps failing to start goes back to |
|
| 310 | 310 | // the one before, which then runs in its place (#85). |
|
| 311 | 311 | if let flow::update::Start::RolledBack(p) = flow::update::on_start(&home.root)? { |
|
| 312 | - | use std::os::unix::process::CommandExt; |
|
| 313 | - | let err = std::process::Command::new(&p.exe).args(std::env::args_os().skip(1)).exec(); |
|
| 314 | - | bail!("starting flow {} again after the rollback: {err}", p.from); |
|
| 312 | + | #[cfg(unix)] |
|
| 313 | + | { |
|
| 314 | + | use std::os::unix::process::CommandExt; |
|
| 315 | + | let err = std::process::Command::new(&p.exe).args(std::env::args_os().skip(1)).exec(); |
|
| 316 | + | bail!("starting flow {} again after the rollback: {err}", p.from); |
|
| 317 | + | } |
|
| 318 | + | #[cfg(not(unix))] |
|
| 319 | + | bail!("rolled back to flow {}; start the runner again", p.from); |
|
| 315 | 320 | } |
|
| 316 | 321 | // The runner's oxy state (identity, user config) stays in its home. |
|
| 317 | 322 | if std::env::var_os("OXY_HOME").is_none() { |
|
| … | 295 unchanged lines |
modified crates/flow/src/outputs.rs +40 −6
| … | 34 unchanged lines |
||
| 35 | 35 | /// [`MAX_ARTIFACT_BYTES`]. The job wrote that tree, so no link is |
|
| 36 | 36 | /// followed on the way: each directory is opened with `O_NOFOLLOW`, and |
|
| 37 | 37 | /// so is the file. |
|
| 38 | + | #[cfg(unix)] |
|
| 38 | 39 | pub fn read_dir_file(base: &Path, inner: &str) -> Result<Vec<u8>> { |
|
| 39 | 40 | use std::ffi::CString; |
|
| 40 | - | use std::io::Read; |
|
| 41 | 41 | use std::os::fd::FromRawFd; |
|
| 42 | - | let parts: Vec<&str> = inner.split('/').filter(|p| !p.is_empty()).collect(); |
|
| 43 | - | if parts.is_empty() || parts.iter().any(|p| *p == "." || *p == "..") { |
|
| 44 | - | bail!("{inner} is not a clean path"); |
|
| 45 | - | } |
|
| 42 | + | let parts = clean_parts(inner)?; |
|
| 46 | 43 | let c = |s: &str| CString::new(s).context("a path holds NUL"); |
|
| 47 | 44 | let mut dir = unsafe { libc::open(c(&base.to_string_lossy())?.as_ptr(), libc::O_DIRECTORY | libc::O_RDONLY | libc::O_CLOEXEC) }; |
|
| 48 | 45 | if dir < 0 { |
|
| … | 14 unchanged lines |
||
| 63 | 60 | unsafe { libc::close(dir) }; |
|
| 64 | 61 | if fd < 0 { |
|
| 65 | 62 | bail!("{inner} is not a regular file in the job's files ({err})"); |
|
| 63 | + | } |
|
| 64 | + | read_open(unsafe { std::fs::File::from_raw_fd(fd) }, inner) |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | /// [`read_dir_file`] on Windows (oxygenflow#83), where a WASM job's files |
|
| 68 | + | /// are a directory: each step is checked to be a directory and not a link |
|
| 69 | + | /// (nor a junction), then the file to be a regular file, before it is read. |
|
| 70 | + | #[cfg(not(unix))] |
|
| 71 | + | pub fn read_dir_file(base: &Path, inner: &str) -> Result<Vec<u8>> { |
|
| 72 | + | let parts = clean_parts(inner)?; |
|
| 73 | + | let mut at = base.to_path_buf(); |
|
| 74 | + | let (last, dirs) = parts.split_last().unwrap(); |
|
| 75 | + | for d in dirs { |
|
| 76 | + | at.push(d); |
|
| 77 | + | let md = std::fs::symlink_metadata(&at).with_context(|| format!("{inner}: {d} is not in the job's files"))?; |
|
| 78 | + | if !md.is_dir() || md.file_type().is_symlink() { |
|
| 79 | + | bail!("{inner}: {d} is not a directory in the job's files"); |
|
| 80 | + | } |
|
| 66 | 81 | } |
|
| 67 | - | let file = unsafe { std::fs::File::from_raw_fd(fd) }; |
|
| 82 | + | at.push(last); |
|
| 83 | + | let md = std::fs::symlink_metadata(&at).with_context(|| format!("{inner} is not in the job's files"))?; |
|
| 84 | + | if !md.is_file() || md.file_type().is_symlink() { |
|
| 85 | + | bail!("{inner} is not a regular file in the job's files (it may be missing or a link)"); |
|
| 86 | + | } |
|
| 87 | + | read_open(std::fs::File::open(&at)?, inner) |
|
| 88 | + | } |
|
| 89 | + | ||
| 90 | + | /// `inner`'s parts, refusing an empty path, `.` and `..`. |
|
| 91 | + | fn clean_parts(inner: &str) -> Result<Vec<&str>> { |
|
| 92 | + | let parts: Vec<&str> = inner.split('/').filter(|p| !p.is_empty()).collect(); |
|
| 93 | + | if parts.is_empty() || parts.iter().any(|p| *p == "." || *p == "..") { |
|
| 94 | + | bail!("{inner} is not a clean path"); |
|
| 95 | + | } |
|
| 96 | + | Ok(parts) |
|
| 97 | + | } |
|
| 98 | + | ||
| 99 | + | /// Read an opened artifact file, at most [`MAX_ARTIFACT_BYTES`]. |
|
| 100 | + | fn read_open(file: std::fs::File, inner: &str) -> Result<Vec<u8>> { |
|
| 101 | + | use std::io::Read; |
|
| 68 | 102 | let meta = file.metadata()?; |
|
| 69 | 103 | if !meta.is_file() { |
|
| 70 | 104 | bail!("{inner} is not a regular file in the job's files (it may be missing or a link)"); |
|
| … | 248 unchanged lines |
modified crates/flow/src/runner.rs +7 −4
| … | 406 unchanged lines |
||
| 407 | 407 | let path = home.root.join(crate::home::RUNNER_TOML); |
|
| 408 | 408 | { |
|
| 409 | 409 | use std::io::Write; |
|
| 410 | - | use std::os::unix::fs::OpenOptionsExt; |
|
| 411 | 410 | let tmp = path.with_extension("toml.tmp"); |
|
| 412 | - | let mut f = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(&tmp)?; |
|
| 411 | + | let mut f = crate::portable::private(std::fs::OpenOptions::new().write(true).create(true).truncate(true)).open(&tmp)?; |
|
| 413 | 412 | f.write_all(text.as_bytes())?; |
|
| 414 | 413 | f.sync_all()?; |
|
| 415 | 414 | std::fs::rename(&tmp, &path)?; |
|
| … | 247 unchanged lines |
||
| 663 | 662 | crate::update::note_runner(&home.root); |
|
| 664 | 663 | // `flow runner update` swapped a new build in and asks this one to |
|
| 665 | 664 | // leave with the notice it gave (#85). |
|
| 666 | - | let handler = on_update as extern "C" fn(libc::c_int) as libc::sighandler_t; |
|
| 667 | - | unsafe { libc::signal(libc::SIGUSR1, handler) }; |
|
| 665 | + | #[cfg(unix)] |
|
| 666 | + | { |
|
| 667 | + | let handler = on_update as extern "C" fn(libc::c_int) as libc::sighandler_t; |
|
| 668 | + | unsafe { libc::signal(libc::SIGUSR1, handler) }; |
|
| 669 | + | } |
|
| 668 | 670 | if let Some(p) = crate::update::pending(&home.root) { |
|
| 669 | 671 | // On trial: a build that does not reach the hub in time goes back. |
|
| 670 | 672 | let root = home.root.clone(); |
|
| … | 15 unchanged lines |
||
| 686 | 688 | /// Set by SIGUSR1: a new build is in place; leave for it. |
|
| 687 | 689 | static UPDATE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); |
|
| 688 | 690 | ||
| 691 | + | #[cfg(unix)] |
|
| 689 | 692 | extern "C" fn on_update(_: libc::c_int) { |
|
| 690 | 693 | UPDATE.store(true, std::sync::atomic::Ordering::SeqCst); |
|
| 691 | 694 | } |
|
| … | 1551 unchanged lines |
modified crates/flow/src/swarm.rs +1 −2
| … | 502 unchanged lines |
||
| 503 | 503 | let made = rcgen::generate_simple_self_signed(vec!["flow-swarm".to_string()]).context("make the swarm's certificate")?; |
|
| 504 | 504 | let (cert, key) = (made.cert.der().to_vec(), made.signing_key.serialize_der()); |
|
| 505 | 505 | { |
|
| 506 | - | use std::os::unix::fs::OpenOptionsExt; |
|
| 507 | - | let mut f = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(&key_path)?; |
|
| 506 | + | let mut f = crate::portable::private(std::fs::OpenOptions::new().write(true).create(true).truncate(true)).open(&key_path)?; |
|
| 508 | 507 | f.write_all(&key)?; |
|
| 509 | 508 | } |
|
| 510 | 509 | std::fs::write(&cert_path, &cert)?; |
|
| … | 473 unchanged lines |
modified crates/flow/src/syscall.rs +2 −34
| … | 19 unchanged lines |
||
| 20 | 20 | use crate::guest::{dev_links, job_env, loopback_up, resolve_program, PATH}; |
|
| 21 | 21 | use crate::home::Home; |
|
| 22 | 22 | use crate::microvm::{Outcome, Sink}; |
|
| 23 | + | pub use crate::tier::disk_dir; |
|
| 24 | + | pub(crate) use crate::tier::{lines, self_hash, FAILED}; |
|
| 23 | 25 | use anyhow::{bail, Context, Result}; |
|
| 24 | 26 | use flow_core::api::{Record, SecretInput}; |
|
| 25 | 27 | use flow_core::{Environment, Job, MountSource, Network, Sandbox}; |
|
| 26 | 28 | use serde::{Deserialize, Serialize}; |
|
| 27 | 29 | use std::ffi::CString; |
|
| 28 | - | use std::io::{BufRead, BufReader, Read}; |
|
| 29 | 30 | use std::path::{Path, PathBuf}; |
|
| 30 | 31 | use std::sync::{mpsc, OnceLock}; |
|
| 31 | 32 | use std::time::{Duration, Instant}; |
|
| … | 23 unchanged lines |
||
| 55 | 56 | } |
|
| 56 | 57 | } |
|
| 57 | 58 | ||
| 58 | - | pub(crate) fn self_hash() -> &'static str { |
|
| 59 | - | static HASH: OnceLock<String> = OnceLock::new(); |
|
| 60 | - | HASH.get_or_init(|| crate::home::hash_file(Path::new("/proc/self/exe")).unwrap_or_else(|_| "unknown".into())) |
|
| 61 | - | } |
|
| 62 | - | ||
| 63 | 59 | fn kernel_release() -> String { |
|
| 64 | 60 | crate::facts::kernel().unwrap_or_else(|| "unknown".into()) |
|
| 65 | 61 | } |
|
| … | 223 unchanged lines |
||
| 289 | 285 | peak_memory_mib, |
|
| 290 | 286 | pruned: Vec::new(), |
|
| 291 | 287 | }) |
|
| 292 | - | } |
|
| 293 | - | ||
| 294 | - | /// Where the `n`th `ephemeral://disk` mount of a job keeps its files. |
|
| 295 | - | pub fn disk_dir(scratch: &Path, n: usize) -> PathBuf { |
|
| 296 | - | scratch.join(format!("mount-{n}")) |
|
| 297 | - | } |
|
| 298 | - | ||
| 299 | - | /// How the helper says the job never started, on its stderr. |
|
| 300 | - | pub(crate) const FAILED: &str = "\u{1e}F "; |
|
| 301 | - | ||
| 302 | - | pub(crate) fn lines(r: impl Read, stdout: bool, tx: mpsc::Sender<(bool, Vec<u8>)>) { |
|
| 303 | - | let mut r = BufReader::new(r); |
|
| 304 | - | let mut buf = Vec::new(); |
|
| 305 | - | loop { |
|
| 306 | - | buf.clear(); |
|
| 307 | - | // A long line is cut at 16 KiB, as flow-init cuts it. |
|
| 308 | - | match (&mut r).take(16 * 1024).read_until(b'\n', &mut buf) { |
|
| 309 | - | Ok(0) | Err(_) => break, |
|
| 310 | - | Ok(_) => { |
|
| 311 | - | if buf.last() == Some(&b'\n') { |
|
| 312 | - | buf.pop(); |
|
| 313 | - | } |
|
| 314 | - | if tx.send((stdout, buf.clone())).is_err() { |
|
| 315 | - | break; |
|
| 316 | - | } |
|
| 317 | - | } |
|
| 318 | - | } |
|
| 319 | - | } |
|
| 320 | 288 | } |
|
| 321 | 289 | ||
| 322 | 290 | /// The image's tree, unpacked from its ext4 file once per hash with |
|
| … | 584 unchanged lines |
modified crates/flow/src/update.rs +16 −5
| … | 184 unchanged lines |
||
| 185 | 185 | /// tell the running runner (if any) to leave within `notice`, finishing |
|
| 186 | 186 | /// what fits and handing back the rest, so its unit starts the new build. |
|
| 187 | 187 | 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 | + | } |
|
| 188 | 191 | if home.config.runner.as_ref().and_then(|r| r.update.as_ref()).is_some_and(|u| !u.allow) { |
|
| 189 | 192 | bail!("flow.toml says [runner.update] allow = false: this runner takes no updates"); |
|
| 190 | 193 | } |
|
| … | 4 unchanged lines |
||
| 195 | 198 | let staged = exe.with_file_name(NEW); |
|
| 196 | 199 | let _ = std::fs::remove_file(&staged); |
|
| 197 | 200 | std::fs::copy(file, &staged).with_context(|| format!("staging {} as {}", file.display(), staged.display()))?; |
|
| 198 | - | { |
|
| 199 | - | use std::os::unix::fs::PermissionsExt; |
|
| 200 | - | std::fs::set_permissions(&staged, std::fs::Permissions::from_mode(0o755))?; |
|
| 201 | - | } |
|
| 201 | + | crate::portable::set_mode(&staged, 0o755)?; |
|
| 202 | 202 | let checked = check(&staged, &home.root).and_then(|to| { |
|
| 203 | 203 | if older(&to, &from) && !allow_older { |
|
| 204 | 204 | bail!("flow {to} is older than this runner's {from} (--allow-older to go back)"); |
|
| … | 16 unchanged lines |
||
| 221 | 221 | Pending { from: from.clone(), to: to.clone(), exe: exe.to_path_buf(), at_ms: now_ms(), notice_secs: notice.as_secs(), starts: 0 }; |
|
| 222 | 222 | save(&home.root, &p)?; |
|
| 223 | 223 | log(&home.root, "installed", &from, &to, "flow runner update --from"); |
|
| 224 | - | let told = running(&home.root).filter(|pid| unsafe { libc::kill(*pid as libc::pid_t, libc::SIGUSR1) } == 0); |
|
| 224 | + | let told = running(&home.root).filter(|pid| tell(*pid)); |
|
| 225 | 225 | Ok(Installed { from, to, exe: exe.to_path_buf(), told }) |
|
| 226 | + | } |
|
| 227 | + | ||
| 228 | + | /// Ask runner `pid` to leave for the new build (SIGUSR1). |
|
| 229 | + | #[cfg(unix)] |
|
| 230 | + | fn tell(pid: u32) -> bool { |
|
| 231 | + | unsafe { libc::kill(pid as libc::pid_t, libc::SIGUSR1) == 0 } |
|
| 232 | + | } |
|
| 233 | + | ||
| 234 | + | #[cfg(not(unix))] |
|
| 235 | + | fn tell(_pid: u32) -> bool { |
|
| 236 | + | false |
|
| 226 | 237 | } |
|
| 227 | 238 | ||
| 228 | 239 | /// The running runner's process id, from its pid file, if that process is |
|
| … | 165 unchanged lines |
modified crates/flow/src/workspace.rs +18 −8
| … | 37 unchanged lines |
||
| 38 | 38 | } |
|
| 39 | 39 | if *kind == NodeKind::Symlink { |
|
| 40 | 40 | let to = String::from_utf8_lossy(&bytes).into_owned(); |
|
| 41 | - | std::os::unix::fs::symlink(&to, &target).with_context(|| format!("symlink {path}"))?; |
|
| 41 | + | crate::portable::symlink(Path::new(&to), &target).with_context(|| format!("symlink {path}"))?; |
|
| 42 | 42 | } else { |
|
| 43 | 43 | std::fs::write(&target, &bytes).with_context(|| format!("write {path}"))?; |
|
| 44 | 44 | if f.mode.exec { |
|
| 45 | - | use std::os::unix::fs::PermissionsExt; |
|
| 46 | - | std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o755))?; |
|
| 45 | + | crate::portable::set_mode(&target, 0o755)?; |
|
| 47 | 46 | } |
|
| 48 | 47 | } |
|
| 49 | 48 | files.push((path.clone(), *blake3::hash(&bytes).as_bytes())); |
|
| … | 29 unchanged lines |
||
| 79 | 78 | std::fs::create_dir_all(&target)?; |
|
| 80 | 79 | } else if ft.is_symlink() { |
|
| 81 | 80 | let to = std::fs::read_link(entry.path())?; |
|
| 82 | - | std::os::unix::fs::symlink(&to, &target)?; |
|
| 81 | + | crate::portable::symlink(&to, &target)?; |
|
| 83 | 82 | files.push((rel_s, *blake3::hash(to.to_string_lossy().as_bytes()).as_bytes())); |
|
| 84 | 83 | } else if ft.is_file() { |
|
| 85 | 84 | std::fs::copy(entry.path(), &target)?; |
|
| … | 16 unchanged lines |
||
| 102 | 101 | .map(|m| m.len().max(4096)) |
|
| 103 | 102 | .sum(); |
|
| 104 | 103 | let mib = used * 12 / 10 / (1 << 20) + 512; |
|
| 105 | - | let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) }; |
|
| 104 | + | let (uid, gid) = owner(); |
|
| 106 | 105 | let status = std::process::Command::new("mkfs.ext4") |
|
| 107 | 106 | .args(["-q", "-F", "-L", "workspace", "-E"]) |
|
| 108 | 107 | .arg(format!("root_owner={uid}:{gid}")) |
|
| … | 46 unchanged lines |
||
| 155 | 154 | /// the workspace. mkfs.ext4 creates the file sparse, so only what the job |
|
| 156 | 155 | /// writes takes space on the runner. |
|
| 157 | 156 | pub fn empty_ext4(out: &Path, mib: u32, label: &str) -> Result<()> { |
|
| 158 | - | let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) }; |
|
| 157 | + | let (uid, gid) = owner(); |
|
| 159 | 158 | let status = std::process::Command::new("mkfs.ext4") |
|
| 160 | 159 | .args(["-q", "-F", "-L", label, "-E"]) |
|
| 161 | 160 | .arg(format!("root_owner={uid}:{gid}")) |
|
| … | 20 unchanged lines |
||
| 182 | 181 | } |
|
| 183 | 182 | } |
|
| 184 | 183 | ||
| 184 | + | /// The runner's user and group, which own a workspace image's root. |
|
| 185 | + | fn owner() -> (u32, u32) { |
|
| 186 | + | #[cfg(unix)] |
|
| 187 | + | { |
|
| 188 | + | unsafe { (libc::getuid(), libc::getgid()) } |
|
| 189 | + | } |
|
| 190 | + | #[cfg(not(unix))] |
|
| 191 | + | { |
|
| 192 | + | (0, 0) |
|
| 193 | + | } |
|
| 194 | + | } |
|
| 195 | + | ||
| 185 | 196 | fn open_up(dir: &Path) { |
|
| 186 | - | use std::os::unix::fs::PermissionsExt; |
|
| 187 | 197 | let Ok(meta) = std::fs::symlink_metadata(dir) else { return }; |
|
| 188 | 198 | if !meta.is_dir() { |
|
| 189 | 199 | return; |
|
| 190 | 200 | } |
|
| 191 | - | let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700)); |
|
| 201 | + | let _ = crate::portable::set_mode(dir, 0o700); |
|
| 192 | 202 | if let Ok(entries) = std::fs::read_dir(dir) { |
|
| 193 | 203 | for e in entries.flatten() { |
|
| 194 | 204 | open_up(&e.path()); |
|
| … | 43 unchanged lines |