Skip to content

Commit d8becee

Browse files
committed
Basic method call support, no args or response handling yet
1 parent 7fcb4a4 commit d8becee

4 files changed

Lines changed: 237 additions & 5 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ edition = "2021"
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9+
dbus = "0.9.7"
910
nu-plugin = "0.89.0"
1011
nu-protocol = { version = "0.89.0", features = ["plugin"] }

src/client.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use dbus::{blocking::LocalConnection, channel::{Channel, BusType}};
2+
use nu_plugin::LabeledError;
3+
use nu_protocol::Spanned;
4+
5+
use crate::config::{DbusClientConfig, DbusBusChoice};
6+
7+
/// Executes D-Bus actions on a connection, handling nushell types
8+
pub struct DbusClient {
9+
config: DbusClientConfig,
10+
conn: LocalConnection,
11+
}
12+
13+
impl DbusClient {
14+
pub fn new(config: DbusClientConfig) -> Result<DbusClient, LabeledError> {
15+
// Try to connect to the correct D-Bus destination, as specified in the config
16+
let channel = match &config.bus_choice.item {
17+
DbusBusChoice::Session => Channel::get_private(BusType::Session),
18+
DbusBusChoice::System => Channel::get_private(BusType::System),
19+
DbusBusChoice::Started => Channel::get_private(BusType::Starter),
20+
DbusBusChoice::Peer(address) => Channel::open_private(address),
21+
DbusBusChoice::Bus(address) => Channel::open_private(address).and_then(|mut ch| {
22+
ch.register()?;
23+
Ok(ch)
24+
}),
25+
}.map_err(|err| {
26+
LabeledError {
27+
label: err.to_string(),
28+
msg: "while connecting to D-Bus as specified here".into(),
29+
span: Some(config.bus_choice.span),
30+
}
31+
})?;
32+
Ok(DbusClient {
33+
config,
34+
conn: LocalConnection::from(channel)
35+
})
36+
}
37+
38+
/// Call a D-Bus method and wait for the response
39+
pub fn call(
40+
&self,
41+
dest: &Spanned<String>,
42+
object: &Spanned<String>,
43+
interface: &Spanned<String>,
44+
method: &Spanned<String>,
45+
) -> Result<(), LabeledError> {
46+
// TODO convert & return response
47+
// TODO accept arguments
48+
// Validate inputs before sending to the dbus lib so we don't panic
49+
macro_rules! validate_with {
50+
($type:ty, $spanned:expr) => (<$type>::new(&$spanned.item).map_err(|msg| {
51+
LabeledError {
52+
label: msg,
53+
msg: "this argument is incorrect".into(),
54+
span: Some($spanned.span),
55+
}
56+
}))
57+
}
58+
let valid_dest = validate_with!(dbus::strings::BusName, dest)?;
59+
let valid_object = validate_with!(dbus::strings::Path, object)?;
60+
let valid_interface = validate_with!(dbus::strings::Interface, interface)?;
61+
let valid_method = validate_with!(dbus::strings::Member, method)?;
62+
63+
// Send method call
64+
let proxy = self.conn.with_proxy(
65+
valid_dest,
66+
valid_object,
67+
self.config.timeout.item
68+
);
69+
let () = proxy.method_call(valid_interface, valid_method, ()).map_err(|err| {
70+
LabeledError {
71+
label: err.to_string(),
72+
msg: "while calling a D-Bus method".into(),
73+
span: Some(self.config.span),
74+
}
75+
})?;
76+
Ok(())
77+
}
78+
}

src/config.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use std::time::Duration;
2+
3+
use nu_plugin::{EvaluatedCall, LabeledError};
4+
use nu_protocol::{Spanned, Span};
5+
6+
/// General configuration related to the D-Bus client connection
7+
#[derive(Debug, Clone)]
8+
pub struct DbusClientConfig {
9+
pub span: Span,
10+
pub bus_choice: Spanned<DbusBusChoice>,
11+
pub timeout: Spanned<Duration>,
12+
}
13+
14+
/// Where to connect to the D-Bus server
15+
#[derive(Debug, Clone, Default)]
16+
pub enum DbusBusChoice {
17+
/// Connect to the session bus
18+
#[default]
19+
Session,
20+
/// Connect to the system bus
21+
System,
22+
/// Connect to the bus that started this process
23+
Started,
24+
/// Connect to a bus instance at the given address
25+
Bus(String),
26+
/// Connect to a non-bus D-Bus server at the given address (will not send Hello)
27+
Peer(String),
28+
}
29+
30+
impl TryFrom<&EvaluatedCall> for DbusClientConfig {
31+
type Error = LabeledError;
32+
33+
fn try_from(call: &EvaluatedCall) -> Result<Self, Self::Error> {
34+
let mut config = DbusClientConfig {
35+
span: call.head,
36+
bus_choice: Spanned { item: DbusBusChoice::default(), span: call.head },
37+
timeout: Spanned { item: Duration::from_secs(2), span: call.head },
38+
};
39+
40+
// Handle recognized config args
41+
for (name, value) in &call.named {
42+
match &name.item[..] {
43+
r#type @ ("session" | "system" | "started") => {
44+
if value.is_none() || value.as_ref().is_some_and(|v| v.is_true()) {
45+
let dest = match r#type {
46+
"session" => DbusBusChoice::Session,
47+
"system" => DbusBusChoice::System,
48+
"started" => DbusBusChoice::Started,
49+
_ => unreachable!()
50+
};
51+
config.bus_choice = Spanned { item: dest, span: name.span };
52+
}
53+
},
54+
r#type @ ("bus" | "peer") => {
55+
if let Some(value) = value {
56+
let address = value.as_string()?;
57+
let dest = match r#type {
58+
"bus" => DbusBusChoice::Bus(address),
59+
"peer" => DbusBusChoice::Peer(address),
60+
_ => unreachable!()
61+
};
62+
config.bus_choice = Spanned { item: dest, span: value.span() };
63+
}
64+
},
65+
"timeout" => {
66+
if let Some(value) = value {
67+
let nanos: u64 = value.as_duration()?.try_into().map_err(|_| {
68+
LabeledError {
69+
label: "Timeout must be a positive duration".into(),
70+
msg: "invalid timeout specified here".into(),
71+
span: Some(value.span()),
72+
}
73+
})?;
74+
let item = Duration::from_nanos(nanos);
75+
config.timeout = Spanned { item, span: value.span() };
76+
}
77+
},
78+
_ => ()
79+
}
80+
}
81+
82+
Ok(config)
83+
}
84+
}

src/main.rs

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,55 @@
11
use nu_plugin::{serve_plugin, MsgPackSerializer, Plugin, EvaluatedCall, LabeledError};
2-
use nu_protocol::{PluginSignature, Value};
2+
use nu_protocol::{PluginSignature, Value, SyntaxShape, PluginExample};
3+
4+
mod config;
5+
mod client;
6+
7+
use config::*;
8+
use client::*;
39

410
fn main() {
511
serve_plugin(&mut NuPluginDbus, MsgPackSerializer)
612
}
713

14+
/// The main plugin interface for nushell
815
struct NuPluginDbus;
916

1017
impl Plugin for NuPluginDbus {
1118
fn signature(&self) -> Vec<PluginSignature> {
1219
vec![
1320
PluginSignature::build("dbus")
14-
.usage("Commands for interacting with D-Bus")
15-
.search_terms(vec!["dbus".into()])
16-
.category(nu_protocol::Category::Platform),
21+
.is_dbus_command()
22+
.usage("Commands for interacting with D-Bus"),
23+
PluginSignature::build("dbus call")
24+
.is_dbus_command()
25+
.accepts_dbus_client_options()
26+
.usage("Call a method and get its response")
27+
.named("timeout", SyntaxShape::Duration, "How long to wait for a response", None)
28+
.required_named("dest", SyntaxShape::String,
29+
"The name of the connection to send the method to",
30+
None)
31+
.required("object", SyntaxShape::String,
32+
"The path to the object to call the method on")
33+
.required("interface", SyntaxShape::String,
34+
"The name of the interface the method belongs to")
35+
.required("method", SyntaxShape::String,
36+
"The name of the method to send")
37+
.plugin_examples(vec![
38+
PluginExample {
39+
example: "dbus call --dest=org.freedesktop.DBus \
40+
/org/freedesktop/DBus org.freedesktop.DBus.Peer Ping".into(),
41+
description: "Ping the D-Bus server itself".into(),
42+
result: None
43+
}
44+
]),
1745
]
1846
}
1947

2048
fn run(
2149
&mut self,
2250
name: &str,
2351
call: &EvaluatedCall,
24-
input: &Value,
52+
_input: &Value,
2553
) -> Result<Value, LabeledError> {
2654
match name {
2755
"dbus" => Err(LabeledError {
@@ -30,6 +58,8 @@ impl Plugin for NuPluginDbus {
3058
span: Some(call.head)
3159
}),
3260

61+
"dbus call" => self.call(call),
62+
3363
_ => Err(LabeledError {
3464
label: "Plugin invoked with unknown command name".into(),
3565
msg: "unknown command".into(),
@@ -38,3 +68,42 @@ impl Plugin for NuPluginDbus {
3868
}
3969
}
4070
}
71+
72+
/// For conveniently adding the base options to a dbus command
73+
trait DbusSignatureUtilExt {
74+
fn is_dbus_command(self) -> Self;
75+
fn accepts_dbus_client_options(self) -> Self;
76+
}
77+
78+
impl DbusSignatureUtilExt for PluginSignature {
79+
fn is_dbus_command(self) -> Self {
80+
self.search_terms(vec!["dbus".into()])
81+
.category(nu_protocol::Category::Platform)
82+
}
83+
84+
fn accepts_dbus_client_options(self) -> Self {
85+
self.switch("session", "Send to the session message bus (default)", None)
86+
.switch("system", "Send to the system message bus", None)
87+
.switch("started", "Send to the bus that started this process, if applicable", None)
88+
.named("bus", SyntaxShape::String, "Send to the bus server at the given address", None)
89+
.named("peer", SyntaxShape::String,
90+
"Send to a non-bus D-Bus server at the given address. \
91+
Will not call the Hello method on initialization.",
92+
None)
93+
}
94+
}
95+
96+
impl NuPluginDbus {
97+
fn call(&self, call: &EvaluatedCall) -> Result<Value, LabeledError> {
98+
let config = DbusClientConfig::try_from(call)?;
99+
let dbus = DbusClient::new(config)?;
100+
dbus.call(
101+
&call.get_flag("dest")?.unwrap(),
102+
&call.req(0)?,
103+
&call.req(1)?,
104+
&call.req(2)?,
105+
)?;
106+
// TODO handle response
107+
Ok(Value::nothing(call.head))
108+
}
109+
}

0 commit comments

Comments
 (0)