Fix 18 issues flagged in audit + bump to v0.6.2
P1-A: normalizer derives `online` from rtnetlink event kind (link.up/down,
route.default.changed, address.added/removed) so bread.network.connected
fires correctly on all systems using rtnetlink.
P1-B: stream_events consumes the subscribe ack before the event loop so the
first line is not printed as garbage.
P1-C: UPowerAdapter::probe() validates D-Bus synchronously before committing;
the sysfs fallback now actually triggers when D-Bus is unavailable.
P2-A: profile.list returns the full profile state (active + history) instead
of the always-empty profiles map.
P2-B: profile history capped at 50 entries in both StateCommand and
apply_event_to_state to prevent unbounded growth.
P2-C: RtnetlinkAdapter::new() no longer spawns an orphaned tokio task;
it validates availability by constructing and immediately dropping the
connection tuple.
P2-D: Lua-side hyprland_request_socket() logs a warn when multiple
Hyprland instances are found, matching the adapter-side behaviour.
P2-E: Malformed JSON from an IPC client returns an error response and
continues rather than closing the entire connection.
P3-A: Remove the `ends_with(".*")` prefix-match shortcut from both the
subscription table and the IPC event filter. `bread.*` now means
one segment (matching documented API semantics: `* = one segment`).
Tests updated accordingly.
P4-A: Remove unused `git2` and `glob` workspace dependencies (left over
from bread-sync extraction).
P4-B: breadd dev-dependency `tempfile` declared via workspace = true.
P4-C: Remove unreachable XDG_CONFIG_HOME branch in modules_dir(); dirs
already reads that var internally before returning None.
P4-D: Delete duplicate send_request_with_stream(); print_doctor() now
uses socket.exists() + send_request() directly.
P5-A: release.yml drops `--lib` from cargo test so integration tests run
in the release gate.
P6-A: bluetooth_spawn / bluetooth_query replace expect() on tokio runtime
construction with error logging / error propagation.
P6-B: Spin loops in lua/mod.rs add std:🧵:yield_now() after the
PAUSE hint to reduce CPU burn under sustained RwLock contention.
P6-C: All Mutex::lock().expect("... poisoned") in lua/mod.rs replaced with
unwrap_or_else(|e| e.into_inner()) for poison recovery.
P7-B: bread.system.startup event moved from main.rs into ipc::Server::serve()
so it fires after the socket is bound (smaller race window for early
subscribers).
This commit is contained in:
parent
0f3136ca8d
commit
3115a4230b
17 changed files with 146 additions and 136 deletions
|
|
@ -93,6 +93,14 @@ impl Server {
|
|||
|
||||
info!(socket = %self.socket_path.display(), "ipc server listening");
|
||||
|
||||
// Emit the startup event after the socket is bound so that clients
|
||||
// connecting immediately after the socket appears can subscribe and receive it.
|
||||
let _ = self.emit_tx.send(BreadEvent::new(
|
||||
"bread.system.startup",
|
||||
AdapterSource::System,
|
||||
serde_json::json!({}),
|
||||
));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown_rx.changed() => {
|
||||
|
|
@ -124,7 +132,22 @@ impl Server {
|
|||
continue;
|
||||
}
|
||||
|
||||
let req: IpcRequest = serde_json::from_str(&line)?;
|
||||
let req: IpcRequest = match serde_json::from_str(&line) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
let err_resp = IpcResponse {
|
||||
id: "?".to_string(),
|
||||
result: None,
|
||||
error: Some(format!("parse error: {e}")),
|
||||
};
|
||||
write_half
|
||||
.write_all(
|
||||
format!("{}\n", serde_json::to_string(&err_resp)?).as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if req.method == "events.subscribe" {
|
||||
let filter = req
|
||||
.params
|
||||
|
|
@ -206,12 +229,8 @@ impl Server {
|
|||
}
|
||||
"profile.list" => {
|
||||
let full = self.state_handle.state_dump().await;
|
||||
let profiles = full
|
||||
.get("profile")
|
||||
.and_then(|v| v.get("profiles"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}));
|
||||
Ok(profiles)
|
||||
let profile = full.get("profile").cloned().unwrap_or_else(|| json!({}));
|
||||
Ok(profile)
|
||||
}
|
||||
"profile.activate" => {
|
||||
let Some(name) = req.params.get("name").and_then(Value::as_str) else {
|
||||
|
|
@ -319,14 +338,8 @@ impl Server {
|
|||
}
|
||||
|
||||
fn matches_filter(event_name: &str, pattern: &str) -> bool {
|
||||
// Delegate to the same glob logic used by the subscription table so that
|
||||
// `bread events --filter "bread.device.**"` behaves identically to
|
||||
// `bread.on("bread.device.**", ...)` in Lua.
|
||||
if pattern.ends_with(".*") {
|
||||
let prefix = &pattern[..pattern.len() - 1];
|
||||
return event_name.starts_with(prefix);
|
||||
}
|
||||
|
||||
// Delegates to the same glob logic as the subscription table:
|
||||
// `*` matches one segment (no dot-crossing), `**` matches any depth.
|
||||
if let Some(prefix) = pattern.strip_suffix(".**") {
|
||||
if event_name == prefix || event_name.starts_with(&format!("{prefix}.")) {
|
||||
return true;
|
||||
|
|
@ -400,7 +413,7 @@ mod tests {
|
|||
#[test]
|
||||
fn filter_dot_star_matches_one_segment_only() {
|
||||
assert!(matches_filter("bread.device.connected", "bread.device.*"));
|
||||
assert!(matches_filter(
|
||||
assert!(!matches_filter(
|
||||
"bread.device.dock.connected",
|
||||
"bread.device.*"
|
||||
));
|
||||
|
|
@ -442,11 +455,9 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn filter_dot_star_at_end_acts_as_prefix_match() {
|
||||
// `bread.*` ending the pattern is treated as a prefix match, so
|
||||
// matches everything under `bread.` regardless of depth. This is
|
||||
// consistent with the subscription table's pattern matcher.
|
||||
fn filter_dot_star_matches_exactly_one_segment() {
|
||||
assert!(matches_filter("bread.alpha", "bread.*"));
|
||||
assert!(matches_filter("bread.alpha.beta", "bread.*"));
|
||||
assert!(!matches_filter("bread.alpha.beta", "bread.*"));
|
||||
assert!(!matches_filter("bread", "bread.*"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue