using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading; using System.Windows.Forms; namespace Sim.Host; /// /// 运行器的全局状态:向服务器发消息、收集控件属性变化、登记控件、等待浏览器的应答。 /// public static class SimHost { // 中文原样输出,不转义成 \uXXXX private static readonly JsonSerializerOptions JsonOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; private static readonly object WriteLock = new(); private static readonly List PendingChanges = new(); private static readonly ConcurrentDictionary ControlsById = new(); private static readonly ConcurrentDictionary Requests = new(); private static TextWriter _output = TextWriter.Null; private static int _nextControlId; private static int _nextRequestId; public static UiDispatcher Ui { get; } = new(); /// 学生程序所在的程序集,用于筛选异常堆栈中属于学生代码的部分 public static Assembly? StudentAssembly { get; set; } /// 运行器启动时调用:协议消息写到真正的标准输出(学生的 Console 输出另行转发) public static void Initialize(Stream stdout) { _output = new StreamWriter(stdout, new UTF8Encoding(false)) { AutoFlush = true }; } // ---------- 发送消息 ---------- public static void Send(JsonObject message) { lock (WriteLock) { FlushChangesLocked(); _output.WriteLine(message.ToJsonString(JsonOptions)); } } /// 记录一个已显示控件的属性变化;在界面线程上会合并到本轮消息处理结束时一起发送 public static void QueueChange(Control control, string prop, object? value) { var change = new JsonObject { ["id"] = control.SimId, ["prop"] = prop, ["value"] = PropValue.From(value) }; lock (WriteLock) { PendingChanges.Add(change); if (!Ui.IsUiThread) FlushChangesLocked(); } } public static void FlushChanges() { lock (WriteLock) FlushChangesLocked(); } private static void FlushChangesLocked() { if (PendingChanges.Count == 0) return; var changes = new JsonArray(); foreach (var change in PendingChanges) changes.Add(change); PendingChanges.Clear(); _output.WriteLine(new JsonObject { ["type"] = "patch", ["changes"] = changes }.ToJsonString(JsonOptions)); } // ---------- 图片 ---------- private static readonly HashSet SentImages = new(); /// /// 学生程序的"程序所在目录"(AppDomain.CurrentDomain.BaseDirectory),可写,程序结束后清掉。 /// 运行器启动时设定;保存的图片按相对这里的路径显示在网页的照片列表里。 /// public static string AppBaseDirectory { get; set; } = AppContext.BaseDirectory; /// 图片在浏览器端的编号;第一次用到时先把图片数据发过去,浏览器缓存后同一张图不再重发 public static string ImageKey(System.Drawing.Image image) => DataKey(image.SimKey, image.Data, image.MimeType); /// /// 图片、声音等数据在浏览器端的编号(key 通常是内容的哈希):第一次用到时先把数据发过去,之后只发编号。 /// public static string DataKey(string key, byte[] data, string mime) { bool isNew; lock (SentImages) isNew = SentImages.Add(key); if (isNew) { Send(new JsonObject { ["type"] = "blob", ["key"] = key, ["mime"] = mime, ["data"] = Convert.ToBase64String(data), }); } return key; } /// 学生程序用 Image.Save 保存了图片:网页的照片列表里显示出来 public static void ImageSaved(string fullPath, System.Drawing.Image image) { var path = fullPath.StartsWith(AppBaseDirectory, StringComparison.Ordinal) ? fullPath[AppBaseDirectory.Length..] : fullPath; Send(new JsonObject { ["type"] = "imageSaved", ["path"] = path, ["image"] = ImageKey(image) }); } public static void SendConsole(string text) => Send(new JsonObject { ["type"] = "console", ["text"] = text }); public static void SendDevice(string device, string action, object? data) { LegacyThreads.CheckPoint(); Send(new JsonObject { ["type"] = "device", ["device"] = device, ["action"] = action, ["data"] = PropValue.From(data), }); } // ---------- 异常 ---------- public static void ReportException(Exception ex) { while (ex is TargetInvocationException { InnerException: not null } wrapper) ex = wrapper.InnerException!; var type = ex.GetType().FullName; // 仿真库自己抛的异常信息已经是中文;.NET 自带的是英文,常见的几种配上中文说明,原文放在括号里 var message = HasChinese(ex.Message) ? $"{ex.Message}({type})" : ExplainException(ex) is { } explained ? $"{explained}({type}: {ex.Message})" : $"{type}: {ex.Message}"; var frame = StudentFrame(ex); var result = new JsonObject { ["type"] = "exception", ["message"] = message, ["detail"] = frame is { } f ? $"出错位置:{f.File} 第 {f.Line} 行({f.Method} 方法)" : StudentStackTrace(ex), }; if (frame is { } at) { result["file"] = at.File; result["line"] = at.Line; } Send(result); } private static bool HasChinese(string text) => text.Any(c => c >= '\u4e00' && c <= '\u9fff'); private static string? ExplainException(Exception ex) => ex switch { FormatException => "文字转换成数字或日期时格式不对,比如文本框是空的,或者里面不是数字", NullReferenceException => "用到的对象是 null:变量还没有赋值,或者对象、窗体还没有 new 出来", IndexOutOfRangeException => "数组的下标超出了范围:下标从 0 开始,最大是长度减 1", ArgumentOutOfRangeException => "下标或参数超出了范围,比如列表、字符串的下标太大,或者下拉框还没有选中", DivideByZeroException => "整数除以了 0", OverflowException => "数字太大或太小,超出了这个类型能表示的范围", InvalidCastException => "类型转换失败,这个对象不能转换成要求的类型", KeyNotFoundException => "字典里没有这个键", FileNotFoundException => "找不到这个文件", DirectoryNotFoundException => "找不到这个文件夹", _ => null, }; /// 堆栈里离出错点最近的一行学生代码:文件名、行号、方法名 private static (string File, int Line, string Method)? StudentFrame(Exception ex) { foreach (var frame in new System.Diagnostics.StackTrace(ex, true).GetFrames()) { var file = frame.GetFileName(); var line = frame.GetFileLineNumber(); if (file == null || line <= 0 || file.Contains("/runtime/Sim.")) continue; return (Path.GetFileName(file), line, frame.GetMethod()?.Name ?? ""); } return null; } /// 只保留学生代码的堆栈行,去掉仿真库内部的调用 private static string StudentStackTrace(Exception ex) { var lines = (ex.StackTrace ?? "").Split('\n').Select(l => l.TrimEnd()); var studentLines = lines.Where(l => l.Contains(".cs:") && !l.Contains("/runtime/Sim.")).ToList(); return string.Join("\n", studentLines.Count > 0 ? studentLines : lines); } // ---------- 控件登记 ---------- public static string NewControlId() => Interlocked.Increment(ref _nextControlId).ToString(); public static void Register(Control control) => ControlsById[control.SimId] = control; public static Control? FindControl(string id) => ControlsById.TryGetValue(id, out var control) ? control : null; // ---------- 请求与应答(如 MessageBox 等待用户点按钮) ---------- /// /// 向浏览器发出请求并阻塞等待应答。在界面线程上调用时会继续处理消息队列(和真实的模态对话框一样,Timer 照常触发)。 /// public static JsonNode? Request(JsonObject message) { LegacyThreads.CheckPoint(); var id = Interlocked.Increment(ref _nextRequestId); var pending = new PendingRequest(); Requests[id] = pending; message["requestId"] = id; Send(message); if (Ui.IsUiThread) Ui.RunUntil(() => pending.Done.IsSet); else pending.Done.Wait(); Requests.TryRemove(id, out _); return pending.Result; } public static void CompleteRequest(int id, JsonNode? result) { if (!Requests.TryGetValue(id, out var pending)) return; pending.Result = result; pending.Done.Set(); Ui.Wake(); } private sealed class PendingRequest { public ManualResetEventSlim Done { get; } = new(); public JsonNode? Result { get; set; } } }