using System.Reflection; using System.Collections.Concurrent; using dotnetCampus.Ipc.Context; using dotnetCampus.Ipc.CompilerServices.GeneratedProxies; using dotnetCampus.Ipc.IpcRouteds.DirectRouteds; using dotnetCampus.Ipc.Pipes; namespace LanMountainDesktop.Shared.IPC; public sealed class PublicIpcHostService : IDisposable, IExternalIpcNotificationPublisher { private static readonly MethodInfo CreateIpcJointMethod = typeof(GeneratedIpcFactory) .GetMethods(BindingFlags.Public | BindingFlags.Static) .Single(method => method.Name == nameof(GeneratedIpcFactory.CreateIpcJoint) && method.IsGenericMethodDefinition && method.GetParameters().Length == 3); private readonly Dictionary<(Type ContractType, string ObjectId), PublicServiceEntry> _services = new(); private readonly ConcurrentDictionary _connectedPeers = new(StringComparer.OrdinalIgnoreCase); private readonly object _gate = new(); private bool _started; private bool _disposed; public PublicIpcHostService(string pipeName = IpcConstants.DefaultPipeName) { PipeName = pipeName; StartedAt = DateTimeOffset.UtcNow; Provider = new IpcProvider(pipeName); RoutedProvider = new JsonIpcDirectRoutedProvider(Provider); } public string PipeName { get; } public DateTimeOffset StartedAt { get; } public IpcProvider Provider { get; } public JsonIpcDirectRoutedProvider RoutedProvider { get; } public Func> PluginDescriptorProvider { get; set; } = static () => Array.Empty(); public void Start() { if (_started) { return; } RoutedProvider.AddRequestHandler(IpcConstants.Routes.SessionGetInfo, () => BuildSessionInfo()); RoutedProvider.AddRequestHandler(IpcConstants.Routes.CatalogGet, () => GetCatalogSnapshot()); Provider.PeerConnected += OnPeerConnected; RoutedProvider.StartServer(); _started = true; } public void RegisterPublicService( TContract implementation, string? objectId = null, string? pluginId = null, params string[] notifyIds) where TContract : class { RegisterPublicService(typeof(TContract), implementation, objectId, pluginId, notifyIds); } public void RegisterPublicService( Type contractType, object implementation, string? objectId = null, string? pluginId = null, IEnumerable? notifyIds = null) { ArgumentNullException.ThrowIfNull(contractType); ArgumentNullException.ThrowIfNull(implementation); var normalizedObjectId = objectId ?? string.Empty; var normalizedNotifyIds = notifyIds? .Where(id => !string.IsNullOrWhiteSpace(id)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() ?? []; lock (_gate) { if (_services.ContainsKey((contractType, normalizedObjectId))) { throw new InvalidOperationException( $"Public IPC contract '{contractType.FullName}' with object id '{normalizedObjectId}' is already registered."); } CreateIpcJointMethod .MakeGenericMethod(contractType) .Invoke(null, [Provider, implementation, string.IsNullOrEmpty(normalizedObjectId) ? null : normalizedObjectId]); _services[(contractType, normalizedObjectId)] = new PublicServiceEntry( contractType, implementation, string.IsNullOrEmpty(normalizedObjectId) ? null : normalizedObjectId, pluginId, normalizedNotifyIds); } if (_started) { _ = NotifyCatalogChangedAsync(); } } public PublicIpcCatalogSnapshot GetCatalogSnapshot() { PublicIpcServiceDescriptor[] services; lock (_gate) { services = _services.Values .Select(entry => new PublicIpcServiceDescriptor( entry.ContractType.FullName ?? entry.ContractType.Name, entry.ContractType.Assembly.GetName().Name ?? string.Empty, entry.ContractType.AssemblyQualifiedName, entry.ObjectId, entry.PluginId, string.IsNullOrWhiteSpace(entry.PluginId), entry.NotifyIds)) .OrderBy(entry => entry.PluginId ?? string.Empty, StringComparer.OrdinalIgnoreCase) .ThenBy(entry => entry.ContractTypeName, StringComparer.OrdinalIgnoreCase) .ToArray(); } var plugins = PluginDescriptorProvider()?.ToArray() ?? Array.Empty(); return new PublicIpcCatalogSnapshot(services, plugins, DateTimeOffset.UtcNow); } public Task PublishStartupProgressAsync( LanMountainDesktop.Shared.Contracts.Launcher.StartupProgressMessage message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); return NotifyAsync(IpcRoutedNotifyIds.LauncherStartupProgress, message, cancellationToken); } public Task PublishLoadingStateAsync( LanMountainDesktop.Shared.Contracts.Launcher.LoadingStateMessage message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); return NotifyAsync(IpcRoutedNotifyIds.LauncherLoadingState, message, cancellationToken); } public async Task NotifyAsync(string notifyId, TPayload payload, CancellationToken cancellationToken = default) where TPayload : class { ArgumentException.ThrowIfNullOrWhiteSpace(notifyId); ArgumentNullException.ThrowIfNull(payload); cancellationToken.ThrowIfCancellationRequested(); foreach (var peer in _connectedPeers.Values) { cancellationToken.ThrowIfCancellationRequested(); try { var client = new JsonIpcDirectRoutedClientProxy(peer); await client.NotifyAsync(notifyId, payload).ConfigureAwait(false); } catch { // Keep notification fan-out best-effort. Broken peers are cleaned by dotnetCampus.Ipc. } } } private Task NotifyCatalogChangedAsync() { return NotifyAsync(IpcRoutedNotifyIds.CatalogChanged, GetCatalogSnapshot()); } private PublicIpcSessionInfo BuildSessionInfo() { return new PublicIpcSessionInfo( PipeName, IpcConstants.ProtocolVersion, [ IpcConstants.Routes.SessionGetInfo, IpcConstants.Routes.CatalogGet, IpcRoutedNotifyIds.CatalogChanged, IpcRoutedNotifyIds.LauncherStartupProgress, IpcRoutedNotifyIds.LauncherLoadingState ], StartedAt); } public void Dispose() { if (_disposed) { return; } _disposed = true; _connectedPeers.Clear(); Provider.PeerConnected -= OnPeerConnected; Provider.Dispose(); } private void OnPeerConnected(object? sender, PeerConnectedArgs e) { if (_disposed) { return; } var peer = e.Peer; _connectedPeers[peer.PeerName] = peer; peer.PeerConnectionBroken -= OnPeerConnectionBroken; peer.PeerConnectionBroken += OnPeerConnectionBroken; } private void OnPeerConnectionBroken(object? sender, IPeerConnectionBrokenArgs e) { if (sender is PeerProxy peer) { _connectedPeers.TryRemove(peer.PeerName, out _); } } private sealed record PublicServiceEntry( Type ContractType, object Implementation, string? ObjectId, string? PluginId, string[] NotifyIds); }