Nushell plugin for interacting with D-Bus
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}