Add bread-utils and bread-onnx: shared crates for ecosystem-wide duplication
bread-utils extracts genuinely duplicated logic found across breadbox, breadclip, breadmon, breadcrumbs, bos-settings, and breadhelp: - hypr: Hyprland socket1 request/response client (breadbox's get_active_workspace + breadclip's position.rs hyprctl_json were near-identical), socket2 path resolution (breadmon), and a version-tolerant `fullscreen` field parser (Hyprland has shipped both bool and int representations across versions). - singleton: correct flock-based single-instance toggle, replacing the TOCTOU-prone read-pid/check-proc/kill/write-pid pattern duplicated verbatim between breadbox and breadclip (breadclip's own comment says "matches breadbox pattern"). - proc: breadcrumbs' timeout-guarded subprocess runner, promoted verbatim as the one implementation in the ecosystem that already got this right. - atomic + xdg: atomic (temp-then-rename) file writes with an optional .bak-before-overwrite variant, and XDG path helpers that never fall back to a literal "~/..." string (the exact breadclip-core and breadpad-shared bug: PathBuf never expands `~`). - tomlcfg (feature "toml"): the load_doc/save_doc TOML-editing discipline bos-settings and breadhelp both implemented byte-for-byte identically in the same fix pass that introduced it. - gtk_popup (feature "gtk"): layer-shell overlay window setup, visible-row navigation, and click-outside-close, deduplicated from breadbox and breadclip (~150 duplicated lines, per both apps' own "same as breadbox" comments). bread-onnx extracts the embedding pipeline (tokenize -> tensor build -> mean-pool -> L2-normalize) duplicated near-verbatim between breadarr and breadsearch, a shared execution-provider session builder with loud EP- registration logging, and a model download+integrity helper. Defaults AMD iGPU acceleration to ort::ep::MIGraphX (not ROCm) per this machine's own breadsearch-gpu-backends lesson: ROCMExecutionProvider silently no-ops to CPU on distro ROCm onnxruntime builds compiled with --use_migraphx. Both crates build and pass their own test suites standalone. Consumer migrations follow in subsequent commits.
This commit is contained in:
parent
394a252f9e
commit
853ee33415
17 changed files with 2503 additions and 5 deletions
174
bread-utils/src/proc.rs
Normal file
174
bread-utils/src/proc.rs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
//! Timeout-guarded subprocess execution.
|
||||
//!
|
||||
//! Promoted verbatim from `breadcrumbs/src/util.rs` (the one implementation
|
||||
//! in the ecosystem that already got this right — see the audit note in
|
||||
//! `bread-utils`'s crate root). Several other repos shell out to
|
||||
//! Wayland/Hyprland tools (`hyprctl`, `grim`, `wl-paste`, ...) via bare
|
||||
//! `std::process::Command` with no timeout at all, so a hung child can wedge
|
||||
//! the whole caller indefinitely. `run`/`run_with_stdin` below kill the
|
||||
//! child and return a failed [`Output`] once `timeout` elapses instead.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[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 subprocess can never wedge the caller.
|
||||
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. Useful for
|
||||
/// handing secrets (e.g. Wi-Fi PSKs, API tokens) to a CLI 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 callers parse (hyprctl JSON keys,
|
||||
// status output, ...) is stable regardless of the user's LANG.
|
||||
.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
|
||||
}
|
||||
|
||||
/// Run a command and parse its stdout as JSON on success. Convenience for the
|
||||
/// very common `hyprctl -j <subcommand>` / `<tool> --json` pattern.
|
||||
pub fn run_json(prog: &str, args: &[&str], timeout: Duration) -> Option<serde_json::Value> {
|
||||
let out = run(prog, args, timeout);
|
||||
if !out.success {
|
||||
return None;
|
||||
}
|
||||
serde_json::from_str(&out.stdout).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn run_captures_stdout() {
|
||||
let out = run("printf", &["hello"], Duration::from_secs(2));
|
||||
assert!(out.success);
|
||||
assert_eq!(out.stdout, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_reports_failure_for_nonzero_exit() {
|
||||
let out = run("sh", &["-c", "exit 3"], Duration::from_secs(2));
|
||||
assert!(!out.success);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_kills_hung_child_after_timeout() {
|
||||
let start = Instant::now();
|
||||
let out = run("sleep", &["30"], Duration::from_millis(200));
|
||||
assert!(!out.success);
|
||||
assert!(start.elapsed() < Duration::from_secs(5), "child was not killed promptly");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_with_stdin_feeds_child_input() {
|
||||
let out = run_with_stdin("cat", &[], Some("secret-data"), Duration::from_secs(2));
|
||||
assert!(out.success);
|
||||
assert_eq!(out.stdout, "secret-data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_json_parses_stdout() {
|
||||
let out = run_json("printf", &["{\"a\":1}"], Duration::from_secs(2));
|
||||
assert_eq!(out.unwrap()["a"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_json_returns_none_on_failure() {
|
||||
let out = run_json("sh", &["-c", "exit 1"], Duration::from_secs(2));
|
||||
assert!(out.is_none());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue