fix(daemon): Remove .expect() panics from PipeWire initialization (#89)

This commit addresses a security and stability vulnerability where failures in PipeWire context setup, connection, or registry acquisition would crash the entire thread (or daemon) via `.expect()` panics.

We now gracefully capture and propagate initialization errors up the call stack. A `sync_channel(0)` is used to signal the success or failure of the initial pipewire setup back to the calling functions (`get_all_devices`, `create_virtual_mic`, `create_link`). This prevents unexpected crashes and improves error resilience.

Also removed unneeded `pw_sender` panics on channel termination by simply dropping/ignoring the result.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
Tarasov Aleksandr
2026-04-27 22:54:17 +03:00
committed by GitHub
parent 1a37729cf1
commit fc2cd5e2da
2 changed files with 120 additions and 35 deletions
+8 -2
View File
@@ -9,7 +9,10 @@ use std::path::PathBuf;
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> { fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
let key_name = key.name(); let key_name = key.name();
let is_valid = (key_name.len() == 1 let is_valid = (key_name.len() == 1
&& key_name.chars().next().is_some_and(|c| c.is_ascii_alphanumeric())) && key_name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphanumeric()))
|| (key_name.starts_with('F') || (key_name.starts_with('F')
&& key_name.len() > 1 && key_name.len() > 1
&& key_name[1..].chars().all(|c| c.is_ascii_digit())); && key_name[1..].chars().all(|c| c.is_ascii_digit()));
@@ -60,7 +63,10 @@ pub fn parse_chord(chord: &str) -> Option<(Modifiers, Key)> {
let key_name = parts[parts.len() - 1]; let key_name = parts[parts.len() - 1];
let is_valid = (key_name.len() == 1 let is_valid = (key_name.len() == 1
&& key_name.chars().next().is_some_and(|c| c.is_ascii_alphanumeric())) && key_name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphanumeric()))
|| (key_name.starts_with('F') || (key_name.starts_with('F')
&& key_name.len() > 1 && key_name.len() > 1
&& key_name[1..].chars().all(|c| c.is_ascii_digit())); && key_name[1..].chars().all(|c| c.is_ascii_digit()));
+112 -33
View File
@@ -9,11 +9,11 @@ use tokio::{
time::{Duration, timeout}, time::{Duration, timeout},
}; };
pub fn setup_pipewire_context() -> (MainLoopRc, ContextRc) { pub fn setup_pipewire_context() -> Result<(MainLoopRc, ContextRc), String> {
pipewire::init(); pipewire::init();
let main_loop = MainLoopRc::new(None).expect("Failed to initialize pipewire main loop"); let main_loop = MainLoopRc::new(None).map_err(|e| e.to_string())?;
let context = ContextRc::new(&main_loop, None).expect("Failed to create pipewire context"); let context = ContextRc::new(&main_loop, None).map_err(|e| e.to_string())?;
(main_loop, context) Ok((main_loop, context))
} }
fn parse_global_object( fn parse_global_object(
@@ -85,8 +85,15 @@ fn parse_global_object(
async fn pw_get_global_objects_thread( async fn pw_get_global_objects_thread(
main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>, main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>,
pw_receiver: pipewire::channel::Receiver<Terminate>, pw_receiver: pipewire::channel::Receiver<Terminate>,
init_sender: std::sync::mpsc::SyncSender<Result<(), String>>,
) { ) {
let (main_loop, context) = setup_pipewire_context(); let (main_loop, context) = match setup_pipewire_context() {
Ok(res) => res,
Err(e) => {
let _ = init_sender.send(Err(e));
return;
}
};
// Stop main loop on Terminate message // Stop main loop on Terminate message
let _receiver = pw_receiver.attach(main_loop.loop_(), { let _receiver = pw_receiver.attach(main_loop.loop_(), {
@@ -94,12 +101,24 @@ async fn pw_get_global_objects_thread(
move |_| _main_loop.quit() move |_| _main_loop.quit()
}); });
let core = context let core = match context.connect(None) {
.connect(None) Ok(core) => core,
.expect("Failed to connect to pipewire context"); Err(e) => {
let registry = core let _ = init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
.get_registry() return;
.expect("Failed to get registry from pipewire context"); }
};
let registry = match core.get_registry() {
Ok(registry) => registry,
Err(e) => {
let _ = init_sender.send(Err(format!(
"Failed to get registry from pipewire context: {}",
e
)));
return;
}
};
let _listener = registry let _listener = registry
.add_listener_local() .add_listener_local()
@@ -115,6 +134,11 @@ async fn pw_get_global_objects_thread(
}) })
.register(); .register();
// Signal successful initialization
if init_sender.send(Ok(())).is_err() {
return;
}
main_loop.run(); main_loop.run();
} }
@@ -122,10 +146,17 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
// Channels to communicate with pipewire thread // Channels to communicate with pipewire thread
let (main_sender, mut main_receiver) = mpsc::channel(10); let (main_sender, mut main_receiver) = mpsc::channel(10);
let (pw_sender, pw_receiver) = pipewire::channel::channel(); let (pw_sender, pw_receiver) = pipewire::channel::channel();
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
// Spawn pipewire thread in background // Spawn pipewire thread in background
let _pw_thread = let _pw_thread = tokio::spawn(async move {
tokio::spawn(async move { pw_get_global_objects_thread(main_sender, pw_receiver).await }); pw_get_global_objects_thread(main_sender, pw_receiver, init_sender).await
});
// Wait for initialization to complete
if let Err(e) = init_receiver.recv()? {
return Err(e.into());
}
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new(); let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
let mut output_devices: HashMap<u32, AudioDevice> = HashMap::new(); let mut output_devices: HashMap<u32, AudioDevice> = HashMap::new();
@@ -150,9 +181,7 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
} }
Ok(None) | Err(_) => { Ok(None) | Err(_) => {
// Pipewire thread is finished and we can collect our devices // Pipewire thread is finished and we can collect our devices
pw_sender let _ = pw_sender.send(Terminate {});
.send(Terminate {})
.expect("Failed to terminate pipewire thread");
for port in ports { for port in ports {
let node_id = port.node_id; let node_id = port.node_id;
@@ -226,12 +255,24 @@ pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> { pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>(); let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
let _pw_thread = thread::spawn(move || { let _pw_thread = thread::spawn(move || {
let (main_loop, context) = setup_pipewire_context(); let (main_loop, context) = match setup_pipewire_context() {
let core = context Ok(res) => res,
.connect(None) Err(e) => {
.expect("Failed to connect to pipewire context"); let _ = init_sender.send(Err(e));
return;
}
};
let core = match context.connect(None) {
Ok(core) => core,
Err(e) => {
let _ =
init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
return;
}
};
let props = properties!( let props = properties!(
"factory.name" => "support.null-audio-sink", "factory.name" => "support.null-audio-sink",
@@ -243,9 +284,13 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
"object.linger" => "false", // Destroy the node on app exit "object.linger" => "false", // Destroy the node on app exit
); );
let _node = core let _node = match core.create_object::<pipewire::node::Node>("adapter", &props) {
.create_object::<pipewire::node::Node>("adapter", &props) Ok(node) => node,
.expect("Failed to create virtual mic"); Err(e) => {
let _ = init_sender.send(Err(format!("Failed to create virtual mic: {}", e)));
return;
}
};
let _receiver = pw_receiver.attach(main_loop.loop_(), { let _receiver = pw_receiver.attach(main_loop.loop_(), {
let _main_loop = main_loop.clone(); let _main_loop = main_loop.clone();
@@ -253,9 +298,16 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
}); });
println!("Virtual mic created"); println!("Virtual mic created");
if init_sender.send(Ok(())).is_err() {
return;
}
main_loop.run(); main_loop.run();
}); });
if let Err(e) = init_receiver.recv()? {
return Err(e.into());
}
Ok(pw_sender) Ok(pw_sender)
} }
@@ -304,12 +356,24 @@ pub fn create_link(
input_fr: Port, input_fr: Port,
) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> { ) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>(); let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
let _pw_thread = thread::spawn(move || { let _pw_thread = thread::spawn(move || {
let (main_loop, context) = setup_pipewire_context(); let (main_loop, context) = match setup_pipewire_context() {
let core = context Ok(res) => res,
.connect(None) Err(e) => {
.expect("Failed to connect to pipewire context"); let _ = init_sender.send(Err(e));
return;
}
};
let core = match context.connect(None) {
Ok(core) => core,
Err(e) => {
let _ =
init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
return;
}
};
let props_fl = properties! { let props_fl = properties! {
"link.output.node" => format!("{}", output_fl.node_id).as_str(), "link.output.node" => format!("{}", output_fl.node_id).as_str(),
@@ -324,12 +388,20 @@ pub fn create_link(
"link.input.port" => format!("{}", input_fr.port_id).as_str(), "link.input.port" => format!("{}", input_fr.port_id).as_str(),
}; };
let _link_fl = core let _link_fl = match core.create_object::<Link>("link-factory", &props_fl) {
.create_object::<Link>("link-factory", &props_fl) Ok(link) => link,
.expect("Failed to create link FL"); Err(e) => {
let _link_fr = core let _ = init_sender.send(Err(format!("Failed to create link FL: {}", e)));
.create_object::<Link>("link-factory", &props_fr) return;
.expect("Failed to create link FR"); }
};
let _link_fr = match core.create_object::<Link>("link-factory", &props_fr) {
Ok(link) => link,
Err(e) => {
let _ = init_sender.send(Err(format!("Failed to create link FR: {}", e)));
return;
}
};
let _receiver = pw_receiver.attach(main_loop.loop_(), { let _receiver = pw_receiver.attach(main_loop.loop_(), {
let _main_loop = main_loop.clone(); let _main_loop = main_loop.clone();
@@ -340,8 +412,15 @@ pub fn create_link(
"Link created: FL: {}-{} FR: {}-{}", "Link created: FL: {}-{} FR: {}-{}",
output_fl.node_id, input_fl.node_id, output_fr.node_id, input_fr.node_id output_fl.node_id, input_fl.node_id, output_fr.node_id, input_fr.node_id
); );
if init_sender.send(Ok(())).is_err() {
return;
}
main_loop.run(); main_loop.run();
}); });
if let Err(e) = init_receiver.recv()? {
return Err(e.into());
}
Ok(pw_sender) Ok(pw_sender)
} }