浏览代码

feat(d2-02): DebugSessionManager acquire/release/heartbeat + 幂等单测

huangjie 2 天之前
父节点
当前提交
968ad59

+ 50 - 0
ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/DebugSessionManagerTests.cs

@@ -0,0 +1,50 @@
+using System;
+using IvfTl.ControlHost.Debug;
+using IvfTl.ControlHost.Tests.Fakes;
+using Xunit;
+namespace IvfTl.ControlHost.Tests
+{
+    public class DebugSessionManagerTests
+    {
+        private DateTime _now = new DateTime(2026, 6, 23, 0, 0, 0);
+        private (DebugSessionManager mgr, FakeGate gate) New(bool canAcquire = true)
+        {
+            var serial = new FakeSerial();
+            var gate = new FakeGate(5, serial) { CanAcquire = canAcquire };
+            var mgr = new DebugSessionManager(sn => gate, () => _now, ttlMs: 10000, log: _ => { });
+            return (mgr, gate);
+        }
+        [Fact]
+        public void Acquire_Returns_SessionId_And_Pauses()
+        {
+            var (mgr, gate) = New();
+            var r = mgr.Acquire(5);
+            Assert.True(r.Ok);
+            Assert.False(string.IsNullOrEmpty((string)r.Result));
+            Assert.True(gate.IsCapturePaused);
+        }
+        [Fact]
+        public void Acquire_Busy_Returns_BUSY()
+        {
+            var (mgr, _) = New(canAcquire: false);
+            var r = mgr.Acquire(5);
+            Assert.False(r.Ok);
+            Assert.Equal("BUSY", r.Code);
+        }
+        [Fact]
+        public void Release_Disposes_Lease_And_Resumes()
+        {
+            var (mgr, gate) = New();
+            string sid = (string)mgr.Acquire(5).Result;
+            var r = mgr.Release(sid);
+            Assert.True(r.Ok);
+            Assert.True(gate.LastLease.Disposed);
+        }
+        [Fact]
+        public void Release_Unknown_Session_Is_Idempotent_Ok()
+        {
+            var (mgr, _) = New();
+            Assert.True(mgr.Release("not-a-session").Ok);
+        }
+    }
+}

+ 65 - 0
ivf_tl_operate_2.0/control/ivf_tl_ControlHost/Debug/DebugSessionManager.cs

@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Concurrent;
+using IvfTl.Hardware;
+namespace IvfTl.ControlHost.Debug
+{
+    /// <summary>
+    /// control 端调试会话后端:会话表 + 借用/归还 + 心跳续约 + 超时自动回收 + 命令分发。
+    /// 安全地基(spec §5):绝不指望 operate 主动还,SweepExpired 超时回收兜底。
+    /// </summary>
+    public sealed class DebugSessionManager
+    {
+        private readonly Func<int, IHouseGate> _gateOf;
+        private readonly Func<DateTime> _clock;
+        private readonly int _ttlMs;
+        private readonly Action<string> _log;
+        private readonly ConcurrentDictionary<string, DebugSession> _sessions = new ConcurrentDictionary<string, DebugSession>();
+        public DebugSessionManager(Func<int, IHouseGate> gateOf, Func<DateTime> clock, int ttlMs, Action<string> log)
+        {
+            _gateOf = gateOf; _clock = clock; _ttlMs = ttlMs; _log = log ?? (_ => { });
+        }
+        public DebugCommandResult Acquire(int houseSn)
+        {
+            var gate = _gateOf(houseSn);
+            if (gate == null) return DebugCommandResult.Fail("NO_HANDLE", $"舱{houseSn}无闸门");
+            var lease = gate.Acquire(HardwareUser.OperateDebug);
+            if (lease == null) return DebugCommandResult.Fail("BUSY", $"舱{houseSn}被占用,借用超时");
+            string sid = Guid.NewGuid().ToString("N");
+            _sessions[sid] = new DebugSession(sid, houseSn, lease, _clock());
+            _log($"[debug] acquire 舱{houseSn} sid={sid}");
+            return DebugCommandResult.Okay(sid);
+        }
+        public DebugCommandResult Heartbeat(string sid)
+        {
+            if (sid != null && _sessions.TryGetValue(sid, out var s)) { s.LastSeen = _clock(); return DebugCommandResult.Okay(); }
+            return DebugCommandResult.Fail("SESSION_EXPIRED", "会话不存在或已过期");
+        }
+        public DebugCommandResult Release(string sid)
+        {
+            if (sid != null && _sessions.TryRemove(sid, out var s))
+            {
+                try { s.Lease.Dispose(); } catch { }
+                _log($"[debug] release sid={sid} 舱{s.HouseSn}");
+            }
+            return DebugCommandResult.Okay();
+        }
+        /// <summary>超时回收:LastSeen + ttl < now 的会话自动归还(spec §5.1)。</summary>
+        public int SweepExpired()
+        {
+            int n = 0; var now = _clock();
+            foreach (var kv in _sessions)
+            {
+                if ((now - kv.Value.LastSeen).TotalMilliseconds > _ttlMs)
+                {
+                    if (_sessions.TryRemove(kv.Key, out var s))
+                    {
+                        try { s.Lease.Dispose(); } catch { }
+                        _log($"[debug] 会话超时自动回收 sid={kv.Key} 舱{s.HouseSn}");
+                        n++;
+                    }
+                }
+            }
+            return n;
+        }
+    }
+}