Skip to content

Commit c3fa402

Browse files
committed
add commands for working with D-Bus properties more easily
1 parent 0c6e231 commit c3fa402

3 files changed

Lines changed: 261 additions & 11 deletions

File tree

src/client.rs

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use dbus::{channel::{Channel, BusType}, Message};
1+
use dbus::{channel::{Channel, BusType}, Message, arg::messageitem::MessageItem};
22
use nu_plugin::LabeledError;
3-
use nu_protocol::{Spanned, Value};
3+
use nu_protocol::{Spanned, Value, Span};
44

55
use crate::{config::{DbusClientConfig, DbusBusChoice}, dbus_type::DbusType, convert::to_message_item, introspection::Node};
66

@@ -111,6 +111,34 @@ impl DbusClient {
111111
}
112112
}
113113

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+
114142
/// Call a D-Bus method and wait for the response
115143
pub fn call(
116144
&self,
@@ -180,4 +208,113 @@ impl DbusClient {
180208

181209
crate::convert::from_message(&resp).map_err(|err| self.error(err, context))
182210
}
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(())
319+
}
183320
}

src/introspection.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ impl Node {
3636
pub fn get_method_args_signature(&self, interface: &str, method: &str) -> Option<String> {
3737
Some(self.get_interface(interface)?.get_method(method)?.in_signature())
3838
}
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+
}
3944
}
4045

4146
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
@@ -62,7 +67,6 @@ impl Interface {
6267
self.signals.iter().find(|s| s.name == name)
6368
}
6469

65-
#[allow(dead_code)]
6670
pub fn get_property(&self, name: &str) -> Option<&Property> {
6771
self.properties.iter().find(|p| p.name == name)
6872
}

src/main.rs

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,32 @@ impl Plugin for NuPluginDbus {
6060
result: None
6161
},
6262
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 \
6589
org.mpris.MediaPlayer2.Player Metadata".into(),
6690
description: "Get the currently playing song in Spotify".into(),
6791
result: Some(Value::record(nu_protocol::record!(
@@ -73,13 +97,60 @@ impl Plugin for NuPluginDbus {
7397
"xesam:url" => str!("https://open.spotify.com/track/51748BvzeeMs4PIdPuyZmv"),
7498
), Span::unknown()))
7599
},
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![
76114
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,
83154
},
84155
]),
85156
]
@@ -99,6 +170,9 @@ impl Plugin for NuPluginDbus {
99170
}),
100171

101172
"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),
102176

103177
_ => Err(LabeledError {
104178
label: "Plugin invoked with unknown command name".into(),
@@ -156,4 +230,39 @@ impl NuPluginDbus {
156230
_ => Ok(Value::list(values, Span::unknown()))
157231
}
158232
}
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()))
267+
}
159268
}

0 commit comments

Comments
 (0)