diff --git a/README.md b/README.md index fe5c6560..e68ff415 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,24 @@ communication, D-Bus helps coordinate process lifecycle; it makes it simple and application or daemon, and to launch applications and daemons on demand when their services are needed. Higher-level bindings are available for various popular frameworks and languages (Qt, GLib, Java, Python, etc.). -[dbus-sharp](https://github.com/mono/dbus-sharp) (a fork of [ndesk-dbus](http://www.ndesk.org/DBusSharp)) is a C# -implementation which targets Mono and .NET 2.0. -Tmds.DBus builds on top of the protocol implementation of dbus-sharp and provides an API based on the asynchronous programming model introduced in .NET 4.5. The library targets .NET Standard 2.0 which means it runs on .NET Framework 4.6.1 (Windows 7 SP1 and later), .NET Core, and .NET 6. You can get Tmds.DBus from [NuGet](https://www.nuget.org/packages/Tmds.DBus). +This source repository provides two libraries for working with D-Bus. -# Tmds.DBus.Protocol +- `Tmds.DBus` is a library that is based on [dbus-sharp](https://github.com/mono/dbus-sharp), which is a fork of [ndesk-dbus](http://www.ndesk.org/DBusSharp)). `Tmds.DBus` builds on top of the protocol implementation of dbus-sharp and provides an API based on the asynchronous programming model introduced in .NET 4.5. -The `Tmds.DBus.Protocol` package provides a low-level API for the D-Bus protocol. Unlike the high-level `Tmds.DBus` library, the protocol library can be used with Native AOT compilation. +- `Tmds.DBus.Protocol` is a library that uses the types introduces in .NET Core 2.1 (like `Span`) that enable writing a low-allocation, high-performance protocol implementation. This library is compatible with NativeAOT/Trimming (introduced in .NET 7). -[affederaffe/Tmds.DBus.SourceGenerator](https://github.com/affederaffe/Tmds.DBus.SourceGenerator) provides a source generator that targets the protocol library. +Both libraries target .NET Standard 2.0 which means it runs on .NET Framework 4.6.1 (Windows 7 SP1 and later), .NET Core, and .NET 6 and higher. -# Tmds.DBus Example +To use `Tmds.DBus.Protocol` with trimming/NativeAOT, use .NET 8 or higher. + +# Code generators + +- [affederaffe/Tmds.DBus.SourceGenerator](https://github.com/affederaffe/Tmds.DBus.SourceGenerator) provides a source generator that targets the `Tmds.DBus.Protocol` library. This source generator supports generating proxy types (to consume objects provided by other services) as well as handler types (to provide objects to other applications). + +- The `Tmds.DBus.Tool` .NET global CLI tool includes a code generator for `Tmds.DBus` and `Tmds.DBus.Protocol`. For the `Tmds.DBus.Protocol` library, the code generator only supports generating proxy types. + +# Example In this section we build an example console application that writes a message when a network interface changes state. To detect the state changes we use the NetworkManager daemon's D-Bus service. @@ -34,33 +40,18 @@ $ dotnet new console -o netmon $ cd netmon ``` -Now we add references to `Tmds.DBus` in `netmon.csproj`. If you need to target framework, `netcoreapp2.0`, add `7.1` below `...` to use `async Task Main` (C# 7.1). - -```xml - - - Exe - net6.0 - - - - - +Now we add references to `Tmds.DBus.Protocol`: ``` - -Let's `restore` to fetch these dependencies: - -```bash -$ dotnet restore +dotnet add package Tmds.DBus.Protocol ``` -Now we'll install the `Tmds.DBus.Tool`. +Next, we'll install the `Tmds.DBus.Tool`. ```bash -$ dotnet tool install -g Tmds.DBus.Tool +$ dotnet tool update -g Tmds.DBus.Tool ``` -Next, we use the `list` command to find out some information about the NetworkManager service: +We use the `list` command to find out some information about the NetworkManager service: ```bash $ dotnet dbus list services --bus system | grep NetworkManager @@ -74,77 +65,118 @@ $ dotnet dbus list objects --bus system --service org.freedesktop.NetworkManager These command show us that the `org.freedesktop.NetworkManager` service is on the `system` bus and has an entry point object at `/org/freedesktop/NetworkManager` which implements `org.freedesktop.NetworkManager`. -Now we'll invoke the `codegen` command to generate C# interfaces for the NetworkManager service. +Now we'll invoke the `codegen` command to generate C# interfaces for the NetworkManager service. We use the `--protocol-api` argument for targetting the `Tmds.DBus.Protocol` library. ```bash -$ dotnet dbus codegen --bus system --service org.freedesktop.NetworkManager +$ dotnet dbus codegen --protocol-api --bus system --service org.freedesktop.NetworkManager ``` This generates a `NetworkManager.DBus.cs` file in the local folder. -We update `Program.cs` to have an async `Main` and instiantiate an `INetworkManager` proxy object. +When we try to compile the code using `dotnet build`, the compiler will give us some errors: -```C# -using System; -using Tmds.DBus; -using NetworkManager.DBus; -using System.Threading.Tasks; - -namespace netmon -{ - class Program - { - static async Task Main(string[] args) - { - Console.WriteLine("Monitoring network state changes. Press Ctrl-C to stop."); - - var systemConnection = Connection.System; - var networkManager = systemConnection.CreateProxy("org.freedesktop.NetworkManager", - "/org/freedesktop/NetworkManager"); +``` +NetworkManager.DBus.cs(871,35): error CS0111: Type 'NetworkManager' already defines a member called 'GetDevicesAsync' with the same parameter types [/tmp/netmon/netmon.csproj] +NetworkManager.DBus.cs(873,35): error CS0111: Type 'NetworkManager' already defines a member called 'GetAllDevicesAsync' with the same parameter types [/tmp/netmon/netmon.csproj] +NetworkManager.DBus.cs(3723,35): error CS0111: Type 'Wireless' already defines a member called 'GetAccessPointsAsync' with the same parameter types [/tmp/netmon/netmon.csproj] +``` - await Task.Delay(int.MaxValue); - } - } -} +These errors occur because the D-Bus interfaces declare D-Bus methods named `GetXyz` and D-Bus properties which are named `Xyz`. The resulting C# methods that are generated have the same name which causes these errors. Because these methods are two ways to get the same information, we'll fix the problem by commenting out the `GetDevicesAsync`/`GetAllDevicesAsync`/`GetAccessPointsAsync` C# methods that are implemented using properties. + +```diff +diff --git a/NetworkManager.DBus.cs b/NetworkManager.DBus.cs +index fab04fd..eb57d16 100644 +--- a/NetworkManager.DBus.cs ++++ b/NetworkManager.DBus.cs +@@ -868,10 +868,10 @@ namespace NetworkManager.DBus + return writer.CreateMessage(); + } + } +- public Task GetDevicesAsync() +- => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Devices"), (Message m, object? s) => ReadMessage_v_ao(m, (NetworkManagerObject)s!), this); +- public Task GetAllDevicesAsync() +- => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "AllDevices"), (Message m, object? s) => ReadMessage_v_ao(m, (NetworkManagerObject)s!), this); ++ // public Task GetDevicesAsync() ++ // => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Devices"), (Message m, object? s) => ReadMessage_v_ao(m, (NetworkManagerObject)s!), this); ++ // public Task GetAllDevicesAsync() ++ // => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "AllDevices"), (Message m, object? s) => ReadMessage_v_ao(m, (NetworkManagerObject)s!), this); + public Task GetCheckpointsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Checkpoints"), (Message m, object? s) => ReadMessage_v_ao(m, (NetworkManagerObject)s!), this); + public Task GetNetworkingEnabledAsync() +@@ -3720,8 +3720,8 @@ namespace NetworkManager.DBus + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Mode"), (Message m, object? s) => ReadMessage_v_u(m, (NetworkManagerObject)s!), this); + public Task GetBitrateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Bitrate"), (Message m, object? s) => ReadMessage_v_u(m, (NetworkManagerObject)s!), this); +- public Task GetAccessPointsAsync() +- => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "AccessPoints"), (Message m, object? s) => ReadMessage_v_ao(m, (NetworkManagerObject)s!), this); ++ // public Task GetAccessPointsAsync() ++ // => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "AccessPoints"), (Message m, object? s) => ReadMessage_v_ao(m, (NetworkManagerObject)s!), this); + public Task GetActiveAccessPointAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "ActiveAccessPoint"), (Message m, object? s) => ReadMessage_v_o(m, (NetworkManagerObject)s!), this); + public Task GetWirelessCapabilitiesAsync() ``` -Note that we are using the static `Connection.System`. `Connection.System` and `Connection.Session` provide a connection to the system bus and session bus. These static members provide a convenient way to share the same `Connection` throughout the application. The connection to the bus is established automatically on first use. Statefull operations (e.g. `Connection.RegisterServiceAsync`) are not allowed. For these use-cases you must create an instance of the -`Connection` and manually connect it. +When we run `dotnet build` again, the compiler errors are gone. -When we look at the `INetworkManager` interface in `NetworkManager.DBus.cs`, we see it has a `GetDevicesAsync` method. +We update `Program.cs` to the following code which uses the `NetworkManager` service to monitor network devices for state changes. ```C# -Task GetDevicesAsync(); -``` +using Connection = Tmds.DBus.Protocol.Connection; +using NetworkManager.DBus; +using Tmds.DBus.Protocol; -This method is returning `ObjectPath[]`. These paths refer to other objects of the D-Bus service. We can use them with `CreateProxy`. Instead, we'll update the method to reflect it is returning `IDevice` objects. +string? systemBusAddress = Address.System; +if (systemBusAddress is null) +{ + Console.Write("Can not determine system bus address"); + return 1; +} -```C# -Task GetDevicesAsync(); -``` +Connection connection = new Connection(Address.System!); +await connection.ConnectAsync(); +Console.WriteLine("Connected to system bus."); -We will now add the code to iterate over the devices and add a signal handler for the state change: +var service = new NetworkManagerService(connection, "org.freedesktop.NetworkManager"); +var networkManager = service.CreateNetworkManager("/org/freedesktop/NetworkManager"); -```C# -foreach (var device in await networkManager.GetDevicesAsync()) +foreach (var devicePath in await networkManager.GetDevicesAsync()) { + var device = service.CreateDevice(devicePath); var interfaceName = await device.GetInterfaceAsync(); + + Console.WriteLine($"Subscribing for state changes of '{interfaceName}'."); await device.WatchStateChangedAsync( - change => Console.WriteLine($"{interfaceName}: {change.oldState} -> {change.newState}") - ); + (Exception? ex, (uint NewState, uint OldState, uint Reason) change) => + { + if (ex is null) + { + Console.WriteLine($"Interface '{interfaceName}' changed from '{change.OldState}' to '{change.NewState}'."); + } + }); +} + +Exception? disconnectReason = await connection.DisconnectedAsync(); +if (disconnectReason is not null) +{ + Console.WriteLine("The connection was closed:"); + Console.WriteLine(disconnectReason); + return 1; } +return 0; ``` When we run our program and change our network interfaces (e.g. turn on/off WiFi) notifications show up: ```bash $ dotnet run -Monitoring network state changes. Press Ctrl-C to stop. -wlp4s0: 100 -> 20 +Connected to system bus. +Subscribing for state changes of 'lo'. +Subscribing for state changes of 'wlp0s20f3'. +Interface 'wlp0s20f3' changed from '100' to '20'. ``` -When we look up the documentation of the StateChanged signal, we find the meaning of the magical constants: -[enum `NMDeviceState`](https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMDeviceState). +In the documentation of the StateChanged signal, we find the meaning of the magical constants: +[enum `NMDeviceState`](https://developer-old.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMDeviceState). We can model this enumeration in C#: @@ -167,19 +199,73 @@ enum DeviceState : uint } ``` -We add the enum to `NetworkManager.DBus.cs` and then update the signature of the `WatchStateChangedAsync` so it -uses `DeviceState` instead of `uint`. +We'll add the enum to `NetworkManager.DBus.cs` and then update `WatchStateChangedAsync` so it uses `DeviceState` instead of `uint` for the state. + +```diff +index eb57d16..663ed69 100644 +--- a/NetworkManager.DBus.cs ++++ b/NetworkManager.DBus.cs +@@ -2573,8 +2573,8 @@ namespace NetworkManager.DBus + return writer.CreateMessage(); + } + } +- public ValueTask WatchStateChangedAsync(Action handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) +- => base.WatchSignalAsync(Service.Destination, __Interface, Path, "StateChanged", (Message m, object? s) => ReadMessage_uuu(m, (NetworkManagerObject)s!), handler, emitOnCapturedContext, flags); ++ public ValueTask WatchStateChangedAsync(Action handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) ++ => base.WatchSignalAsync(Service.Destination, __Interface, Path, "StateChanged", (Message m, object? s) => ((DeviceState, DeviceState, uint))ReadMessage_uuu(m, (NetworkManagerObject)s!), handler, emitOnCapturedContext, flags); + public Task SetUdiAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); +@@ -5792,4 +5792,21 @@ namespace NetworkManager.DBus + public bool HasChanged(string property) => Array.IndexOf(Changed, property) != -1; + public bool IsInvalidated(string property) => Array.IndexOf(Invalidated, property) != -1; + } ++ ++ enum DeviceState : uint ++ { ++ Unknown = 0, ++ Unmanaged = 10, ++ Unavailable = 20, ++ Disconnected = 30, ++ Prepare = 40, ++ Config = 50, ++ NeedAuth = 60, ++ IpConfig = 70, ++ IpCheck = 80, ++ Secondaries = 90, ++ Activated = 100, ++ Deactivating = 110, ++ Failed = 120 ++ } + } +``` -```C# -Task WatchStateChangedAsync(Action<(DeviceState newState, DeviceState oldState, uint reason)> action); +Now, we update `Program.cs` to use `DeviceState`: +```cs + await device.WatchStateChangedAsync( + (Exception? ex, (DeviceState NewState, DeviceState OldState, uint Reason) change) => + { + if (ex is null) + { + Console.WriteLine($"Interface '{interfaceName}' changed from '{change.OldState}' to '{change.NewState}'."); + } + }); ``` When we run our application again, we see more meaningful messages. ```bash $ dotnet run -Monitoring network state changes. Press Ctrl-C to stop. -wlp4s0: Activated -> Unavailable +Connected to system bus. +Subscribing for state changes of 'lo'. +Subscribing for state changes of 'wlp0s20f3'. +Interface 'wlp0s20f3' changed from 'Activated' to 'Unavailable'. +``` + +The resulting application is compatible with NativeAOT and trimming. To publish it as a NativeAOT application, run: + +```bash +$ dotnet publish /p:PublishAot=true ``` # CI Packages