diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3951c0d..51176b9 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 6.x.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/HandyIpc.Generator/ClientProxy.cs b/HandyIpc.Generator/ClientProxy.cs index 07aa896..ebd7ac9 100644 --- a/HandyIpc.Generator/ClientProxy.cs +++ b/HandyIpc.Generator/ClientProxy.cs @@ -7,7 +7,7 @@ namespace HandyIpc.Generator { public static class ClientProxy { - public static string Generate(INamedTypeSymbol @interface, IReadOnlyCollection methods) + public static string Generate(INamedTypeSymbol @interface, IReadOnlyCollection methods, IReadOnlyCollection events) { var (@namespace, className, typeParameters) = @interface.GenerateNameFromInterface(); string interfaceType = @interface.ToFullDeclaration(); @@ -30,12 +30,54 @@ public class {nameof(ClientProxy)}{className}{typeParameters} : {interfaceType} private readonly Sender _sender; private readonly ISerializer _serializer; private readonly string _key; +{Text(events.Any() ? @" + private readonly AwaiterManager _awaiterManager; +" : RemoveLineIfEmpty)} + +{events.For(item => $@" + private event {item.Type.ToTypeDeclaration()} _{item.Name}; +")} +{events.For(item => + { + IParameterSymbol eSymbol = ((INamedTypeSymbol)item.Type).DelegateInvokeMethod!.Parameters[1]; + string eType = eSymbol.Type.ToTypeDeclaration(); + return $@" + + public event {item.Type.ToTypeDeclaration()} {item.Name} + {{ + add + {{ + if (_{item.Name} == null) + {{ + _awaiterManager.Subscribe(""{item.Name}"", args => + {{ + var e = ({eType})_serializer.Deserialize(args, typeof({eType})); + _{item.Name}?.Invoke(this, e); + }}); + }} + + _{item.Name} += value; + }} + remove + {{ + _{item.Name} -= value; + if (_{item.Name} == null) + {{ + _awaiterManager.Unsubscribe(""{item.Name}""); + }} + }} + }} +"; + })} public {nameof(ClientProxy)}{className}(Sender sender, ISerializer serializer, string key) {{ _sender = sender; _serializer = serializer; _key = key; +{Text(events.Any() ? @" + _awaiterManager = new AwaiterManager(key, sender, serializer); +" : RemoveLineIfEmpty)} }} {methods.For(method => { diff --git a/HandyIpc.Generator/DiagnosticDescriptors.cs b/HandyIpc.Generator/DiagnosticDescriptors.cs index 5469557..59965e8 100644 --- a/HandyIpc.Generator/DiagnosticDescriptors.cs +++ b/HandyIpc.Generator/DiagnosticDescriptors.cs @@ -26,15 +26,24 @@ internal static class DiagnosticDescriptors true, helpLinkUri: $"{HelpLinkUri}#hi002"); - public static readonly DiagnosticDescriptor EventWithoutReturn = new( + public static readonly DiagnosticDescriptor UseStandardEventHandler = new( "HI003", - "Events are not allowed to have return values", - "The event '{0}' cannot have a return value. Consider using an event handler without a return value.", + "Standard event declarations must be used", + "The event '{0}' does not use the standard event declaration. Consider using an event signature like 'void EventHandler(object this, T eventArgs)'.", HandyIpc, DiagnosticSeverity.Error, true, helpLinkUri: $"{HelpLinkUri}#hi003"); + public static readonly DiagnosticDescriptor ContainsNotSupportedMembers = new( + "HI004", + "This interface contains members that are not supported", + "This interface '{0}' contains members that are not supported. Consider removing non-method or non-event members.", + HandyIpc, + DiagnosticSeverity.Error, + true, + helpLinkUri: $"{HelpLinkUri}#hi004"); + public static readonly DiagnosticDescriptor MustContainsMethod = new( "HI100", "The contract interface must contains methods", @@ -43,24 +52,6 @@ internal static class DiagnosticDescriptors DiagnosticSeverity.Warning, true, helpLinkUri: $"{HelpLinkUri}#hi100"); - - public static readonly DiagnosticDescriptor UseStandardEventHandler = new( - "HI101", - "Standard event declarations should be used", - "The event '{0}' does not use the standard event declaration. Consider using either System.EventHandler or System.EventHandler to declare the event.", - HandyIpc, - DiagnosticSeverity.Warning, - true, - helpLinkUri: $"{HelpLinkUri}#hi101"); - - public static readonly DiagnosticDescriptor ContainsNotSupportedMembers = new( - "HI102", - "This interface contains members that are not supported", - "This interface '{0}' contains members that are not supported. Consider removing non-method or non-event members.", - HandyIpc, - DiagnosticSeverity.Warning, - true, - helpLinkUri: $"{HelpLinkUri}#hi102"); } #pragma warning restore RS2008 // Enable analyzer release tracking } diff --git a/HandyIpc.Generator/Dispatcher.cs b/HandyIpc.Generator/Dispatcher.cs index 1c2a0b3..65ce671 100644 --- a/HandyIpc.Generator/Dispatcher.cs +++ b/HandyIpc.Generator/Dispatcher.cs @@ -7,7 +7,7 @@ namespace HandyIpc.Generator { public static class Dispatcher { - public static string Generate(INamedTypeSymbol @interface, IReadOnlyCollection methods) + public static string Generate(INamedTypeSymbol @interface, IReadOnlyCollection methods, IReadOnlyCollection events) { var (@namespace, className, typeParameters) = @interface.GenerateNameFromInterface(); string interfaceType = @interface.ToFullDeclaration(); @@ -26,7 +26,7 @@ namespace {@namespace} [global::System.Diagnostics.DebuggerNonUserCode] [global::System.Reflection.Obfuscation(Exclude = true)] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - public class {nameof(Dispatcher)}{className}{typeParameters} : IMethodDispatcher + public class {nameof(Dispatcher)}{className}{typeParameters} : IMethodDispatcher{(events.Any() ? ", INotifiable" : null)} {@interface.TypeParameters.For(typeParameter => $@" {typeParameter.ToGenericConstraint()} ")} @@ -36,6 +36,10 @@ public class {nameof(Dispatcher)}{className}{typeParameters} : IMethodDispatcher private readonly Lazy> _genericMethodMapping; " : RemoveLineIfEmpty)} +{Text(events.Any() ? @" + public NotifierManager NotifierManager { get; set; } + +" : RemoveLineIfEmpty)} public {nameof(Dispatcher)}{className}({interfaceType} instance) {{ _instance = instance; @@ -43,6 +47,9 @@ public class {nameof(Dispatcher)}{className}{typeParameters} : IMethodDispatcher _genericMethodMapping = new Lazy>( () => GeneratorHelper.GetGenericMethodMapping(typeof({interfaceType}), _instance)); " : RemoveLineIfEmpty)} +{events.For(item => $@" + instance.{item.Name} += (_, e) => NotifierManager.Publish(""{item.Name}"", e); +")} }} public async Task Dispatch(Context ctx, Func next) @@ -120,8 +127,6 @@ public async Task Dispatch(Context ctx, Func next) default: throw new ArgumentOutOfRangeException(""No matching remote method was found.""); }} - - await next(); }} }} }} diff --git a/HandyIpc.Generator/Extensions.cs b/HandyIpc.Generator/Extensions.cs index e038329..e552f7d 100644 --- a/HandyIpc.Generator/Extensions.cs +++ b/HandyIpc.Generator/Extensions.cs @@ -6,7 +6,14 @@ namespace HandyIpc.Generator { internal static class Extensions { - public static INamedTypeSymbol TaskTypeSymbol { get; set; } = null!; + private static INamedTypeSymbol TaskTypeSymbol = null!; + private static INamedTypeSymbol ObjectTypeSymbol = null!; + + public static void Initialize(Compilation compilation) + { + TaskTypeSymbol = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!; + ObjectTypeSymbol = compilation.GetTypeByMetadataName("System.Object")!; + } public static string ToFullDeclaration(this ISymbol symbol) { @@ -122,5 +129,14 @@ public static IEnumerable EnumerateSelfAndBaseType(this ITypeSymbol current = current.BaseType; } } + + public static bool IsStdEventHandler(this IEventSymbol symbol) + { + IMethodSymbol methodSymbol = ((INamedTypeSymbol)symbol.Type).DelegateInvokeMethod!; + + return methodSymbol.ReturnsVoid && + methodSymbol.Parameters.Length == 2 && + methodSymbol.Parameters[0].Type.Equals(ObjectTypeSymbol, SymbolEqualityComparer.Default); + } } } diff --git a/HandyIpc.Generator/HandyIpc.Generator.csproj b/HandyIpc.Generator/HandyIpc.Generator.csproj index 2da2e8c..81d90a4 100644 --- a/HandyIpc.Generator/HandyIpc.Generator.csproj +++ b/HandyIpc.Generator/HandyIpc.Generator.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 0.5.0 + 0.6.0 latest diff --git a/HandyIpc.Generator/ServerProxy.cs b/HandyIpc.Generator/ServerProxy.cs index c65bb9c..10186cf 100644 --- a/HandyIpc.Generator/ServerProxy.cs +++ b/HandyIpc.Generator/ServerProxy.cs @@ -7,7 +7,7 @@ namespace HandyIpc.Generator { public static class ServerProxy { - public static string Generate(INamedTypeSymbol @interface, IReadOnlyCollection methods) + public static string Generate(INamedTypeSymbol @interface, IReadOnlyCollection methods, IReadOnlyCollection events) { var (@namespace, className, typeParameters) = @interface.GenerateNameFromInterface(); string interfaceType = @interface.ToFullDeclaration(); @@ -26,9 +26,16 @@ public class {nameof(ServerProxy)}{className}{typeParameters} : {interfaceType} {{ private readonly {interfaceType} _instance; +{events.For(item => $@" + public event {item.Type.ToTypeDeclaration()} {item.Name}; +")} + public {nameof(ServerProxy)}{className}({interfaceType} instance) {{ _instance = instance; +{events.For(item => $@" + instance.{item.Name} += (sender, e) => {item.Name}?.Invoke(sender, e); +")} }} {methods.For(method => { diff --git a/HandyIpc.Generator/SourceGenerator.cs b/HandyIpc.Generator/SourceGenerator.cs index f00f48e..a846f90 100644 --- a/HandyIpc.Generator/SourceGenerator.cs +++ b/HandyIpc.Generator/SourceGenerator.cs @@ -14,7 +14,7 @@ public class SourceGenerator : ISourceGenerator { public void Execute(GeneratorExecutionContext context) { - //Debugger.Launch(); + //System.Diagnostics.Debugger.Launch(); if (context.SyntaxReceiver is not SyntaxReceiver receiver) { @@ -22,9 +22,9 @@ public void Execute(GeneratorExecutionContext context) } Compilation compilation = context.Compilation; + Extensions.Initialize(compilation); INamedTypeSymbol? ipcContractAttributeSymbol = compilation.GetTypeByMetadataName("HandyIpc.IpcContractAttribute"); - Extensions.TaskTypeSymbol = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!; if (ipcContractAttributeSymbol is null) { context.ReportDiagnostic(Diagnostic.Create(HandyIpcNotReferenced, Location.None)); @@ -42,14 +42,30 @@ public void Execute(GeneratorExecutionContext context) // WORKAROUND: The @interface must not be null here. .Select(@interface => @interface!) .Where(@interface => ContainsAttribute(@interface, ipcContractAttributeSymbol)); - }) - .Select(@interface => ( - @interface, - methods: @interface.GetMembers().OfType().ToList().AsReadOnly())); + }); var fileNameCounter = new Dictionary(); - foreach (var (@interface, methods) in contractInterfaces) + foreach (var @interface in contractInterfaces) { + ISymbol[] members = @interface.GetMembers().ToArray(); + IMethodSymbol[] methods = members.OfType() + .Where(item => item.MethodKind + is not MethodKind.EventAdd + and not MethodKind.EventRemove + and not MethodKind.EventRaise) + .ToArray(); + IEventSymbol[] events = members.OfType().ToArray(); + + if (members.Length != methods.Length + events.Length * 3) + { + foreach (Location location in @interface.Locations) + { + context.ReportDiagnostic(Diagnostic.Create(ContainsNotSupportedMembers, location, @interface.Name)); + } + + continue; + } + if (@interface.Interfaces.Length > 0) { foreach (Location location in @interface.Locations) @@ -60,7 +76,7 @@ public void Execute(GeneratorExecutionContext context) continue; } - if (!methods.Any()) + if (!methods.Any() && !events.Any()) { foreach (Location location in @interface.Locations) { @@ -70,9 +86,21 @@ public void Execute(GeneratorExecutionContext context) continue; } - string clientProxySource = ClientProxy.Generate(@interface, methods); - string serverProxySource = ServerProxy.Generate(@interface, methods); - string dispatcherSource = Dispatcher.Generate(@interface, methods); + bool hasInvalidEvent = false; + foreach (var location in events.Where(@event => !@event.IsStdEventHandler()).SelectMany(@event => @event.Locations)) + { + hasInvalidEvent = true; + context.ReportDiagnostic(Diagnostic.Create(UseStandardEventHandler, location, @interface.Name)); + } + + if (hasInvalidEvent) + { + continue; + } + + string clientProxySource = ClientProxy.Generate(@interface, methods, events); + string serverProxySource = ServerProxy.Generate(@interface, methods, events); + string dispatcherSource = Dispatcher.Generate(@interface, methods, events); string fileName = GetUniqueString(@interface.Name, fileNameCounter); diff --git a/HandyIpc.NamedPipe/HandyIpc.NamedPipe.csproj b/HandyIpc.NamedPipe/HandyIpc.NamedPipe.csproj index b35b5f0..97c58a0 100644 --- a/HandyIpc.NamedPipe/HandyIpc.NamedPipe.csproj +++ b/HandyIpc.NamedPipe/HandyIpc.NamedPipe.csproj @@ -3,12 +3,12 @@ HandyIpc.NamedPipe netstandard2.0 - 0.5.0 + 0.6.0 true - + diff --git a/HandyIpc.Serializer.Json/HandyIpc.Serializer.Json.csproj b/HandyIpc.Serializer.Json/HandyIpc.Serializer.Json.csproj index 9da4512..7f3d478 100644 --- a/HandyIpc.Serializer.Json/HandyIpc.Serializer.Json.csproj +++ b/HandyIpc.Serializer.Json/HandyIpc.Serializer.Json.csproj @@ -2,14 +2,14 @@ HandyIpc.Serializer.Json - netstandard2.0 - 0.5.0 + netstandard2.0 + 0.6.0 true - + diff --git a/HandyIpc.Socket/HandyIpc.Socket.csproj b/HandyIpc.Socket/HandyIpc.Socket.csproj index 026ed47..70f5b9d 100644 --- a/HandyIpc.Socket/HandyIpc.Socket.csproj +++ b/HandyIpc.Socket/HandyIpc.Socket.csproj @@ -2,13 +2,13 @@ HandyIpc.Socket - netstandard2.0 - 0.5.0 + netstandard2.0 + 0.6.0 true - + diff --git a/HandyIpc.Tests/BuildInTypeTest.cs b/HandyIpc.Tests/BuildInTypeTest.cs index 56e5b06..8affa15 100644 --- a/HandyIpc.Tests/BuildInTypeTest.cs +++ b/HandyIpc.Tests/BuildInTypeTest.cs @@ -36,7 +36,7 @@ public void TestBuildInTypesWithSocket() private static void TestCases(IBuildInType instance) { - Assert.Throws(instance.TestVoidWithoutParams); + Helper.AssertInnerException(instance.TestVoidWithoutParams); instance.TestDoNothing(); diff --git a/HandyIpc.Tests/EventTypeTest.cs b/HandyIpc.Tests/EventTypeTest.cs new file mode 100644 index 0000000..3e5e532 --- /dev/null +++ b/HandyIpc.Tests/EventTypeTest.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; +using HandyIpc; +using HandyIpcTests.Fixtures; +using HandyIpcTests.Interfaces; +using Xunit; + +namespace HandyIpcTests +{ + [Collection(nameof(CollectionFixture))] + public class EventTypeTest + { + private readonly NamedPipeFixture _namedPipeFixture; + private readonly SocketFixture _socketFixture; + + public EventTypeTest(NamedPipeFixture namedPipeFixture, SocketFixture socketFixture) + { + _namedPipeFixture = namedPipeFixture; + _socketFixture = socketFixture; + } + + [Fact] + public async Task TestEventHandlerWithSocket() + { + var instance = _socketFixture.Client.Resolve(); + await TestEventHandlerSubscribeAndUnsubscribe(instance); + } + + [Fact] + public async Task TestEventHandlerWithNamedPipe() + { + var instance = _namedPipeFixture.Client.Resolve(); + await TestEventHandlerSubscribeAndUnsubscribe(instance); + } + + private static async Task TestEventHandlerSubscribeAndUnsubscribe(IEventType instance) + { + // Some issues will occur only when the number of tests is high. + // In particular, it tests whether the event calls are synchronized. + const int testCount = 10000; + + int count = 0; + Task WrapAsAsync(IEventType source) + { + /* + * BUG: 以下代码,若是严格以 ADD-RAISE-REMOVE 为一组执行,则是正常的,但由于 RAISE 是(非阻塞)异步的(仅用 Push 方法添加事件到队列), + * 故有可能造成这样的执行顺序:ADD1-RAISE1-ADD2-REMOVE1-RAISE2,其中 ADD2 由于 Dispatcher.g.cs 中的“引用计数”机制,是不会发送订阅事件的, + * 故 REMOVE1 将移除 Server 端的订阅,造成 Server 没有 connection 来处理 RAISE2 的消息,故 await WrapAsAsync() 将永远等待下去。。 + */ + + TaskCompletionSource tcs = new(); + source.Changed += OnChanged; + source.RaiseChanged(EventArgs.Empty); + return tcs.Task; + + void OnChanged(object? sender, EventArgs e) + { + source.Changed -= OnChanged; + count++; + tcs.SetResult(); + } + } + + for (int i = 0; i < testCount; i++) + { + await WrapAsAsync(instance); + Assert.Equal(i + 1, count); + } + } + } +} diff --git a/HandyIpc.Tests/Fixtures/ProtocolFixtureBase.cs b/HandyIpc.Tests/Fixtures/ProtocolFixtureBase.cs index 3d067da..5e0036b 100644 --- a/HandyIpc.Tests/Fixtures/ProtocolFixtureBase.cs +++ b/HandyIpc.Tests/Fixtures/ProtocolFixtureBase.cs @@ -18,7 +18,8 @@ protected ProtocolFixtureBase(ContainerClientBuilder clientBuilder, ContainerSer serverBuilder .Register() .Register(typeof(IGenericType<,>), typeof(GenericTypeImpl<,>)) - .Register(); + .Register() + .Register(); _server = serverBuilder.Build(); _server.Start(); diff --git a/HandyIpc.Tests/GenericTypeTest.cs b/HandyIpc.Tests/GenericTypeTest.cs index 8e78d18..4a92f13 100644 --- a/HandyIpc.Tests/GenericTypeTest.cs +++ b/HandyIpc.Tests/GenericTypeTest.cs @@ -47,7 +47,7 @@ public async Task TestCases(IGenericType remote) Assert.Equal(expected, actual); } - await Assert.ThrowsAsync(async () => await remote.TestAsync()); + await Helper.AssertInnerException(remote.TestAsync); { ClassWithNewCtor.InitialName = Guid.NewGuid().ToString(); diff --git a/HandyIpc.Tests/HandyIpc.Tests.csproj b/HandyIpc.Tests/HandyIpc.Tests.csproj index 3dfe908..9e01595 100644 --- a/HandyIpc.Tests/HandyIpc.Tests.csproj +++ b/HandyIpc.Tests/HandyIpc.Tests.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 false HandyIpcTests diff --git a/HandyIpc.Tests/Helper.cs b/HandyIpc.Tests/Helper.cs new file mode 100644 index 0000000..d24192f --- /dev/null +++ b/HandyIpc.Tests/Helper.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using HandyIpc.Exceptions; +using Xunit; + +namespace HandyIpcTests +{ + public static class Helper + { + public static void AssertInnerException(Action function) + { + try + { + function(); + } + catch (IpcException e) + { + Assert.Equal(typeof(T), e.InnerException!.GetType()); + } + } + + public static async Task AssertInnerException(Func function) + { + try + { + await function(); + } + catch (IpcException e) + { + Assert.Equal(typeof(T), e.InnerException!.GetType()); + } + } + } +} diff --git a/HandyIpc.Tests/Implementations/EventType.cs b/HandyIpc.Tests/Implementations/EventType.cs new file mode 100644 index 0000000..942e85f --- /dev/null +++ b/HandyIpc.Tests/Implementations/EventType.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel; +using HandyIpcTests.Interfaces; + +namespace HandyIpcTests.Implementations +{ + internal class EventType : IEventType + { + public event EventHandler? Changed; + + public event EventHandler? EventWithArgs; + + public event PropertyChangedEventHandler? PropertyChanged; + + public void RaiseChanged(EventArgs e) + { + Changed?.Invoke(this, e); + } + } +} diff --git a/HandyIpc.Tests/Interfaces/IEmpty.cs b/HandyIpc.Tests/Interfaces/IEmpty.cs deleted file mode 100644 index 027a0dc..0000000 --- a/HandyIpc.Tests/Interfaces/IEmpty.cs +++ /dev/null @@ -1,9 +0,0 @@ -using HandyIpc; - -namespace HandyIpcTests.Interfaces -{ - [IpcContract] - public interface IEmpty - { - } -} diff --git a/HandyIpc.Tests/Interfaces/IEventType.cs b/HandyIpc.Tests/Interfaces/IEventType.cs new file mode 100644 index 0000000..5751d84 --- /dev/null +++ b/HandyIpc.Tests/Interfaces/IEventType.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel; +using HandyIpc; + +namespace HandyIpcTests.Interfaces +{ + [IpcContract] + public interface IEventType + { + event EventHandler Changed; + + event EventHandler EventWithArgs; + + event PropertyChangedEventHandler PropertyChanged; + + public void RaiseChanged(EventArgs e); + } +} diff --git a/HandyIpc.Tests/Mock/MockDataGenerator.cs b/HandyIpc.Tests/Mock/MockDataGenerator.cs index 1d1ec46..1e78cc0 100644 --- a/HandyIpc.Tests/Mock/MockDataGenerator.cs +++ b/HandyIpc.Tests/Mock/MockDataGenerator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -111,7 +111,7 @@ public static IEnumerable Chars() public static IEnumerable ByteArrays() { - return new[] { new byte[0] } + return new[] { Array.Empty() } .Concat(Enumerable .Range(0, 1000) .Select(_ => diff --git a/HandyIpc.Tests/TaskReturnTypeTest.cs b/HandyIpc.Tests/TaskReturnTypeTest.cs index 36d7066..b11d46b 100644 --- a/HandyIpc.Tests/TaskReturnTypeTest.cs +++ b/HandyIpc.Tests/TaskReturnTypeTest.cs @@ -75,13 +75,13 @@ private static async Task TestCases(ITaskReturnType instance) instance.SyncMethod(); - Assert.Throws(instance.SyncMethodWithException); + Helper.AssertInnerException(instance.SyncMethodWithException); await instance.TestDoNothing(); - await Assert.ThrowsAsync(instance.TestException); + await Helper.AssertInnerException(instance.TestException); - await Assert.ThrowsAsync(() => instance.TestGenericException(string.Empty)); + await Helper.AssertInnerException(() => instance.TestGenericException(string.Empty)); Assert.Equal(await local.TestReturnDouble(), await instance.TestReturnDouble()); diff --git a/HandyIpc/ContainerClientBuilder.cs b/HandyIpc/ContainerClientBuilder.cs index b8a6115..0ebf659 100644 --- a/HandyIpc/ContainerClientBuilder.cs +++ b/HandyIpc/ContainerClientBuilder.cs @@ -1,5 +1,6 @@ using System; using HandyIpc.Core; +using HandyIpc.Logger; namespace HandyIpc { @@ -11,7 +12,7 @@ public class ContainerClientBuilder : IClientConfiguration private Func _serializerFactory = () => throw new InvalidOperationException( $"Must invoke the {nameof(IServerConfiguration)}.Use(Func<{nameof(ISerializer)}> factory) method " + "to register a factory before invoking the Build method."); - private Func _loggerFactory = () => new DebugLogger(); + private Func _loggerFactory = () => new TraceLogger(); public IClientConfiguration Use(Func factory) { diff --git a/HandyIpc/ContainerServer.cs b/HandyIpc/ContainerServer.cs index 9894b98..bc31dc4 100644 --- a/HandyIpc/ContainerServer.cs +++ b/HandyIpc/ContainerServer.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using HandyIpc.Core; +using HandyIpc.Logger; namespace HandyIpc { @@ -37,59 +38,82 @@ public void Start() #pragma warning restore 4014 IsRunning = true; + _logger.Info("IPC service has been started..."); } public void Stop() { _cancellationTokenSource?.Cancel(); IsRunning = false; + _logger.Info("IPC service has been stopped."); } public void Dispose() { Stop(); _server.Dispose(); + _logger.Info("IPC service has been disposed."); } private async Task StartAsync(CancellationToken token) { while (!token.IsCancellationRequested) { - IConnection connection = await _server.WaitForConnectionAsync(); - RequestHandler handler = _middleware.ToHandler(_serializer, _logger); - + IConnection connection = await _server.WaitForConnectionAsync().ConfigureAwait(false); if (token.IsCancellationRequested) { break; } + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.Debug($"A new connection is established. (hashCode: {connection.GetHashCode()})"); + } + // Do not await the request handler, and go to await next stream connection directly. #pragma warning disable 4014 - HandleRequestAsync(connection, handler, token); + HandleRequestAsync(connection, token); #pragma warning restore 4014 } } - private async Task HandleRequestAsync(IConnection connection, RequestHandler handler, CancellationToken token) + private async Task HandleRequestAsync(IConnection connection, CancellationToken token) { + Task Handler(Context context) => _middleware(context, () => Task.CompletedTask); + + bool canDisposeConnection = true; try { - while (true) + while (!token.IsCancellationRequested) { - if (token.IsCancellationRequested) + byte[] input = await connection.ReadAsync(token).ConfigureAwait(false); + if (input.Length == 0) { + // #19, #22: + // When the remote side closes the link, the read does NOT throw any exception, + // only returns 0 bytes data, and does NOT BLOCK, causing a high frequency dead loop. + // Differences in behavior: + // - NamedPipe: After multiple non-blocking loops, an ArgumentException is thrown and the loop is terminated, + // so for the most part it behaves normally. + // - Socket(Tcp): Always in the unblock loop. break; } - byte[] buffer = await connection.ReadAsync(token); - // #19, #22: - if (buffer.Length == 0) + Context ctx = new() + { + Input = input, + Connection = connection, + Logger = _logger, + Serializer = _serializer, + }; + await Handler(ctx).ConfigureAwait(false); + await connection.WriteAsync(ctx.Output, token).ConfigureAwait(false); + + if (ctx.ForgetConnection) { + canDisposeConnection = false; break; } - - byte[] output = await handler(buffer); - await connection.WriteAsync(output, token); } } catch (OperationCanceledException) @@ -102,7 +126,15 @@ private async Task HandleRequestAsync(IConnection connection, RequestHandler han } finally { - connection.Dispose(); + if (canDisposeConnection) + { + connection.Dispose(); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.Debug($"A connection is released. (hashCode: {connection.GetHashCode()}, disposed: {canDisposeConnection})"); + } } } } diff --git a/HandyIpc/ContainerServerBuilder.cs b/HandyIpc/ContainerServerBuilder.cs index 5a28781..3cdf67b 100644 --- a/HandyIpc/ContainerServerBuilder.cs +++ b/HandyIpc/ContainerServerBuilder.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using HandyIpc.Core; +using HandyIpc.Logger; namespace HandyIpc { @@ -15,7 +17,7 @@ public class ContainerServerBuilder : IServerConfiguration, IContainerRegistry private Func _serializerFactory = () => throw new InvalidOperationException( $"Must invoke the {nameof(IServerConfiguration)}.Use(Func<{nameof(ISerializer)}> factory) method " + "to register a factory before invoking the Build method."); - private Func _loggerFactory = () => new DebugLogger(); + private Func _loggerFactory = () => new TraceLogger(); public IServerConfiguration Use(Func factory) { @@ -50,31 +52,48 @@ public IContainerRegistry Register(Type interfaceType, Func fact public IContainerServer Build() { Dictionary map = new(); + ConcurrentDictionary notifiers = new(); foreach (var (key, type, factory) in _interfaceMap) { - Middleware methodDispatcher = CreateDispatcher(type, factory).Dispatch; + object dispatcher = CreateDispatcher(type, factory); + if (dispatcher is INotifiable notifiable) + { + notifiable.NotifierManager = notifiers.GetOrAdd(key, _ => new NotifierManager(_serializerFactory())); + } + + Middleware methodDispatcher = ((IMethodDispatcher)dispatcher).Dispatch; map.Add(key, methodDispatcher); } foreach (var (key, type, factory) in _genericInterfaceMap) { Middleware methodDispatcher = Middlewares.GetMethodDispatcher( - genericTypes => CreateDispatcher( - type.MakeGenericType(genericTypes), - () => factory(genericTypes))); + genericTypes => + { + object dispatcher = CreateDispatcher( + type.MakeGenericType(genericTypes), + () => factory(genericTypes)); + if (dispatcher is INotifiable notifiable) + { + notifiable.NotifierManager = notifiers.GetOrAdd(key, _ => new NotifierManager(_serializerFactory())); + } + + return (IMethodDispatcher)dispatcher; + }); map.Add(key, methodDispatcher); } Middleware middleware = Middlewares.Compose( Middlewares.Heartbeat, Middlewares.ExceptionHandler, - Middlewares.RequestParser, - Middlewares.GetInterfaceMiddleware(map)); + Middlewares.GetRequestHandler(map), + Middlewares.GetSubscriptionHandler(notifiers), + Middlewares.NotFound); return new ContainerServer(_serverFactory(), middleware, _serializerFactory(), _loggerFactory()); } - private static IMethodDispatcher CreateDispatcher(Type interfaceType, Func factory) + private static object CreateDispatcher(Type interfaceType, Func factory) { object instance = factory(); @@ -95,9 +114,7 @@ private static IMethodDispatcher CreateDispatcher(Type interfaceType, Func : PoolBase where T : IDisposable + public sealed class AsyncPool : PoolBase where T : IDisposable { private readonly Func> _factory; private readonly Func> _checkValue; - public AsyncPool(Func> factory, Func>? checkValue = null) + public AsyncPool(Func> factory, Func> checkValue) { _factory = factory; - _checkValue = checkValue ?? (_ => Task.FromResult(true)); + _checkValue = checkValue; } public async Task> RentAsync() diff --git a/HandyIpc/Core/AwaiterManager.cs b/HandyIpc/Core/AwaiterManager.cs new file mode 100644 index 0000000..cada430 --- /dev/null +++ b/HandyIpc/Core/AwaiterManager.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using HandyIpc.Exceptions; + +namespace HandyIpc.Core +{ + public class AwaiterManager + { + private readonly ConcurrentDictionary _awaiters = new(); + private readonly string _key; + private readonly ISerializer _serializer; + private readonly Sender _sender; + + public AwaiterManager(string key, Sender sender, ISerializer serializer) + { + _key = key; + _sender = sender; + _serializer = serializer; + } + + public void Subscribe(string name, Action callback) + { + IConnection connection = _sender.ConnectionPool.Rent().Value; + Awaiter awaiter = _awaiters.GetOrAdd(name, _ => new Awaiter(callback, connection)); + + byte[] addResult = connection.Invoke(Subscription.Add(_key, name, _serializer)); + if (!addResult.IsUnit()) + { + throw new IpcException(); + } + + Task.Run(() => LoopWait(name, awaiter)); + } + + public void Unsubscribe(string name) + { + if (_awaiters.TryRemove(name, out Awaiter awaiter)) + { + awaiter.Cancellation.Cancel(); + // Send a signal to dispose the remote connection. + _sender.Invoke(Subscription.Remove(_key, name, _serializer)); + } + } + + private void LoopWait(string name, Awaiter awaiter) + { + using IConnection connection = awaiter.Connection; + using CancellationTokenSource cancellation = awaiter.Cancellation; + CancellationToken token = cancellation.Token; + + while (!token.IsCancellationRequested) + { + // Will blocked until accepted a notification. + byte[] input = connection.Read(); + if (token.IsCancellationRequested) + { + break; + } + + if (input.Length == 0) + { + // The server unexpectedly closes the connection and the client should retry automatically. + _awaiters.TryRemove(name, out _); + Subscribe(name, awaiter.Handler); + break; + } + + try + { + // The Read() and Write() are used to ensure that calls are synchronized (blocked) + // and that the Write() must be called after the execution of the Handler() is completed. + awaiter.Handler(input); + } + finally + { + connection.Write(Signals.Unit); + } + } + } + + private class Awaiter + { + public Action Handler { get; } + + public IConnection Connection { get; } + + public CancellationTokenSource Cancellation { get; } = new(); + + public Awaiter(Action handler, IConnection connection) + { + Handler = handler; + Connection = connection; + } + } + } +} diff --git a/HandyIpc/Core/ByteExtensions.cs b/HandyIpc/Core/ByteExtensions.cs index 1aaef71..ee65365 100644 --- a/HandyIpc/Core/ByteExtensions.cs +++ b/HandyIpc/Core/ByteExtensions.cs @@ -5,6 +5,11 @@ namespace HandyIpc.Core { internal static class ByteExtensions { + internal static byte[] Slice(this byte[] bytes, (int start, int length) range) + { + return bytes.Slice(range.start, range.length); + } + internal static byte[] Slice(this byte[] bytes, int start, int length) { byte[] result = new byte[length]; diff --git a/HandyIpc/Core/Context.cs b/HandyIpc/Core/Context.cs index 503b401..953dca6 100644 --- a/HandyIpc/Core/Context.cs +++ b/HandyIpc/Core/Context.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using HandyIpc.Logger; namespace HandyIpc.Core { @@ -7,23 +7,24 @@ public class Context { private static readonly byte[] EmptyBytes = Array.Empty(); - public byte[] Input { get; } + // FIXME: Replace 'set;' with 'init;' + public byte[] Input { get; set; } = null!; public byte[] Output { get; set; } = EmptyBytes; - public IDictionary Items { get; } = new Dictionary(); - public Request? Request { get; set; } - public ISerializer Serializer { get; } + public ISerializer Serializer { get; set; } = null!; + + public ILogger Logger { get; set; } = null!; - public ILogger Logger { get; } + public IConnection Connection { get; set; } = null!; - public Context(byte[] input, ISerializer serializer, ILogger logger) - { - Input = input; - Serializer = serializer; - Logger = logger; - } + /// + /// Gets or sets a bool value to indicate to connection ownership has been transferred + /// and the service loop should end without dealing with its lifecycle. + /// (do NOT dispose the connection, as the connection ownership has been taken over by another object). + /// + public bool ForgetConnection { get; set; } } } diff --git a/HandyIpc/Core/GeneratorHelper.cs b/HandyIpc/Core/GeneratorHelper.cs index a675485..6f75de3 100644 --- a/HandyIpc/Core/GeneratorHelper.cs +++ b/HandyIpc/Core/GeneratorHelper.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; +using HandyIpc.Exceptions; namespace HandyIpc.Core { @@ -39,7 +40,7 @@ public static Type ExtractTaskValueType(Type taskType) public static T UnpackResponse(byte[] bytes, ISerializer serializer) { bool hasValue = Response.TryParse(bytes, typeof(T), serializer, out object? value, out Exception? exception); - return hasValue ? (T)value! : throw exception!; + return hasValue ? (T)value! : throw new IpcException("An unexpected exception occurs during an ipc call.", exception!); } public static IReadOnlyDictionary GetGenericMethodMapping(Type interfaceType, object instance) diff --git a/HandyIpc/Core/ILogger.cs b/HandyIpc/Core/ILogger.cs deleted file mode 100644 index ed69b2e..0000000 --- a/HandyIpc/Core/ILogger.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace HandyIpc.Core -{ - public interface ILogger - { - void Error(string message, Exception? exception = null); - - void Warning(string message, Exception? exception = null); - - void Info(string message, Exception? exception = null); - } -} diff --git a/HandyIpc/Core/INotifiable.cs b/HandyIpc/Core/INotifiable.cs new file mode 100644 index 0000000..139b170 --- /dev/null +++ b/HandyIpc/Core/INotifiable.cs @@ -0,0 +1,7 @@ +namespace HandyIpc.Core +{ + public interface INotifiable + { + NotifierManager NotifierManager { get; set; } + } +} diff --git a/HandyIpc/Core/Middlewares.cs b/HandyIpc/Core/Middlewares.cs index 1f8ed63..93d95cc 100644 --- a/HandyIpc/Core/Middlewares.cs +++ b/HandyIpc/Core/Middlewares.cs @@ -2,13 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using HandyIpc.Logger; namespace HandyIpc.Core { public delegate Task Middleware(Context context, Func next); - public delegate Task RequestHandler(byte[] input); - public static class Middlewares { #region Build-in middlewares @@ -37,33 +36,79 @@ public static async Task ExceptionHandler(Context ctx, Func next) } } - public static async Task RequestParser(Context ctx, Func next) + public static Middleware GetRequestHandler(IReadOnlyDictionary map) { - if (Request.TryParse(ctx.Input, ctx.Serializer, out Request request)) + return async (ctx, next) => { - ctx.Request = request; + if (Request.TryParse(ctx.Input, ctx.Serializer, out Request request)) + { + ctx.Request = request; + if (map.TryGetValue(request.Name, out Middleware middleware)) + { + if (ctx.Logger.IsEnabled(LogLevel.Debug)) + { + ctx.Logger.Debug("Before processing an ipc request. " + + $"(connection: {ctx.Connection.GetHashCode()}, name: {request.Name}, methodName: {request.MethodName})"); + } + + await middleware(ctx, next); + if (ctx.Logger.IsEnabled(LogLevel.Debug)) + { + ctx.Logger.Debug("After processing an ipc request. " + + $"(connection: {ctx.Connection.GetHashCode()}, name: {request.Name}, methodName: {request.MethodName})"); + } + + return; + } + } + await next(); - } - else - { - throw new ArgumentException("Invalid request bytes."); - } + }; } - public static Middleware GetInterfaceMiddleware(IReadOnlyDictionary map) + public static Middleware GetSubscriptionHandler(IReadOnlyDictionary notifiers) { return async (ctx, next) => { - Request request = CheckRequest(ctx); - - if (map.TryGetValue(request.Name, out Middleware middleware)) - { - await middleware(ctx, next); - } - else + if (Subscription.TryParse(ctx.Input, ctx.Serializer, out Subscription subscription)) { - throw new NotSupportedException("Unknown interface invoked."); + switch (subscription.Type) + { + case SubscriptionType.Add: + { + if (notifiers.TryGetValue(subscription.Name, out NotifierManager manager)) + { + manager.Subscribe(subscription.CallbackName, subscription.ProcessId, ctx.Connection); + ctx.Output = Signals.Unit; + ctx.ForgetConnection = true; + } + + if (ctx.Logger.IsEnabled(LogLevel.Debug)) + { + ctx.Logger.Debug("Add an event subscription. " + + $"(connection: {ctx.Connection.GetHashCode()}, name: {subscription.Name}, eventName: {subscription.CallbackName}, pid: {subscription.ProcessId})"); + } + } + return; + case SubscriptionType.Remove: + { + if (notifiers.TryGetValue(subscription.Name, out NotifierManager manager)) + { + manager.Unsubscribe(subscription.CallbackName, subscription.ProcessId); + } + + ctx.Output = Signals.Unit; + if (ctx.Logger.IsEnabled(LogLevel.Debug)) + { + ctx.Logger.Debug("Remove an event subscription. " + + $"(connection: {ctx.Connection.GetHashCode()}, name: {subscription.Name}, eventName: {subscription.CallbackName}, pid: {subscription.ProcessId})"); + } + } + return; + } } + + await next(); }; } @@ -71,7 +116,7 @@ public static Middleware GetMethodDispatcher(Func get { return async (ctx, next) => { - Request request = CheckRequest(ctx); + Request request = EnsureRequest(ctx); if (request.TypeArguments.Any()) { @@ -85,13 +130,25 @@ public static Middleware GetMethodDispatcher(Func get }; } - private static Request CheckRequest(Context ctx) + public static Task NotFound(Context ctx, Func next) + { + string message = ctx.Request is { } request + ? $"Unknown interface method ({request.Name}.{request.MethodName}) invoked." + : "Unknown interface method invoked."; + + ctx.Logger.Warning(message); + ctx.Output = Response.Error(new NotSupportedException(message), ctx.Serializer); + + return Task.CompletedTask; + } + + private static Request EnsureRequest(Context ctx) { Request? request = ctx.Request; if (request is null) { - throw new InvalidOperationException($"The {nameof(Context.Request)} must be parsed from {nameof(Context.Input)} before it can be used."); + throw new InvalidOperationException($"The {nameof(Context.Request)} must be parsed from {nameof(Context.Input)} before it be used."); } return request; @@ -113,15 +170,5 @@ public static Middleware Compose(params Middleware[] middlewareArray) { return middlewareArray.Compose(); } - - public static RequestHandler ToHandler(this Middleware middleware, ISerializer serializer, ILogger logger) - { - return async input => - { - var ctx = new Context(input, serializer, logger); - await middleware(ctx, () => Task.CompletedTask); - return ctx.Output; - }; - } } } diff --git a/HandyIpc/Core/NotifierManager.cs b/HandyIpc/Core/NotifierManager.cs new file mode 100644 index 0000000..ab87cc3 --- /dev/null +++ b/HandyIpc/Core/NotifierManager.cs @@ -0,0 +1,117 @@ +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace HandyIpc.Core +{ + public class NotifierManager + { + private readonly ISerializer _serializer; + private readonly ConcurrentDictionary _notifiers = new(); + + public NotifierManager(ISerializer serializer) + { + _serializer = serializer; + } + + public void Publish(string name, T args) + { + if (_notifiers.TryGetValue(name, out Notifier notifier)) + { + notifier.Push(_serializer.Serialize(args)); + } + } + + public void Subscribe(string name, int processId, IConnection connection) + { + Notifier notifier = _notifiers.GetOrAdd(name, _ => new Notifier()); + notifier.Subscribe(processId, connection); + } + + public void Unsubscribe(string name, int processId) + { + if (_notifiers.TryGetValue(name, out Notifier notifier)) + { + notifier.Unsubscribe(processId); + } + } + + private class Notifier + { + private readonly ConcurrentDictionary _connections = new(); + private readonly BlockingCollection _queue = new(); + + private CancellationTokenSource? _source; + + public Notifier() => Start(); + + public void Push(byte[] bytes) + { + if (_connections.IsEmpty) + { + _source?.Cancel(); + _source = null; + return; + } + + if (_source is null or { IsCancellationRequested: true }) + { + Start(); + } + + _queue.Add(bytes); + } + + public void Subscribe(int processId, IConnection connection) + { + _connections[processId] = connection; + } + + public void Unsubscribe(int processId) + { + if (_connections.TryRemove(processId, out IConnection connection)) + { + connection.Dispose(); + } + } + + private void Start() + { + while (_queue.TryTake(out _)) + { + // Clear history queue. + } + + _source = new CancellationTokenSource(); + Task.Run(() => Publish(_source.Token)); + } + + private void Publish(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + byte[] bytes = _queue.Take(token); + var connections = _connections.ToArray(); + foreach (var item in connections) + { + int processId = item.Key; + IConnection connection = item.Value; + + try + { + byte[] result = connection.Invoke(bytes); + if (!result.IsUnit()) + { + Unsubscribe(processId); + } + } + catch + { + Unsubscribe(processId); + } + } + } + } + } + } +} diff --git a/HandyIpc/Core/Pool.cs b/HandyIpc/Core/Pool.cs index 956e883..e0b940c 100644 --- a/HandyIpc/Core/Pool.cs +++ b/HandyIpc/Core/Pool.cs @@ -2,15 +2,15 @@ namespace HandyIpc.Core { - internal sealed class Pool : PoolBase where T : IDisposable + public sealed class Pool : PoolBase where T : IDisposable { private readonly Func _factory; private readonly Func _checkValue; - public Pool(Func factory, Func? checkValue = null) + public Pool(Func factory, Func checkValue) { _factory = factory; - _checkValue = checkValue ?? (_ => true); + _checkValue = checkValue; } public RentedValue Rent() diff --git a/HandyIpc/Core/PoolBase.cs b/HandyIpc/Core/PoolBase.cs index 87a3c08..c78f699 100644 --- a/HandyIpc/Core/PoolBase.cs +++ b/HandyIpc/Core/PoolBase.cs @@ -3,9 +3,9 @@ namespace HandyIpc.Core { - internal abstract class PoolBase : IDisposable where TValue : IDisposable + public abstract class PoolBase : IDisposable where T : IDisposable { - protected readonly ConcurrentBag Cache = new(); + protected readonly ConcurrentBag Cache = new(); private bool _isDisposed; @@ -23,7 +23,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { - while (Cache.TryTake(out TValue item)) + while (Cache.TryTake(out T item)) { item.Dispose(); } diff --git a/HandyIpc/Core/RentedValue.cs b/HandyIpc/Core/RentedValue.cs index ed50281..a0f45be 100644 --- a/HandyIpc/Core/RentedValue.cs +++ b/HandyIpc/Core/RentedValue.cs @@ -2,13 +2,13 @@ namespace HandyIpc.Core { - internal readonly struct RentedValue : IDisposable + public readonly struct RentedValue : IDisposable { - private readonly Action _dispose; + private readonly Action _dispose; - public TValue Value { get; } + public T Value { get; } - public RentedValue(TValue value, Action dispose) + public RentedValue(T value, Action dispose) { _dispose = dispose; Value = value; diff --git a/HandyIpc/Core/Request.cs b/HandyIpc/Core/Request.cs index 3d2b1cf..808b3db 100644 --- a/HandyIpc/Core/Request.cs +++ b/HandyIpc/Core/Request.cs @@ -12,11 +12,9 @@ namespace HandyIpc.Core [Obfuscation(Exclude = true)] public class Request { - private const string ReqHeader = "handyipc/req"; + private const string ReqHeader = "hi/req"; - private static readonly byte[] Version = { 1 }; private static readonly byte[] ReqHeaderBytes = Encoding.ASCII.GetBytes(ReqHeader); - private static readonly byte[] EmptyArray = BitConverter.GetBytes(0); private static readonly IReadOnlyList EmptyTypeList = Enumerable.Empty().ToList().AsReadOnly(); private static readonly IReadOnlyList EmptyObjectList = Enumerable.Empty().ToList().AsReadOnly(); @@ -41,7 +39,7 @@ public class Request /// public string Name { - get => _name ??= Deserialize(_nameRange); + get => _name ??= _serializer.Deserialize(_bytes.Slice(_nameRange)); set => _name = value; } @@ -50,7 +48,7 @@ public string Name /// public IReadOnlyList TypeArguments { - get => _typeArguments ??= DeserializeArray(_typeArgumentsRange); + get => _typeArguments ??= _serializer.DeserializeArray(_bytes.Slice(_typeArgumentsRange)); set => _typeArguments = value; } @@ -59,7 +57,7 @@ public IReadOnlyList TypeArguments /// public string MethodName { - get => _methodName ??= Deserialize(_methodNameRange); + get => _methodName ??= _serializer.Deserialize(_bytes.Slice(_methodNameRange)); set => _methodName = value; } @@ -68,7 +66,7 @@ public string MethodName /// public IReadOnlyList MethodTypeArguments { - get => _methodTypeArguments ??= DeserializeArray(_methodTypeArgumentsRange); + get => _methodTypeArguments ??= _serializer.DeserializeArray(_bytes.Slice(_methodTypeArgumentsRange)); set => _methodTypeArguments = value; } @@ -77,7 +75,7 @@ public IReadOnlyList MethodTypeArguments /// public IReadOnlyList Arguments { - get => _arguments ??= DeserializeArray(_argumentsRange, index => _argumentTypes[index]); + get => _arguments ??= _serializer.DeserializeArray(_bytes.Slice(_argumentsRange), _argumentTypes); set => _arguments = value; } @@ -105,7 +103,6 @@ public byte[] ToBytes() /* * < Header > * | Req Token | - * | Version | * < Layout Table > * | NameLength | * | TypeArgumentsLength | @@ -121,15 +118,14 @@ public byte[] ToBytes() */ byte[] nameBytes = _serializer.Serialize(Name, typeof(string)); - byte[] typeArgumentsBytes = SerializeArray(TypeArguments); + byte[] typeArgumentsBytes = _serializer.SerializeArray(TypeArguments); byte[] methodNameBytes = _serializer.Serialize(MethodName, typeof(string)); - byte[] methodTypeArgumentsBytes = SerializeArray(MethodTypeArguments); - byte[] argumentsBytes = SerializeArray(Arguments, index => _argumentTypes[index]); + byte[] methodTypeArgumentsBytes = _serializer.SerializeArray(MethodTypeArguments); + byte[] argumentsBytes = _serializer.SerializeArray(Arguments, _argumentTypes); byte[][] bytesList = { ReqHeaderBytes, - Version, BitConverter.GetBytes(nameBytes.Length), BitConverter.GetBytes(typeArgumentsBytes.Length), BitConverter.GetBytes(methodNameBytes.Length), @@ -153,8 +149,7 @@ public static bool TryParse(byte[] bytes, ISerializer serializer, out Request re return false; } - // Skip header and version bytes. - int offset = ReqHeaderBytes.Length + 1; + int offset = ReqHeaderBytes.Length; // Skip layout table, 5 is six field in bytes table. int start = offset + sizeof(int) * 5; @@ -179,53 +174,5 @@ private static (int start, int end) GetRangeAndMoveNext(byte[] bytes, ref int of return range; } - - private byte[] SerializeArray(IReadOnlyList array, Func? typeProvider = null) - { - if (array.Count == 0) - { - return EmptyArray; - } - - typeProvider ??= (_ => typeof(T)); - - List result = new(); - for (int i = 0; i < array.Count; i++) - { - byte[] bytes = _serializer.Serialize(array[i], typeProvider(i)); - result.Add(BitConverter.GetBytes(bytes.Length)); - result.Add(bytes); - } - - return result.ConcatBytes(); - } - - private T Deserialize((int start, int length) range) - { - (int start, int length) = range; - return (T)_serializer.Deserialize(_bytes.Slice(start, length), typeof(T))!; - } - - private IReadOnlyList DeserializeArray((int start, int length) range, Func? typeProvider = null) - { - typeProvider ??= (_ => typeof(T)); - - (int start, int length) = range; - int end = start + length; - List result = new(); - - for (int offset = start, index = 0; offset < end; index++) - { - int dataLength = BitConverter.ToInt32(_bytes, offset); - offset += sizeof(int); - - T data = (T)_serializer.Deserialize(_bytes.Slice(offset, dataLength), typeProvider(index))!; - offset += dataLength; - - result.Add(data); - } - - return result; - } } } diff --git a/HandyIpc/Core/Response.cs b/HandyIpc/Core/Response.cs index b87c467..55d8fe5 100644 --- a/HandyIpc/Core/Response.cs +++ b/HandyIpc/Core/Response.cs @@ -6,9 +6,8 @@ namespace HandyIpc.Core { public static class Response { - private const string ResHeader = "handyipc/res"; + private const string ResHeader = "hi/res"; - private static readonly byte[] Version = { 1 }; private static readonly byte[] ResHeaderBytes = Encoding.ASCII.GetBytes(ResHeader); private static readonly byte[] ResponseValueFlag = { 1 }; @@ -17,7 +16,6 @@ public static class Response public static byte[] Unit { get; } = new[] { ResHeaderBytes, - Version, ResponseValueFlag, BitConverter.GetBytes(Signals.Unit.Length), Signals.Unit, @@ -28,7 +26,6 @@ public static byte[] Value(object? value, Type? type, ISerializer serializer) List result = new() { ResHeaderBytes, - Version, ResponseValueFlag, }; @@ -49,7 +46,6 @@ public static byte[] Error(Exception exception, ISerializer serializer) List result = new() { ResHeaderBytes, - Version, ResponseErrorFlag, BitConverter.GetBytes(typeBytes.Length), typeBytes, @@ -67,9 +63,7 @@ public static bool TryParse(byte[] bytes, Type valueType, ISerializer serializer throw new ArgumentException("The bytes is not valid response data.", nameof(bytes)); } - // Skip the version number, because the current version is the first one - // and there is no need to consider compatibility issues. - int offset = ResHeaderBytes.Length + Version.Length; + int offset = ResHeaderBytes.Length; bool hasValue = bytes.Slice(offset, 1)[0] == ResponseValueFlag[0]; offset++; if (hasValue) diff --git a/HandyIpc/Core/Sender.cs b/HandyIpc/Core/Sender.cs index 1ade28c..45cc5f6 100644 --- a/HandyIpc/Core/Sender.cs +++ b/HandyIpc/Core/Sender.cs @@ -6,25 +6,26 @@ namespace HandyIpc.Core { public sealed class Sender : IDisposable { - private readonly Pool _connectionPool; - private readonly AsyncPool _asyncConnectionPool; + public Pool ConnectionPool { get; } + + public AsyncPool AsyncConnectionPool { get; } internal Sender(IClient client) { - _connectionPool = new Pool(client.Connect, CheckConnection); - _asyncConnectionPool = new AsyncPool(client.ConnectAsync, CheckAsyncConnection); + ConnectionPool = new Pool(client.Connect, CheckConnection); + AsyncConnectionPool = new AsyncPool(client.ConnectAsync, CheckAsyncConnection); } public byte[] Invoke(byte[] bytes) { - using RentedValue invokeOwner = _connectionPool.Rent(); + using RentedValue invokeOwner = ConnectionPool.Rent(); byte[] response = invokeOwner.Value.Invoke(bytes); return response; } public async Task InvokeAsync(byte[] bytes) { - using RentedValue invokeOwner = await _asyncConnectionPool.RentAsync(); + using RentedValue invokeOwner = await AsyncConnectionPool.RentAsync(); byte[] response = await invokeOwner.Value.InvokeAsync(bytes, CancellationToken.None); return response; } @@ -59,8 +60,8 @@ private static async Task CheckAsyncConnection(IConnection connection) public void Dispose() { - _connectionPool.Dispose(); - _asyncConnectionPool.Dispose(); + ConnectionPool.Dispose(); + AsyncConnectionPool.Dispose(); } } } diff --git a/HandyIpc/Core/SerializerExtensions.cs b/HandyIpc/Core/SerializerExtensions.cs new file mode 100644 index 0000000..b421485 --- /dev/null +++ b/HandyIpc/Core/SerializerExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace HandyIpc.Core +{ + public static class SerializerExtensions + { + private static readonly byte[] EmptyArray = BitConverter.GetBytes(0); + + public static byte[] Serialize(this ISerializer serializer, T value) + { + return serializer.Serialize(value, typeof(T)); + } + + public static byte[] SerializeArray(this ISerializer serializer, IReadOnlyList array, IReadOnlyList? types = null) + { + if (array.Count == 0) + { + return EmptyArray; + } + + List result = new(); + for (int i = 0; i < array.Count; i++) + { + byte[] bytes = serializer.Serialize(array[i], types == null ? typeof(T) : types[i]); + result.Add(BitConverter.GetBytes(bytes.Length)); + result.Add(bytes); + } + + return result.ConcatBytes(); + } + + public static T Deserialize(this ISerializer serializer, byte[] bytes) + { + return (T)serializer.Deserialize(bytes, typeof(T))!; + } + + public static IReadOnlyList DeserializeArray(this ISerializer serializer, byte[] bytes, IReadOnlyList? types = null) + { + List result = new(); + + for (int offset = 0, i = 0; offset < bytes.Length; i++) + { + int dataLength = BitConverter.ToInt32(bytes, offset); + offset += sizeof(int); + + T data = (T)serializer.Deserialize(bytes.Slice(offset, dataLength), types == null ? typeof(T) : types[i])!; + offset += dataLength; + + result.Add(data); + } + + return result; + } + } +} diff --git a/HandyIpc/Core/StreamConnection.cs b/HandyIpc/Core/StreamConnection.cs index 3b50c73..9820980 100644 --- a/HandyIpc/Core/StreamConnection.cs +++ b/HandyIpc/Core/StreamConnection.cs @@ -20,8 +20,8 @@ public virtual void Write(byte[] bytes) public virtual async Task WriteAsync(byte[] bytes, CancellationToken token) { - await _stream.WriteAsync(bytes, 0, bytes.Length, token); - await _stream.FlushAsync(token); + await _stream.WriteAsync(bytes, 0, bytes.Length, token).ConfigureAwait(false); + await _stream.FlushAsync(token).ConfigureAwait(false); } public virtual byte[] Read() diff --git a/HandyIpc/Core/StreamExtensions.cs b/HandyIpc/Core/StreamExtensions.cs index 0772928..edf3b79 100644 --- a/HandyIpc/Core/StreamExtensions.cs +++ b/HandyIpc/Core/StreamExtensions.cs @@ -41,7 +41,7 @@ internal static async Task ReadAllBytesAsync(this Stream self, Cancellat } byte[] bytes = new byte[BatchBufferSize]; - int actualCount = await self.ReadAsync(bytes, 0, BatchBufferSize, token); + int actualCount = await self.ReadAsync(bytes, 0, BatchBufferSize, token).ConfigureAwait(false); if (CollectBytes(collector, bytes, actualCount)) { diff --git a/HandyIpc/Core/Subscription.cs b/HandyIpc/Core/Subscription.cs new file mode 100644 index 0000000..e58cd83 --- /dev/null +++ b/HandyIpc/Core/Subscription.cs @@ -0,0 +1,69 @@ +using System.Diagnostics; +using System.Text; + +namespace HandyIpc.Core +{ + public enum SubscriptionType { Add, Remove } + + public class Subscription + { + private const string CallbackHeader = "hi/cb"; + + private static readonly byte[] CallbackHeaderBytes = Encoding.ASCII.GetBytes(CallbackHeader); + private static readonly int ProcessIdSelf = Process.GetCurrentProcess().Id; + + public SubscriptionType Type { get; set; } + + public string Name { get; set; } = string.Empty; + + public string CallbackName { get; set; } = string.Empty; + + public int ProcessId { get; set; } + + internal static bool TryParse(byte[] bytes, ISerializer serializer, out Subscription subscription) + { + subscription = null!; + if (!CallbackHeaderBytes.EqualsHeaderBytes(bytes)) + { + return false; + } + + if (serializer.Deserialize( + bytes.Slice(CallbackHeaderBytes.Length, bytes.Length - CallbackHeaderBytes.Length), + typeof(Subscription)) is not Subscription result) + { + return false; + } + + subscription = result; + return true; + } + + internal static byte[] Add(string key, string name, ISerializer serializer) + { + return GetBytes(SubscriptionType.Add, key, name, serializer); + } + + internal static byte[] Remove(string key, string name, ISerializer serializer) + { + return GetBytes(SubscriptionType.Remove, key, name, serializer); + } + + private static byte[] GetBytes(SubscriptionType type, string key, string name, ISerializer serializer) + { + byte[] payload = serializer.Serialize(new Subscription + { + Type = type, + Name = key, + ProcessId = ProcessIdSelf, + CallbackName = name, + }, typeof(Subscription)); + + return new byte[][] + { + CallbackHeaderBytes, + payload, + }.ConcatBytes(); + } + } +} diff --git a/HandyIpc/DebugLogger.cs b/HandyIpc/DebugLogger.cs deleted file mode 100644 index 0320817..0000000 --- a/HandyIpc/DebugLogger.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Diagnostics; -using HandyIpc.Core; - -namespace HandyIpc -{ - public sealed class DebugLogger : ILogger - { - public void Error(string message, Exception? exception = null) - { - Print(nameof(Error), message, exception); - } - - public void Warning(string message, Exception? exception = null) - { - Print(nameof(Warning), message, exception); - } - - public void Info(string message, Exception? exception = null) - { - Print(nameof(Info), message, exception); - } - - private static void Print(string level, string message, Exception? exception = null) - { - Debug.WriteLine($"[HandyIpc] [{level}] [{DateTime.Now:HH:mm:ss.fff}] " + - $"{message}{Environment.NewLine}" + - $"{exception?.GetType().Name}: {exception?.Message}{Environment.NewLine}{exception?.StackTrace}"); - } - } -} diff --git a/HandyIpc/Exceptions/IpcException.cs b/HandyIpc/Exceptions/IpcException.cs new file mode 100644 index 0000000..782fedb --- /dev/null +++ b/HandyIpc/Exceptions/IpcException.cs @@ -0,0 +1,34 @@ +using System; + +namespace HandyIpc.Exceptions +{ + public class IpcException : Exception + { + public IpcException() + { + } + + public IpcException(string message) : base(message) + { + } + + public IpcException(string message, Exception innerException) : base(message, innerException) + { + } + } + + public class IpcProtocolException : IpcException + { + public IpcProtocolException() + { + } + + public IpcProtocolException(string message) : base(message) + { + } + + public IpcProtocolException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/HandyIpc/HandyIpc.csproj b/HandyIpc/HandyIpc.csproj index b131042..409e65e 100644 --- a/HandyIpc/HandyIpc.csproj +++ b/HandyIpc/HandyIpc.csproj @@ -2,8 +2,8 @@ HandyIpc - netstandard2.0;net462 - 0.5.3 + netstandard2.0 + 0.6.0 true diff --git a/HandyIpc/IConfiguration.cs b/HandyIpc/IConfiguration.cs index 920cf94..0433de3 100644 --- a/HandyIpc/IConfiguration.cs +++ b/HandyIpc/IConfiguration.cs @@ -1,5 +1,6 @@ using System; using HandyIpc.Core; +using HandyIpc.Logger; namespace HandyIpc { diff --git a/HandyIpc/Logger/ILogger.cs b/HandyIpc/Logger/ILogger.cs new file mode 100644 index 0000000..016b78d --- /dev/null +++ b/HandyIpc/Logger/ILogger.cs @@ -0,0 +1,9 @@ +namespace HandyIpc.Logger +{ + public interface ILogger + { + LogLevel EnabledLevel { get; set; } + + void Log(LogLevel level, LogInfo info); + } +} diff --git a/HandyIpc/Logger/LogInfo.cs b/HandyIpc/Logger/LogInfo.cs new file mode 100644 index 0000000..0ec808f --- /dev/null +++ b/HandyIpc/Logger/LogInfo.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; + +namespace HandyIpc.Logger +{ + public readonly struct LogInfo + { + public DateTime Timestamp { get; } + + public string Message { get; } + + public string ScopeName { get; } + + public int ThreadId { get; } + + public Exception? Exception { get; } + + public LogInfo(string message, Exception? exception = null, string? scopeName = null, int? threadId = null) + { + Timestamp = DateTime.Now; + Message = message; + Exception = exception; + ScopeName = scopeName ?? "UnknownScope"; + ThreadId = threadId ?? Thread.CurrentThread.ManagedThreadId; + } + } +} diff --git a/HandyIpc/Logger/LogLevel.cs b/HandyIpc/Logger/LogLevel.cs new file mode 100644 index 0000000..bb2d940 --- /dev/null +++ b/HandyIpc/Logger/LogLevel.cs @@ -0,0 +1,11 @@ +namespace HandyIpc.Logger +{ + public enum LogLevel + { + Debug, + Info, + Warning, + Error, + Fatal, + } +} diff --git a/HandyIpc/Logger/LoggerExtensions.cs b/HandyIpc/Logger/LoggerExtensions.cs new file mode 100644 index 0000000..26a9cc1 --- /dev/null +++ b/HandyIpc/Logger/LoggerExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Runtime.CompilerServices; + +namespace HandyIpc.Logger +{ + public static class LoggerExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsEnabled(this ILogger logger, LogLevel level) + { + return level >= logger.EnabledLevel; + } + + public static void Fatal(this ILogger logger, string message, Exception? exception = null, [CallerMemberName] string? scopeName = null, int? threadId = null) + { + logger.Log(LogLevel.Fatal, new LogInfo(message, exception, scopeName, threadId)); + } + + public static void Error(this ILogger logger, string message, Exception? exception = null, [CallerMemberName] string? scopeName = null, int? threadId = null) + { + logger.Log(LogLevel.Error, new LogInfo(message, exception, scopeName, threadId)); + } + + public static void Warning(this ILogger logger, string message, Exception? exception = null, [CallerMemberName] string? scopeName = null, int? threadId = null) + { + logger.Log(LogLevel.Warning, new LogInfo(message, exception, scopeName, threadId)); + } + + public static void Info(this ILogger logger, string message, Exception? exception = null, [CallerMemberName] string? scopeName = null, int? threadId = null) + { + logger.Log(LogLevel.Info, new LogInfo(message, exception, scopeName, threadId)); + } + + public static void Debug(this ILogger logger, string message, Exception? exception = null, [CallerMemberName] string? scopeName = null, int? threadId = null) + { + logger.Log(LogLevel.Debug, new LogInfo(message, exception, scopeName, threadId)); + } + } +} diff --git a/HandyIpc/Logger/TraceLogger.cs b/HandyIpc/Logger/TraceLogger.cs new file mode 100644 index 0000000..a8927b3 --- /dev/null +++ b/HandyIpc/Logger/TraceLogger.cs @@ -0,0 +1,31 @@ +using System; +using System.Diagnostics; + +namespace HandyIpc.Logger +{ + public sealed class TraceLogger : ILogger + { + public LogLevel EnabledLevel { get; set; } = LogLevel.Info; + + public void Log(LogLevel level, LogInfo info) + { + if (!this.IsEnabled(level)) + { + return; + } + + string message = + $"[HandyIpc] {info.Timestamp:HH:mm.ss.fff} {info.ScopeName} [{info.ThreadId}]{Environment.NewLine}" + + $"{info.Message}{Environment.NewLine}"; + + Exception? exception = info.Exception; + if (exception is not null) + { + message += $"{exception.Message}{Environment.NewLine}" + + $"{exception.StackTrace}{Environment.NewLine}"; + } + + Trace.WriteLine(message, level.ToString()); + } + } +} diff --git a/HandyIpc/TypeExtensions.cs b/HandyIpc/TypeExtensions.cs index 9301532..a19a5b6 100644 --- a/HandyIpc/TypeExtensions.cs +++ b/HandyIpc/TypeExtensions.cs @@ -5,6 +5,19 @@ namespace HandyIpc { + public class BoastcastEvent + { + public IDisposable Subscribe(Action handler) + { + return null!; + } + + public void Publish(T message) + { + + } + } + internal static class TypeExtensions { internal static string GetDefaultKey(this Type interfaceType) diff --git a/README.md b/README.md index 89f406d..2a36175 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,19 @@ English | [中文](./README.zh-CN.md) -HandyIpc is an out-of-the-box inter-process communication (IPC) library, similar to WCF for remote method calls, but lighter in comparison, eliminating all the tedious configuration. You only need to read this README from the beginning to the master. +HandyIpc is an out-of-the-box inter-process communication (IPC) library, similar to WCF for remote method calls, but lighter in comparison, eliminating all the tedious configuration. You only need to read this README from a beginner to a master. This library provides a high-level RMI (remote method invocation) API. Its underlying communication can be implemented by whatever you like, such as Named Pipe, MMF (memory mapping file) or Socket, and this framework does not care about the specific implementation. +# Features + +1. [x] Support for basic method and event. +2. [x] Support for generic interface. +3. [x] Support for generic method (parameter type allow contains nested generic types). +4. [x] Support for async methods with `Task or Task` as return value. +5. [x] Support communication using NamedPipe or Socket (tcp). +6. [x] Compile-time checks are provided for unsupported cases, see [here](https://github.com/HandyOrg/HandyIpc/wiki/Diagnostic-Messages) for details. + ## NuGet | Package | Description | NuGet | @@ -105,10 +114,3 @@ var result2 = demo1.GetTypeName(); // "String" var result3 = await demo1.GetDefaultAsync(); // null var result3 = await demo2.GetDefaultAsync(); // 0 ``` - -## TODO List - -1. [x] Support for generic interface. -2. [x] Support for `Task/Task` return value in interface method. -3. [x] Support for generic methods (parameter type allow contains nested generic types). -4. [x] NOT support for interface inheritance. diff --git a/README.zh-CN.md b/README.zh-CN.md index 321bd9c..c94501c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,12 +2,21 @@ [English](./README.md) | 中文 -HandyIpc 是一个开箱即用的进程间通讯(IPC)库,对远程方法的调用类似 [WCF](https://docs.microsoft.com/en-us/dotnet/framework/wcf/whats-wcf),但相比之下更为轻量,免去了一切繁琐的配置,从入门到精通只需读完此 README。 +HandyIpc 是一个开箱即用的进程间通讯(IPC)库,对远程方法的调用类似 [WCF](https://docs.microsoft.com/en-us/dotnet/framework/wcf/whats-wcf),但相比之下更为轻量,免去了繁琐的配置,从入门到精通只需读完此 README。 本仓库提供了一组 High-Level 的 API 用于远程方法调用。它的底层通讯协议可以任意选择,如:Named Pipe、MMF(内存映射文件)或 Socket 等,框架本身并不关心具体的实现。 一句话概括本仓库的 API 的设计理念:一个远程的 Ioc 容器。熟悉 Ioc 容器的朋友应该了解:Ioc 容器大致分为注册对象(`Register()`)和取用对象(`Resolve()`)两个操作,而一个 IPC 库无非就是将这两个操作拆分到了两个进程中,即:在服务端注册对象,在客户端取用对象。(当然,Ioc 容器还有一个很重要的功能:自动根据依赖关系对接口类型进行赋值,本库当然没有这个功能,也完全不需要实现这一功能。) +# 功能 + +1. [x] 支持基础的方法和事件。 +2. [x] 支持泛型接口。 +3. [x] 支持泛型方法。(允许任意嵌套的泛型参数)。 +4. [x] 支持以 `Task/Task` 作为返回值的异步方法。 +5. [x] 支持使用 NamedPipe 或 Socket 进行通讯。 +6. [x] 对于不支持的情况,提供了丰富的编译时检查,详情见[这里](https://github.com/HandyOrg/HandyIpc/wiki/Diagnostic-Messages)。 + ## NuGet | 包名 | 描述 | NuGet | @@ -106,10 +115,3 @@ var result2 = demo1.GetTypeName(); // "String" var result3 = await demo1.GetDefaultAsync(); // null var result3 = await demo2.GetDefaultAsync(); // 0 ``` - -## TODO List - -1. [x] Support for generic interface. -2. [x] Support for `Task/Task` return value in interface method. -3. [x] Support for generic methods (parameter type allow contains nested generic types). -4. [x] NOT support for interface inheritance.