第 0 阶段:教材项目 2 任务 1 在网页中原样运行
- runtime:仿真 WinForms 子集(Form、Button、Label、TextBox、ComboBox、 GroupBox、Timer、MessageBox、界面线程消息循环、跨线程检查)、 教材设备类库(DeviceCtrlHelper、LED 控件)、运行器(常驻编译服务 + 运行模式) - server:Bun + Hono,WebSocket 会话,每次运行一个进程,消息转发 - web:Vue + Naive UI,WinForms 风格窗体渲染、MessageBox、实训台 LED 屏 - 端到端测试与 100 个程序并发的压力测试 - 教材逐任务摘要(docs/教材摘要) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
98be8331bc
71 files changed
+7919
No files matched your search
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Sim.Host;
|
||||
|
||||
/// <summary>把控件属性值转换成发给浏览器的 JSON(格式见 web/src/winforms/format.ts)</summary>
|
||||
public static class PropValue
|
||||
{
|
||||
public static JsonNode? From(object? value) => value switch
|
||||
{
|
||||
null => null,
|
||||
string s => JsonValueOf(s),
|
||||
bool b => JsonValueOf(b),
|
||||
char c => JsonValueOf(c == '\0' ? "" : c.ToString()),
|
||||
int or long or short or byte => JsonValueOf(Convert.ToInt64(value, CultureInfo.InvariantCulture)),
|
||||
float or double or decimal => JsonValueOf(Convert.ToDouble(value, CultureInfo.InvariantCulture)),
|
||||
Enum e => JsonValueOf(e.ToString()),
|
||||
Point p => new JsonObject { ["x"] = p.X, ["y"] = p.Y },
|
||||
Size s => new JsonObject { ["width"] = s.Width, ["height"] = s.Height },
|
||||
Color c => ColorCss(c),
|
||||
Font f => new JsonObject
|
||||
{
|
||||
["family"] = f.Name,
|
||||
["size"] = f.SizeInPoints,
|
||||
["bold"] = f.Bold,
|
||||
["italic"] = f.Italic,
|
||||
["underline"] = f.Underline,
|
||||
["strikeout"] = f.Strikeout,
|
||||
},
|
||||
Padding p => new JsonObject { ["left"] = p.Left, ["top"] = p.Top, ["right"] = p.Right, ["bottom"] = p.Bottom },
|
||||
IEnumerable list => ListOf(list),
|
||||
_ => JsonValueOf(value.ToString() ?? ""),
|
||||
};
|
||||
|
||||
/// <summary>未设置的颜色(Color.Empty)返回 null,表示沿用父控件的颜色</summary>
|
||||
public static JsonNode? ColorCss(Color color)
|
||||
{
|
||||
if (color.IsEmpty) return null;
|
||||
if (color.A == 0) return "transparent";
|
||||
if (color.A < 255)
|
||||
return $"rgba({color.R},{color.G},{color.B},{(color.A / 255.0).ToString("0.###", CultureInfo.InvariantCulture)})";
|
||||
return $"#{color.R:x2}{color.G:x2}{color.B:x2}";
|
||||
}
|
||||
|
||||
private static JsonArray ListOf(IEnumerable items)
|
||||
{
|
||||
var array = new JsonArray();
|
||||
foreach (var item in items) array.Add(From(item is string or null ? item : item.ToString()));
|
||||
return array;
|
||||
}
|
||||
|
||||
private static JsonNode JsonValueOf<T>(T value) => JsonValue.Create(value)!;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 运行器与服务器之间的消息格式,对应 shared/protocol.ts,两边修改时要同步。
|
||||
// 运行器 → 服务器的消息在各处用 JsonObject 直接构造;这里只集中定义从服务器收到的动作。
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Sim.Host;
|
||||
|
||||
/// <summary>浏览器上报的用户操作(shared/protocol.ts 中的 UserAction)</summary>
|
||||
public sealed class UserAction
|
||||
{
|
||||
public required string Action { get; init; }
|
||||
public string? Value { get; init; }
|
||||
public int Index { get; init; }
|
||||
public string Button { get; init; } = "left";
|
||||
public int X { get; init; }
|
||||
public int Y { get; init; }
|
||||
|
||||
public static UserAction FromJson(JsonObject json) => new()
|
||||
{
|
||||
Action = (string?)json["action"] ?? "",
|
||||
Value = (string?)json["value"],
|
||||
Index = (int?)json["index"] ?? -1,
|
||||
Button = (string?)json["button"] ?? "left",
|
||||
X = (int?)json["x"] ?? 0,
|
||||
Y = (int?)json["y"] ?? 0,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
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));
|
||||
}
|
||||
|
||||
public static void SendConsole(string text) => Send(new JsonObject { ["type"] = "console", ["text"] = text });
|
||||
|
||||
public static void SendDevice(string device, string action, object? data) => 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!;
|
||||
Send(new JsonObject
|
||||
{
|
||||
["type"] = "exception",
|
||||
["message"] = $"{ex.GetType().FullName}: {ex.Message}",
|
||||
["detail"] = StudentStackTrace(ex),
|
||||
});
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace Sim.Host;
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 WinForms 的界面线程消息循环:所有用户操作、Timer.Tick、Invoke 都排队到界面线程上依次执行。
|
||||
/// </summary>
|
||||
public sealed class UiDispatcher
|
||||
{
|
||||
private readonly BlockingCollection<Action> _queue = new();
|
||||
|
||||
public Thread? UiThread { get; private set; }
|
||||
|
||||
public bool IsUiThread => Thread.CurrentThread == UiThread;
|
||||
|
||||
/// <summary>由运行器在启动学生程序的线程上调用一次</summary>
|
||||
public void BindToCurrentThread() => UiThread = Thread.CurrentThread;
|
||||
|
||||
public void Post(Action action) => _queue.Add(action);
|
||||
|
||||
/// <summary>在界面线程上同步执行(Control.Invoke、SynchronizationContext.Send)</summary>
|
||||
public void Send(Action action)
|
||||
{
|
||||
if (IsUiThread)
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
using var done = new ManualResetEventSlim();
|
||||
Exception? error = null;
|
||||
Post(() =>
|
||||
{
|
||||
try { action(); }
|
||||
catch (Exception ex) { error = ex; }
|
||||
finally { done.Set(); }
|
||||
});
|
||||
done.Wait();
|
||||
if (error != null) throw new System.Reflection.TargetInvocationException(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行消息循环,直到 <paramref name="done"/> 为真。可以嵌套:
|
||||
/// Application.Run、ShowDialog、MessageBox.Show 都会在这里等待,期间 Timer 等照常执行。
|
||||
/// </summary>
|
||||
public void RunUntil(Func<bool> done)
|
||||
{
|
||||
while (!done())
|
||||
{
|
||||
if (_queue.TryTake(out var action, 50)) Execute(action);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>处理队列里已有的消息后返回(Application.DoEvents)</summary>
|
||||
public void DoEvents()
|
||||
{
|
||||
while (_queue.TryTake(out var action)) Execute(action);
|
||||
}
|
||||
|
||||
/// <summary>唤醒等待中的消息循环,让它重新检查结束条件</summary>
|
||||
public void Wake() => Post(static () => { });
|
||||
|
||||
private static void Execute(Action action)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 真实 WinForms 会弹出"未经处理的异常"对话框,点"继续"后程序接着运行,这里同样继续
|
||||
SimHost.ReportException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SimHost.FlushChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user