Daemon release-ready

This commit is contained in:
Breadway 2026-05-11 12:21:23 +08:00
parent 730a8b61d7
commit d537fc9318
9 changed files with 358 additions and 1207 deletions

View file

@ -1,21 +1,24 @@
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::RwLock;
use bread_shared::{AdapterSource, BreadEvent, RawEvent};
use serde_json::{json, Value};
use crate::core::types::DeviceClass;
/// How many multiples of `dedup_window_ms` an entry must be idle before eviction.
const EVICT_MULTIPLIER: u64 = 60;
pub struct EventNormalizer {
dedup_window_ms: u64,
recent: Mutex<HashMap<String, u64>>,
recent: RwLock<HashMap<String, u64>>,
}
impl EventNormalizer {
pub fn new(dedup_window_ms: u64) -> Self {
Self {
dedup_window_ms,
recent: Mutex::new(HashMap::new()),
recent: RwLock::new(HashMap::new()),
}
}
@ -164,16 +167,36 @@ impl EventNormalizer {
fn accept(&self, event: &BreadEvent) -> bool {
let key = format!("{}:{}", event.event, event.data);
let mut recent = self.recent.lock().expect("normalizer dedup mutex poisoned");
let now = event.timestamp;
// Fast path: check under read lock first.
{
let recent = self.recent.read().unwrap_or_else(|p| p.into_inner());
if let Some(last) = recent.get(&key) {
if now.saturating_sub(*last) < self.dedup_window_ms {
return false;
}
}
}
// Slow path: acquire write lock, re-check, insert, and periodically evict.
let mut recent = self.recent.write().unwrap_or_else(|p| p.into_inner());
// Re-check after acquiring write lock (another thread may have inserted between locks).
if let Some(last) = recent.get(&key) {
if now.saturating_sub(*last) < self.dedup_window_ms {
return false;
}
}
recent.insert(key, now);
recent.insert(key.clone(), now);
// Evict stale entries to prevent unbounded growth.
let evict_before = now.saturating_sub(self.dedup_window_ms.saturating_mul(EVICT_MULTIPLIER));
if evict_before > 0 {
recent.retain(|_, &mut last| last >= evict_before);
}
true
}
}

View file

@ -32,10 +32,19 @@ impl SubscriptionTable {
return false;
};
// swap_remove moves the last element into `idx`. We need to update by_id
// for that element. But first, remove its stale entry (it was at the last
// position before the swap); then re-insert it at the new position.
let last_idx = self.entries.len() - 1;
self.entries.swap_remove(idx);
if let Some(swapped) = self.entries.get(idx) {
self.by_id.insert(swapped.id, idx);
if idx < self.entries.len() {
// The element that was at `last_idx` is now at `idx`.
let swapped_id = self.entries[idx].id;
self.by_id.remove(&swapped_id); // remove stale last_idx entry
self.by_id.insert(swapped_id, idx);
}
true
}

View file

@ -8,6 +8,7 @@ use anyhow::{anyhow, Result};
use bread_shared::{AdapterSource, BreadEvent};
use mlua::{Function, Lua, LuaSerdeExt, RegistryKey, Value};
use tokio::sync::{mpsc, oneshot};
use tokio::task;
use tracing::{error, info, warn};
use crate::core::config::Config;
@ -239,13 +240,27 @@ impl LuaEngine {
profile_tbl.set("activate", activate_fn)?;
bread.set("profile", profile_tbl)?;
// Fire-and-forget: the process is launched on a blocking thread and the
// Lua handler returns immediately. The Lua runtime is never stalled waiting
// for a slow or hanging process. Exit code is logged but not returned to Lua.
let exec_fn = self.lua.create_function(move |_lua, cmd: String| {
let status = std::process::Command::new("sh")
.arg("-lc")
.arg(&cmd)
.status()
.map_err(mlua::Error::external)?;
Ok(status.code().unwrap_or_default())
task::spawn_blocking(move || {
match std::process::Command::new("sh")
.arg("-lc")
.arg(&cmd)
.status()
{
Ok(status) => {
if !status.success() {
tracing::warn!(cmd = %cmd, code = ?status.code(), "bread.exec exited non-zero");
}
}
Err(err) => {
tracing::error!(cmd = %cmd, error = %err, "bread.exec failed to spawn");
}
}
});
Ok(())
})?;
bread.set("exec", exec_fn)?;