Nushell plugin for interacting with D-Bus
0
fork

Configure Feed

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

Basic method call support, no args or response handling yet

+237 -5
+1
Cargo.toml
··· 6 6 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 7 8 8 [dependencies] 9 + dbus = "0.9.7" 9 10 nu-plugin = "0.89.0" 10 11 nu-protocol = { version = "0.89.0", features = ["plugin"] }
+78
src/client.rs
··· 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 + }
+84
src/config.rs
··· 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 + }
+74 -5
src/main.rs
··· 1 1 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::*; 3 9 4 10 fn main() { 5 11 serve_plugin(&mut NuPluginDbus, MsgPackSerializer) 6 12 } 7 13 14 + /// The main plugin interface for nushell 8 15 struct NuPluginDbus; 9 16 10 17 impl Plugin for NuPluginDbus { 11 18 fn signature(&self) -> Vec<PluginSignature> { 12 19 vec![ 13 20 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 + ]), 17 45 ] 18 46 } 19 47 ··· 21 49 &mut self, 22 50 name: &str, 23 51 call: &EvaluatedCall, 24 - input: &Value, 52 + _input: &Value, 25 53 ) -> Result<Value, LabeledError> { 26 54 match name { 27 55 "dbus" => Err(LabeledError { ··· 29 57 msg: "add --help to see subcommands".into(), 30 58 span: Some(call.head) 31 59 }), 60 + 61 + "dbus call" => self.call(call), 32 62 33 63 _ => Err(LabeledError { 34 64 label: "Plugin invoked with unknown command name".into(), ··· 38 68 } 39 69 } 40 70 } 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 + }