From 0f9a856e967b2d55d7d0fc77887f7b9c038bea37 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 8 Jun 2024 18:44:11 +0800 Subject: [PATCH] Addendum --- Avalonia.Desktop.slnf | 1 + Avalonia.sln | 7 + .../Avalonia.FreeDesktop.csproj | 2 + src/Linux/Tmds.DBus/Connection2.cs | 1063 +++++++++++++++++ src/Linux/Tmds.DBus/ObjectPath2.cs | 179 +++ 5 files changed, 1252 insertions(+) create mode 100644 src/Linux/Tmds.DBus/Connection2.cs create mode 100644 src/Linux/Tmds.DBus/ObjectPath2.cs diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index b32d6e3d70..914a74c27e 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -41,6 +41,7 @@ "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj", "src\\Linux\\Tmds.DBus.Protocol\\Tmds.DBus.Protocol.csproj", "src\\Linux\\Tmds.DBus.SourceGenerator\\Tmds.DBus.SourceGenerator.csproj", + "src\\Linux\\Tmds.DBus\\Tmds.DBus.csproj", "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj", "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index 36f88e3592..8c4ad8b03e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -306,6 +306,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.Protocol", "src\L EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.SourceGenerator", "src\Linux\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator.csproj", "{FFE8B040-B467-424A-9DDB-6155DC1EB62E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus", "src\Linux\Tmds.DBus\Tmds.DBus.csproj", "{1ABAB94E-D687-4E3B-8489-39EDCFA91653}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -714,6 +716,10 @@ Global {FFE8B040-B467-424A-9DDB-6155DC1EB62E}.Debug|Any CPU.Build.0 = Debug|Any CPU {FFE8B040-B467-424A-9DDB-6155DC1EB62E}.Release|Any CPU.ActiveCfg = Release|Any CPU {FFE8B040-B467-424A-9DDB-6155DC1EB62E}.Release|Any CPU.Build.0 = Release|Any CPU + {1ABAB94E-D687-4E3B-8489-39EDCFA91653}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1ABAB94E-D687-4E3B-8489-39EDCFA91653}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1ABAB94E-D687-4E3B-8489-39EDCFA91653}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1ABAB94E-D687-4E3B-8489-39EDCFA91653}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -803,6 +809,7 @@ Global {9AE1B827-21AC-4063-AB22-C8804B7F931E} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {18B242C7-33BC-4B40-B12C-82B20F2BF638} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {FFE8B040-B467-424A-9DDB-6155DC1EB62E} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} + {1ABAB94E-D687-4E3B-8489-39EDCFA91653} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index 995c57dc64..168ad8b82a 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -20,6 +20,8 @@ OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all"/> + + diff --git a/src/Linux/Tmds.DBus/Connection2.cs b/src/Linux/Tmds.DBus/Connection2.cs new file mode 100644 index 0000000000..b67d897c5d --- /dev/null +++ b/src/Linux/Tmds.DBus/Connection2.cs @@ -0,0 +1,1063 @@ +// Copyright 2016 Tom Deseyn +// This software is made available under the MIT License +// See COPYING for details + +using System; +using System.Net; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tmds.DBus.CodeGen; +using Tmds.DBus.Protocol; + +namespace Tmds.DBus +{ + /// + /// Connection with a D-Bus peer. + /// + public class Connection2 : IConnection + { + [Flags] + private enum ConnectionType + { + None = 0, + ClientManual = 1, + ClientAutoConnect = 2, + Server = 4 + } + + /// + /// Assembly name where the dynamically generated code resides. + /// + public const string DynamicAssemblyName = "Tmds.DBus.Emit, PublicKey=002400000480000094000000060200000024000052534131000400000100010071a8770f460cce31df0feb6f94b328aebd55bffeb5c69504593df097fdd9b29586dbd155419031834411c8919516cc565dee6b813c033676218496edcbe7939c0dd1f919f3d1a228ebe83b05a3bbdbae53ce11bcf4c04a42d8df1a83c2d06cb4ebb0b447e3963f48a1ca968996f3f0db8ab0e840a89d0a5d5a237e2f09189ed3"; + + private static Connection2 s_systemConnection2; + private static Connection2 s_sessionConnection2; + private static readonly object NoDispose = new object(); + + /// + /// An AutoConnect Connection to the system bus. + /// + public static Connection2 System => s_systemConnection2 ?? CreateSystemConnection(); + + /// + /// An AutoConnect Connection to the session bus. + /// + public static Connection2 Session => s_sessionConnection2 ?? CreateSessionConnection(); + + private class ProxyFactory : IProxyFactory + { + public Connection2 Connection2 { get; } + public ProxyFactory(Connection2 connection2) + { + Connection2 = connection2; + } + public T CreateProxy(string serviceName, ObjectPath2 path2) + { + return Connection2.CreateProxy(serviceName, path2); + } + } + + private readonly object _gate = new object(); + private readonly Dictionary _registeredObjects = new Dictionary(); + private readonly Func> _connectFunction; + private readonly Action _disposeAction; + private readonly SynchronizationContext _synchronizationContext; + private readonly bool _runContinuationsAsynchronously; + private readonly ConnectionType _connectionType; + + private ConnectionState _state = ConnectionState.Created; + private bool _disposed = false; + private IProxyFactory _factory; + private DBusConnection _dbusConnection; + private Task _dbusConnectionTask; + private TaskCompletionSource _dbusConnectionTcs; + private CancellationTokenSource _connectCts; + private Exception _disconnectReason; + private IDBus _bus; + private EventHandler _stateChangedEvent; + private object _disposeUserToken = NoDispose; + + private IDBus DBus + { + get + { + if (_bus != null) + { + return _bus; + } + lock (_gate) + { + _bus = _bus ?? CreateProxy(DBusConnection.DBusServiceName, DBusConnection.DBusObjectPath2); + return _bus; + } + } + } + + /// + /// Occurs when the state changes. + /// + /// + /// The event handler will be called when it is added to the event. + /// The event handler is invoked on the ConnectionOptions.SynchronizationContext. + /// + public event EventHandler StateChanged + { + add + { + lock (_gate) + { + _stateChangedEvent += value; + if (_state != ConnectionState.Created) + { + EmitConnectionStateChanged(value); + } + } + } + remove + { + lock (_gate) + { + _stateChangedEvent -= value; + } + } + } + + /// + /// Creates a new Connection with a specific address. + /// + /// Address of the D-Bus peer. + public Connection2(string address) : + this(new ClientConnectionOptions(address)) + { } + + /// + /// Creates a new Connection with specific ConnectionOptions. + /// + /// + public Connection2(ConnectionOptions connectionOptions) + { + if (connectionOptions == null) + throw new ArgumentNullException(nameof(connectionOptions)); + + _factory = new ProxyFactory(this); + _synchronizationContext = connectionOptions.SynchronizationContext; + if (connectionOptions is ClientConnectionOptions clientConnectionOptions) + { + _connectionType = clientConnectionOptions.AutoConnect ? ConnectionType.ClientAutoConnect : ConnectionType.ClientManual ; + _connectFunction = clientConnectionOptions.SetupAsync; + _disposeAction = clientConnectionOptions.Teardown; + _runContinuationsAsynchronously = clientConnectionOptions.RunContinuationsAsynchronously; + } + else if (connectionOptions is ServerConnectionOptions serverConnectionOptions) + { + _connectionType = ConnectionType.Server; + _state = ConnectionState.Connected; + _dbusConnection = new DBusConnection(localServer: true, runContinuationsAsynchronously: false); + _dbusConnectionTask = Task.FromResult(_dbusConnection); + serverConnectionOptions.Connection2 = this; + } + else + { + throw new NotSupportedException($"Unknown ConnectionOptions type: '{typeof(ConnectionOptions).FullName}'"); + } + } + + /// + /// Connect with the remote peer. + /// + /// + /// Information about the established connection. + /// + /// There was an error establishing the connection. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection was closed after it was established. + public async Task ConnectAsync() + => (await DoConnectAsync().ConfigureAwait(false)).ConnectionInfo; + + private async Task DoConnectAsync() + { + Task connectionTask = null; + bool alreadyConnecting = false; + lock (_gate) + { + if (_disposed) + { + ThrowDisposed(); + } + + if (_connectionType == ConnectionType.ClientManual) + { + if (_state != ConnectionState.Created) + { + throw new InvalidOperationException("Can only connect once"); + } + } + else + { + if (_state == ConnectionState.Connecting || _state == ConnectionState.Connected) + { + connectionTask = _dbusConnectionTask; + alreadyConnecting = true; + } + } + if (!alreadyConnecting) + { + _connectCts = new CancellationTokenSource(); + _dbusConnectionTcs = new TaskCompletionSource(); + _dbusConnectionTask = _dbusConnectionTcs.Task; + connectionTask = _dbusConnectionTask; + _state = ConnectionState.Connecting; + + EmitConnectionStateChanged(); + } + } + + if (alreadyConnecting) + { + return await connectionTask.ConfigureAwait(false); + } + + DBusConnection connection; + object disposeUserToken = NoDispose; + try + { + ClientSetupResult connectionContext = await _connectFunction().ConfigureAwait(false); + disposeUserToken = connectionContext.TeardownToken; + connection = await DBusConnection.ConnectAsync(connectionContext, _runContinuationsAsynchronously, OnDisconnect, _connectCts.Token).ConfigureAwait(false); + } + catch (ConnectException ce) + { + if (disposeUserToken != NoDispose) + { + _disposeAction?.Invoke(disposeUserToken); + } + Disconnect(dispose: false, exception: ce); + throw; + } + catch (Exception e) + { + if (disposeUserToken != NoDispose) + { + _disposeAction?.Invoke(disposeUserToken); + } + var ce = new ConnectException(e.Message, e); + Disconnect(dispose: false, exception: ce); + throw ce; + } + lock (_gate) + { + if (_state == ConnectionState.Connecting) + { + _disposeUserToken = disposeUserToken; + _dbusConnection = connection; + _connectCts.Dispose(); + _connectCts = null; + _state = ConnectionState.Connected; + _dbusConnectionTcs.SetResult(connection); + _dbusConnectionTcs = null; + + EmitConnectionStateChanged(); + } + else + { + connection.Dispose(); + if (disposeUserToken != NoDispose) + { + _disposeAction?.Invoke(disposeUserToken); + } + } + ThrowIfNotConnected(); + } + return connection; + } + + /// + /// Disposes the connection. + /// + public void Dispose() + { + Disconnect(dispose: true, exception: CreateDisposedException()); + } + + /// + /// Creates a proxy object that represents a remote D-Bus object. + /// + /// Interface of the D-Bus object. + /// Name of the service that exposes the object. + /// Object path of the object. + /// + /// Proxy object. + /// + public T CreateProxy(string serviceName, ObjectPath2 path2) + { + CheckNotConnectionType(ConnectionType.Server); + return (T)CreateProxy(typeof(T), serviceName, path2); + } + + /// + /// Releases a service name assigned to the connection. + /// + /// Name of the service. + /// + /// true when the name was assigned to this connection; false when the name was not assigned to this connection. + /// + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection was closed after it was established. + /// Error returned by remote peer. + /// + /// This operation is not supported for AutoConnection connections. + /// + public async Task UnregisterServiceAsync(string serviceName) + { + CheckNotConnectionType(ConnectionType.ClientAutoConnect); + var connection = GetConnectedConnection(); + var reply = await connection.ReleaseNameAsync(serviceName).ConfigureAwait(false); + return reply == ReleaseNameReply.ReplyReleased; + } + + /// + /// Queues a service name registration for the connection. + /// + /// Name of the service. + /// Action invoked when the service name is assigned to the connection. + /// Action invoked when the service name is no longer assigned to the connection. + /// Options for the registration. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection was closed after it was established. + /// Error returned by remote peer. + /// Unexpected reply. + /// + /// This operation is not supported for AutoConnection connections. + /// + public async Task QueueServiceRegistrationAsync(string serviceName, Action onAquired = null, Action onLost = null, ServiceRegistrationOptions options = ServiceRegistrationOptions.Default) + { + CheckNotConnectionType(ConnectionType.ClientAutoConnect); + var connection = GetConnectedConnection(); + if (!options.HasFlag(ServiceRegistrationOptions.AllowReplacement) && (onLost != null)) + { + throw new ArgumentException($"{nameof(onLost)} can only be set when {nameof(ServiceRegistrationOptions.AllowReplacement)} is also set", nameof(onLost)); + } + + RequestNameOptions requestOptions = RequestNameOptions.None; + if (options.HasFlag(ServiceRegistrationOptions.ReplaceExisting)) + { + requestOptions |= RequestNameOptions.ReplaceExisting; + } + if (options.HasFlag(ServiceRegistrationOptions.AllowReplacement)) + { + requestOptions |= RequestNameOptions.AllowReplacement; + } + var reply = await connection.RequestNameAsync(serviceName, requestOptions, onAquired, onLost, CaptureSynchronizationContext()).ConfigureAwait(false); + switch (reply) + { + case RequestNameReply.PrimaryOwner: + case RequestNameReply.InQueue: + return; + case RequestNameReply.Exists: + case RequestNameReply.AlreadyOwner: + default: + throw new ProtocolException("Unexpected reply"); + } + } + + /// + /// Queues a service name registration for the connection. + /// + /// Name of the service. + /// Options for the registration. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection was closed after it was established. + /// Error returned by remote peer. + /// Unexpected reply. + /// + /// This operation is not supported for AutoConnection connections. + /// + public Task QueueServiceRegistrationAsync(string serviceName, ServiceRegistrationOptions options) + => QueueServiceRegistrationAsync(serviceName, null, null, options); + + /// + /// Requests a service name to be assigned to the connection. + /// + /// Name of the service. + /// Action invoked when the service name is no longer assigned to the connection. + /// + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection was closed after it was established. + /// Error returned by remote peer. + /// + /// This operation is not supported for AutoConnection connections. + /// + public async Task RegisterServiceAsync(string serviceName, Action onLost = null, ServiceRegistrationOptions options = ServiceRegistrationOptions.Default) + { + CheckNotConnectionType(ConnectionType.ClientAutoConnect); + var connection = GetConnectedConnection(); + if (!options.HasFlag(ServiceRegistrationOptions.AllowReplacement) && (onLost != null)) + { + throw new ArgumentException($"{nameof(onLost)} can only be set when {nameof(ServiceRegistrationOptions.AllowReplacement)} is also set", nameof(onLost)); + } + + RequestNameOptions requestOptions = RequestNameOptions.DoNotQueue; + if (options.HasFlag(ServiceRegistrationOptions.ReplaceExisting)) + { + requestOptions |= RequestNameOptions.ReplaceExisting; + } + if (options.HasFlag(ServiceRegistrationOptions.AllowReplacement)) + { + requestOptions |= RequestNameOptions.AllowReplacement; + } + var reply = await connection.RequestNameAsync(serviceName, requestOptions, null, onLost, CaptureSynchronizationContext()).ConfigureAwait(false); + switch (reply) + { + case RequestNameReply.PrimaryOwner: + return; + case RequestNameReply.Exists: + throw new InvalidOperationException("Service is registered by another connection"); + case RequestNameReply.AlreadyOwner: + throw new InvalidOperationException("Service is already registered by this connection"); + case RequestNameReply.InQueue: + default: + throw new ProtocolException("Unexpected reply"); + } + } + + /// + /// Requests a service name to be assigned to the connection. + /// + /// Name of the service. + /// + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection was closed after it was established. + /// Error returned by remote peer. + /// + /// This operation is not supported for AutoConnection connections. + /// + public Task RegisterServiceAsync(string serviceName, ServiceRegistrationOptions options) + => RegisterServiceAsync(serviceName, null, options); + + /// + /// Publishes an object. + /// + /// Object to publish. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection was closed. + public Task RegisterObjectAsync(IDBusObject o) + { + return RegisterObjectsAsync(new[] { o }); + } + + /// + /// Publishes objects. + /// + /// Objects to publish. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection was closed. + /// + /// This operation is not supported for AutoConnection connections. + /// + public async Task RegisterObjectsAsync(IEnumerable objects) + { + CheckNotConnectionType(ConnectionType.ClientAutoConnect); + var connection = GetConnectedConnection(); + var assembly = DynamicAssembly.Instance; + var registrations = new List(); + foreach (var o in objects) + { + var implementationType = assembly.GetExportTypeInfo(o.GetType()); + var objectPath = o.ObjectPath2; + var registration = (DBusAdapter)Activator.CreateInstance(implementationType.AsType(), _dbusConnection, objectPath, o, _factory, CaptureSynchronizationContext()); + registrations.Add(registration); + } + + lock (_gate) + { + connection.AddMethodHandlers(registrations.Select(r => new KeyValuePair(r.Path2, r.HandleMethodCall))); + + foreach (var registration in registrations) + { + _registeredObjects.Add(registration.Path2, registration); + } + } + try + { + foreach (var registration in registrations) + { + await registration.WatchSignalsAsync().ConfigureAwait(false); + } + lock (_gate) + { + foreach (var registration in registrations) + { + registration.CompleteRegistration(); + } + } + } + catch + { + lock (_gate) + { + foreach (var registration in registrations) + { + registration.Unregister(); + _registeredObjects.Remove(registration.Path2); + } + connection.RemoveMethodHandlers(registrations.Select(r => r.Path2)); + } + throw; + } + } + + /// + /// Unpublishes an object. + /// + /// Path of object to unpublish. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection is closed. + public void UnregisterObject(ObjectPath2 path2) + => UnregisterObjects(new[] { path2 }); + + /// + /// Unpublishes an object. + /// + /// object to unpublish. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection is closed. + public void UnregisterObject(IDBusObject o) + => UnregisterObject(o.ObjectPath2); + + /// + /// Unpublishes objects. + /// + /// Paths of objects to unpublish. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection is closed. + /// + /// This operation is not supported for AutoConnection connections. + /// + public void UnregisterObjects(IEnumerable paths) + { + CheckNotConnectionType(ConnectionType.ClientAutoConnect); + lock (_gate) + { + var connection = GetConnectedConnection(); + + foreach(var objectPath in paths) + { + DBusAdapter registration; + if (_registeredObjects.TryGetValue(objectPath, out registration)) + { + registration.Unregister(); + _registeredObjects.Remove(objectPath); + } + } + + connection.RemoveMethodHandlers(paths); + } + } + + /// + /// Unpublishes objects. + /// + /// Objects to unpublish. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection is closed. + /// + /// This operation is not supported for AutoConnection connections. + /// + public void UnregisterObjects(IEnumerable objects) + => UnregisterObjects(objects.Select(o => o.ObjectPath2)); + + /// + /// List services that can be activated. + /// + /// + /// List of activatable services. + /// + /// There was an error establishing the connection. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection is closed. + public Task ListActivatableServicesAsync() + => DBus.ListActivatableNamesAsync(); + + /// + /// Resolves the local address for a service. + /// + /// Name of the service. + /// + /// Local address of service. null is returned when the service name is not available. + /// + /// There was an error establishing the connection. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection is closed. + public async Task ResolveServiceOwnerAsync(string serviceName) + { + try + { + return await DBus.GetNameOwnerAsync(serviceName).ConfigureAwait(false); + } + catch (DBusException e) when (e.ErrorName == "org.freedesktop.DBus.Error.NameHasNoOwner") + { + return null; + } + catch + { + throw; + } + } + + /// + /// Activates a service. + /// + /// Name of the service. + /// + /// The result of the activation. + /// + /// There was an error establishing the connection. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection is closed. + public Task ActivateServiceAsync(string serviceName) + => DBus.StartServiceByNameAsync(serviceName, 0); + + /// + /// Checks if a service is available. + /// + /// Name of the service. + /// + /// true when the service is available, false otherwise. + /// + /// There was an error establishing the connection. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection is closed. + public Task IsServiceActiveAsync(string serviceName) + => DBus.NameHasOwnerAsync(serviceName); + + /// + /// Resolves the local address for a service. + /// + /// Name of the service. + /// Action invoked when the local name of the service changes. + /// Action invoked when the connection closes. + /// + /// Disposable that allows to stop receiving notifications. + /// + /// There was an error establishing the connection. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection is closed. + /// + /// The event handler will be called when the service name is already registered. + /// + public async Task ResolveServiceOwnerAsync(string serviceName, Action handler, Action onError = null) + { + if (serviceName == "*") + { + serviceName = ".*"; + } + + var synchronizationContext = CaptureSynchronizationContext(); + var wrappedDisposable = new WrappedDisposable(synchronizationContext); + bool namespaceLookup = serviceName.EndsWith(".*", StringComparison.Ordinal); + bool _eventEmitted = false; + var _gate = new object(); + var _emittedServices = namespaceLookup ? new List() : null; + + Action handleEvent = (ownerChange, ex) => { + if (ex != null) + { + if (onError == null) + { + return; + } + wrappedDisposable.Call(onError, ex, disposes: true); + return; + } + bool first = false; + lock (_gate) + { + if (namespaceLookup) + { + first = _emittedServices?.Contains(ownerChange.ServiceName) == false; + _emittedServices?.Add(ownerChange.ServiceName); + } + else + { + first = _eventEmitted == false; + _eventEmitted = true; + } + } + if (first) + { + if (ownerChange.NewOwner == null) + { + return; + } + ownerChange.OldOwner = null; + } + wrappedDisposable.Call(handler, ownerChange); + }; + + var connection = await GetConnectionTask().ConfigureAwait(false); + wrappedDisposable.Disposable = await connection.WatchNameOwnerChangedAsync(serviceName, handleEvent).ConfigureAwait(false); + if (namespaceLookup) + { + serviceName = serviceName.Substring(0, serviceName.Length - 2); + } + try + { + if (namespaceLookup) + { + var services = await ListServicesAsync().ConfigureAwait(false); + foreach (var service in services) + { + if (service.StartsWith(serviceName, StringComparison.Ordinal) + && ( (service.Length == serviceName.Length) + || (service[serviceName.Length] == '.') + || (serviceName.Length == 0 && service[0] != ':'))) + { + var currentName = await ResolveServiceOwnerAsync(service).ConfigureAwait(false); + lock (_gate) + { + if (currentName != null && !_emittedServices.Contains(serviceName)) + { + var e = new ServiceOwnerChangedEventArgs(service, null, currentName); + handleEvent(e, null); + } + } + } + } + lock (_gate) + { + _emittedServices = null; + } + } + else + { + var currentName = await ResolveServiceOwnerAsync(serviceName).ConfigureAwait(false); + lock (_gate) + { + if (currentName != null && !_eventEmitted) + { + var e = new ServiceOwnerChangedEventArgs(serviceName, null, currentName); + handleEvent(e, null); + } + } + } + return wrappedDisposable; + } + catch (Exception ex) + { + handleEvent(default(ServiceOwnerChangedEventArgs), ex); + } + + return wrappedDisposable; + } + + /// + /// List services that are available. + /// + /// + /// List of available services. + /// + /// There was an error establishing the connection. + /// The connection has been disposed. + /// The operation is invalid in the current state. + /// The connection is closed. + public Task ListServicesAsync() + => DBus.ListNamesAsync(); + + internal Task StartServerAsync(string address) + { + lock (_gate) + { + ThrowIfNotConnected(); + return _dbusConnection.StartServerAsync(address); + } + } + + // Used by tests + internal void Connect(DBusConnection dbusConnection) + { + lock (_gate) + { + if (_state != ConnectionState.Created) + { + throw new InvalidOperationException("Can only connect once"); + } + _dbusConnection = dbusConnection; + _dbusConnectionTask = Task.FromResult(_dbusConnection); + _state = ConnectionState.Connected; + } + } + + private object CreateProxy(Type interfaceType, string busName, ObjectPath2 path2) + { + var assembly = DynamicAssembly.Instance; + var implementationType = assembly.GetProxyTypeInfo(interfaceType); + + DBusObjectProxy instance = (DBusObjectProxy)Activator.CreateInstance(implementationType.AsType(), + new object[] { this, _factory, busName, path2 }); + + return instance; + } + + private void OnDisconnect(Exception e) + { + Disconnect(dispose: false, exception: e); + } + + private void ThrowIfNotConnected() + => ThrowIfNotConnected(_disposed, _state, _disconnectReason); + + internal static void ThrowIfNotConnected(bool disposed, ConnectionState state, Exception disconnectReason) + { + if (disposed) + { + ThrowDisposed(); + } + if (state == ConnectionState.Disconnected) + { + throw new DisconnectedException(disconnectReason); + } + else if (state == ConnectionState.Created) + { + throw new InvalidOperationException("Not Connected"); + } + else if (state == ConnectionState.Connecting) + { + throw new InvalidOperationException("Connecting"); + } + } + + internal static Exception CreateDisposedException() + => new ObjectDisposedException(typeof(Connection2).FullName); + + private static void ThrowDisposed() + { + throw CreateDisposedException(); + } + + internal static void ThrowIfNotConnecting(bool disposed, ConnectionState state, Exception disconnectReason) + { + if (disposed) + { + ThrowDisposed(); + } + if (state == ConnectionState.Disconnected) + { + throw new DisconnectedException(disconnectReason); + } + else if (state == ConnectionState.Created) + { + throw new InvalidOperationException("Not Connected"); + } + else if (state == ConnectionState.Connected) + { + throw new InvalidOperationException("Already Connected"); + } + } + + private Task GetConnectionTask() + { + var connectionTask = Volatile.Read(ref _dbusConnectionTask); + if (connectionTask != null) + { + return connectionTask; + } + if (_connectionType == ConnectionType.ClientAutoConnect) + { + return DoConnectAsync(); + } + else + { + return Task.FromResult(GetConnectedConnection()); + } + } + + private DBusConnection GetConnectedConnection() + { + var connection = Volatile.Read(ref _dbusConnection); + if (connection != null) + { + return connection; + } + lock (_gate) + { + ThrowIfNotConnected(); + return _dbusConnection; + } + } + + private void CheckNotConnectionType(ConnectionType disallowed) + { + if ((_connectionType & disallowed) != ConnectionType.None) + { + if (_connectionType == ConnectionType.ClientAutoConnect) + { + throw new InvalidOperationException($"Operation not supported for {nameof(ClientConnectionOptions.AutoConnect)} Connection."); + } + else if (_connectionType == ConnectionType.Server) + { + throw new InvalidOperationException($"Operation not supported for Server-based Connection."); + } + } + } + + private void Disconnect(bool dispose, Exception exception) + { + lock (_gate) + { + if (dispose) + { + _disposed = true; + } + var previousState = _state; + if (previousState == ConnectionState.Disconnecting || previousState == ConnectionState.Disconnected || previousState == ConnectionState.Created) + { + return; + } + + _disconnectReason = exception; + + var connection = _dbusConnection; + var connectionCts = _connectCts;; + var dbusConnectionTask = _dbusConnectionTask; + var dbusConnectionTcs = _dbusConnectionTcs; + var disposeUserToken = _disposeUserToken; + _dbusConnection = null; + _connectCts = null; + _dbusConnectionTask = null; + _dbusConnectionTcs = null; + _disposeUserToken = NoDispose; + + foreach (var registeredObject in _registeredObjects) + { + registeredObject.Value.Unregister(); + } + _registeredObjects.Clear(); + + _state = ConnectionState.Disconnecting; + EmitConnectionStateChanged(); + + connectionCts?.Cancel(); + connectionCts?.Dispose(); + dbusConnectionTcs?.SetException( + dispose ? CreateDisposedException() : + exception.GetType() == typeof(ConnectException) ? exception : + new DisconnectedException(exception)); + connection?.Disconnect(dispose, exception); + if (disposeUserToken != NoDispose) + { + _disposeAction?.Invoke(disposeUserToken); + } + + if (_state == ConnectionState.Disconnecting) + { + _state = ConnectionState.Disconnected; + EmitConnectionStateChanged(); + } + } + } + + private void EmitConnectionStateChanged(EventHandler handler = null) + { + var disconnectReason = _disconnectReason; + if (_state == ConnectionState.Connecting) + { + _disconnectReason = null; + } + + if (handler == null) + { + handler = _stateChangedEvent; + } + + if (handler == null) + { + return; + } + + if (disconnectReason != null + && disconnectReason.GetType() != typeof(ConnectException) + && disconnectReason.GetType() != typeof(ObjectDisposedException) + && disconnectReason.GetType() != typeof(DisconnectedException)) + { + disconnectReason = new DisconnectedException(disconnectReason); + } + var connectionInfo = _state == ConnectionState.Connected ? _dbusConnection.ConnectionInfo : null; + var stateChangeEvent = new ConnectionStateChangedEventArgs(_state, disconnectReason, connectionInfo); + + + if (_synchronizationContext != null && SynchronizationContext.Current != _synchronizationContext) + { + _synchronizationContext.Post(_ => handler(this, stateChangeEvent), null); + } + else + { + handler(this, stateChangeEvent); + } + } + + internal async Task CallMethodAsync(Message message) + { + var connection = await GetConnectionTask().ConfigureAwait(false); + try + { + return await connection.CallMethodAsync(message).ConfigureAwait(false); + } + catch (DisconnectedException) when (_connectionType == ConnectionType.ClientAutoConnect) + { + connection = await GetConnectionTask().ConfigureAwait(false); + return await connection.CallMethodAsync(message).ConfigureAwait(false); + } + } + + internal async Task WatchSignalAsync(ObjectPath2 path2, string @interface, string signalName, SignalHandler handler) + { + var connection = await GetConnectionTask().ConfigureAwait(false); + try + { + return await connection.WatchSignalAsync(path2, @interface, signalName, handler).ConfigureAwait(false); + } + catch (DisconnectedException) when (_connectionType == ConnectionType.ClientAutoConnect) + { + connection = await GetConnectionTask().ConfigureAwait(false); + return await connection.WatchSignalAsync(path2, @interface, signalName, handler).ConfigureAwait(false); + } + } + + internal SynchronizationContext CaptureSynchronizationContext() => _synchronizationContext; + + private static Connection2 CreateSessionConnection() => CreateConnection(Address.Session, ref s_sessionConnection2); + + private static Connection2 CreateSystemConnection() => CreateConnection(Address.System, ref s_systemConnection2); + + private static Connection2 CreateConnection(string address, ref Connection2 connection2) + { + address = address ?? "unix:"; + if (Volatile.Read(ref connection2) != null) + { + return connection2; + } + var newConnection = new Connection2(new ClientConnectionOptions(address) { AutoConnect = true, SynchronizationContext = null }); + Interlocked.CompareExchange(ref connection2, newConnection, null); + return connection2; + } + } +} diff --git a/src/Linux/Tmds.DBus/ObjectPath2.cs b/src/Linux/Tmds.DBus/ObjectPath2.cs new file mode 100644 index 0000000000..2d93c3e20f --- /dev/null +++ b/src/Linux/Tmds.DBus/ObjectPath2.cs @@ -0,0 +1,179 @@ +// Copyright 2006 Alp Toker +// Copyright 2010 Alan McGovern +// This software is made available under the MIT License +// See COPYING for details + +using System; + +namespace Tmds.DBus +{ + /// + /// Path to D-Bus object. + /// + public struct ObjectPath2 : IComparable, IComparable, IEquatable + { + /// + /// Root path ("/"). + /// + public static readonly ObjectPath2 Root = new ObjectPath2("/"); + + internal readonly string Value; + + /// + /// Creates a new ObjectPath. + /// + /// path. + public ObjectPath2(string value) + { + if (value == null) + throw new ArgumentNullException("value"); + + Validate(value); + + this.Value = value; + } + + static void Validate(string value) + { + if (!value.StartsWith("/", StringComparison.Ordinal)) + throw new ArgumentException("value"); + if (value.EndsWith("/", StringComparison.Ordinal) && value.Length > 1) + throw new ArgumentException("ObjectPath cannot end in '/'"); + + bool multipleSlash = false; + + foreach (char c in value) + { + bool valid = (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c == '_' + || (!multipleSlash && c == '/'); + + if (!valid) + { + var message = string.Format("'{0}' is not a valid character in an ObjectPath", c); + throw new ArgumentException(message, "value"); + } + + multipleSlash = c == '/'; + } + + } + + /// + /// Compares the current instance with another object of the same type and returns an integer that + /// indicates whether the current instance precedes, follows, or occurs in the same position in + /// the sort order as the other object. + /// + public int CompareTo(ObjectPath2 other) + { + return Value.CompareTo(other.Value); + } + + /// + /// Compares the current instance with another object of the same type and returns an integer that + /// indicates whether the current instance precedes, follows, or occurs in the same position in + /// the sort order as the other object. + /// + public int CompareTo(object otherObject) + { + var other = otherObject as ObjectPath2?; + + if (other == null) + return 1; + + return Value.CompareTo(other.Value.Value); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + public bool Equals(ObjectPath2 other) + { + return Value == other.Value; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + public override bool Equals(object o) + { + var b = o as ObjectPath2?; + + if (b == null) + return false; + + return Value.Equals(b.Value.Value, StringComparison.Ordinal); + } + + /// + /// Determines whether two specified ObjectPaths have the same value. + /// + public static bool operator==(ObjectPath2 a, ObjectPath2 b) + { + return a.Value == b.Value; + } + + /// + /// Determines whether two specified ObjectPaths have different values. + /// + public static bool operator!=(ObjectPath2 a, ObjectPath2 b) + { + return !(a == b); + } + + /// + /// Returns the hash code for this ObjectPath. + /// + public override int GetHashCode() + { + if (Value == null) + { + return 0; + } + return Value.GetHashCode(); + } + + /// + /// Returns a string that represents the current object. + /// + public override string ToString() + { + return Value; + } + + /// + /// Creates the ObjectPath that is represented by the string value. + /// + /// path. + public static implicit operator ObjectPath2(string value) + { + return new ObjectPath2(value); + } + + //this may or may not prove useful + internal string[] Decomposed + { + get + { + return Value.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + } + } + + internal ObjectPath2 Parent + { + get + { + if (Value == Root.Value) + return null; + + string par = Value.Substring(0, Value.LastIndexOf('/')); + if (par == String.Empty) + par = "/"; + + return new ObjectPath2(par); + } + } + } +}