using System; using System.IO; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using IvfTl.ControlHost.Debug; namespace IvfTl.ControlHost { /// /// control 进程内的本地 HTTP 小服务,只监听 127.0.0.1:port。 /// 阶段1:/ping、/status。阶段2:/status 补全(rich) + /serial/pause|resume(借串口) + /shutdown(受护栏停止)。 /// public class ControlHttpServer { private readonly int _port; private readonly Func _pingProvider; // /ping 轻量存活 private readonly Func _statusProvider; // /status 完整快照(阶段2 §6 三块) private readonly Func _shutdownHandler; // /shutdown(token 校验后安全停机) private readonly Func _serialPauseHandler; // /serial/pause(借串口:control 让路该舱) private readonly Func _serialResumeHandler; // /serial/resume(归还:恢复采集) private readonly Action _log; private readonly DebugSessionManager _debug; private HttpListener _listener; private CancellationTokenSource _cts; public ControlHttpServer( int port, Func pingProvider, Func statusProvider, Func shutdownHandler, Func serialPauseHandler, Func serialResumeHandler, Action log, DebugSessionManager debug = null) { _port = port; _pingProvider = pingProvider; _statusProvider = statusProvider; _shutdownHandler = shutdownHandler; _serialPauseHandler = serialPauseHandler; _serialResumeHandler = serialResumeHandler; _log = log ?? (_ => { }); _debug = debug; } public void Start() { _listener = new HttpListener(); // 仅本机回环,拒绝外部访问(防外部调停机/借串口)。 _listener.Prefixes.Add($"http://127.0.0.1:{_port}/"); _listener.Start(); _cts = new CancellationTokenSource(); _log($"ControlHttpServer 监听 http://127.0.0.1:{_port}/"); Task.Run(() => Loop(_cts.Token)); } private async Task Loop(CancellationToken token) { while (!token.IsCancellationRequested) { HttpListenerContext ctx; try { ctx = await _listener.GetContextAsync(); } catch (Exception ex) { if (!token.IsCancellationRequested) _log("HttpListener 异常:" + ex.Message); break; } try { Handle(ctx); } catch (Exception ex) { _log("处理请求异常:" + ex.Message); } } } private void Handle(HttpListenerContext ctx) { string path = ctx.Request.Url.AbsolutePath.TrimEnd('/').ToLowerInvariant(); string method = ctx.Request.HttpMethod.ToUpperInvariant(); string body; int code = 200; switch (path) { case "/ping": body = JsonConvert.SerializeObject(_pingProvider()); break; case "/status": body = JsonConvert.SerializeObject(_statusProvider()); break; case "/shutdown": if (method != "POST") { code = 405; body = Err("method not allowed"); break; } { string token = ReadField(ctx, "token"); bool ok = _shutdownHandler != null && _shutdownHandler(token ?? ""); code = ok ? 200 : 403; body = "{\"ok\":" + (ok ? "true" : "false") + (ok ? "" : ",\"error\":\"token invalid\"") + "}"; } break; case "/serial/pause": case "/serial/resume": if (method != "POST") { code = 405; body = Err("method not allowed"); break; } { int houseSn = ReadIntField(ctx, "houseSn"); bool isPause = path == "/serial/pause"; var handler = isPause ? _serialPauseHandler : _serialResumeHandler; bool ok = handler != null && houseSn > 0 && handler(houseSn); code = ok ? 200 : 400; body = "{\"ok\":" + (ok ? "true" : "false") + ",\"houseSn\":" + houseSn + (ok ? "" : ",\"error\":\"bad houseSn or handler\"") + "}"; } break; case "/debug/acquire": if (method != "POST") { code = 405; body = Err("method not allowed"); break; } { int houseSn = ReadIntField(ctx, "houseSn"); var r = _debug != null ? _debug.Acquire(houseSn) : DebugCommandResult.Fail("NO_HANDLE", "debug 未装配"); code = r.Ok ? 200 : 409; body = JsonConvert.SerializeObject(r); } break; case "/debug/heartbeat": if (method != "POST") { code = 405; body = Err("method not allowed"); break; } { var r = _debug != null ? _debug.Heartbeat(ReadField(ctx, "sessionId")) : DebugCommandResult.Fail("SESSION_EXPIRED", "debug 未装配"); code = r.Ok ? 200 : 410; body = JsonConvert.SerializeObject(r); } break; case "/debug/release": if (method != "POST") { code = 405; body = Err("method not allowed"); break; } { var r = _debug != null ? _debug.Release(ReadField(ctx, "sessionId")) : DebugCommandResult.Okay(); code = 200; body = JsonConvert.SerializeObject(r); } break; case "/debug/command": if (method != "POST") { code = 405; body = Err("method not allowed"); break; } { var jo = ReadBody(ctx); string sid = jo?["sessionId"]?.ToString(); string op = jo?["op"]?.ToString(); var argsObj = jo?["args"] as Newtonsoft.Json.Linq.JObject; var r = _debug != null ? _debug.Execute(sid, op, argsObj) : DebugCommandResult.Fail("SESSION_EXPIRED", "debug 未装配"); code = r.Ok ? 200 : (r.Code == "SESSION_EXPIRED" ? 410 : (r.Code == "OUT_OF_RANGE" ? 400 : 200)); body = JsonConvert.SerializeObject(r); } break; case "/debug/preview/stream": if (method != "GET") { code = 405; body = Err("method not allowed"); break; } { string sid = ctx.Request.QueryString["sessionId"]; if (_debug == null || sid == null || !_debug.TryGet(sid, out var session)) { code = 404; body = Err("session not found"); break; // 走统一收尾返回 404 } // 校验通过:分流起后台推流线程,不走统一收尾(那会 Close 流终止推流)。 StartPreviewStream(ctx, session); return; } default: code = 404; body = Err("not found"); break; } byte[] buf = Encoding.UTF8.GetBytes(body); ctx.Response.StatusCode = code; ctx.Response.ContentType = "application/json"; ctx.Response.ContentLength64 = buf.Length; ctx.Response.OutputStream.Write(buf, 0, buf.Length); ctx.Response.OutputStream.Close(); } private static string Err(string msg) => "{\"ok\":false,\"error\":\"" + msg + "\"}"; /// 读 POST JSON body 的某字符串字段(失败返回 null)。 private string ReadField(HttpListenerContext ctx, string field) { try { using (var sr = new StreamReader(ctx.Request.InputStream, ctx.Request.ContentEncoding ?? Encoding.UTF8)) { string raw = sr.ReadToEnd(); if (string.IsNullOrEmpty(raw)) return null; var jo = JObject.Parse(raw); return jo[field]?.ToString(); } } catch (Exception ex) { _log("解析请求体异常:" + ex.Message); return null; } } private int ReadIntField(HttpListenerContext ctx, string field) { string s = ReadField(ctx, field); return int.TryParse(s, out int v) ? v : -1; } /// 把 POST body 整体解析为 JObject(失败返回 null)。/debug/command 多字段用。 private Newtonsoft.Json.Linq.JObject ReadBody(HttpListenerContext ctx) { try { using (var sr = new StreamReader(ctx.Request.InputStream, ctx.Request.ContentEncoding ?? Encoding.UTF8)) { string raw = sr.ReadToEnd(); return string.IsNullOrEmpty(raw) ? null : Newtonsoft.Json.Linq.JObject.Parse(raw); } } catch (Exception ex) { _log("解析 body 异常:" + ex.Message); return null; } } /// /// 推流分支:起专用后台线程,抓帧→JPEG→multipart 持续写。 /// HttpListener 工作线程立即返回(本方法起线程后即返回),不被推流阻塞。 /// 任何退出路径都标记 session.StreamBroken,会话靠心跳 TTL 看门狗最终回收(spec §7)。 /// private void StartPreviewStream(HttpListenerContext ctx, IvfTl.ControlHost.Debug.DebugSession session) { var resp = ctx.Response; resp.StatusCode = 200; resp.ContentType = IvfTl.ControlHost.Debug.MjpegStreamWriter.ContentType; resp.SendChunked = true; // 流式,长度未知 resp.Headers.Add("Cache-Control", "no-cache"); var t = new Thread(() => { int errCount = 0; var cam = session.Lease?.Camera; try { if (cam == null) { _log($"[debug] 推流舱{session.HouseSn} 无相机句柄,放弃"); return; } cam.SetOpMode(1); // 实时模式(0=单帧/1=实时,见 ICamera 注释) var outStream = resp.OutputStream; while (true) { // 会话已被回收(release/超时)→ 停推流 if (!_debug.TryGet(session.SessionId, out _)) { _log($"[debug] 推流舱{session.HouseSn} 会话已失效,停"); break; } try { byte[] bgr = cam.GrabStable(); // 走全进程相机锁,与采集/对焦串行 if (bgr == null) { Thread.Sleep(100); continue; } byte[] jpeg = IvfTl.ControlHost.Debug.MjpegStreamWriter.EncodeJpeg(bgr, cam.Width, cam.Height); if (jpeg == null) { Thread.Sleep(100); continue; } byte[] frame = IvfTl.ControlHost.Debug.MjpegStreamWriter.FrameBytes(jpeg); outStream.Write(frame, 0, frame.Length); outStream.Flush(); errCount = 0; Thread.Sleep(66); // ~15fps(spec §4.2) } catch (IOException) { _log($"[debug] 推流舱{session.HouseSn} 客户端断开"); break; } // operate 关预览/崩溃:正常退出 catch (HttpListenerException) { _log($"[debug] 推流舱{session.HouseSn} 连接断开"); break; } catch (Exception ex) { errCount++; _log($"[debug] 推流舱{session.HouseSn} 抓帧/编码异常({errCount}/5): {ex.Message}"); if (errCount >= 5) { _log($"[debug] 推流舱{session.HouseSn} 连续错误过多,停"); break; } Thread.Sleep(500); } } } catch (Exception ex) { _log($"[debug] 推流舱{session.HouseSn} 线程异常: {ex.Message}"); } finally { session.StreamBroken = true; // 可回收快信号;会话最终由心跳 TTL 看门狗收(不在此 Dispose,避免与命令分发/超时回收争 lease) try { resp.OutputStream.Close(); } catch { } try { resp.Close(); } catch { } _log($"[debug] 推流舱{session.HouseSn} 线程结束"); } }); t.IsBackground = true; t.Name = $"MjpegStream-h{session.HouseSn}"; t.Start(); } public void Stop() { try { _cts?.Cancel(); _listener?.Stop(); _listener?.Close(); } catch (Exception ex) { _log("ControlHttpServer 停止异常:" + ex.Message); } } } }