diff --git a/src/Tmds.DBus.Protocol/Connection.cs b/src/Tmds.DBus.Protocol/Connection.cs index 4bb6bc69..973c4073 100644 --- a/src/Tmds.DBus.Protocol/Connection.cs +++ b/src/Tmds.DBus.Protocol/Connection.cs @@ -249,12 +249,19 @@ public async ValueTask AddMatchAsync(MatchRule rule, MessageValu } public void AddMethodHandler(IMethodHandler methodHandler) - => AddMethodHandlers([ methodHandler ]); + => UpdateMethodHandlers((dictionary, handler) => dictionary.AddMethodHandler(handler), methodHandler); public void AddMethodHandlers(IReadOnlyList methodHandlers) - { - GetConnection().AddMethodHandlers(methodHandlers); - } + => UpdateMethodHandlers((dictionary, handlers) => dictionary.AddMethodHandlers(handlers), methodHandlers); + + public void RemoveMethodHandler(string path) + => UpdateMethodHandlers((dictionary, path) => dictionary.RemoveMethodHandler(path), path); + + public void RemoveMethodHandlers(IEnumerable paths) + => UpdateMethodHandlers((dictionary, paths) => dictionary.RemoveMethodHandlers(paths), paths); + + private void UpdateMethodHandlers(Action update, T state) + => GetConnection().UpdateMethodHandlers(update, state); private static Connection CreateConnection(ref Connection? field, string? address) { diff --git a/src/Tmds.DBus.Protocol/DBusConnection.cs b/src/Tmds.DBus.Protocol/DBusConnection.cs index b86924ca..3ddcbb5a 100644 --- a/src/Tmds.DBus.Protocol/DBusConnection.cs +++ b/src/Tmds.DBus.Protocol/DBusConnection.cs @@ -490,16 +490,11 @@ private void EmitOnSynchronizationContextHelper(Observer observer, Synchronizati _currentSynchronizationContext = null; } - public void AddMethodHandlers(IReadOnlyList methodHandlers) + public void UpdateMethodHandlers(Action update, T state) { lock (_gate) { - if (_state == ConnectionState.Disconnected) - { - return; - } - - _pathNodes.AddMethodHandlers(methodHandlers); + update(_pathNodes, state); } } diff --git a/src/Tmds.DBus.Protocol/IMethodHandlerDictionary.cs b/src/Tmds.DBus.Protocol/IMethodHandlerDictionary.cs new file mode 100644 index 00000000..00bf17e4 --- /dev/null +++ b/src/Tmds.DBus.Protocol/IMethodHandlerDictionary.cs @@ -0,0 +1,9 @@ +namespace Tmds.DBus.Protocol; + +interface IMethodHandlerDictionary +{ + void AddMethodHandlers(IReadOnlyList methodHandlers); + void AddMethodHandler(IMethodHandler methodHandler); + void RemoveMethodHandler(string path); + void RemoveMethodHandlers(IEnumerable paths); +} \ No newline at end of file diff --git a/src/Tmds.DBus.Protocol/PathNodeDictionary.cs b/src/Tmds.DBus.Protocol/PathNodeDictionary.cs index e1285d2d..443a0421 100644 --- a/src/Tmds.DBus.Protocol/PathNodeDictionary.cs +++ b/src/Tmds.DBus.Protocol/PathNodeDictionary.cs @@ -74,8 +74,19 @@ public void CopyChildNamesTo(MethodContext methodContext) } } -sealed class PathNodeDictionary : Dictionary +sealed class PathNodeDictionary : IMethodHandlerDictionary { + private readonly Dictionary _dictionary = new(); + + public bool TryGetValue(string path, [NotNullWhen(true)]out PathNode? pathNode) + => _dictionary.TryGetValue(path, out pathNode); + + // For tests: + public PathNode this[string path] + => _dictionary[path]; + public int Count + => _dictionary.Count; + public void AddMethodHandlers(IReadOnlyList methodHandlers) { if (methodHandlers is null) @@ -89,22 +100,8 @@ public void AddMethodHandlers(IReadOnlyList methodHandlers) for (int i = 0; i < methodHandlers.Count; i++) { IMethodHandler methodHandler = methodHandlers[i] ?? throw new ArgumentNullException("methodHandler"); - string path = methodHandler.Path ?? throw new ArgumentNullException(nameof(methodHandler.Path)); - // Validate the path starts with '/' and has no empty sections. - // GetParentPath relies on this. - if (path[0] != '/' || path.IndexOf("//", StringComparison.Ordinal) != -1) - { - throw new FormatException($"The path '{path}' is not valid."); - } - - PathNode node = GetOrCreateNode(path); - - if (node.MethodHandler is not null) - { - throw new InvalidOperationException($"A method handler is already registered for the path '{path}'."); - } - node.MethodHandler = methodHandler; + AddMethodHandler(methodHandler); registeredCount++; } @@ -121,7 +118,7 @@ public void AddMethodHandlers(IReadOnlyList methodHandlers) private PathNode GetOrCreateNode(string path) { #if NET6_0_OR_GREATER - ref PathNode? node = ref CollectionsMarshal.GetValueRefOrAddDefault(this, path, out bool exists); + ref PathNode? node = ref CollectionsMarshal.GetValueRefOrAddDefault(_dictionary, path, out bool exists); if (exists) { return node!; @@ -129,12 +126,12 @@ private PathNode GetOrCreateNode(string path) PathNode newNode = new PathNode(); node = newNode; #else - if (this.TryGetValue(path, out PathNode? node)) + if (_dictionary.TryGetValue(path, out PathNode? node)) { return node; } PathNode newNode = new PathNode(); - Add(path, newNode); + _dictionary.Add(path, newNode); #endif string? parentPath = GetParentPath(path); if (parentPath is not null) @@ -178,7 +175,7 @@ private void RemoveMethodHandlers(IReadOnlyList methodHandlers, for (int i = 0; i < count; i++) { string path = methodHandlers[i].Path; - if (this.Remove(path, out PathNode? node)) + if (_dictionary.Remove(path, out PathNode? node)) { nodes[j++] = (path, node); node.MethodHandler = null; @@ -206,52 +203,104 @@ private void RemoveMethodHandlers(IReadOnlyList methodHandlers, for (int i = 0; i < count; i++) { var node = nodes[i]; - this[node.Path] = node.Node; + _dictionary[node.Path] = node.Node; } + } - void RemoveFromParent(string path, PathNode node) + private void RemoveFromParent(string path, PathNode node) + { + PathNode? parent = node.Parent; + if (parent is null) { - PathNode? parent = node.Parent; - if (parent is null) + return; + } + Debug.Assert(parent.ChildNameCount >= 1, "node is expected to be a known child"); + if (parent.ChildNameCount == 1) // We're the only child. + { + if (parent.MethodHandler is not null) { - return; + // Parent is still needed for the MethodHandler. + parent.ClearChildNames(); } - Debug.Assert(parent.ChildNameCount >= 1, "node is expected to be a known child"); - if (parent.ChildNameCount == 1) // We're the only child. + else { - if (parent.MethodHandler is not null) - { - // Parent is still needed for the MethodHandler. - parent.ClearChildNames(); - } - else - { // Suppress netstandard2.0 nullability warnings around NetstandardExtensions.Remove. #if NETSTANDARD2_0 #pragma warning disable CS8620 #pragma warning disable CS8604 #endif - - // Parent is no longer needed. - string parentPath = GetParentPath(path)!; - Debug.Assert(parentPath is not null); - this.Remove(parentPath, out PathNode? parentNode); - Debug.Assert(parentNode is not null); - RemoveFromParent(parentPath, parentNode); + // Parent is no longer needed. + string parentPath = GetParentPath(path)!; + Debug.Assert(parentPath is not null); + _dictionary.Remove(parentPath, out PathNode? parentNode); + Debug.Assert(parentNode is not null); + RemoveFromParent(parentPath, parentNode); #if NETSTANDARD2_0 #pragma warning restore CS8620 #pragma warning restore CS8604 #endif - } + } + } + else + { + string childName = GetChildName(path); + parent.RemoveChildName(childName); + } + } + + public void AddMethodHandler(IMethodHandler methodHandler) + { + string path = methodHandler.Path ?? throw new ArgumentNullException(nameof(methodHandler.Path)); + + // Validate the path starts with '/' and has no empty sections. + // GetParentPath relies on this. + if (path[0] != '/' || path.IndexOf("//", StringComparison.Ordinal) != -1) + { + throw new FormatException($"The path '{path}' is not valid."); + } + + PathNode node = GetOrCreateNode(path); + + if (node.MethodHandler is not null) + { + throw new InvalidOperationException($"A method handler is already registered for the path '{path}'."); + } + node.MethodHandler = methodHandler; + } + + public void RemoveMethodHandler(string path) + { + if (path is null) + { + throw new ArgumentNullException(nameof(path)); + } + if (_dictionary.Remove(path, out PathNode? node)) + { + if (node.ChildNameCount > 0) + { + // Node is still needed for its children. + node.MethodHandler = null; + _dictionary.Add(path, node); } else { - string childName = GetChildName(path); - parent.RemoveChildName(childName); + RemoveFromParent(path, node); } } } + public void RemoveMethodHandlers(IEnumerable paths) + { + if (paths is null) + { + throw new ArgumentNullException(nameof(paths)); + } + foreach (var path in paths) + { + RemoveMethodHandler(path); + } + } + private static readonly RemoveKeyComparer RemoveKeyComparerInstance = new(); sealed class RemoveKeyComparer : IComparer<(string Path, PathNode Node)> diff --git a/src/Tmds.DBus.Protocol/Polyfill/NullableAttributes.cs b/src/Tmds.DBus.Protocol/Polyfill/NullableAttributes.cs new file mode 100644 index 00000000..3cd6ed4e --- /dev/null +++ b/src/Tmds.DBus.Protocol/Polyfill/NullableAttributes.cs @@ -0,0 +1,144 @@ +namespace System.Diagnostics.CodeAnalysis +{ +#if NETSTANDARD2_0 + + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class AllowNullAttribute : Attribute { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class DisallowNullAttribute : Attribute { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute { } + + /// Specifies that an output will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } + +#endif + +#if !NETCOREAPP || NETCOREAPP3_1 + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } + +#endif +} diff --git a/test/Tmds.DBus.Protocol.Tests/PathNodeDictionaryTests.cs b/test/Tmds.DBus.Protocol.Tests/PathNodeDictionaryTests.cs index d4f4a11a..b08e7c5d 100644 --- a/test/Tmds.DBus.Protocol.Tests/PathNodeDictionaryTests.cs +++ b/test/Tmds.DBus.Protocol.Tests/PathNodeDictionaryTests.cs @@ -214,7 +214,7 @@ public void BadPathRemovesAddedMethodHandlers() new MethodHandler("invalid_path"), ])); - Assert.Empty(dictionary); + Assert.Equal(0, dictionary.Count); } [Fact] @@ -273,6 +273,109 @@ public void RemoveHandlersDoesntRemovePreExistingParentNodes() AssertChildNames([ "node2" ], root1); } + [Fact] + public void Remove() + { + var dictionary = new PathNodeDictionary(); + dictionary.AddMethodHandlers( + [ + new MethodHandler("/root1"), + ]); + Assert.Equal(2, dictionary.Count); + + dictionary.RemoveMethodHandler("/root1"); + + Assert.Equal(0, dictionary.Count); + } + + [Fact] + public void RemoveRemovesUnneededParentNodes() + { + var dictionary = new PathNodeDictionary(); + dictionary.AddMethodHandlers( + [ + new MethodHandler("/root1/node1"), + ]); + Assert.Equal(3, dictionary.Count); + + dictionary.RemoveMethodHandler("/root1/node1"); + + Assert.Equal(0, dictionary.Count); + } + + [Fact] + public void RemoveRetainsNeededParentNodes() + { + var dictionary = new PathNodeDictionary(); + dictionary.AddMethodHandlers( + [ + new MethodHandler("/root1"), + new MethodHandler("/root1/node1"), + ]); + Assert.Equal(3, dictionary.Count); + + dictionary.RemoveMethodHandler("/root1/node1"); + + Assert.Equal(2, dictionary.Count); + + PathNode root = dictionary["/"]; + Assert.Null(root.Parent); + Assert.Null(root.MethodHandler); + AssertChildNames([ "root1" ], root); + + PathNode root1 = dictionary["/root1"]; + Assert.Equal(root, root1.Parent); + Assert.NotNull(root1.MethodHandler); + AssertChildNames([ ], root1); + } + + [Fact] + public void RemoveRetainsForChildNodes() + { + var dictionary = new PathNodeDictionary(); + dictionary.AddMethodHandlers( + [ + new MethodHandler("/root1"), + new MethodHandler("/root1/node1"), + ]); + Assert.Equal(3, dictionary.Count); + + dictionary.RemoveMethodHandler("/root1"); + + PathNode root = dictionary["/"]; + Assert.Null(root.Parent); + Assert.Null(root.MethodHandler); + AssertChildNames([ "root1" ], root); + + PathNode root1 = dictionary["/root1"]; + Assert.Equal(root, root1.Parent); + Assert.Null(root1.MethodHandler); + AssertChildNames([ "node1" ], root1); + + PathNode node1 = dictionary["/root1/node1"]; + Assert.Equal(root1, node1.Parent); + Assert.NotNull(node1.MethodHandler); + AssertChildNames([ ], node1); + + Assert.Equal(3, dictionary.Count); + } + + [Fact] + public void RemoveMultiple() + { + var dictionary = new PathNodeDictionary(); + dictionary.AddMethodHandlers( + [ + new MethodHandler("/root1"), + new MethodHandler("/root2"), + ]); + Assert.Equal(3, dictionary.Count); + + dictionary.RemoveMethodHandlers([ "/root1", "/root2" ]); + + Assert.Equal(0, dictionary.Count); + } + private void AssertChildNames(string[] expectedChildNames, PathNode node) { var methodContext = new MethodContext(null!, null!, default);