From b544d5cccd76735e034c8055918a50927b437474 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期二, 10 十一月 2026 08:29:04 +0800
Subject: [PATCH] fix: 维护模式重复进入失败与按钮灰死 + Broker 并发验证用例

---
 Plugin.DeviceDebug/Model/DebugSessionManager.cs |    6 +
 tests/Aging.MvpTests/BrokerE2ETests.cs          |  187 ++++++++++++++++++++++++++++++++++++++++++++++
 Plugin.IntegrationTest/MainWindow.xaml.cs       |    7 +
 Plugin.DeviceDebug/DeviceDebugTab.xaml.cs       |    5 +
 4 files changed, 203 insertions(+), 2 deletions(-)

diff --git a/Plugin.DeviceDebug/DeviceDebugTab.xaml.cs b/Plugin.DeviceDebug/DeviceDebugTab.xaml.cs
index e868f3e..adbff2b 100644
--- a/Plugin.DeviceDebug/DeviceDebugTab.xaml.cs
+++ b/Plugin.DeviceDebug/DeviceDebugTab.xaml.cs
@@ -432,6 +432,11 @@
                     CurrentSession = null;
                     log.Info($"璁惧 {BoundDeviceId} 浼氳瘽宸插叧闂紝缁存姢绉熺害宸查噴鏀�");
                 }
+
+                // 閲嶆柊鎵撳紑 ReadOnly 浼氳瘽锛岃鐢ㄦ埛鍙互鍐嶆杩涘叆缁存姢妯″紡
+                CurrentSession = _sessionManager.OpenSession(BoundDeviceId, "", "operator", CurrentEffectiveVersion);
+                UpdateButtonStates();
+                LoadOperationLogs();
             }
             catch (Exception ex)
             {
diff --git a/Plugin.DeviceDebug/Model/DebugSessionManager.cs b/Plugin.DeviceDebug/Model/DebugSessionManager.cs
index 16cd94d..7357907 100644
--- a/Plugin.DeviceDebug/Model/DebugSessionManager.cs
+++ b/Plugin.DeviceDebug/Model/DebugSessionManager.cs
@@ -289,8 +289,10 @@
                         _resourceManager.ReleaseReservationAsync(
                             ownerToken, resourceKey, CancellationToken.None).GetAwaiter().GetResult();
 
-                        _sessionOwnerTokens.Remove(sessionId);
-                        log.Info($"宸查噴鏀捐澶� {session.DeviceId} 鐨勮祫婧愰绾�");
+                        // 娉ㄦ剰锛氫笉鍒犻櫎 _sessionOwnerTokens[sessionId]锛�
+                        // 浠ょ墝蹇呴』淇濇寔绋冲畾锛圱okenId 涓嶅彉锛夛紝鍚﹀垯涓嬫杩涘叆缁存姢鏃� new 鍑虹殑鏂� Token
+                        // 鏃犳硶鍦� LayeredResourceManager._grantsByToken 涓壘鍒版棫绉熺害 鈫� 閲婃斁钀界┖ 鈫� 绉熺害姘镐箙娉勬紡
+                        log.Info($"宸查噴鏀捐澶� {session.DeviceId} 鐨勮祫婧愰绾︼紙浠ょ墝淇濈暀渚涘鐢級");
                     }
                     catch (Exception ex)
                     {
diff --git a/Plugin.IntegrationTest/MainWindow.xaml.cs b/Plugin.IntegrationTest/MainWindow.xaml.cs
index 995ecc0..ea96761 100644
--- a/Plugin.IntegrationTest/MainWindow.xaml.cs
+++ b/Plugin.IntegrationTest/MainWindow.xaml.cs
@@ -721,6 +721,13 @@
         {
             try
             {
+                // 妫�鏌ユ槸鍚﹀凡鏈� DeviceBroker 杩涚▼鍦ㄨ繍琛岋紙閬垮厤澶氬疄渚嬩簤鎶㈢閬�/arb.db锛�
+                if (System.Diagnostics.Process.GetProcessesByName("DeviceBroker").Length > 0)
+                {
+                    Log("[INFO] DeviceBroker 瀹夸富宸插湪杩愯锛岃烦杩囧惎鍔�");
+                    return;
+                }
+
                 var model = new DeviceConfigModel();
                 string dbPath = Path.Combine(_testDir, "data.db");
                 if (!File.Exists(dbPath)) return;
diff --git a/tests/Aging.MvpTests/BrokerE2ETests.cs b/tests/Aging.MvpTests/BrokerE2ETests.cs
index dcd0d15..b9315ea 100644
--- a/tests/Aging.MvpTests/BrokerE2ETests.cs
+++ b/tests/Aging.MvpTests/BrokerE2ETests.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Diagnostics;
 using System.IO;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Aging.Infrastructure.Broker;
@@ -212,6 +213,192 @@
             _clientLease.Release(grant, "test-end");
         }
 
+        // ==================== 骞跺彂涓庣敓鍛藉懆鏈� ====================
+
+        /// <summary>鍗曞鎴风杩炵画 5 娆″揩閫熷彂閫侊細楠岃瘉 SessionGate 涓茶鍖栦笉涓㈠懡浠ゃ�佺绾︿笉琚閲婃斁銆�</summary>
+        [Fact]
+        public async Task SingleClient_RapidSend5Times_AllSucceed_LeaseStaysActive()
+        {
+            var grant = GrabLease();
+            var hash = DeviceConfigHash.Compute(_dataDb);
+            using var driver = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
+            await RetryAsync(() => driver.ConnectAsync(CancellationToken.None));
+
+            for (int i = 0; i < 5; i++)
+            {
+                var tx = await driver.SendCommandAsync($"SEND:0x{i:X3}:AA", CancellationToken.None);
+                Assert.Contains("OK:TX", tx);
+            }
+
+            // 5 娆℃搷浣滃悗绉熺害浠� Active锛堟湭琚剰澶栭噴鏀炬垨杩囨湡锛�
+            Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
+            _clientLease.Release(grant, "test-end");
+        }
+
+        /// <summary>
+        /// 涓や釜 Broker 浠g悊椹卞姩鍚屾椂鎸佹湁鍚屼竴璁惧鐨勬椿璺冪绾︼紝骞跺彂鍙戦�佸悇 5 娆°��
+        /// 鐢变簬 arb.db 浠� ResourceKey 涓轰富閿彧鍏佽涓�琛� Active锛岀浜屼釜绉熺害閫氳繃鐩存帴 SQL 鎻掑叆妯℃嫙
+        /// 锛圡VP 鏍呮爮浠呮牎楠� Phase=Active + 鏈繃鏈燂紝涓嶆瘮瀵� OwnerId锛夈��
+        /// 楠岃瘉 SessionGate(SemaphoreSlim=1) 灏嗕袱涓苟鍙戣姹備覆琛屾墽琛岋紝涓よ�呭潎鎴愬姛銆�
+        /// </summary>
+        [Fact]
+        public async Task TwoClients_ConcurrentSend_BothSucceed_SessionGateSerializes()
+        {
+            // 瀹㈡埛绔� A锛氱粡姝e父 Acquire 鑾峰彇 Production 绉熺害
+            var grantA = GrabLease();
+            var hash = DeviceConfigHash.Compute(_dataDb);
+            using var driverA = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
+            await RetryAsync(() => driverA.ConnectAsync(CancellationToken.None));
+
+            // 瀹㈡埛绔� B锛氱洿鎺ュ線 arb.db 鎻掑叆涓�鏉� Maintenance Active 绉熺害琛岋紝妯℃嫙鍙︿竴涓寔鏈夎��
+            InsertDirectLeaseRow(DevId, "concurrent-client-B", OwnerTypeV3.Maintenance, ttlSeconds: 30);
+            using var driverB = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
+            await RetryAsync(() => driverB.ConnectAsync(CancellationToken.None));
+
+            // 骞跺彂鍙戦�佸悇 5 娆�
+            var tasksA = Enumerable.Range(0, 5).Select(i =>
+                driverA.SendCommandAsync($"SEND:0x{i:X3}:AA", CancellationToken.None)).ToArray();
+            var tasksB = Enumerable.Range(0, 5).Select(i =>
+                driverB.SendCommandAsync($"SEND:0x{i + 0x10:X3}:BB", CancellationToken.None)).ToArray();
+
+            var results = await Task.WhenAll(tasksA.Concat(tasksB));
+            Assert.All(results, r => Assert.Contains("OK:TX", r));
+
+            // 涓や釜绉熺害琛屽潎淇濇寔 Active
+            Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
+            _clientLease.Release(grantA, "test-end-A");
+        }
+
+        /// <summary>
+        /// 涓や釜 Production 瀹㈡埛绔珵浜夊悓涓�璁惧绉熺害锛氱涓�涓垚鍔熻幏鍙栵紝绗簩涓簲寰楀埌 ConflictHeld銆�
+        /// </summary>
+        [Fact]
+        public void TwoProductionClients_SecondGetsConflictHeld()
+        {
+            var grantA = GrabLease();
+            Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
+
+            // 绗簩涓鎴风锛堜笉鍚� OwnerId锛屽悓 OwnerType=Production锛�
+            var secondLease = new DbLeaseStore(_arbDb, "second-client");
+            var outcome = secondLease.Acquire(new AcquireRequest
+            {
+                OwnerId = "competing-client",
+                OwnerType = OwnerTypeV3.Production,
+                ResourceKeys = new[] { DevId },
+                TtlSeconds = 30,
+                WaitTimeoutMs = 0
+            });
+
+            Assert.Equal(AcquireResultCode.ConflictHeld, outcome.Code);
+            Assert.NotNull(outcome.Holder);
+
+            _clientLease.Release(grantA, "test-end");
+        }
+
+        /// <summary>
+        /// 瀹屾暣鐢熷懡鍛ㄦ湡锛欰cquire 鈫� Connect 鈫� Send 鈫� Release 鈫� 鍐� Acquire 鈫� 鍐� Send銆�
+        /// 楠岃瘉绉熺害閲婃斁鍚庤祫婧愬洖鍒� Free锛屽彲琚噸鏂拌幏鍙栧苟缁х画浣跨敤銆�
+        /// </summary>
+        [Fact]
+        public async Task LeaseLifecycle_AcquireSendReleaseAcquireSendAgain_AllSucceed()
+        {
+            var hash = DeviceConfigHash.Compute(_dataDb);
+
+            // 绗竴杞細鑾峰彇绉熺害 鈫� 杩炴帴 鈫� 鍙戦�� 鈫� 閲婃斁
+            LeaseGrant grant1;
+            {
+                var outcome = _clientLease.Acquire(new AcquireRequest
+                {
+                    OwnerId = "lifecycle-1",
+                    OwnerType = OwnerTypeV3.Production,
+                    ResourceKeys = new[] { DevId },
+                    TtlSeconds = 30
+                });
+                Assert.Equal(AcquireResultCode.Acquired, outcome.Code);
+                grant1 = outcome.Grants[0];
+            }
+
+            using (var driver1 = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash))
+            {
+                await RetryAsync(() => driver1.ConnectAsync(CancellationToken.None));
+                var tx = await driver1.SendCommandAsync("SEND:0x100:AA", CancellationToken.None);
+                Assert.Contains("OK:TX", tx);
+            }
+
+            Assert.True(_clientLease.Release(grant1, "lifecycle-1-end"));
+            Assert.Equal(LeasePhase.Free, _clientLease.PhaseOf(DevId));
+
+            // 绗簩杞細閲嶆柊鑾峰彇 鈫� 杩炴帴 鈫� 鍙戦��
+            LeaseGrant grant2;
+            {
+                var outcome = _clientLease.Acquire(new AcquireRequest
+                {
+                    OwnerId = "lifecycle-2",
+                    OwnerType = OwnerTypeV3.Production,
+                    ResourceKeys = new[] { DevId },
+                    TtlSeconds = 30
+                });
+                Assert.Equal(AcquireResultCode.Acquired, outcome.Code);
+                grant2 = outcome.Grants[0];
+            }
+
+            using (var driver2 = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash))
+            {
+                await RetryAsync(() => driver2.ConnectAsync(CancellationToken.None));
+                var tx = await driver2.SendCommandAsync("SEND:0x200:BB", CancellationToken.None);
+                Assert.Contains("OK:TX", tx);
+            }
+
+            _clientLease.Release(grant2, "lifecycle-2-end");
+        }
+
+        /// <summary>
+        /// 骞跺彂鍘嬪姏锛氬崟瀹㈡埛绔� 20 涓� Task 鍚屾椂鍙戦�侊紝楠岃瘉 SessionGate + 绠¢亾鍐欏叆閿佷笉宕╂簝銆�
+        /// </summary>
+        [Fact]
+        public async Task SingleClient_ConcurrentSend20Tasks_AllSucceed()
+        {
+            var grant = GrabLease();
+            var hash = DeviceConfigHash.Compute(_dataDb);
+            using var driver = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
+            await RetryAsync(() => driver.ConnectAsync(CancellationToken.None));
+
+            var tasks = Enumerable.Range(0, 20).Select(i =>
+                driver.SendCommandAsync($"SEND:0x{i:X3}:DD", CancellationToken.None)).ToArray();
+
+            var results = await Task.WhenAll(tasks);
+            Assert.All(results, r => Assert.Contains("OK:TX", r));
+
+            Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
+            _clientLease.Release(grant, "test-end");
+        }
+
+        // ==================== 杈呭姪 ====================
+
+        /// <summary>缁曡繃 Acquire 浜掓枼锛岀洿鎺ュ悜 arb.db 鎻掑叆涓�鏉� Active 绉熺害琛岋紙娴嬭瘯骞跺彂鍦烘櫙鐢級銆�</summary>
+        private void InsertDirectLeaseRow(string deviceId, string ownerId, OwnerTypeV3 ownerType, int ttlSeconds)
+        {
+            long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+            using var conn = new System.Data.SQLite.SQLiteConnection(
+                $"Data Source={_arbDb};Version=3;");
+            conn.Open();
+            using var cmd = conn.CreateCommand();
+            cmd.CommandText = @"
+                INSERT OR REPLACE INTO resource_state
+                    (ResourceKey, Generation, Phase, LeaseId, OwnerId, OwnerType,
+                     ProcessInstanceId, MachineName, HostProcessId, Epoch, Reason,
+                     AcquiredUtcMs, ExpiresUtcMs, UpdatedUtcMs)
+                VALUES (@key, 1, 1, @lid, @oid, @otype, '', 'local', 0, 1, 'direct-insert',
+                        @now, @exp, @now)";
+            cmd.Parameters.AddWithValue("@key", DbLeaseStore.NormKey(deviceId));
+            cmd.Parameters.AddWithValue("@lid", "lease_" + Guid.NewGuid().ToString("N").Substring(0, 12));
+            cmd.Parameters.AddWithValue("@oid", ownerId);
+            cmd.Parameters.AddWithValue("@otype", (int)ownerType);
+            cmd.Parameters.AddWithValue("@now", now);
+            cmd.Parameters.AddWithValue("@exp", now + ttlSeconds * 1000L);
+            cmd.ExecuteNonQuery();
+        }
+
         private sealed class CanFrameish { public string Id; public string Data; public string Src; }
 
         [Fact]

--
Gitblit v1.9.1