From 59416fb273d20313e236e03c0f839a371192534f Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 17 Jul 2026 09:35:23 +0800 Subject: [PATCH 1/2] Promote util::run to bread_utils::proc; delegate write_atomic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This crate's util::run/run_with_stdin/run_ok/Output was cited as the one correct, timeout-guarded subprocess implementation in the ecosystem worth sharing — it's now promoted verbatim into bread-utils and re-exported here so every existing crate::util::{run, ...} call site (notify.rs, nm.rs, status.rs, tailscale.rs) keeps working unchanged. write_atomic similarly delegates to bread_utils::atomic::write_atomic (path dependency for now, see the TODO in Cargo.toml). home_dir/command_exists/timestamp stay local — they're breadcrumbs-specific, not duplicated elsewhere. New worktree for this repo (none existed from tonight's earlier passes, since breadcrumbs needed no audit fixes then). Builds clean; all 74 unit tests + 25 CLI integration tests pass. --- Cargo.lock | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + src/util.rs | 153 +++++---------------------------------------- 3 files changed, 191 insertions(+), 140 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 222284d..0efeaea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,7 +38,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -49,19 +49,35 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "bread-utils" +version = "0.2.3" +dependencies = [ + "dirs", + "serde", + "serde_json", ] [[package]] name = "breadcrumbs" version = "2.1.5" dependencies = [ + "bread-utils", "clap", "serde", "serde_json", "toml", ] +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.6.1" @@ -108,12 +124,44 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -148,6 +196,21 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.8.0" @@ -160,6 +223,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -178,6 +247,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "serde" version = "1.0.228" @@ -247,6 +327,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.23" @@ -300,12 +400,27 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -315,6 +430,63 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "winnow" version = "0.7.15" diff --git a/Cargo.toml b/Cargo.toml index b62a77a..25c0424 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } toml = "0.8" serde_json = "1" +# TODO(owner): switch to tag-pinned git dependency once bread-utils is merged and tagged, matching the bread-theme pattern +bread-utils = { path = "../bread-ecosystem-fix-worktree/bread-utils" } [profile.release] opt-level = "s" diff --git a/src/util.rs b/src/util.rs index db9bc8d..ffd63d2 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,9 +1,5 @@ -use std::fs; -use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use std::thread; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub fn home_dir() -> PathBuf { std::env::var_os("HOME") @@ -16,35 +12,13 @@ pub fn home_dir() -> PathBuf { /// concurrent reader (the watch daemon reloads config every tick) and never /// leaves a half-written file behind on crash. Because the temp file is created /// with `mode` up front, secrets never exist world-readable even briefly. +/// +/// Delegates to `bread_utils::atomic::write_atomic` — this crate's own +/// version of this function was promoted verbatim into the shared +/// `bread-utils` crate (see that crate's `atomic` module doc comment) as +/// the one implementation in the ecosystem that already got this right. pub fn write_atomic(path: &Path, contents: &str, mode: u32) -> std::io::Result<()> { - let dir = path.parent().unwrap_or_else(|| Path::new(".")); - fs::create_dir_all(dir)?; - let stem = path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("breadcrumbs"); - let tmp = dir.join(format!(".{stem}.tmp.{}", std::process::id())); - - let mut open = fs::OpenOptions::new(); - open.write(true).create(true).truncate(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - open.mode(mode); - } - #[cfg(not(unix))] - let _ = mode; - - let res = (|| { - let mut f = open.open(&tmp)?; - f.write_all(contents.as_bytes())?; - f.sync_all()?; - fs::rename(&tmp, path) - })(); - if res.is_err() { - let _ = fs::remove_file(&tmp); - } - res + bread_utils::atomic::write_atomic(path, contents, Some(mode)) } pub fn command_exists(name: &str) -> bool { @@ -58,111 +32,14 @@ pub fn command_exists(name: &str) -> bool { false } -#[derive(Debug, Clone)] -pub struct Output { - pub success: bool, - pub stdout: String, - pub stderr: String, -} - -impl Output { - pub fn failed() -> Output { - Output { - success: false, - stdout: String::new(), - stderr: String::new(), - } - } -} - -/// Run a command with a hard timeout. The child is killed if it overruns so a -/// hung nmcli/tailscale can never wedge the daemon. -pub fn run(prog: &str, args: &[&str], timeout: Duration) -> Output { - run_with_stdin(prog, args, None, timeout) -} - -/// Like [`run`], but feeds `stdin` to the child's standard input. Used to hand -/// secrets (e.g. Wi-Fi PSKs) to `nmcli --ask` without exposing them in argv, -/// where any local user could read them via `ps`. -pub fn run_with_stdin(prog: &str, args: &[&str], stdin: Option<&str>, timeout: Duration) -> Output { - let stdin_cfg = if stdin.is_some() { - Stdio::piped() - } else { - Stdio::null() - }; - let mut child = match Command::new(prog) - .args(args) - // Pin the C locale so message text we parse (nmcli states, monitor - // lines) is stable English regardless of the user's LANG. SSID/value - // bytes are unaffected. - .env("LC_ALL", "C") - .env("LANG", "C") - .stdin(stdin_cfg) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - { - Ok(c) => c, - Err(_) => return Output::failed(), - }; - - let mut stdout_pipe = child.stdout.take(); - let mut stderr_pipe = child.stderr.take(); - - let out_handle = thread::spawn(move || { - let mut buf = String::new(); - if let Some(ref mut p) = stdout_pipe { - let _ = p.read_to_string(&mut buf); - } - buf - }); - let err_handle = thread::spawn(move || { - let mut buf = String::new(); - if let Some(ref mut p) = stderr_pipe { - let _ = p.read_to_string(&mut buf); - } - buf - }); - - // Feed stdin only after the reader threads are draining stdout/stderr, so a - // child that writes more than a pipe buffer before consuming stdin can't - // deadlock against our blocking write. - if let Some(data) = stdin { - if let Some(mut sink) = child.stdin.take() { - let _ = sink.write_all(data.as_bytes()); - // Drop closes the pipe so the child's read sees EOF. - } - } - - let start = Instant::now(); - let status = loop { - match child.try_wait() { - Ok(Some(s)) => break Some(s), - Ok(None) => { - if start.elapsed() >= timeout { - let _ = child.kill(); - let _ = child.wait(); - break None; - } - thread::sleep(Duration::from_millis(50)); - } - Err(_) => break None, - } - }; - - let stdout = out_handle.join().unwrap_or_default(); - let stderr = err_handle.join().unwrap_or_default(); - - Output { - success: status.map(|s| s.success()).unwrap_or(false), - stdout, - stderr, - } -} - -pub fn run_ok(prog: &str, args: &[&str], timeout: Duration) -> bool { - run(prog, args, timeout).success -} +// `Output`, `run`, `run_with_stdin`, and `run_ok` used to be implemented +// directly in this module; that implementation was promoted verbatim into +// `bread_utils::proc` as the shared timeout-guarded subprocess runner for +// the whole ecosystem (several sibling repos shelled out with no timeout at +// all). Re-exported here so every existing `crate::util::{run, ...}` call +// site in this crate keeps working unchanged. +#[allow(unused_imports)] // Output isn't named directly elsewhere in this crate, but stays part of this module's public API. +pub use bread_utils::proc::{run, run_ok, run_with_stdin, Output}; /// Local "YYYY-MM-DD HH:MM:SS". Uses `date` for correct local time, falling /// back to a dependency-free UTC computation if it is unavailable. From c10e8dc5e89c3cd1be9d9c9970cf53e2a9ba7a4a Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 19 Jul 2026 03:52:39 +0800 Subject: [PATCH 2/2] Switch to tag-pinned bread-ecosystem deps; bump version to v2.1.6 --- .github/workflows/ci.yml | 38 -------------------------------------- Cargo.lock | 33 +++++++++++++++++---------------- Cargo.toml | 4 ++-- README.md | 2 +- packaging/arch/PKGBUILD | 4 ++-- 5 files changed, 22 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 5abaed1..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - workflow_dispatch: - -env: - CARGO_TERM_COLOR: always - RUSTFLAGS: "-D warnings" - -jobs: - test: - name: fmt · clippy · test · build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - - - name: Format check - run: cargo fmt --all -- --check - - - name: Clippy - run: cargo clippy --all-targets -- -D warnings - - - name: Test - run: cargo test --all --verbose - - - name: Release build - run: cargo build --release --verbose diff --git a/Cargo.lock b/Cargo.lock index 0efeaea..90deee0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,7 +54,8 @@ dependencies = [ [[package]] name = "bread-utils" -version = "0.2.3" +version = "0.3.0" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.3.0#8e82d2d833e992ce939a5b836f910ee109f2e939" dependencies = [ "dirs", "serde", @@ -63,7 +64,7 @@ dependencies = [ [[package]] name = "breadcrumbs" -version = "2.1.5" +version = "2.1.6" dependencies = [ "bread-utils", "clap", @@ -80,9 +81,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.6.1" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" dependencies = [ "clap_builder", "clap_derive", @@ -90,9 +91,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -213,9 +214,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "once_cell_polyfill" @@ -240,9 +241,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -290,9 +291,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -318,9 +319,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -498,6 +499,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 25c0424..e5e4be9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadcrumbs" -version = "2.1.5" +version = "2.1.6" edition = "2021" description = "Profile-aware Wi-Fi state machine with Tailscale handling and self-healing watch daemon" license = "MIT" @@ -15,7 +15,7 @@ serde = { version = "1", features = ["derive"] } toml = "0.8" serde_json = "1" # TODO(owner): switch to tag-pinned git dependency once bread-utils is merged and tagged, matching the bread-theme pattern -bread-utils = { path = "../bread-ecosystem-fix-worktree/bread-utils" } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.3.0" } [profile.release] opt-level = "s" diff --git a/README.md b/README.md index 6bc6220..5ac913f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ breadcrumbs sits on top of NetworkManager (`nmcli`) and manages your Wi-Fi based ## Installation ```bash -git clone https://github.com/Breadway/breadcrumbs +git clone https://git.breadway.dev/Breadway/breadcrumbs cd breadcrumbs cargo build --release # Copy to somewhere on your PATH: diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 8b84450..364bfab 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -1,11 +1,11 @@ -# Maintainer: Breadway +# Maintainer: Breadway pkgname=breadcrumbs pkgver=2.1.0 pkgrel=1 pkgdesc="Profile-aware Wi-Fi state machine with Tailscale integration" arch=('x86_64') -url="https://github.com/Breadway/breadcrumbs" +url="https://git.breadway.dev/Breadway/breadcrumbs" license=('MIT') # Some Rust deps (ring/mlua) build vendored C/asm into static archives; makepkg's # default -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read,