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:
Breadway 2026-06-23 12:45:56 +08:00
parent 0f3136ca8d
commit 3115a4230b
17 changed files with 146 additions and 136 deletions

View file

@ -24,7 +24,7 @@ jobs:
run: cargo build --release --locked run: cargo build --release --locked
- name: test - name: test
run: cargo test --release --locked --workspace --lib run: cargo test --release --locked --workspace
- name: prepare artifacts - name: prepare artifacts
run: | run: |

6
Cargo.lock generated
View file

@ -293,7 +293,7 @@ dependencies = [
[[package]] [[package]]
name = "bread-cli" name = "bread-cli"
version = "0.6.1" version = "0.6.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bread-shared", "bread-shared",
@ -311,7 +311,7 @@ dependencies = [
[[package]] [[package]]
name = "bread-shared" name = "bread-shared"
version = "0.6.1" version = "0.6.2"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@ -319,7 +319,7 @@ dependencies = [
[[package]] [[package]]
name = "breadd" name = "breadd"
version = "0.6.1" version = "0.6.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",

View file

@ -18,8 +18,6 @@ tokio = { version = "1.40", features = ["full"] }
anyhow = "1.0" anyhow = "1.0"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
git2 = "0.18"
dirs = "5.0" dirs = "5.0"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
tempfile = "3" tempfile = "3"
glob = "0.3"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "bread-cli" name = "bread-cli"
version = "0.6.1" version = "0.6.2"
edition = "2021" edition = "2021"
[[bin]] [[bin]]

View file

@ -374,6 +374,18 @@ async fn stream_events(
.await?; .await?;
let mut lines = BufReader::new(read_half).lines(); let mut lines = BufReader::new(read_half).lines();
// Consume the subscribe ack before entering the event loop.
match lines.next_line().await? {
Some(ack) => {
let v: Value = serde_json::from_str(&ack)?;
if let Some(err) = v.get("error").and_then(Value::as_str) {
anyhow::bail!("{err}");
}
}
None => anyhow::bail!("daemon closed connection during subscribe"),
}
while let Some(line) = lines.next_line().await? { while let Some(line) = lines.next_line().await? {
let value: Value = serde_json::from_str(&line)?; let value: Value = serde_json::from_str(&line)?;
if raw_json { if raw_json {
@ -507,10 +519,7 @@ async fn watch_reload(socket: &Path) -> Result<()> {
} }
async fn print_doctor(socket: &Path) -> Result<()> { async fn print_doctor(socket: &Path) -> Result<()> {
let stream = match UnixStream::connect(socket).await { if !socket.exists() {
Ok(stream) => stream,
Err(err) => {
if err.kind() == io::ErrorKind::NotFound {
println!("bread doctor"); println!("bread doctor");
println!(" daemon ✗ not running"); println!(" daemon ✗ not running");
println!(" socket {} (not found)", socket.display()); println!(" socket {} (not found)", socket.display());
@ -519,11 +528,8 @@ async fn print_doctor(socket: &Path) -> Result<()> {
println!(" view logs: journalctl --user -u breadd -f"); println!(" view logs: journalctl --user -u breadd -f");
return Ok(()); return Ok(());
} }
return Err(err.into());
}
};
let response = send_request_with_stream(stream, "health", json!({})).await?; let response = send_request(socket, "health", json!({})).await?;
render_doctor(&response); render_doctor(&response);
Ok(()) Ok(())
} }
@ -585,33 +591,6 @@ fn render_doctor(health: &Value) {
} }
} }
async fn send_request_with_stream(
stream: UnixStream,
method: &str,
params: Value,
) -> Result<Value> {
let (read_half, mut write_half) = stream.into_split();
let request = json!({
"id": "1",
"method": method,
"params": params,
});
write_half
.write_all(format!("{}\n", serde_json::to_string(&request)?).as_bytes())
.await?;
let mut lines = BufReader::new(read_half).lines();
let Some(line) = lines.next_line().await? else {
anyhow::bail!("daemon closed connection without response");
};
let response: Value = serde_json::from_str(&line)?;
if let Some(error) = response.get("error").and_then(Value::as_str) {
anyhow::bail!(error.to_string());
}
Ok(response.get("result").cloned().unwrap_or_else(|| json!({})))
}
fn config_directory() -> PathBuf { fn config_directory() -> PathBuf {
if let Ok(xdg) = env::var("XDG_CONFIG_HOME") { if let Ok(xdg) = env::var("XDG_CONFIG_HOME") {
return Path::new(&xdg).join("bread"); return Path::new(&xdg).join("bread");

View file

@ -134,9 +134,6 @@ pub fn modules_dir() -> PathBuf {
if let Some(cfg) = dirs::config_dir() { if let Some(cfg) = dirs::config_dir() {
return cfg.join("bread").join("modules"); return cfg.join("bread").join("modules");
} }
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
return PathBuf::from(xdg).join("bread").join("modules");
}
if let Ok(home) = std::env::var("HOME") { if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home) return PathBuf::from(home)
.join(".config") .join(".config")

View file

@ -1,6 +1,6 @@
[package] [package]
name = "bread-shared" name = "bread-shared"
version = "0.6.1" version = "0.6.2"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View file

@ -1,6 +1,6 @@
[package] [package]
name = "breadd" name = "breadd"
version = "0.6.1" version = "0.6.2"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -23,4 +23,4 @@ netlink-packet-core = "0.4"
libc = "0.2" libc = "0.2"
[dev-dependencies] [dev-dependencies]
tempfile = "3.13" tempfile.workspace = true

View file

@ -76,16 +76,17 @@ impl Manager {
} }
if self.config.adapters.power.enabled { if self.config.adapters.power.enabled {
// Prefer UPower DBus adapter; fall back to sysfs poller // Prefer UPower D-Bus adapter; fall back to sysfs poller if D-Bus is unavailable.
let upower = power_upower::UPowerAdapter::new(); match power_upower::UPowerAdapter::probe().await {
if let Ok(adapter) = upower { Ok(adapter) => self.spawn_adapter(adapter),
self.spawn_adapter(adapter); Err(e) => {
} else { info!("upower unavailable ({e}), falling back to sysfs power poller");
self.spawn_adapter(power::PowerAdapter::new( self.spawn_adapter(power::PowerAdapter::new(
self.config.adapters.power.poll_interval_secs, self.config.adapters.power.poll_interval_secs,
)); ));
} }
} }
}
if self.config.adapters.bluetooth.enabled { if self.config.adapters.bluetooth.enabled {
let adapter = bluetooth::BluetoothAdapter::new(); let adapter = bluetooth::BluetoothAdapter::new();

View file

@ -16,17 +16,10 @@ pub struct RtnetlinkAdapter;
impl RtnetlinkAdapter { impl RtnetlinkAdapter {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
// Try to create a connection to validate presence of rtnetlink // Validate that rtnetlink is available; drop the result immediately.
let conn = new_connection(); new_connection().map_err(|e| anyhow!(e))?;
match conn {
Ok((connection, _handle, _messages)) => {
// Spawn and immediately drop the connection task; we just validated
tokio::spawn(connection);
Ok(Self) Ok(Self)
} }
Err(e) => Err(anyhow!(e)),
}
}
} }
#[async_trait] #[async_trait]

View file

@ -15,9 +15,11 @@ use super::Adapter;
pub struct UPowerAdapter; pub struct UPowerAdapter;
impl UPowerAdapter { impl UPowerAdapter {
pub fn new() -> Result<Self> { /// Try to connect to the D-Bus system bus. Returns Err if D-Bus is unavailable.
// Attempt to connect to system bus to validate availability pub async fn probe() -> Result<Self> {
// We don't actually open the connection here because zbus::Connection::system() is async. let _ = zbus::Connection::system()
.await
.map_err(|e| anyhow::anyhow!("D-Bus system bus unavailable: {e}"))?;
Ok(Self) Ok(Self)
} }
} }

View file

@ -382,22 +382,39 @@ impl EventNormalizer {
} }
fn normalize_network(&self, raw: &RawEvent) -> Vec<BreadEvent> { fn normalize_network(&self, raw: &RawEvent) -> Vec<BreadEvent> {
let online = raw // The sysfs NetworkAdapter puts `online: bool` directly in the payload.
// The rtnetlink adapter omits it; derive connectivity from the event kind instead.
let online = if let Some(v) = raw.payload.get("online").and_then(Value::as_bool) {
v
} else {
match raw.kind.as_str() {
"link.up" | "address.added" => true,
"link.down" | "address.removed" => false,
"route.default.changed" => raw
.payload .payload
.get("online") .get("gateway")
.and_then(Value::as_bool) .map(|v| !v.is_null())
.unwrap_or(false); .unwrap_or(false),
_ => return vec![],
}
};
let name = if online { let name = if online {
"bread.network.connected" "bread.network.connected"
} else { } else {
"bread.network.disconnected" "bread.network.disconnected"
}; };
let mut data = raw.payload.clone();
if let Some(obj) = data.as_object_mut() {
obj.insert("online".to_string(), Value::Bool(online));
}
vec![BreadEvent { vec![BreadEvent {
event: name.to_string(), event: name.to_string(),
timestamp: raw.timestamp, timestamp: raw.timestamp,
source: AdapterSource::Network, source: AdapterSource::Network,
data: raw.payload.clone(), data,
}] }]
} }

View file

@ -315,6 +315,9 @@ async fn handle_command(
let mut guard = state.write().await; let mut guard = state.write().await;
if guard.profile.active != name { if guard.profile.active != name {
let previous = guard.profile.active.clone(); let previous = guard.profile.active.clone();
if guard.profile.history.len() >= 50 {
guard.profile.history.remove(0);
}
guard.profile.history.push(previous); guard.profile.history.push(previous);
guard.profile.active = name; guard.profile.active = name;
} }
@ -450,6 +453,9 @@ fn apply_event_to_state(state: &mut RuntimeState, event: &BreadEvent) {
if let Some(name) = event.data.get("name").and_then(Value::as_str) { if let Some(name) = event.data.get("name").and_then(Value::as_str) {
if state.profile.active != name { if state.profile.active != name {
let previous = state.profile.active.clone(); let previous = state.profile.active.clone();
if state.profile.history.len() >= 50 {
state.profile.history.remove(0);
}
state.profile.history.push(previous); state.profile.history.push(previous);
state.profile.active = name.to_string(); state.profile.active = name.to_string();
} }

View file

@ -67,11 +67,6 @@ impl SubscriptionTable {
} }
fn matches_pattern(pattern: &str, event_name: &str) -> bool { fn matches_pattern(pattern: &str, event_name: &str) -> bool {
if pattern.ends_with(".*") {
let prefix = &pattern[..pattern.len() - 1];
return event_name.starts_with(prefix);
}
if let Some(prefix) = pattern.strip_suffix(".**") { if let Some(prefix) = pattern.strip_suffix(".**") {
if event_name == prefix { if event_name == prefix {
return true; return true;
@ -150,11 +145,11 @@ mod tests {
#[test] #[test]
fn single_segment_wildcard() { fn single_segment_wildcard() {
assert!(matches_pattern( assert!(matches_pattern("bread.device.*", "bread.device.foo"));
assert!(!matches_pattern(
"bread.device.*", "bread.device.*",
"bread.device.dock.connected" "bread.device.dock.connected"
)); ));
assert!(matches_pattern("bread.device.*", "bread.device.foo"));
assert!(!matches_pattern("bread.device.*", "bread.device")); assert!(!matches_pattern("bread.device.*", "bread.device"));
} }

View file

@ -93,6 +93,14 @@ impl Server {
info!(socket = %self.socket_path.display(), "ipc server listening"); 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 { loop {
tokio::select! { tokio::select! {
_ = shutdown_rx.changed() => { _ = shutdown_rx.changed() => {
@ -124,7 +132,22 @@ impl Server {
continue; 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" { if req.method == "events.subscribe" {
let filter = req let filter = req
.params .params
@ -206,12 +229,8 @@ impl Server {
} }
"profile.list" => { "profile.list" => {
let full = self.state_handle.state_dump().await; let full = self.state_handle.state_dump().await;
let profiles = full let profile = full.get("profile").cloned().unwrap_or_else(|| json!({}));
.get("profile") Ok(profile)
.and_then(|v| v.get("profiles"))
.cloned()
.unwrap_or_else(|| json!({}));
Ok(profiles)
} }
"profile.activate" => { "profile.activate" => {
let Some(name) = req.params.get("name").and_then(Value::as_str) else { 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 { fn matches_filter(event_name: &str, pattern: &str) -> bool {
// Delegate to the same glob logic used by the subscription table so that // Delegates to the same glob logic as the subscription table:
// `bread events --filter "bread.device.**"` behaves identically to // `*` matches one segment (no dot-crossing), `**` matches any depth.
// `bread.on("bread.device.**", ...)` in Lua.
if pattern.ends_with(".*") {
let prefix = &pattern[..pattern.len() - 1];
return event_name.starts_with(prefix);
}
if let Some(prefix) = pattern.strip_suffix(".**") { if let Some(prefix) = pattern.strip_suffix(".**") {
if event_name == prefix || event_name.starts_with(&format!("{prefix}.")) { if event_name == prefix || event_name.starts_with(&format!("{prefix}.")) {
return true; return true;
@ -400,7 +413,7 @@ mod tests {
#[test] #[test]
fn filter_dot_star_matches_one_segment_only() { fn filter_dot_star_matches_one_segment_only() {
assert!(matches_filter("bread.device.connected", "bread.device.*")); assert!(matches_filter("bread.device.connected", "bread.device.*"));
assert!(matches_filter( assert!(!matches_filter(
"bread.device.dock.connected", "bread.device.dock.connected",
"bread.device.*" "bread.device.*"
)); ));
@ -442,11 +455,9 @@ mod tests {
} }
#[test] #[test]
fn filter_dot_star_at_end_acts_as_prefix_match() { fn filter_dot_star_matches_exactly_one_segment() {
// `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.
assert!(matches_filter("bread.alpha", "bread.*")); 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.*"));
} }
} }

View file

@ -254,23 +254,23 @@ impl LuaEngine {
self.lua = Lua::new(); self.lua = Lua::new();
self.handlers self.handlers
.lock() .lock()
.expect("lua handlers mutex poisoned") .unwrap_or_else(|e| e.into_inner())
.clear(); .clear();
self.watch_ids self.watch_ids
.lock() .lock()
.expect("lua watch ids mutex poisoned") .unwrap_or_else(|e| e.into_inner())
.clear(); .clear();
self.modules self.modules
.lock() .lock()
.expect("lua modules mutex poisoned") .unwrap_or_else(|e| e.into_inner())
.clear(); .clear();
self.module_decls self.module_decls
.lock() .lock()
.expect("lua module decls mutex poisoned") .unwrap_or_else(|e| e.into_inner())
.clear(); .clear();
self.module_order self.module_order
.lock() .lock()
.expect("lua module order mutex poisoned") .unwrap_or_else(|e| e.into_inner())
.clear(); .clear();
self.install_api()?; self.install_api()?;
@ -1138,7 +1138,7 @@ impl LuaEngine {
let mut decl_map = self let mut decl_map = self
.module_decls .module_decls
.lock() .lock()
.expect("module decls mutex poisoned"); .unwrap_or_else(|e| e.into_inner());
decl_map.clear(); decl_map.clear();
for decl in &ordered { for decl in &ordered {
decl_map.insert(decl.name.clone(), decl.clone()); decl_map.insert(decl.name.clone(), decl.clone());
@ -1176,7 +1176,7 @@ impl LuaEngine {
*self *self
.module_order .module_order
.lock() .lock()
.expect("module order mutex poisoned") = load_order; .unwrap_or_else(|e| e.into_inner()) = load_order;
Ok(()) Ok(())
} }
@ -1229,7 +1229,7 @@ impl LuaEngine {
fn handle_event(&self, id: SubscriptionId, event: BreadEvent) -> Result<()> { fn handle_event(&self, id: SubscriptionId, event: BreadEvent) -> Result<()> {
let (callback, filter, raw_kind, kind, module) = { let (callback, filter, raw_kind, kind, module) = {
let handlers = self.handlers.lock().expect("lua handlers mutex poisoned"); let handlers = self.handlers.lock().unwrap_or_else(|e| e.into_inner());
let Some(entry) = handlers.get(&id) else { let Some(entry) = handlers.get(&id) else {
return Ok(()); return Ok(());
}; };
@ -1290,7 +1290,7 @@ impl LuaEngine {
fn handle_timer(&self, id: TimerId) -> Result<()> { fn handle_timer(&self, id: TimerId) -> Result<()> {
let (callback, repeating) = { let (callback, repeating) = {
let timers = self.timers.lock().expect("lua timers mutex poisoned"); let timers = self.timers.lock().unwrap_or_else(|e| e.into_inner());
let Some(entry) = timers.get(&id) else { let Some(entry) = timers.get(&id) else {
return Ok(()); return Ok(());
}; };
@ -1334,7 +1334,7 @@ impl LuaEngine {
let order = self let order = self
.module_order .module_order
.lock() .lock()
.expect("module order mutex poisoned") .unwrap_or_else(|e| e.into_inner())
.clone(); .clone();
for name in order { for name in order {
if let Some(hook) = self.get_module_hook(&name, "on_reload") { if let Some(hook) = self.get_module_hook(&name, "on_reload") {
@ -1356,7 +1356,7 @@ impl LuaEngine {
let order = self let order = self
.module_order .module_order
.lock() .lock()
.expect("module order mutex poisoned") .unwrap_or_else(|e| e.into_inner())
.clone(); .clone();
for name in order.into_iter().rev() { for name in order.into_iter().rev() {
if let Some(hook) = self.get_module_hook(&name, "on_unload") { if let Some(hook) = self.get_module_hook(&name, "on_unload") {
@ -1792,6 +1792,7 @@ fn state_value_to_lua<'lua>(
break g; break g;
} }
std::hint::spin_loop(); std::hint::spin_loop();
std::thread::yield_now();
}; };
let mut value = let mut value =
serde_json::to_value(&*snapshot).map_err(|e| LuaError::external(e.to_string()))?; serde_json::to_value(&*snapshot).map_err(|e| LuaError::external(e.to_string()))?;
@ -1820,6 +1821,7 @@ fn module_store_get(
break g; break g;
} }
std::hint::spin_loop(); std::hint::spin_loop();
std::thread::yield_now();
}; };
let entry = guard.modules.iter().find(|m| m.name == module)?; let entry = guard.modules.iter().find(|m| m.name == module)?;
entry.store.get(key).cloned() entry.store.get(key).cloned()
@ -1836,6 +1838,7 @@ fn module_store_set(
break g; break g;
} }
std::hint::spin_loop(); std::hint::spin_loop();
std::thread::yield_now();
}; };
if let Some(entry) = guard.modules.iter_mut().find(|m| m.name == module) { if let Some(entry) = guard.modules.iter_mut().find(|m| m.name == module) {
entry.store.insert(key, value); entry.store.insert(key, value);
@ -2210,7 +2213,13 @@ fn hyprland_request_socket() -> Result<PathBuf> {
hypr_dir.display() hypr_dir.display()
)), )),
1 => Ok(sockets.remove(0)), 1 => Ok(sockets.remove(0)),
_ => Ok(sockets.remove(0)), _ => {
warn!(
"multiple Hyprland instances found in {}; using the first one",
hypr_dir.display()
);
Ok(sockets.remove(0))
}
} }
} }
@ -2274,11 +2283,17 @@ where
Fut: std::future::Future<Output = ()>, Fut: std::future::Future<Output = ()>,
{ {
std::thread::spawn(move || { std::thread::spawn(move || {
tokio::runtime::Builder::new_current_thread() let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build() .build()
.expect("bluetooth action thread") {
.block_on(factory()); Ok(rt) => rt,
Err(e) => {
tracing::error!(error = %e, "bluetooth action: failed to build tokio runtime");
return;
}
};
rt.block_on(factory());
}); });
} }
@ -2292,11 +2307,13 @@ where
{ {
let (tx, rx) = std::sync::mpsc::sync_channel(1); let (tx, rx) = std::sync::mpsc::sync_channel(1);
std::thread::spawn(move || { std::thread::spawn(move || {
let result = tokio::runtime::Builder::new_current_thread() let result = match tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build() .build()
.expect("bluetooth query thread") {
.block_on(factory()); Ok(rt) => rt.block_on(factory()),
Err(e) => Err(anyhow::anyhow!("bluetooth query: failed to build tokio runtime: {e}")),
};
let _ = tx.send(result); let _ = tx.send(result);
}); });
rx.recv() rx.recv()

View file

@ -8,7 +8,7 @@ use std::sync::atomic::AtomicU64;
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use bread_shared::{AdapterSource, BreadEvent, RawEvent}; use bread_shared::{BreadEvent, RawEvent};
use tokio::sync::{broadcast, mpsc, watch, RwLock}; use tokio::sync::{broadcast, mpsc, watch, RwLock};
use tracing::{error, info}; use tracing::{error, info};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
@ -105,12 +105,6 @@ async fn main() -> Result<()> {
}); });
} }
let _ = normalized_tx.send(BreadEvent::new(
"bread.system.startup",
AdapterSource::System,
serde_json::json!({}),
));
let ipc_server = ipc::Server::new( let ipc_server = ipc::Server::new(
config.socket_path(), config.socket_path(),
state_handle, state_handle,