Nushell plugin for interacting with D-Bus
at main 399 lines 14 kB view raw
1use dbus::{ 2 Message, 3 arg::messageitem::MessageItem, 4 channel::{BusType, Channel}, 5}; 6use nu_protocol::{LabeledError, Spanned, Value}; 7 8use crate::{ 9 config::{DbusBusChoice, DbusClientConfig}, 10 convert::to_message_item, 11 dbus_type::DbusType, 12 introspection::Node, 13 pattern::Pattern, 14}; 15 16/// Executes D-Bus actions on a connection, handling nushell types 17pub struct DbusClient { 18 config: DbusClientConfig, 19 conn: Channel, 20} 21 22// Convenience macros for error handling 23macro_rules! validate_with { 24 ($type:ty, $spanned:expr) => { 25 <$type>::new(&$spanned.item) 26 .map_err(|msg| LabeledError::new("Invalid argument").with_label(msg, $spanned.span)) 27 }; 28} 29 30impl DbusClient { 31 pub fn new(config: DbusClientConfig) -> Result<DbusClient, LabeledError> { 32 // Try to connect to the correct D-Bus destination, as specified in the config 33 let channel = match &config.bus_choice.item { 34 DbusBusChoice::Session => Channel::get_private(BusType::Session), 35 DbusBusChoice::System => Channel::get_private(BusType::System), 36 DbusBusChoice::Started => Channel::get_private(BusType::Starter), 37 DbusBusChoice::Peer(address) => Channel::open_private(address), 38 DbusBusChoice::Bus(address) => Channel::open_private(address).and_then(|mut ch| { 39 ch.register()?; 40 Ok(ch) 41 }), 42 } 43 .map_err(|err| { 44 LabeledError::new(err.to_string()).with_label( 45 "while connecting to D-Bus as specified here", 46 config.bus_choice.span, 47 ) 48 })?; 49 Ok(DbusClient { 50 config, 51 conn: channel, 52 }) 53 } 54 55 fn error(&self, err: impl std::fmt::Display, msg: impl std::fmt::Display) -> LabeledError { 56 LabeledError::new(err.to_string()).with_label(msg.to_string(), self.config.span) 57 } 58 59 /// Introspect a D-Bus object 60 pub fn introspect( 61 &self, 62 dest: &Spanned<String>, 63 object: &Spanned<String>, 64 ) -> Result<Node, LabeledError> { 65 let context = "while introspecting a D-Bus method"; 66 let valid_dest = validate_with!(dbus::strings::BusName, dest)?; 67 let valid_object = validate_with!(dbus::strings::Path, object)?; 68 69 // Create the introspection method call 70 let message = Message::new_method_call( 71 valid_dest, 72 valid_object, 73 "org.freedesktop.DBus.Introspectable", 74 "Introspect", 75 ) 76 .map_err(|err| self.error(err, context))?; 77 78 // Send and get the response 79 let resp = self 80 .conn 81 .send_with_reply_and_block(message, self.config.timeout.item) 82 .map_err(|err| self.error(err, context))?; 83 84 // Parse it to a Node 85 let xml: &str = resp 86 .get1() 87 .ok_or_else(|| self.error("Introspect method returned the wrong type", context))?; 88 89 Node::from_xml(xml).map_err(|err| self.error(err, context)) 90 } 91 92 /// Try to use introspection to get the signature of a method 93 fn get_method_signature_by_introspection( 94 &self, 95 dest: &Spanned<String>, 96 object: &Spanned<String>, 97 interface: &Spanned<String>, 98 method: &Spanned<String>, 99 ) -> Result<Vec<DbusType>, LabeledError> { 100 let node = self.introspect(dest, object)?; 101 102 if let Some(sig) = node.get_method_args_signature(&interface.item, &method.item) { 103 DbusType::parse_all(&sig).map_err(|err| { 104 LabeledError::new(format!( 105 "while getting interface {:?} method {:?} signature: {}", 106 interface.item, method.item, err 107 )) 108 .with_label( 109 "try running with --no-introspect or --signature", 110 self.config.span, 111 ) 112 }) 113 } else { 114 Err(LabeledError::new(format!( 115 "Method {:?} not found on {:?}", 116 method.item, interface.item 117 )) 118 .with_label("check that this method/interface is correct", method.span)) 119 } 120 } 121 122 /// Try to use introspection to get the signature of a property 123 fn get_property_signature_by_introspection( 124 &self, 125 dest: &Spanned<String>, 126 object: &Spanned<String>, 127 interface: &Spanned<String>, 128 property: &Spanned<String>, 129 ) -> Result<Vec<DbusType>, LabeledError> { 130 let node = self.introspect(dest, object)?; 131 132 if let Some(sig) = node.get_property_signature(&interface.item, &property.item) { 133 DbusType::parse_all(sig).map_err(|err| { 134 LabeledError::new(format!( 135 "while getting interface {:?} property {:?} signature: {}", 136 interface.item, property.item, err 137 )) 138 .with_label( 139 "try running with --no-introspect or --signature", 140 self.config.span, 141 ) 142 }) 143 } else { 144 Err(LabeledError::new(format!( 145 "Property {:?} not found on {:?}", 146 property.item, interface.item 147 )) 148 .with_label( 149 "check that this property or interface is correct", 150 property.span, 151 )) 152 } 153 } 154 155 /// Call a D-Bus method and wait for the response 156 pub fn call( 157 &self, 158 dest: &Spanned<String>, 159 object: &Spanned<String>, 160 interface: &Spanned<String>, 161 method: &Spanned<String>, 162 signature: Option<&Spanned<String>>, 163 args: &[Value], 164 ) -> Result<Vec<Value>, LabeledError> { 165 let context = "while calling a D-Bus method"; 166 167 // Validate inputs before sending to the dbus lib so we don't panic 168 let valid_dest = validate_with!(dbus::strings::BusName, dest)?; 169 let valid_object = validate_with!(dbus::strings::Path, object)?; 170 let valid_interface = validate_with!(dbus::strings::Interface, interface)?; 171 let valid_method = validate_with!(dbus::strings::Member, method)?; 172 173 // Parse the signature 174 let mut valid_signature = signature 175 .map(|s| { 176 DbusType::parse_all(&s.item).map_err(|err| { 177 LabeledError::new(err).with_label("in signature specified here", s.span) 178 }) 179 }) 180 .transpose()?; 181 182 // If not provided, try introspection (unless disabled) 183 if valid_signature.is_none() && self.config.introspect { 184 match self.get_method_signature_by_introspection(dest, object, interface, method) { 185 Ok(sig) => { 186 valid_signature = Some(sig); 187 } 188 Err(err) => { 189 eprintln!( 190 "Warning: D-Bus introspection failed on {:?}. \ 191 Use `--no-introspect` or pass `--signature` to silence this warning. \ 192 Cause: {}", 193 object.item, err 194 ); 195 } 196 } 197 } 198 199 if let Some(sig) = &valid_signature { 200 if sig.len() != args.len() { 201 self.error( 202 format!("expected {} arguments, got {}", sig.len(), args.len()), 203 context, 204 ); 205 } 206 } 207 208 // Construct the method call message 209 let mut message = 210 Message::new_method_call(valid_dest, valid_object, valid_interface, valid_method) 211 .map_err(|err| self.error(err, context))?; 212 213 // Convert the args to message items 214 let sigs_iter = valid_signature 215 .iter() 216 .flatten() 217 .map(Some) 218 .chain(std::iter::repeat(None)); 219 for (val, sig) in args.iter().zip(sigs_iter) { 220 message = message.append1(to_message_item(val, sig)?); 221 } 222 223 // Send it on the channel and get the response 224 let resp = self 225 .conn 226 .send_with_reply_and_block(message, self.config.timeout.item) 227 .map_err(|err| self.error(err, context))?; 228 229 crate::convert::from_message(&resp, self.config.span) 230 .map_err(|err| self.error(err, context)) 231 } 232 233 /// Get a D-Bus property from the given object 234 pub fn get( 235 &self, 236 dest: &Spanned<String>, 237 object: &Spanned<String>, 238 interface: &Spanned<String>, 239 property: &Spanned<String>, 240 ) -> Result<Value, LabeledError> { 241 let interface_val = Value::string(&interface.item, interface.span); 242 let property_val = Value::string(&property.item, property.span); 243 244 self.call( 245 dest, 246 object, 247 &Spanned { 248 item: "org.freedesktop.DBus.Properties".into(), 249 span: self.config.span, 250 }, 251 &Spanned { 252 item: "Get".into(), 253 span: self.config.span, 254 }, 255 Some(&Spanned { 256 item: "ss".into(), 257 span: self.config.span, 258 }), 259 &[interface_val, property_val], 260 ) 261 .map(|val| val.into_iter().nth(0).unwrap_or_default()) 262 } 263 264 /// Get all D-Bus properties from the given object 265 pub fn get_all( 266 &self, 267 dest: &Spanned<String>, 268 object: &Spanned<String>, 269 interface: &Spanned<String>, 270 ) -> Result<Value, LabeledError> { 271 let interface_val = Value::string(&interface.item, interface.span); 272 273 self.call( 274 dest, 275 object, 276 &Spanned { 277 item: "org.freedesktop.DBus.Properties".into(), 278 span: self.config.span, 279 }, 280 &Spanned { 281 item: "GetAll".into(), 282 span: self.config.span, 283 }, 284 Some(&Spanned { 285 item: "s".into(), 286 span: self.config.span, 287 }), 288 &[interface_val], 289 ) 290 .map(|val| val.into_iter().nth(0).unwrap_or_default()) 291 } 292 293 /// Set a D-Bus property on the given object 294 pub fn set( 295 &self, 296 dest: &Spanned<String>, 297 object: &Spanned<String>, 298 interface: &Spanned<String>, 299 property: &Spanned<String>, 300 signature: Option<&Spanned<String>>, 301 value: &Value, 302 ) -> Result<(), LabeledError> { 303 let context = "while setting a D-Bus property"; 304 305 // Validate inputs before sending to the dbus lib so we don't panic 306 let valid_dest = validate_with!(dbus::strings::BusName, dest)?; 307 let valid_object = validate_with!(dbus::strings::Path, object)?; 308 309 // Parse the signature 310 let mut valid_signature = signature 311 .map(|s| { 312 DbusType::parse_all(&s.item).map_err(|err| { 313 LabeledError::new(err).with_label("in signature specified here", s.span) 314 }) 315 }) 316 .transpose()?; 317 318 // If not provided, try introspection (unless disabled) 319 if valid_signature.is_none() && self.config.introspect { 320 match self.get_property_signature_by_introspection(dest, object, interface, property) { 321 Ok(sig) => { 322 valid_signature = Some(sig); 323 } 324 Err(err) => { 325 eprintln!( 326 "Warning: D-Bus introspection failed on {:?}. \ 327 Use `--no-introspect` or pass `--signature` to silence this warning. \ 328 Cause: {}", 329 object.item, err 330 ); 331 } 332 } 333 } 334 335 if let Some(sig) = &valid_signature { 336 if sig.len() != 1 { 337 self.error( 338 format!( 339 "expected single object signature, but there are {}", 340 sig.len() 341 ), 342 context, 343 ); 344 } 345 } 346 347 // Construct the method call message 348 let message = Message::new_method_call( 349 valid_dest, 350 valid_object, 351 "org.freedesktop.DBus.Properties", 352 "Set", 353 ) 354 .map_err(|err| self.error(err, context))? 355 .append2(&interface.item, &property.item) 356 .append1( 357 // Box it in a variant as required for property setting 358 MessageItem::Variant(Box::new(to_message_item( 359 value, 360 valid_signature.as_ref().map(|s| &s[0]), 361 )?)), 362 ); 363 364 // Send it on the channel and get the response 365 self.conn 366 .send_with_reply_and_block(message, self.config.timeout.item) 367 .map_err(|err| self.error(err, context))?; 368 369 Ok(()) 370 } 371 372 pub fn list(&self, pattern: Option<&Pattern>) -> Result<Vec<String>, LabeledError> { 373 let context = "while listing D-Bus connection names"; 374 375 let message = Message::new_method_call( 376 "org.freedesktop.DBus", 377 "/org/freedesktop/DBus", 378 "org.freedesktop.DBus", 379 "ListNames", 380 ) 381 .map_err(|err| self.error(err, context))?; 382 383 self.conn 384 .send_with_reply_and_block(message, self.config.timeout.item) 385 .map_err(|err| self.error(err, context)) 386 .and_then(|reply| reply.read1().map_err(|err| self.error(err, context))) 387 .map(|names: Vec<String>| { 388 // Filter the names by the pattern 389 if let Some(pattern) = pattern { 390 names 391 .into_iter() 392 .filter(|name| pattern.is_match(name)) 393 .collect() 394 } else { 395 names 396 } 397 }) 398 } 399}