- 虚拟时钟:实训台时钟面板可拨时间、1×/10×/60× 倍速、恢复真实时间;编译时把 DateTime.Now / Today / UtcNow 改写成 SimClock;评分规则加 clock 步骤 - 项目资源(Properties.Resources):平台素材库(自绘开关图片、合成门铃声),"项目资源"对话框勾选, 平台生成 Properties/Resources.Designer.cs;设计器图片框的"图片"属性可选资源;程序目录下的 Resources 指向素材库;未添加资源时给中文提示 - Windows Media Player(AxWMPLib):设置 URL 自动播放、Ctlcontrols.play/stop,浏览器里播放声音, 实训台加广播喇叭;编译时给窗体嵌空资源,OcxState 取到 null 不报错;图片和声音统一按内容缓存(blob) - TrackBar:拖动、点轨道、方向键,先 ValueChanged 再 Scroll;设计器加"滑动条""媒体播放器" - 评分规则加 value、media 检查;示例项目 7 任务 1–3(修正书上开关灯判断写反、漏填串口列表) - 修正:Properties/ 下的资源类不当作窗体;带命名空间的 System.AppDomain.CurrentDomain.BaseDirectory 放行 - 测试:虚拟时钟、TrackBar、项目 7 示例评分、作业起点带资源类 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152 lines
6.1 KiB
C#
152 lines
6.1 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Runtime.Loader;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using System.Threading;
|
|
using System.Windows.Forms;
|
|
using Sim.Host;
|
|
|
|
namespace Sim.Runner;
|
|
|
|
/// <summary>
|
|
/// 运行一个已编译的学生程序:在模拟的界面线程上执行 Main,主线程从标准输入读取浏览器的操作。
|
|
/// 这个类不能引用 Roslyn 的任何类型,否则运行模式也会加载编译器。
|
|
/// </summary>
|
|
internal static class StudentProgramHost
|
|
{
|
|
public static int Run(string dir)
|
|
{
|
|
var processStart = Process.GetCurrentProcess().StartTime;
|
|
SimHost.Initialize(Console.OpenStandardOutput());
|
|
var console = new ConsoleForwarder();
|
|
Console.SetOut(console);
|
|
Console.SetIn(TextReader.Null); // 标准输入用于协议,学生的 Console.ReadLine 读不到内容
|
|
|
|
// 和中文 Windows 一致:DateTime.ToString() 等按 zh-CN 格式输出。
|
|
// Linux 上的区域数据把时间写成 09:05:03,中文 Windows 是 9:05:03(书上的截图也是),这里改成 Windows 的写法
|
|
var zhCN = (CultureInfo)CultureInfo.GetCultureInfo("zh-CN").Clone();
|
|
zhCN.DateTimeFormat.LongTimePattern = "H:mm:ss";
|
|
zhCN.DateTimeFormat.ShortTimePattern = "H:mm";
|
|
zhCN = CultureInfo.ReadOnly(zhCN);
|
|
CultureInfo.DefaultThreadCurrentCulture = zhCN;
|
|
CultureInfo.DefaultThreadCurrentUICulture = zhCN;
|
|
CultureInfo.CurrentCulture = zhCN;
|
|
CultureInfo.CurrentUICulture = zhCN;
|
|
|
|
UseWritableBaseDirectory(dir);
|
|
|
|
var assembly = AssemblyLoadContext.Default.LoadFromStream(
|
|
new MemoryStream(File.ReadAllBytes(Path.Combine(dir, CompileServer.AssemblyFile))),
|
|
new MemoryStream(File.ReadAllBytes(Path.Combine(dir, CompileServer.PdbFile))));
|
|
SimHost.StudentAssembly = assembly;
|
|
var entryPoint = assembly.EntryPoint;
|
|
if (entryPoint == null)
|
|
{
|
|
SimHost.Send(new JsonObject { ["type"] = "exception", ["message"] = "程序中找不到 Main 方法", ["detail"] = "" });
|
|
return 1;
|
|
}
|
|
|
|
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
|
|
{
|
|
// 学生创建的后台线程抛出未处理异常时,和真实程序一样整个程序结束
|
|
if (e.ExceptionObject is Exception ex) SimHost.ReportException(ex);
|
|
console.Flush();
|
|
};
|
|
|
|
var uiThread = new Thread(() =>
|
|
{
|
|
SimHost.Ui.BindToCurrentThread();
|
|
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
|
|
SimHost.Send(new JsonObject { ["type"] = "started", ["ms"] = (long)(DateTime.Now - processStart).TotalMilliseconds });
|
|
try
|
|
{
|
|
var parameters = entryPoint.GetParameters().Length == 0 ? null : new object[] { Array.Empty<string>() };
|
|
entryPoint.Invoke(null, parameters);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SimHost.ReportException(ex);
|
|
}
|
|
finally
|
|
{
|
|
console.Flush();
|
|
SimHost.FlushChanges();
|
|
Environment.Exit(0);
|
|
}
|
|
}) { Name = "UI" };
|
|
uiThread.Start();
|
|
|
|
ReadActions();
|
|
// 服务器关闭了标准输入:结束运行
|
|
Environment.Exit(0);
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 学生程序的"程序所在目录":和在 VS 里按 F5 一样是工程下的 bin/Debug/,也是当前目录,可以写文件
|
|
/// (教材把摄像头照片存到 AppDomain.CurrentDomain.BaseDirectory + "Camera/")。
|
|
/// 沙箱里工程目录只读,改用沙箱自己的临时目录;两种情况下文件都在程序结束后清掉。
|
|
/// </summary>
|
|
private static void UseWritableBaseDirectory(string projectDir)
|
|
{
|
|
string baseDir;
|
|
try
|
|
{
|
|
baseDir = Directory.CreateDirectory(Path.Combine(projectDir, "bin", "Debug")).FullName;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
baseDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "StudentApp", "bin", "Debug")).FullName;
|
|
}
|
|
baseDir = Path.TrimEndingDirectorySeparator(baseDir) + Path.DirectorySeparatorChar;
|
|
// 程序目录下的 Resources 文件夹指向素材库:教材用 BaseDirectory + "//Resources//ring.mp3" 播放铃声
|
|
var resources = Path.Combine(baseDir, "Resources");
|
|
if (!Path.Exists(resources)) Directory.CreateSymbolicLink(resources, ProjectResources.Directory);
|
|
AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", baseDir);
|
|
Directory.SetCurrentDirectory(baseDir);
|
|
SimHost.AppBaseDirectory = baseDir;
|
|
}
|
|
|
|
/// <summary>读取浏览器发来的操作,排队到界面线程执行</summary>
|
|
private static void ReadActions()
|
|
{
|
|
using var input = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false));
|
|
while (input.ReadLine() is { } line)
|
|
{
|
|
JsonObject? message;
|
|
try
|
|
{
|
|
message = JsonNode.Parse(line) as JsonObject;
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
continue;
|
|
}
|
|
switch ((string?)message?["type"])
|
|
{
|
|
case "action":
|
|
if (SimHost.FindControl((string?)message!["id"] ?? "") is { } control)
|
|
{
|
|
var action = UserAction.FromJson(message);
|
|
SimHost.Ui.Post(() => control.PerformUserAction(action));
|
|
}
|
|
break;
|
|
case "board":
|
|
DeviceCtrlLibrary.TrainingBoard.Update(message!);
|
|
break;
|
|
case "reply":
|
|
SimHost.CompleteRequest((int?)message!["requestId"] ?? 0, message["result"]?.DeepClone());
|
|
break;
|
|
case "stop":
|
|
Environment.Exit(0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|