Nushell plugin for interacting with D-Bus
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add commands for working with D-Bus properties more easily

+261 -11
+139 -2
src/client.rs
··· 1 - use dbus::{channel::{Channel, BusType}, Message}; 1 + use dbus::{channel::{Channel, BusType}, Message, arg::messageitem::MessageItem}; 2 2 use nu_plugin::LabeledError; 3 - use nu_protocol::{Spanned, Value}; 3 + use nu_protocol::{Spanned, Value, Span}; 4 4 5 5 use crate::{config::{DbusClientConfig, DbusBusChoice}, dbus_type::DbusType, convert::to_message_item, introspection::Node}; 6 6 ··· 111 111 } 112 112 } 113 113 114 + /// Try to use introspection to get the signature of a property 115 + fn get_property_signature_by_introspection( 116 + &self, 117 + dest: &Spanned<String>, 118 + object: &Spanned<String>, 119 + interface: &Spanned<String>, 120 + property: &Spanned<String>, 121 + ) -> Result<Vec<DbusType>, LabeledError> { 122 + let node = self.introspect(dest, object)?; 123 + 124 + if let Some(sig) = node.get_property_signature(&interface.item, &property.item) { 125 + DbusType::parse_all(&sig).map_err(|err| LabeledError { 126 + label: format!("while getting interface {:?} property {:?} signature: {}", 127 + interface.item, 128 + property.item, 129 + err), 130 + msg: "try running with --no-introspect or --signature".into(), 131 + span: Some(self.config.span), 132 + }) 133 + } else { 134 + Err(LabeledError { 135 + label: format!("Property {:?} not found on {:?}", property.item, interface.item), 136 + msg: "check that this property/interface is correct".into(), 137 + span: Some(property.span), 138 + }) 139 + } 140 + } 141 + 114 142 /// Call a D-Bus method and wait for the response 115 143 pub fn call( 116 144 &self, ··· 179 207 .map_err(|err| self.error(err, context))?; 180 208 181 209 crate::convert::from_message(&resp).map_err(|err| self.error(err, context)) 210 + } 211 + 212 + /// Get a D-Bus property from the given object 213 + pub fn get( 214 + &self, 215 + dest: &Spanned<String>, 216 + object: &Spanned<String>, 217 + interface: &Spanned<String>, 218 + property: &Spanned<String>, 219 + ) -> Result<Value, LabeledError> { 220 + let interface_val = Value::string(&interface.item, interface.span); 221 + let property_val = Value::string(&property.item, property.span); 222 + 223 + self.call( 224 + dest, 225 + object, 226 + &Spanned { item: "org.freedesktop.DBus.Properties".into(), span: Span::unknown() }, 227 + &Spanned { item: "Get".into(), span: Span::unknown() }, 228 + Some(&Spanned { item: "ss".into(), span: Span::unknown() }), 229 + &[interface_val, property_val] 230 + ).map(|val| val.into_iter().nth(0).unwrap_or(Value::nothing(Span::unknown()))) 231 + } 232 + 233 + /// Get all D-Bus properties from the given object 234 + pub fn get_all( 235 + &self, 236 + dest: &Spanned<String>, 237 + object: &Spanned<String>, 238 + interface: &Spanned<String>, 239 + ) -> Result<Value, LabeledError> { 240 + let interface_val = Value::string(&interface.item, interface.span); 241 + 242 + self.call( 243 + dest, 244 + object, 245 + &Spanned { item: "org.freedesktop.DBus.Properties".into(), span: Span::unknown() }, 246 + &Spanned { item: "GetAll".into(), span: Span::unknown() }, 247 + Some(&Spanned { item: "s".into(), span: Span::unknown() }), 248 + &[interface_val] 249 + ).map(|val| val.into_iter().nth(0).unwrap_or(Value::nothing(Span::unknown()))) 250 + } 251 + 252 + /// Set a D-Bus property on the given object 253 + pub fn set( 254 + &self, 255 + dest: &Spanned<String>, 256 + object: &Spanned<String>, 257 + interface: &Spanned<String>, 258 + property: &Spanned<String>, 259 + signature: Option<&Spanned<String>>, 260 + value: &Value, 261 + ) -> Result<(), LabeledError> { 262 + let context = "while setting a D-Bus property"; 263 + 264 + // Validate inputs before sending to the dbus lib so we don't panic 265 + let valid_dest = validate_with!(dbus::strings::BusName, dest)?; 266 + let valid_object = validate_with!(dbus::strings::Path, object)?; 267 + 268 + // Parse the signature 269 + let mut valid_signature = signature.map(|s| DbusType::parse_all(&s.item).map_err(|err| { 270 + LabeledError { 271 + label: err, 272 + msg: "in signature specified here".into(), 273 + span: Some(s.span), 274 + } 275 + })).transpose()?; 276 + 277 + // If not provided, try introspection (unless disabled) 278 + if valid_signature.is_none() && self.config.introspect { 279 + match self.get_property_signature_by_introspection(dest, object, interface, property) { 280 + Ok(sig) => { 281 + valid_signature = Some(sig); 282 + }, 283 + Err(err) => { 284 + eprintln!("Warning: D-Bus introspection failed on {:?}. \ 285 + Use `--no-introspect` or pass `--signature` to silence this warning. \ 286 + Cause: {}", 287 + object.item, 288 + err.label); 289 + } 290 + } 291 + } 292 + 293 + if let Some(sig) = &valid_signature { 294 + if sig.len() != 1 { 295 + self.error(format!( 296 + "expected single object signature, but there are {}", sig.len()), context); 297 + } 298 + } 299 + 300 + // Construct the method call message 301 + let message = Message::new_method_call( 302 + valid_dest, 303 + valid_object, 304 + "org.freedesktop.DBus.Properties", 305 + "Set", 306 + ).map_err(|err| self.error(err, context))? 307 + .append2(&interface.item, &property.item) 308 + .append1( 309 + // Box it in a variant as required for property setting 310 + MessageItem::Variant(Box::new( 311 + to_message_item(value, valid_signature.as_ref().map(|s| &s[0]))?)) 312 + ); 313 + 314 + // Send it on the channel and get the response 315 + self.conn.send_with_reply_and_block(message, self.config.timeout.item) 316 + .map_err(|err| self.error(err, context))?; 317 + 318 + Ok(()) 182 319 } 183 320 }
+5 -1
src/introspection.rs
··· 36 36 pub fn get_method_args_signature(&self, interface: &str, method: &str) -> Option<String> { 37 37 Some(self.get_interface(interface)?.get_method(method)?.in_signature()) 38 38 } 39 + 40 + /// Find the signature of a property on an interface on this node 41 + pub fn get_property_signature(&self, interface: &str, property: &str) -> Option<&str> { 42 + Some(&self.get_interface(interface)?.get_property(property)?.r#type) 43 + } 39 44 } 40 45 41 46 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] ··· 62 67 self.signals.iter().find(|s| s.name == name) 63 68 } 64 69 65 - #[allow(dead_code)] 66 70 pub fn get_property(&self, name: &str) -> Option<&Property> { 67 71 self.properties.iter().find(|p| p.name == name) 68 72 }
+117 -8
src/main.rs
··· 60 60 result: None 61 61 }, 62 62 PluginExample { 63 - example: "dbus call --dest=org.mpris.MediaPlayer2.spotify \ 64 - /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties Get \ 63 + example: "dbus call --dest=org.freedesktop.Notifications \ 64 + /org/freedesktop/Notifications org.freedesktop.Notifications \ 65 + Notify \"Floppy disks\" 0 \"media-floppy\" \"Rarely seen\" \ 66 + \"But sometimes still used\" [] {} 5000".into(), 67 + description: "Show a notification on the desktop for 5 seconds".into(), 68 + result: None 69 + }, 70 + ]), 71 + PluginSignature::build("dbus get") 72 + .is_dbus_command() 73 + .accepts_dbus_client_options() 74 + .usage("Get a D-Bus property") 75 + .named("timeout", SyntaxShape::Duration, "How long to wait for a response", None) 76 + .required_named("dest", SyntaxShape::String, 77 + "The name of the connection to read the property from", 78 + None) 79 + .required("object", SyntaxShape::String, 80 + "The path to the object to read the property from") 81 + .required("interface", SyntaxShape::String, 82 + "The name of the interface the property belongs to") 83 + .required("property", SyntaxShape::String, 84 + "The name of the property to read") 85 + .plugin_examples(vec![ 86 + PluginExample { 87 + example: "dbus get --dest=org.mpris.MediaPlayer2.spotify \ 88 + /org/mpris/MediaPlayer2 \ 65 89 org.mpris.MediaPlayer2.Player Metadata".into(), 66 90 description: "Get the currently playing song in Spotify".into(), 67 91 result: Some(Value::record(nu_protocol::record!( ··· 73 97 "xesam:url" => str!("https://open.spotify.com/track/51748BvzeeMs4PIdPuyZmv"), 74 98 ), Span::unknown())) 75 99 }, 100 + ]), 101 + PluginSignature::build("dbus get-all") 102 + .is_dbus_command() 103 + .accepts_dbus_client_options() 104 + .usage("Get all D-Bus property for the given objects") 105 + .named("timeout", SyntaxShape::Duration, "How long to wait for a response", None) 106 + .required_named("dest", SyntaxShape::String, 107 + "The name of the connection to read the property from", 108 + None) 109 + .required("object", SyntaxShape::String, 110 + "The path to the object to read the property from") 111 + .required("interface", SyntaxShape::String, 112 + "The name of the interface the property belongs to") 113 + .plugin_examples(vec![ 76 114 PluginExample { 77 - example: "dbus call --dest=org.freedesktop.Notifications \ 78 - /org/freedesktop/Notifications org.freedesktop.Notifications \ 79 - Notify \"Floppy disks\" 0 \"media-floppy\" \"Rarely seen\" \ 80 - \"But sometimes still used\" [] {} 5000".into(), 81 - description: "Show a notification on the desktop for 5 seconds".into(), 82 - result: None 115 + example: "dbus get-all --dest=org.mpris.MediaPlayer2.spotify \ 116 + /org/mpris/MediaPlayer2 \ 117 + org.mpris.MediaPlayer2.Player".into(), 118 + description: "Get the current player state of Spotify".into(), 119 + result: Some(Value::record(nu_protocol::record!( 120 + "CanPlay" => Value::bool(true, Span::unknown()), 121 + "Volume" => Value::float(0.43, Span::unknown()), 122 + "PlaybackStatus" => str!("Paused"), 123 + ), Span::unknown())) 124 + }, 125 + ]), 126 + PluginSignature::build("dbus set") 127 + .is_dbus_command() 128 + .accepts_dbus_client_options() 129 + .usage("Get all D-Bus property for the given objects") 130 + .named("timeout", SyntaxShape::Duration, "How long to wait for a response", None) 131 + .named("signature", SyntaxShape::String, 132 + "Signature of the value to set, in D-Bus format.\n \ 133 + If not provided, it will be determined from introspection.\n \ 134 + If --no-introspect is specified and this is not provided, it will \ 135 + be guessed (poorly)", None) 136 + .required_named("dest", SyntaxShape::String, 137 + "The name of the connection to write the property on", 138 + None) 139 + .required("object", SyntaxShape::String, 140 + "The path to the object to write the property on") 141 + .required("interface", SyntaxShape::String, 142 + "The name of the interface the property belongs to") 143 + .required("property", SyntaxShape::String, 144 + "The name of the property to write") 145 + .required("value", SyntaxShape::Any, 146 + "The value to write to the property") 147 + .plugin_examples(vec![ 148 + PluginExample { 149 + example: "dbus set --dest=org.mpris.MediaPlayer2.spotify \ 150 + /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player \ 151 + Volume 0.5".into(), 152 + description: "Set the volume of Spotify to 50%".into(), 153 + result: None, 83 154 }, 84 155 ]), 85 156 ] ··· 99 170 }), 100 171 101 172 "dbus call" => self.call(call), 173 + "dbus get" => self.get(call), 174 + "dbus get-all" => self.get_all(call), 175 + "dbus set" => self.set(call), 102 176 103 177 _ => Err(LabeledError { 104 178 label: "Plugin invoked with unknown command name".into(), ··· 155 229 1 if flatten => Ok(values.into_iter().nth(0).unwrap()), 156 230 _ => Ok(Value::list(values, Span::unknown())) 157 231 } 232 + } 233 + 234 + fn get(&self, call: &EvaluatedCall) -> Result<Value, LabeledError> { 235 + let config = DbusClientConfig::try_from(call)?; 236 + let dbus = DbusClient::new(config)?; 237 + dbus.get( 238 + &call.get_flag("dest")?.unwrap(), 239 + &call.req(0)?, 240 + &call.req(1)?, 241 + &call.req(2)?, 242 + ) 243 + } 244 + 245 + fn get_all(&self, call: &EvaluatedCall) -> Result<Value, LabeledError> { 246 + let config = DbusClientConfig::try_from(call)?; 247 + let dbus = DbusClient::new(config)?; 248 + dbus.get_all( 249 + &call.get_flag("dest")?.unwrap(), 250 + &call.req(0)?, 251 + &call.req(1)?, 252 + ) 253 + } 254 + 255 + fn set(&self, call: &EvaluatedCall) -> Result<Value, LabeledError> { 256 + let config = DbusClientConfig::try_from(call)?; 257 + let dbus = DbusClient::new(config)?; 258 + dbus.set( 259 + &call.get_flag("dest")?.unwrap(), 260 + &call.req(0)?, 261 + &call.req(1)?, 262 + &call.req(2)?, 263 + call.get_flag("signature")?.as_ref(), 264 + &call.req(3)?, 265 + )?; 266 + Ok(Value::nothing(Span::unknown())) 158 267 } 159 268 }