Skip to content

Commit bc9b915

Browse files
committed
Add NetworkMonitorPortal
1 parent f39f8b3 commit bc9b915

10 files changed

Lines changed: 257 additions & 0 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ The package `LinuxDesktopUtils.XDGDesktopPortals` makes the following [XDG Deskt
1616
- [x] `OpenFile`
1717
- [x] `SaveFile`
1818
- [ ] `SaveFiles`
19+
- [ ] [Network Monitor](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.NetworkMonitor.html) version 3
20+
- [x] `GetAvailable` added in version 2
21+
- [x] `GetMetered` added in version 2
22+
- [x] `GetConnectivity` added in version 2
23+
- [x] `GetStatus` added in version 3
24+
- [x] `CanReach` added in version 3
25+
- [ ] Signal: `changed`
1926
- [x] [OpenURI](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html) version 5
2027
- [x] `OpenURI`
2128
- [x] `OpenFile`

examples/LinuxDesktopUtils.Examples/Program.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Diagnostics.CodeAnalysis;
33
using System.IO;
4+
using System.Net;
45
using System.Runtime.InteropServices;
56
using System.Threading;
67
using System.Threading.Tasks;
@@ -32,6 +33,20 @@ private static async Task RunAsync()
3233

3334
try
3435
{
36+
var networkMonitorPortal = await connectionManager.GetNetworkMonitorPortalAsync();
37+
38+
var networkStatus = await networkMonitorPortal.GetStatusAsync();
39+
Console.WriteLine($"Network status: {networkStatus}");
40+
41+
var canReachGoogle = await networkMonitorPortal.CanReachAsync(new Uri("https://google.com"));
42+
Console.WriteLine($"Can reach google: {canReachGoogle}");
43+
44+
var canReachCloudFlareDnsIpv4 = await networkMonitorPortal.CanReachAsync(IPAddress.Parse("1.1.1.1"), port: 51);
45+
Console.WriteLine($"Can reach cloudflare DNS (IPv4): {canReachCloudFlareDnsIpv4}");
46+
47+
var canReachCloudFlareDnsIpv6 = await networkMonitorPortal.CanReachAsync(IPAddress.Parse("2606:4700:4700::1111"), port: 51);
48+
Console.WriteLine($"Can reach cloudflare DNS (IPv6): {canReachCloudFlareDnsIpv6}");
49+
3550
var accountPortal = await connectionManager.GetAccountPortalAsync();
3651
var res = await accountPortal.GetUserInformationAsync(options: new AccountPortal.GetUserInformationOptions
3752
{

src/LinuxDesktopUtils.XDGDesktopPortal/DesktopPortalConnectionManager.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ internal string GetWindowIdentifier(Optional<WindowIdentifier> preferredIdentifi
5757
/// </summary>
5858
public ValueTask<FileChooserPortal> GetFileChooserPortalAsync() => GetPortalAsync(FileChooserPortal.CreateAsync);
5959

60+
/// <summary>
61+
/// Gets the <see cref="NetworkMonitorPortal"/>.
62+
/// </summary>
63+
/// <returns></returns>
64+
public ValueTask<NetworkMonitorPortal> GetNetworkMonitorPortalAsync() => GetPortalAsync(NetworkMonitorPortal.CreateAsync);
65+
6066
/// <summary>
6167
/// Gets the <see cref="OpenUriPortal"/>.
6268
/// </summary>

src/LinuxDesktopUtils.XDGDesktopPortal/LinuxDesktopUtils.XDGDesktopPortal.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
<Link>DBusXml\org.freedesktop.portal.FileChooser.xml</Link>
2727
</AdditionalFiles>
2828

29+
<AdditionalFiles Include="..\..\extern\flatpak\xdg-desktop-portal\data\org.freedesktop.portal.NetworkMonitor.xml">
30+
<DBusGeneratorMode>Proxy</DBusGeneratorMode>
31+
<Link>DBusXml\org.freedesktop.portal.NetworkMonitor.xml</Link>
32+
</AdditionalFiles>
33+
2934
<AdditionalFiles Include="..\..\extern\flatpak\xdg-desktop-portal\data\org.freedesktop.portal.OpenURI.xml">
3035
<DBusGeneratorMode>Proxy</DBusGeneratorMode>
3136
<Link>DBusXml\org.freedesktop.portal.OpenURI.xml</Link>

src/LinuxDesktopUtils.XDGDesktopPortal/LinuxDesktopUtils.XDGDesktopPortal.csproj.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=portals/@EntryIndexedValue">True</s:Boolean>
44
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=portals_005Caccount/@EntryIndexedValue">True</s:Boolean>
55
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=portals_005Cfilechooser/@EntryIndexedValue">True</s:Boolean>
6+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=portals_005Cnetworkmonitor/@EntryIndexedValue">True</s:Boolean>
67
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=portals_005Copenuri/@EntryIndexedValue">True</s:Boolean>
78
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=portals_005Csecret/@EntryIndexedValue">True</s:Boolean>
89
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=portals_005Ctrash/@EntryIndexedValue">True</s:Boolean>

src/LinuxDesktopUtils.XDGDesktopPortal/Portals/Account/AccountPortal.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ internal static async ValueTask<AccountPortal> CreateAsync(DesktopPortalConnecti
4747
/// <param name="windowIdentifier">Identifier of the parent window.</param>
4848
/// <param name="options">Additional options.</param>
4949
/// <param name="cancellationToken">CancellationToken to cancel the request.</param>
50+
/// <exception cref="PortalVersionException">Thrown if the installed portal backend doesn't support this method.</exception>
5051
public async Task<Response<GetUserInformationResults>> GetUserInformationAsync(
5152
Optional<WindowIdentifier> windowIdentifier = default,
5253
GetUserInformationOptions? options = null,
5354
Optional<CancellationToken> cancellationToken = default)
5455
{
56+
const uint addedInVersion = 1;
57+
PortalVersionException.ThrowIf(requiredVersion: addedInVersion, availableVersion: _version);
5558
if (cancellationToken.HasValue) cancellationToken.Value.ThrowIfCancellationRequested();
5659

5760
options ??= new GetUserInformationOptions();
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using JetBrains.Annotations;
2+
3+
namespace LinuxDesktopUtils.XDGDesktopPortal;
4+
5+
public partial class NetworkMonitorPortal
6+
{
7+
/// <summary>
8+
/// Represents the connectivity status returned by the network monitor.
9+
/// </summary>
10+
[PublicAPI]
11+
public enum ConnectivityStatus : uint
12+
{
13+
/// <summary>
14+
/// The host is not configured with a route to the internet.
15+
/// </summary>
16+
LocalOnly = 1,
17+
18+
/// <summary>
19+
/// The host is connected to a network, but can’t reach the full internet.
20+
/// </summary>
21+
Limited = 2,
22+
23+
/// <summary>
24+
/// The host is behind a captive portal and cannot reach the full internet.
25+
/// </summary>
26+
Captive = 3,
27+
28+
/// <summary>
29+
/// The host connected to a network, and can reach the full internet.
30+
/// </summary>
31+
Full = 4,
32+
}
33+
}
34+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using JetBrains.Annotations;
4+
using Tmds.DBus.Protocol;
5+
6+
namespace LinuxDesktopUtils.XDGDesktopPortal;
7+
8+
public partial class NetworkMonitorPortal
9+
{
10+
/// <summary>
11+
/// Results of <see cref="NetworkMonitorPortal.GetStatusAsync"/>.
12+
/// </summary>
13+
[PublicAPI]
14+
public record GetStatusResults
15+
{
16+
/// <summary>
17+
/// Whether the network is available.
18+
/// </summary>
19+
public bool IsAvailable { get; set; }
20+
21+
/// <summary>
22+
/// Whether the network is metered.
23+
/// </summary>
24+
public bool IsMetered { get; set; }
25+
26+
/// <summary>
27+
/// The level of connectivity.
28+
/// </summary>
29+
public ConnectivityStatus Status { get; set; }
30+
31+
internal static GetStatusResults From(Dictionary<string, VariantValue> varDict)
32+
{
33+
var res = new GetStatusResults();
34+
35+
if (varDict.TryGetValue("available", out var availableValue))
36+
{
37+
var isAvailable = availableValue.GetBool();
38+
res.IsAvailable = isAvailable;
39+
}
40+
41+
if (varDict.TryGetValue("metered", out var meteredValue))
42+
{
43+
var isMetered = meteredValue.GetBool();
44+
res.IsMetered = isMetered;
45+
}
46+
47+
if (varDict.TryGetValue("connectivity", out var connectivityValue))
48+
{
49+
var rawStatus = connectivityValue.GetUInt32();
50+
if (rawStatus is < (uint)ConnectivityStatus.LocalOnly or > (uint)ConnectivityStatus.Full)
51+
throw new NotSupportedException($"Portal returned invalid connectivity status: `{rawStatus}`");
52+
53+
res.Status = (ConnectivityStatus)rawStatus;
54+
}
55+
56+
return res;
57+
}
58+
}
59+
}
60+
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System;
2+
using System.Net;
3+
using System.Threading.Tasks;
4+
using JetBrains.Annotations;
5+
using Tmds.DBus.SourceGenerator;
6+
7+
namespace LinuxDesktopUtils.XDGDesktopPortal;
8+
9+
/// <summary>
10+
/// Network monitoring portal.
11+
///
12+
/// The NetworkMonitor portal provides network status information to sandboxed applications.
13+
/// </summary>
14+
/// <remarks>
15+
/// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.NetworkMonitor.html.
16+
/// </remarks>
17+
[PublicAPI]
18+
public partial class NetworkMonitorPortal : IPortal
19+
{
20+
private readonly DesktopPortalConnectionManager _connectionManager;
21+
private readonly OrgFreedesktopPortalNetworkMonitorProxy _instance;
22+
private readonly uint _version;
23+
24+
/// <inheritdoc/>
25+
uint IPortal.Version => _version;
26+
27+
private NetworkMonitorPortal(
28+
DesktopPortalConnectionManager connectionManager,
29+
OrgFreedesktopPortalNetworkMonitorProxy instance,
30+
uint version)
31+
{
32+
_connectionManager = connectionManager;
33+
_instance = instance;
34+
_version = version;
35+
}
36+
37+
internal static async ValueTask<NetworkMonitorPortal> CreateAsync(DesktopPortalConnectionManager connectionManager)
38+
{
39+
var instance = new OrgFreedesktopPortalNetworkMonitorProxy(connectionManager.GetConnection(), destination: DBusHelper.BusName, path: DBusHelper.ObjectPath);
40+
var version = await instance.GetVersionPropertyAsync().ConfigureAwait(false);
41+
42+
return new NetworkMonitorPortal(connectionManager, instance, version);
43+
}
44+
45+
/// <summary>
46+
/// Returns whether the network is considered available. That is, whether the system as a default route for at least one of IPv4 or IPv6.
47+
/// </summary>
48+
/// <exception cref="PortalVersionException">Thrown if the installed portal backend doesn't support this method.</exception>
49+
public async Task<bool> GetAvailableAsync()
50+
{
51+
const uint addedInVersion = 2;
52+
PortalVersionException.ThrowIf(requiredVersion: addedInVersion, availableVersion: _version);
53+
54+
var result = await _instance.GetAvailableAsync().ConfigureAwait(false);
55+
return result;
56+
}
57+
58+
/// <summary>
59+
/// Returns whether the network is considered metered. That is, whether the system as traffic flowing through the default connection that is subject ot limitations by service providers.
60+
/// </summary>
61+
/// <exception cref="PortalVersionException">Thrown if the installed portal backend doesn't support this method.</exception>
62+
public async Task<bool> GetMeteredAsync()
63+
{
64+
const uint addedInVersion = 2;
65+
PortalVersionException.ThrowIf(requiredVersion: addedInVersion, availableVersion: _version);
66+
67+
var result = await _instance.GetMeteredAsync().ConfigureAwait(false);
68+
return result;
69+
}
70+
71+
/// <summary>
72+
/// Returns more detailed information about the host’s network connectivity.
73+
/// </summary>
74+
/// <exception cref="PortalVersionException">Thrown if the installed portal backend doesn't support this method.</exception>
75+
/// <exception cref="NotSupportedException">Thrown if the portal returned an unknown connectivity status.</exception>
76+
public async Task<ConnectivityStatus> GetConnectivityAsync()
77+
{
78+
const uint addedInVersion = 2;
79+
PortalVersionException.ThrowIf(requiredVersion: addedInVersion, availableVersion: _version);
80+
81+
var result = await _instance.GetConnectivityAsync().ConfigureAwait(false);
82+
if (result is < (uint)ConnectivityStatus.LocalOnly or > (uint)ConnectivityStatus.Full)
83+
throw new NotSupportedException($"Portal returned invalid connectivity status: `{result}`");
84+
85+
return (ConnectivityStatus)result;
86+
}
87+
88+
/// <summary>
89+
/// Returns values from <see cref="GetAvailableAsync"/>, <see cref="GetMeteredAsync"/>, and <see cref="GetConnectivityAsync"/>
90+
/// in one call.
91+
/// </summary>
92+
/// <exception cref="PortalVersionException">Thrown if the installed portal backend doesn't support this method.</exception>
93+
/// <exception cref="NotSupportedException">Thrown if the portal returned an unknown connectivity status.</exception>
94+
public async Task<GetStatusResults> GetStatusAsync()
95+
{
96+
const uint addedInVersion = 3;
97+
PortalVersionException.ThrowIf(requiredVersion: addedInVersion, availableVersion: _version);
98+
99+
var values = await _instance.GetStatusAsync().ConfigureAwait(false);
100+
var res = GetStatusResults.From(values);
101+
return res;
102+
}
103+
104+
/// <inheritdoc cref="CanReachAsync(System.String, System.UInt32)"/>
105+
public Task<bool> CanReachAsync(Uri uri) => CanReachAsync(uri.Host, (uint)uri.Port);
106+
107+
/// <inheritdoc cref="CanReachAsync(System.String, System.UInt32)"/>
108+
public Task<bool> CanReachAsync(IPAddress address, uint port) => CanReachAsync(address.ToString(), port);
109+
110+
/// <summary>
111+
/// Returns whether the given hostname is believed to be reachable.
112+
/// </summary>
113+
/// <exception cref="PortalVersionException">Thrown if the installed portal backend doesn't support this method.</exception>
114+
public async Task<bool> CanReachAsync(string hostname, uint port)
115+
{
116+
const uint addedInVersion = 3;
117+
PortalVersionException.ThrowIf(requiredVersion: addedInVersion, availableVersion: _version);
118+
119+
var res = await _instance.CanReachAsync(hostname, port).ConfigureAwait(false);
120+
return res;
121+
}
122+
}

src/LinuxDesktopUtils.XDGDesktopPortal/Portals/Trash/TrashPortal.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ internal static async ValueTask<TrashPortal> CreateAsync(DesktopPortalConnection
4646
/// </summary>
4747
/// <param name="file">Absolute path to the file.</param>
4848
/// <returns>Whether the file was trashed successfully</returns>
49+
/// <exception cref="PortalVersionException">Thrown if the installed portal backend doesn't support this method.</exception>
4950
public async Task<bool> TrashFileAsync(FilePath file)
5051
{
52+
const uint addedInVersion = 1;
53+
PortalVersionException.ThrowIf(requiredVersion: addedInVersion, availableVersion: _version);
54+
5155
using var safeFileHandle = File.OpenHandle(file.Value, FileMode.Open, FileAccess.ReadWrite);
5256

5357
var result = await _instance.TrashFileAsync(

0 commit comments

Comments
 (0)