using System;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace ivf_tl_Operate.Debug
{
///
/// operate 端调试会话客户端:封 acquire/command/release/heartbeat 的本地 HTTP + 心跳定时器 + 会话失效回调。
/// 一次会话:AcquireAsync 成功起心跳,ReleaseAsync 停心跳。
/// 任一请求返回 code=SESSION_EXPIRED(control 端 TTL 已回收该会话)→ 触发 OnSessionExpired(UI 据此提示"请重新进入调试")并停心跳。
/// 心跳是必需的:control 端 10s TTL 看门狗收不到心跳会自动回收会话、把借用的舱还回去恢复采集。
///
public sealed class DebugSessionClient : IDisposable
{
private readonly string _baseUrl;
private readonly HttpClient _http;
private readonly bool _ownsHttp;
private string _sessionId;
private Timer _heartbeatTimer;
private int _expiredFired; // 0=未触发 1=已触发(Interlocked 闸,保证 OnSessionExpired 只触发一次)
/// 心跳间隔(ms),默认 2.5s(远小于 control 端 10s TTL,容忍偶发卡顿)。
public int HeartbeatIntervalMs { get; set; } = 2500;
/// 会话失效回调(收到 SESSION_EXPIRED 时触发一次)。
public Action OnSessionExpired { get; set; }
public string SessionId => _sessionId;
public DebugSessionClient(string baseUrl, HttpClient http = null)
{
_baseUrl = baseUrl.TrimEnd('/');
_ownsHttp = http == null; // 自建的由本类 Dispose;注入的归调用方
_http = http ?? new HttpClient();
}
private async Task PostAsync(string path, object body)
{
var content = new StringContent(body == null ? "{}" : JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
var resp = await _http.PostAsync($"{_baseUrl}{path}", content);
string s = await resp.Content.ReadAsStringAsync();
var r = JsonConvert.DeserializeObject(s) ?? new AcquireResult { Ok = false, Error = "空响应" };
// Interlocked 闸:并发请求同收 SESSION_EXPIRED 时,回调只触发一次。
if (r.Code == "SESSION_EXPIRED" && Interlocked.CompareExchange(ref _expiredFired, 1, 0) == 0)
{
StopHeartbeat();
try { OnSessionExpired?.Invoke(); } catch { }
}
return r;
}
/// 借用某舱。成功则记录 sessionId 并起心跳。返回体含 cultivating/embryoCount(供确认框)。
public async Task AcquireAsync(int houseSn)
{
var r = await PostAsync("/debug/acquire", new { houseSn });
if (r.Ok && !string.IsNullOrEmpty(r.SessionId))
{
_sessionId = r.SessionId;
Interlocked.Exchange(ref _expiredFired, 0);
StartHeartbeat();
}
return r;
}
/// 在当前会话上执行一个操作(电机/读数/阀门等)。
public Task CommandAsync(string op, object args)
=> PostAsync("/debug/command", new { sessionId = _sessionId, op, args });
/// 归还会话(幂等):停心跳、清 sessionId、通知 control。多次调用不抛。
public async Task ReleaseAsync()
{
// 主动归还:抢先置闸,挡掉 release 与"在途心跳"的竞态——
// 在途心跳若晚于 release 到达 control,会话已删会回 SESSION_EXPIRED,
// 不置闸就会在用户正常关调试时误弹"会话已失效"框。
Interlocked.Exchange(ref _expiredFired, 1);
StopHeartbeat();
var sid = _sessionId;
_sessionId = null;
if (sid != null) { try { await PostAsync("/debug/release", new { sessionId = sid }); } catch { } }
}
private void StartHeartbeat()
{
StopHeartbeat();
_heartbeatTimer = new Timer(async _ =>
{
var sid = _sessionId;
if (sid == null) return;
try { await PostAsync("/debug/heartbeat", new { sessionId = sid }); } catch { }
}, null, HeartbeatIntervalMs, HeartbeatIntervalMs);
}
private void StopHeartbeat()
{
try { _heartbeatTimer?.Dispose(); } catch { }
_heartbeatTimer = null;
}
public void Dispose()
{
StopHeartbeat();
if (_ownsHttp) { try { _http?.Dispose(); } catch { } } // 只释放自建的;注入的归调用方
}
}
}