Transform the single binary crate into a 9-crate workspace monorepo: - Root Cargo.toml as [workspace] manager with resolver = "2" - zesdex-entities: Domain entity types (session, settings, store, message, etc.) - zesdex-utils: Pure utility functions (error, logger, pagination, slug, clipboard) - zesdex-dto: Data Transfer Objects for LLM provider API communication - zesdex-ipc: Unix-socket IPC layer (client/server/framing/protocol) - zesdex-iam: Identity & Access Management (Clean Architecture: domain/application/infrastructure) - zesdex-cms: Content Management (Clean Architecture: domain/application/infrastructure) - zesdex-middleware: HTTP middleware (Auth, CORS, Rate Limiting) - zesdex-libs: Composition root (AppContext, DB init, JWT, Argon2) - zesdex-backend: Main binary entry point + seed/migrate binaries - DevOps: Dockerfile, docker-compose, Nix (flake/shell/default), CI/CD updates - Remove dead root src/ and src-misc/ directories All crate re-exports maintain backward compatibility with original crate::model::*, crate::dto::*, crate::ipc::* module paths. Feature crates enforce strict layer separation: domain -> application -> infrastructure with generic trait-based dependency injection.
87 lines
3.0 KiB
Rust
87 lines
3.0 KiB
Rust
#![allow(
|
|
clippy::cast_possible_truncation,
|
|
clippy::cast_sign_loss,
|
|
clippy::cast_precision_loss,
|
|
clippy::cast_possible_wrap
|
|
)]
|
|
//! Global registry of running background bash jobs, and control operations
|
|
//! (output polling, kill) exposed to the rest of the app.
|
|
//!
|
|
//! Flow: a process-wide `Mutex<HashMap<String, BashJob>>` (lazily built via
|
|
//! `OnceLock`) holds every job spawned via `bgbash::job::spawn_bash_job` →
|
|
//! `bash_output` drains new lines for a given job id → `bash_kill` removes
|
|
//! a job from the map and signals its child process.
|
|
//!
|
|
//! Why: a single static map (rather than storing jobs in `AppStateRest`)
|
|
//! lets background jobs outlive the borrow of any particular state mutation
|
|
//! and be looked up by id from tool calls issued at arbitrary points.
|
|
use std::collections::HashMap;
|
|
use std::sync::Mutex;
|
|
use std::sync::OnceLock;
|
|
|
|
use super::job::BashJob;
|
|
|
|
/// Lazily-initialised, process-wide registry of background bash jobs keyed
|
|
/// by job id.
|
|
///
|
|
/// Return: a reference to the static `Mutex<HashMap<...>>`, created on
|
|
/// first access.
|
|
pub(crate) fn bash_jobs_map() -> &'static Mutex<HashMap<String, BashJob>> {
|
|
static JOBS: OnceLock<Mutex<HashMap<String, BashJob>>> = OnceLock::new();
|
|
JOBS.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
/// Drain any newly available output lines from a background bash job.
|
|
///
|
|
/// Flow: look up the job by id → repeatedly call `try_read_line()` until it
|
|
/// returns `None` → collect into a Vec.
|
|
///
|
|
/// Why: non-blocking; a job that hasn't produced new output yields no lines
|
|
/// rather than blocking the caller.
|
|
///
|
|
/// Return: `Some(lines)` if at least one new line was read, `None` if the
|
|
/// job doesn't exist, the lock is poisoned, or there was nothing new to read.
|
|
pub fn bash_output(id: &str) -> Option<Vec<String>> {
|
|
let mut map = bash_jobs_map().lock().ok()?;
|
|
let job = map.get_mut(id)?;
|
|
let mut lines = Vec::new();
|
|
while let Some(line) = job.try_read_line() {
|
|
lines.push(line);
|
|
}
|
|
if lines.is_empty() {
|
|
None
|
|
} else {
|
|
Some(lines)
|
|
}
|
|
}
|
|
|
|
/// Terminate a running background bash job and remove it from the registry.
|
|
///
|
|
/// Flow: remove the job from the map → if it has a valid child PID, send
|
|
/// `SIGTERM` to it (unix only) → return.
|
|
///
|
|
/// Why: removing from the map first means a concurrent lookup can no longer
|
|
/// see the job even if the signal delivery is delayed.
|
|
///
|
|
/// Return: `Ok(())` on success, `Err` if the lock is poisoned or no job
|
|
/// with that id exists.
|
|
pub fn bash_kill(id: &str) -> anyhow::Result<()> {
|
|
let mut map = bash_jobs_map()
|
|
.lock()
|
|
.map_err(|e| anyhow::anyhow!("lock error: {e}"))?;
|
|
let job = map.remove(id);
|
|
match job {
|
|
Some(job) => {
|
|
// Actually terminate the child process via its PID
|
|
if job.child_pid > 0 {
|
|
#[cfg(unix)]
|
|
unsafe {
|
|
libc::kill(job.child_pid as i32, libc::SIGTERM);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
None => anyhow::bail!("bash job '{id}' not found"),
|
|
}
|
|
}
|