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); }
}
}
}