From 60212cd38330a77f73312a950723d1182bb6acf9 Mon Sep 17 00:00:00 2001 From: CreeperAWA Date: Sat, 19 Sep 2026 22:30:55 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(linkage):=20=E9=87=8D=E6=9E=84Clas?= =?UTF-8?q?sIsland=20IPC=E8=BF=9E=E6=8E=A5=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=9C=8D=E5=8A=A1=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增ClassIslandIpcConnection统一管理IPC连接,替换原分散在两个服务中的连接代码 2. 重构课程联动服务的快照比较逻辑,忽略倒计时等动态字段避免误触发状态变更 3. 调整课程联动刷新策略,仅在完全离线时轮询并延长轮询间隔 4. 统一通知服务的IPC连接逻辑,复用新的连接管理类 --- SecRandom/App.axaml.cs | 7 +- .../Linkage/ClassIslandIpcConnection.cs | 294 ++++++++++++++++++ .../Linkage/ClassIslandScheduleSource.cs | 104 +------ .../Services/Linkage/CourseLinkageService.cs | 22 +- .../Notification/NotificationService.cs | 143 ++------- 5 files changed, 365 insertions(+), 205 deletions(-) create mode 100644 SecRandom/Services/Linkage/ClassIslandIpcConnection.cs diff --git a/SecRandom/App.axaml.cs b/SecRandom/App.axaml.cs index 9d3aba70e..7b9da8f03 100644 --- a/SecRandom/App.axaml.cs +++ b/SecRandom/App.axaml.cs @@ -878,7 +878,11 @@ private void BuildHost(IPlatformServiceRoot platform) services.AddSingleton(); services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); - services.AddSingleton(); + services.AddSingleton(serviceProvider => + new NotificationService( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService>(), + serviceProvider.GetRequiredService())); services.AddSingleton(serviceProvider => new MusicLibraryService( serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService>(), @@ -888,6 +892,7 @@ private void BuildHost(IPlatformServiceRoot platform) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs b/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs new file mode 100644 index 000000000..bfdb12130 --- /dev/null +++ b/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs @@ -0,0 +1,294 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using ClassIsland.Shared.IPC; +using ClassIsland.Shared.IPC.Abstractions.Services; +using dotnetCampus.Ipc.CompilerServices.GeneratedProxies; +using dotnetCampus.Ipc.Pipes; +using Microsoft.Extensions.Logging; +using SecRandom4Ci.Interface.Services; + +namespace SecRandom.Services.Linkage; + +public sealed class ClassIslandIpcConnection : IDisposable +{ + private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(5); + private static readonly TimeSpan JsonRouteReadyDelay = TimeSpan.FromSeconds(1); + private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(5); + private static readonly TimeSpan MinRetryDelay = TimeSpan.FromSeconds(5); + private static readonly TimeSpan MaxRetryDelay = TimeSpan.FromMinutes(5); + private static readonly Version MinimumPluginVersion = new(1, 2, 0, 0); + + private readonly ILogger _logger; + private readonly SemaphoreSlim _connectionGate = new(1, 1); + private readonly object _stateLock = new(); + + private IpcClient? _client; + private IPublicLessonsService? _lessonsService; + private ISecRandomService? _notificationService; + private DateTimeOffset _nextConnectAttempt = DateTimeOffset.MinValue; + private TimeSpan _currentRetryDelay = MinRetryDelay; + private bool _isDisposed; + private volatile int _connectionState; // 0=disconnected, 1=connecting, 2=connected + + public IPublicLessonsService? LessonsService + { + get + { + lock (_stateLock) + { + return _lessonsService; + } + } + } + + public ISecRandomService? NotificationService + { + get + { + lock (_stateLock) + { + return _notificationService; + } + } + } + + public bool IsConnected + { + get + { + lock (_stateLock) + { + return _lessonsService is not null; + } + } + } + + public event EventHandler? StateChanged; + + public ClassIslandIpcConnection(ILogger logger) + { + _logger = logger; + } + + public async Task GetLessonsServiceAsync(CancellationToken cancellationToken = default) + { + var service = LessonsService; + if (service is not null) + return service; + + // Trigger connection if not already trying + if (Interlocked.CompareExchange(ref _connectionState, 1, 0) == 0) + { + _ = Task.Run(() => TryConnectAsync(cancellationToken)); + } + + // Wait for connection with timeout + var timeout = TimeSpan.FromSeconds(10); + var start = DateTime.UtcNow; + while (DateTime.UtcNow - start < timeout) + { + service = LessonsService; + if (service is not null) + return service; + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + return null; + } + + public async Task GetNotificationServiceAsync(CancellationToken cancellationToken = default) + { + var service = NotificationService; + if (service is not null) + return service; + + // Ensure connection is attempted + await GetLessonsServiceAsync(cancellationToken).ConfigureAwait(false); + return NotificationService; + } + + private async Task TryConnectAsync(CancellationToken cancellationToken) + { + try + { + await EnsureConnectedAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + Interlocked.Exchange(ref _connectionState, 0); + } + } + + private async Task EnsureConnectedAsync(CancellationToken cancellationToken) + { + if (DateTimeOffset.UtcNow < _nextConnectAttempt) + return false; + + await _connectionGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (IsConnected) + return true; + + if (DateTimeOffset.UtcNow < _nextConnectAttempt) + return false; + + var client = new IpcClient(); + + // Subscribe to ClassIsland lifecycle notifications (NOT CurrentTimeStateChanged which fires every second) + client.JsonIpcProvider.AddNotifyHandler(IpcRoutedNotifyIds.OnClassNotifyId, OnClassIslandStateChanged); + client.JsonIpcProvider.AddNotifyHandler(IpcRoutedNotifyIds.OnBreakingTimeNotifyId, OnClassIslandStateChanged); + client.JsonIpcProvider.AddNotifyHandler(IpcRoutedNotifyIds.OnAfterSchoolNotifyId, OnClassIslandStateChanged); + + try + { + await client.Connect().WaitAsync(ConnectTimeout, cancellationToken).ConfigureAwait(false); + await Task.Delay(JsonRouteReadyDelay, cancellationToken).ConfigureAwait(false); + + if (client.PeerProxy is null) + { + DisposeClient(client); + ScheduleRetry(); + return false; + } + + // Handle connection broken - on the PEER, not the provider + client.PeerProxy!.PeerConnectionBroken += (_, _) => OnPeerConnectionBroken(); + + var lessons = GeneratedIpcFactory.CreateIpcProxy(client.Provider, client.PeerProxy); + + // Test if lessons service works (ClassIsland core IPC) + bool lessonsWork = false; + try + { + _ = lessons.IsTimerRunning; + lessonsWork = true; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Lessons service test failed: {ex.Message}"); + } + + // Try to get notification service (SecRandom4Ci plugin - optional) + ISecRandomService? notification = null; + try + { + notification = client.Provider.CreateIpcProxy(client.PeerProxy); + var isAlive = notification.IsAlive(); + if (!string.Equals(isAlive, "Yes", StringComparison.Ordinal)) + { + notification = null; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Notification service not available: {ex.Message}"); + notification = null; + } + + if (!lessonsWork) + { + _logger.LogDebug("ClassIsland IPC 连接成功但课程服务不可用。"); + DisposeClient(client); + ScheduleRetry(); + return false; + } + + _client = client; + _lessonsService = lessons; + _notificationService = notification; + _nextConnectAttempt = DateTimeOffset.MinValue; + _currentRetryDelay = MinRetryDelay; + + if (notification is not null) + { + _logger.LogInformation("已连接到 ClassIsland IPC:管道={PipeName},SecRandom4Ci 插件可用。", IpcClient.PipeName); + } + else + { + _logger.LogInformation("已连接到 ClassIsland IPC:管道={PipeName},仅课程联动可用(未安装 SecRandom4Ci 插件)。", IpcClient.PipeName); + } + + // Notify state changed on successful connection + StateChanged?.Invoke(this, EventArgs.Empty); + return true; + } + catch (Exception exception) + { + _logger.LogDebug(exception, "连接 ClassIsland IPC 失败,将在 {RetryDelay} 后重试。", _currentRetryDelay); + DisposeClient(client); + ScheduleRetry(); + return false; + } + } + finally + { + _connectionGate.Release(); + } + } + + private void OnPeerConnectionBroken() + { + if (_isDisposed) + return; + + _logger.LogDebug("ClassIsland IPC 连接已断开,将尝试重连。"); + InvalidateConnection(); + ScheduleRetry(); + + // Trigger reconnection in background + _ = Task.Run(() => TryConnectAsync(CancellationToken.None)); + + // Notify state changed on disconnection + StateChanged?.Invoke(this, EventArgs.Empty); + } + + private void OnClassIslandStateChanged() + { + if (_isDisposed) + return; + + StateChanged?.Invoke(this, EventArgs.Empty); + } + + private void InvalidateConnection() + { + lock (_stateLock) + { + _lessonsService = null; + _notificationService = null; + } + DisposeClient(_client); + _client = null; + } + + private static void DisposeClient(IpcClient? client) + { + if (client is null) + return; + + try + { + client.Provider.Dispose(); + } + catch (Exception) + { + } + } + + private void ScheduleRetry() + { + _nextConnectAttempt = DateTimeOffset.UtcNow.Add(_currentRetryDelay); + _currentRetryDelay = TimeSpan.FromSeconds(Math.Min(_currentRetryDelay.TotalSeconds * 2, MaxRetryDelay.TotalSeconds)); + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + InvalidateConnection(); + _connectionGate.Dispose(); + } +} \ No newline at end of file diff --git a/SecRandom/Services/Linkage/ClassIslandScheduleSource.cs b/SecRandom/Services/Linkage/ClassIslandScheduleSource.cs index 8643ba502..48934384d 100644 --- a/SecRandom/Services/Linkage/ClassIslandScheduleSource.cs +++ b/SecRandom/Services/Linkage/ClassIslandScheduleSource.cs @@ -4,31 +4,31 @@ using ClassIsland.Shared.Enums; using ClassIsland.Shared.IPC; using ClassIsland.Shared.IPC.Abstractions.Services; -using dotnetCampus.Ipc.CompilerServices.GeneratedProxies; -using dotnetCampus.Ipc.Pipes; using Microsoft.Extensions.Logging; using SecRandom.Core.Models.Linkage; namespace SecRandom.Services.Linkage; -public sealed class ClassIslandScheduleSource(ILogger logger) : ICourseScheduleSource +public sealed class ClassIslandScheduleSource : ICourseScheduleSource { - private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(5); - private static readonly TimeSpan JsonRouteReadyDelay = TimeSpan.FromSeconds(1); - private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(5); - private readonly SemaphoreSlim _connectionGate = new(1, 1); - private IpcClient? _client; - private IPublicLessonsService? _lessons; + private readonly ClassIslandIpcConnection _ipcConnection; + private readonly ILogger _logger; private string _lastKnownCourseName = string.Empty; private DateOnly? _lastKnownCourseDate; - private DateTimeOffset _nextConnectAttempt = DateTimeOffset.MinValue; public string SourceName => "ClassIsland"; public event EventHandler? StateChanged; + public ClassIslandScheduleSource(ClassIslandIpcConnection ipcConnection, ILogger logger) + { + _ipcConnection = ipcConnection; + _logger = logger; + _ipcConnection.StateChanged += (_, _) => StateChanged?.Invoke(this, EventArgs.Empty); + } + public async Task GetSnapshotAsync(CancellationToken cancellationToken = default) { - var lessons = await GetLessonsAsync(cancellationToken).ConfigureAwait(false); + var lessons = await _ipcConnection.GetLessonsServiceAsync(cancellationToken).ConfigureAwait(false); if (lessons is null) return CourseScheduleSnapshot.Unavailable(SourceName, ScheduleErrorCodes.ClassIslandUnavailable); @@ -85,6 +85,8 @@ public async Task GetSnapshotAsync(CancellationToken can var currentCourseRemaining = state == CourseTimeState.OnClass ? Positive(lessons.OnBreakingTimeLeftTime) : null; + // Version only includes stable identifiers (schedule index + state), NOT countdown timers + // This prevents false StateChanged triggers from continuously changing OnClassLeftTime/OnBreakingTimeLeftTime return new CourseScheduleSnapshot( true, state, @@ -99,89 +101,11 @@ public async Task GetSnapshotAsync(CancellationToken can } catch (Exception exception) { - logger.LogDebug(exception, "读取 ClassIsland 日程状态失败。"); - InvalidateConnection(); + _logger.LogDebug(exception, "读取 ClassIsland 日程状态失败。"); return CourseScheduleSnapshot.Unavailable(SourceName, ScheduleErrorCodes.ClassIslandReadFailed); } } - private async Task GetLessonsAsync(CancellationToken cancellationToken) - { - if (_lessons is not null) - return _lessons; - if (DateTimeOffset.UtcNow < _nextConnectAttempt) - return null; - - await _connectionGate.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - if (_lessons is not null) - return _lessons; - if (DateTimeOffset.UtcNow < _nextConnectAttempt) - return null; - - var client = new IpcClient(); - client.JsonIpcProvider.AddNotifyHandler(IpcRoutedNotifyIds.OnClassNotifyId, OnClassIslandStateChanged); - client.JsonIpcProvider.AddNotifyHandler(IpcRoutedNotifyIds.OnBreakingTimeNotifyId, OnClassIslandStateChanged); - client.JsonIpcProvider.AddNotifyHandler(IpcRoutedNotifyIds.OnAfterSchoolNotifyId, OnClassIslandStateChanged); - client.JsonIpcProvider.AddNotifyHandler(IpcRoutedNotifyIds.CurrentTimeStateChangedNotifyId, OnClassIslandStateChanged); - await client.Connect().WaitAsync(ConnectTimeout, cancellationToken).ConfigureAwait(false); - // ClassIsland establishes its JSON routed peer asynchronously after the transport connection. - await Task.Delay(JsonRouteReadyDelay, cancellationToken).ConfigureAwait(false); - if (client.PeerProxy is null) - { - DisposeClient(client); - ScheduleRetry(); - return null; - } - - _client = client; - _lessons = GeneratedIpcFactory.CreateIpcProxy(client.Provider, client.PeerProxy); - _nextConnectAttempt = DateTimeOffset.MinValue; - logger.LogInformation("已连接到 ClassIsland IPC:管道={PipeName}。", IpcClient.PipeName); - return _lessons; - } - catch (Exception exception) - { - logger.LogDebug(exception, "连接 ClassIsland IPC 失败,将在 {RetryDelay} 后重试。", RetryDelay); - InvalidateConnection(); - ScheduleRetry(); - return null; - } - finally - { - _connectionGate.Release(); - } - } - - private void OnClassIslandStateChanged() - { - StateChanged?.Invoke(this, EventArgs.Empty); - } - - private void InvalidateConnection() - { - _lessons = null; - DisposeClient(_client); - _client = null; - } - - private static void DisposeClient(IpcClient? client) - { - try - { - client?.Provider.Dispose(); - } - catch (Exception) - { - } - } - - private void ScheduleRetry() - { - _nextConnectAttempt = DateTimeOffset.UtcNow.Add(RetryDelay); - } - private static string NormalizeSubjectName(string? name) { var normalized = name?.Trim() ?? string.Empty; diff --git a/SecRandom/Services/Linkage/CourseLinkageService.cs b/SecRandom/Services/Linkage/CourseLinkageService.cs index f1a690300..ee5b1fd54 100644 --- a/SecRandom/Services/Linkage/CourseLinkageService.cs +++ b/SecRandom/Services/Linkage/CourseLinkageService.cs @@ -58,8 +58,11 @@ public async Task RefreshAsync(CancellationToken cancellationToken = default) var next = source is null ? CourseScheduleSnapshot.Unavailable("Off") : await source.GetSnapshotAsync(cancellationToken).ConfigureAwait(false); - if (Equals(_snapshot, next)) + + // Semantic comparison: ignore time-varying fields (countdown timers) + if (SnapshotsEqual(_snapshot, next)) return; + _snapshot = next; stateChanged = true; } @@ -71,7 +74,7 @@ public async Task RefreshAsync(CancellationToken cancellationToken = default) { _logger.LogWarning(exception, "刷新课程联动状态失败。"); var unavailable = CourseScheduleSnapshot.Unavailable("Unknown", exception.Message); - if (!Equals(_snapshot, unavailable)) + if (!SnapshotsEqual(_snapshot, unavailable)) { _snapshot = unavailable; stateChanged = true; @@ -86,6 +89,18 @@ public async Task RefreshAsync(CancellationToken cancellationToken = default) NotifyStateChanged(); } + private static bool SnapshotsEqual(CourseScheduleSnapshot a, CourseScheduleSnapshot b) + { + return a.IsAvailable == b.IsAvailable + && a.State == b.State + && a.Source == b.Source + && a.Version == b.Version + && a.CurrentCourse?.Name == b.CurrentCourse?.Name + && a.PreviousCourse?.Name == b.PreviousCourse?.Name + && a.NextCourse?.Name == b.NextCourse?.Name + && a.Error == b.Error; + } + public bool IsConfirmedBreakTime => _snapshot.IsAvailable && _snapshot.State == CourseTimeState.Breaking && !IsWithinEnableWindow(_snapshot); @@ -126,8 +141,9 @@ public string GetSubjectFilter() public TimeSpan GetNextRefreshDelay() { + // Only poll when completely unavailable - otherwise rely on events if (Settings.DataSource == LinkageDataSource.ClassIsland && !_snapshot.IsAvailable) - return TimeSpan.FromSeconds(5); + return TimeSpan.FromMinutes(5); List candidates = []; if (_snapshot.TimeUntilNextCourse is { } untilNext && untilNext > TimeSpan.Zero) diff --git a/SecRandom/Services/Notification/NotificationService.cs b/SecRandom/Services/Notification/NotificationService.cs index 32b34b2fa..a3c21f3f0 100644 --- a/SecRandom/Services/Notification/NotificationService.cs +++ b/SecRandom/Services/Notification/NotificationService.cs @@ -1,11 +1,11 @@ using Avalonia.Threading; using ClassIsland.Shared.IPC; -using dotnetCampus.Ipc.CompilerServices.GeneratedProxies; using Microsoft.Extensions.Logging; using SecRandom.Core.Enums; using SecRandom.Core.Models.SubConfigs; using SecRandom.Core.Services.Config; using SecRandom.Core.Services.Draw; +using SecRandom.Services.Linkage; using SecRandom.Shared.Models.Profile; using SecRandom4Ci.Interface.Enums; using SecRandom4Ci.Interface.Models; @@ -16,26 +16,22 @@ namespace SecRandom.Services.Notification; public sealed class NotificationService : IDisposable { - private static readonly Version MinimumPluginVersion = new(1, 2, 0, 0); - private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(1); private static readonly TimeSpan InvocationTimeout = TimeSpan.FromSeconds(1); - private static readonly TimeSpan JsonRouteReadyDelay = TimeSpan.FromMilliseconds(100); private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(5); private readonly MainConfigHandler _configHandler; private readonly ILogger _logger; + private readonly ClassIslandIpcConnection _ipcConnection; private readonly CryptoRandomSource _previewRandom = new(); private readonly SemaphoreSlim _sendGate = new(1, 1); - private IpcClient? _classIslandClient; - private ISecRandomService? _classIslandService; - private DateTimeOffset _nextClassIslandConnectAttempt = DateTimeOffset.MinValue; private bool _quickDrawBuiltInPreviewActive; private bool _isDisposed; - public NotificationService(MainConfigHandler configHandler, ILogger logger) + public NotificationService(MainConfigHandler configHandler, ILogger logger, ClassIslandIpcConnection ipcConnection) { _configHandler = configHandler; _logger = logger; + _ipcConnection = ipcConnection; } public void QueueStudents( @@ -270,90 +266,52 @@ private async Task SendToClassIslandAsync(NotificationData notification, Action? if (_isDisposed) return; - var service = await GetClassIslandServiceAsync().ConfigureAwait(false); + var service = await _ipcConnection.GetNotificationServiceAsync().ConfigureAwait(false); if (service is null) { builtInFallback?.Invoke(); return; } - if (!string.Equals( - await InvokeClassIslandAsync(service.IsAlive).ConfigureAwait(false), - "Yes", - StringComparison.Ordinal)) + string? isAlive = null; + try + { + isAlive = service.IsAlive(); + } + catch (AggregateException aggEx) when (aggEx.InnerExceptions.Count == 1) + { + System.Diagnostics.Debug.WriteLine($"IPC notification IsAlive failed: {aggEx.InnerExceptions[0].Message}"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"IPC notification IsAlive failed: {ex.Message}"); + } + + if (!string.Equals(isAlive, "Yes", StringComparison.Ordinal)) { _logger.LogDebug("SecRandom4Ci 通知服务未响应。"); - InvalidateClassIslandConnection(); - ScheduleClassIslandRetry(); builtInFallback?.Invoke(); return; } - await InvokeClassIslandAsync(() => service.ShowNotification(notification)).ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.LogDebug(exception, "通过 SecRandom4Ci 插件发送 ClassIsland 通知失败。"); - InvalidateClassIslandConnection(); - ScheduleClassIslandRetry(); - builtInFallback?.Invoke(); - } - finally - { - _sendGate.Release(); - } - } - - private async Task GetClassIslandServiceAsync() - { - if (_isDisposed) - return null; - if (_classIslandService is not null) - return _classIslandService; - if (DateTimeOffset.UtcNow < _nextClassIslandConnectAttempt) - return null; - - IpcClient? client = null; - try - { - client = new IpcClient(); - await client.Connect().WaitAsync(ConnectTimeout).ConfigureAwait(false); - // ClassIsland initializes the JSON routed peer after the named pipe connects. - await Task.Delay(JsonRouteReadyDelay).ConfigureAwait(false); - if (client.PeerProxy is null) + try { - DisposeClient(client); - ScheduleClassIslandRetry(); - return null; + service.ShowNotification(notification); } - - var service = client.Provider.CreateIpcProxy(client.PeerProxy); - var pluginVersion = await InvokeClassIslandAsync(service.GetPluginVersion).ConfigureAwait(false); - if (pluginVersion is null || pluginVersion < MinimumPluginVersion || - !string.Equals( - await InvokeClassIslandAsync(service.IsAlive).ConfigureAwait(false), - "Yes", - StringComparison.Ordinal)) + catch (AggregateException aggEx) when (aggEx.InnerExceptions.Count == 1) { - _logger.LogDebug("SecRandom4Ci 插件不可用或版本低于 {MinimumPluginVersion}。", MinimumPluginVersion); - DisposeClient(client); - ScheduleClassIslandRetry(); - return null; + System.Diagnostics.Debug.WriteLine($"IPC notification ShowNotification failed: {aggEx.InnerExceptions[0].Message}"); + builtInFallback?.Invoke(); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "通过 SecRandom4Ci 插件发送 ClassIsland 通知失败。"); + builtInFallback?.Invoke(); } - - _classIslandClient = client; - _classIslandService = service; - _nextClassIslandConnectAttempt = DateTimeOffset.MinValue; - _logger.LogInformation("已连接到 ClassIsland IPC,SecRandom4Ci {PluginVersion} 通知服务可用。", pluginVersion); - return _classIslandService; } - catch (Exception exception) + finally { - _logger.LogDebug(exception, "连接 ClassIsland IPC 的 SecRandom4Ci 通知服务失败。"); - DisposeClient(client); - InvalidateClassIslandConnection(); - ScheduleClassIslandRetry(); - return null; + _sendGate.Release(); } } @@ -396,42 +354,6 @@ private List DrawPreviewItems(List candidates, int drawCount) return candidates.Take(count).ToList(); } - private void InvalidateClassIslandConnection() - { - _classIslandService = null; - DisposeClient(_classIslandClient); - _classIslandClient = null; - } - - private static void DisposeClient(IpcClient? client) - { - if (client is null) - return; - - try - { - client.Provider.Dispose(); - } - catch (Exception) - { - } - } - - private static Task InvokeClassIslandAsync(Func invoke) - { - return Task.Run(invoke).WaitAsync(InvocationTimeout); - } - - private static Task InvokeClassIslandAsync(Action invoke) - { - return Task.Run(invoke).WaitAsync(InvocationTimeout); - } - - private void ScheduleClassIslandRetry() - { - _nextClassIslandConnectAttempt = DateTimeOffset.UtcNow.Add(RetryDelay); - } - private static ResultType GetResultType(NotificationSettingsType type) { return type switch @@ -467,6 +389,5 @@ private static string DisplayValue(string primary, string fallback) public void Dispose() { _isDisposed = true; - InvalidateClassIslandConnection(); } } From 70c2b272fe491da0e3d49bc92c7bf47523a4c02c Mon Sep 17 00:00:00 2001 From: CreeperAWA Date: Mon, 21 Sep 2026 23:02:40 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(ipc):=20=E4=BF=AE=E5=A4=8DIPC=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E7=AB=9E=E6=80=81=E4=B8=8E=E6=96=AD=E5=BC=80=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 为IPC连接断开添加500ms延迟重连,避免和ClassIsland的广播循环竞争 2. 新增对IPC连接断开异常的捕获处理,分别在存活检测和通知发送场景中静默处理断开情况并触发内置回退 --- .../Services/Linkage/ClassIslandIpcConnection.cs | 11 +++++++++-- .../Services/Notification/NotificationService.cs | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs b/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs index bfdb12130..af60b7946 100644 --- a/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs +++ b/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs @@ -236,8 +236,15 @@ private void OnPeerConnectionBroken() InvalidateConnection(); ScheduleRetry(); - // Trigger reconnection in background - _ = Task.Run(() => TryConnectAsync(CancellationToken.None)); + // Delay reconnection to avoid race with ClassIsland's broadcast loop + // ClassIsland broadcasts currentTimeStateChanged every second; + // immediate reconnect can race with its BroadcastNotificationAsync + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false); + if (!_isDisposed) + await TryConnectAsync(CancellationToken.None).ConfigureAwait(false); + }); // Notify state changed on disconnection StateChanged?.Invoke(this, EventArgs.Empty); diff --git a/SecRandom/Services/Notification/NotificationService.cs b/SecRandom/Services/Notification/NotificationService.cs index a3c21f3f0..884dcbd47 100644 --- a/SecRandom/Services/Notification/NotificationService.cs +++ b/SecRandom/Services/Notification/NotificationService.cs @@ -282,6 +282,10 @@ private async Task SendToClassIslandAsync(NotificationData notification, Action? { System.Diagnostics.Debug.WriteLine($"IPC notification IsAlive failed: {aggEx.InnerExceptions[0].Message}"); } + catch (dotnetCampus.Ipc.Exceptions.IpcPeerConnectionBrokenException) + { + // Peer disconnected during IsAlive check + } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"IPC notification IsAlive failed: {ex.Message}"); @@ -303,6 +307,11 @@ private async Task SendToClassIslandAsync(NotificationData notification, Action? System.Diagnostics.Debug.WriteLine($"IPC notification ShowNotification failed: {aggEx.InnerExceptions[0].Message}"); builtInFallback?.Invoke(); } + catch (dotnetCampus.Ipc.Exceptions.IpcPeerConnectionBrokenException) + { + // Peer disconnected during notification - trigger fallback silently + builtInFallback?.Invoke(); + } catch (Exception ex) { _logger.LogDebug(ex, "通过 SecRandom4Ci 插件发送 ClassIsland 通知失败。"); From 4e498f7e93e7059f84632151ecba93ebe11cf07e Mon Sep 17 00:00:00 2001 From: CreeperAWA Date: Tue, 22 Sep 2026 23:59:16 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E6=96=AD=E5=BC=80=20IPC=20=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=90=8E=E9=87=8D=E7=BD=AE=E9=87=8D=E8=AF=95=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E5=92=8C=E5=BB=B6=E8=BF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重置下次连接尝试时间为最小值,当前重试延迟为最小延迟,允许立即重连,避免与 ClassIsland 的广播循环产生竞争。 --- SecRandom/Services/Linkage/ClassIslandIpcConnection.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs b/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs index af60b7946..d7895e2a9 100644 --- a/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs +++ b/SecRandom/Services/Linkage/ClassIslandIpcConnection.cs @@ -234,7 +234,9 @@ private void OnPeerConnectionBroken() _logger.LogDebug("ClassIsland IPC 连接已断开,将尝试重连。"); InvalidateConnection(); - ScheduleRetry(); + // 重置重试时间,允许立即重连(500ms 后) + _nextConnectAttempt = DateTimeOffset.MinValue; + _currentRetryDelay = MinRetryDelay; // Delay reconnection to avoid race with ClassIsland's broadcast loop // ClassIsland broadcasts currentTimeStateChanged every second;