Files
xuyueandClaude Opus 5.5 f6024dc113
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
试用前走查的修改:报错看得见、学生找得到作业、运行区和设计器的若干体验问题
- 运行区:编译错误、运行异常、控制台输出和保存的图片移到窗体下面,不再被实训台挤成 0 高;缩放和放大按钮移到桌面上方,不压窗体的关闭按钮;放大后也能停止程序;点停止后清掉窗体
- 运行异常:常见 .NET 异常配中文说明,显示出错的文件、行号和方法,点击跳到代码
- 运行中的下拉列表挂到页面最外层,能伸出窗体和分组框(小窗体也能选到 COM4、COM5)
- 学生登录后进"我的作业";任务面板从工具栏下面开始,不挡导航;记住上次做的实验,从数据库页面返回、恢复数据时默认用它
- 代码页和设计页左栏都占 3/5,文件标签放不下时换行;实验选择框加宽;关闭粘性滚动
- 设计器:双击生成的事件方法带缩进;双击工具箱放进选中控件所在的容器并错开位置;新控件比容器宽时按比例缩小;无边框窗体不画标题栏
- 中文界面:代码编辑器(查找替换等)、属性窗口的颜色选项
- SQL:DATEADD、DATEDIFF、DATEPART 的日期单位(day、hour)解析成 IdentifierLiteral 时报"不支持",补上
- 项目 8 的预置环境数据改成建库那天之前的 7 天;p6-t3 评分项改为"按日期查询环境数据"
- 没有 AI 建议时不显示"全部采纳 AI 建议"和相关说明;导出 CSV 只给纯数字的文字加制表符

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 08:49:42 +08:00

236 lines
9.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 运行器的全局状态:向服务器发消息、收集控件属性变化、登记控件、等待浏览器的应答。
/// </summary>
public static class SimHost
{
// 中文原样输出,不转义成 \uXXXX
private static readonly JsonSerializerOptions JsonOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
private static readonly object WriteLock = new();
private static readonly List<JsonObject> PendingChanges = new();
private static readonly ConcurrentDictionary<string, Control> ControlsById = new();
private static readonly ConcurrentDictionary<int, PendingRequest> Requests = new();
private static TextWriter _output = TextWriter.Null;
private static int _nextControlId;
private static int _nextRequestId;
public static UiDispatcher Ui { get; } = new();
/// <summary>学生程序所在的程序集,用于筛选异常堆栈中属于学生代码的部分</summary>
public static Assembly? StudentAssembly { get; set; }
/// <summary>运行器启动时调用:协议消息写到真正的标准输出(学生的 Console 输出另行转发)</summary>
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));
}
}
/// <summary>记录一个已显示控件的属性变化;在界面线程上会合并到本轮消息处理结束时一起发送</summary>
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<string> SentImages = new();
/// <summary>
/// 学生程序的"程序所在目录"(AppDomain.CurrentDomain.BaseDirectory),可写,程序结束后清掉。
/// 运行器启动时设定;保存的图片按相对这里的路径显示在网页的照片列表里。
/// </summary>
public static string AppBaseDirectory { get; set; } = AppContext.BaseDirectory;
/// <summary>图片在浏览器端的编号;第一次用到时先把图片数据发过去,浏览器缓存后同一张图不再重发</summary>
public static string ImageKey(System.Drawing.Image image) => DataKey(image.SimKey, image.Data, image.MimeType);
/// <summary>
/// 图片、声音等数据在浏览器端的编号(key 通常是内容的哈希):第一次用到时先把数据发过去,之后只发编号。
/// </summary>
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;
}
/// <summary>学生程序用 Image.Save 保存了图片:网页的照片列表里显示出来</summary>
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,
};
/// <summary>堆栈里离出错点最近的一行学生代码:文件名、行号、方法名</summary>
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;
}
/// <summary>只保留学生代码的堆栈行,去掉仿真库内部的调用</summary>
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 等待用户点按钮) ----------
/// <summary>
/// 向浏览器发出请求并阻塞等待应答。在界面线程上调用时会继续处理消息队列(和真实的模态对话框一样,Timer 照常触发)。
/// </summary>
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; }
}
}