using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using TrajectoryPlanningVisualization; namespace TrajectoryPlanningVisualizationVerificationHost; internal static class ServerChecks { public static void Run() { RejectsUnauthorizedAndMalformedRequests(); LimitsSseClientsAndKeepsPublishNonBlocking(); CollectsHistoryWithoutClientsAndStopsCleanly(); } private static void RejectsUnauthorizedAndMalformedRequests() { int port; using (var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 20 })) { session.Publish(DynamicSnapshot(0)); PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot()); Verification.Equal(info.Token, session.Start(StaticSnapshot()).Token, "repeated start is idempotent"); port = info.Uri.Port; Verification.Equal("127.0.0.1", info.Uri.Host, "server binds loopback"); Verification.Equal(64, info.Token.Length, "session token is 256 bits of hex"); Verification.Equal(403, SendStatus(port, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"), "missing token is forbidden"); Verification.Equal(404, SendStatus(port, Get("/missing?token=" + info.Token)), "unknown authorized route is not found"); Verification.Equal(405, SendStatus(port, "POST /api/bootstrap?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\n\r\n"), "non-GET request is rejected"); Verification.Equal(400, SendStatus(port, "GET /api/bootstrap HTTP/1.0\r\nHost: localhost\r\n\r\n"), "non-HTTP/1.1 request is malformed"); Verification.Equal(400, SendStatus(port, "GET /api/bootstrap?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\nX-Large: " + new string('a', 16 * 1024) + "\r\n\r\n"), "oversized request header is rejected"); HttpResponse bootstrap = SendResponse(port, Get("/api/bootstrap?token=" + info.Token)); Verification.Equal(200, bootstrap.Status, "authorized bootstrap succeeds"); Verification.Equal(bootstrap.ContentLength, Encoding.UTF8.GetByteCount(bootstrap.Body), "normal response has byte-accurate content length"); Verification.True(bootstrap.Body.Contains("sessionNameChinese"), "bootstrap contains static snapshot"); session.Stop(); session.Stop(); session.Publish(DynamicSnapshot(99)); } Verification.True(CanBindReleasedPort(port), "stop releases loopback port"); } private static void LimitsSseClientsAndKeepsPublishNonBlocking() { using var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 100 }); PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot()); using TcpClient first = OpenSseClient(info); using TcpClient second = OpenSseClient(info); Verification.Equal(503, SendStatus(info.Uri.Port, Get("/api/events?token=" + info.Token)), "third SSE client is rejected"); Task publisher = Task.Run(() => { for (int i = 1; i <= 10000; i++) { session.Publish(DynamicSnapshot(i)); } }); Verification.True(publisher.Wait(TimeSpan.FromSeconds(1)), "slow SSE client does not block Publish"); session.Stop(); } private static void CollectsHistoryWithoutClientsAndStopsCleanly() { int port; using (var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 20 })) { PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot()); port = info.Uri.Port; for (int i = 1; i <= 3; i++) { session.Publish(DynamicSnapshot(i)); Thread.Sleep(120); } using TcpClient client = OpenSseClient(info); string frame = ReadUntil(client.GetStream(), "\n\n", TimeSpan.FromSeconds(2)); Verification.True(frame.Contains("cycleVersion\":1") && frame.Contains("cycleVersion\":2") && frame.Contains("cycleVersion\":3"), "dispatcher collects bounded history without browsers"); Stopwatch stopWatch = Stopwatch.StartNew(); session.Stop(); Verification.True(stopWatch.Elapsed <= TimeSpan.FromSeconds(1), "normal stop is bounded"); string terminal = ReadUntil(client.GetStream(), "\n\n", TimeSpan.FromSeconds(1)); Verification.True(terminal.Contains("event: end") && terminal.Contains("会话已结束"), "normal stop offers a terminal SSE event"); } Verification.True(CanBindReleasedPort(port), "port can be rebound after normal stop"); } private static TcpClient OpenSseClient(PlanningVisualizationSessionInfo info) { var client = new TcpClient(); client.Connect(IPAddress.Loopback, info.Uri.Port); byte[] request = Encoding.ASCII.GetBytes(Get("/api/events?token=" + info.Token)); NetworkStream stream = client.GetStream(); stream.Write(request, 0, request.Length); string headers = ReadUntil(stream, "\r\n\r\n", TimeSpan.FromSeconds(2)); Verification.True(headers.StartsWith("HTTP/1.1 200", StringComparison.Ordinal), "authorized SSE succeeds"); return client; } private static int SendStatus(int port, string request) { return SendResponse(port, request).Status; } private static HttpResponse SendResponse(int port, string request) { using var client = new TcpClient(); client.Connect(IPAddress.Loopback, port); NetworkStream stream = client.GetStream(); byte[] bytes = Encoding.ASCII.GetBytes(request); stream.Write(bytes, 0, bytes.Length); string headers = ReadUntil(stream, "\r\n\r\n", TimeSpan.FromSeconds(2)); string[] lines = headers.Split(new[] { "\r\n" }, StringSplitOptions.None); string[] statusParts = lines[0].Split(' '); int status = int.Parse(statusParts[1]); int contentLength = 0; foreach (string line in lines) { if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) { contentLength = int.Parse(line.Substring("Content-Length:".Length).Trim()); } } byte[] body = ReadExactly(stream, contentLength, TimeSpan.FromSeconds(2)); return new HttpResponse(status, contentLength, Encoding.UTF8.GetString(body)); } private static string Get(string target) { return "GET " + target + " HTTP/1.1\r\nHost: localhost\r\n\r\n"; } private static string ReadUntil(NetworkStream stream, string delimiter, TimeSpan timeout) { var bytes = new List(); byte[] delimiterBytes = Encoding.ASCII.GetBytes(delimiter); stream.ReadTimeout = (int)timeout.TotalMilliseconds; while (true) { int next = stream.ReadByte(); if (next < 0) { throw new InvalidOperationException("connection ended before expected response delimiter"); } bytes.Add((byte)next); if (bytes.Count >= delimiterBytes.Length) { bool found = true; for (int index = 0; index < delimiterBytes.Length; index++) { if (bytes[bytes.Count - delimiterBytes.Length + index] != delimiterBytes[index]) { found = false; break; } } if (found) { return Encoding.UTF8.GetString(bytes.ToArray()); } } } } private static byte[] ReadExactly(NetworkStream stream, int count, TimeSpan timeout) { byte[] result = new byte[count]; stream.ReadTimeout = (int)timeout.TotalMilliseconds; int offset = 0; while (offset < count) { int read = stream.Read(result, offset, count - offset); if (read == 0) { throw new InvalidOperationException("connection ended before response body"); } offset += read; } return result; } private static bool CanBindReleasedPort(int port) { try { using var listener = new TcpListener(IPAddress.Loopback, port); listener.Start(); return true; } catch (SocketException) { return false; } } private static PlanningVisualizationStaticSnapshot StaticSnapshot() { return new PlanningVisualizationStaticSnapshot( "验证会话", new VisualizationBounds(0d, 1d, 0d, 1d), null, Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty()); } private static PlanningVisualizationDynamicSnapshot DynamicSnapshot(long sequence) { return new PlanningVisualizationDynamicSnapshot( sequence, DateTimeOffset.UtcNow, "运行中", 0, "forward", new VisualizationPose(0d, 0d, 0d), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), new VisualizationCycleSummary(sequence, DateTimeOffset.UtcNow, "成功", true, 1d, 0, "forward", "rolling", "none", null, null, "")); } private sealed class HttpResponse { public HttpResponse(int status, int contentLength, string body) { Status = status; ContentLength = contentLength; Body = body; } public int Status { get; } public int ContentLength { get; } public string Body { get; } } }