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; /// /// 运行一个已编译的学生程序:在模拟的界面线程上执行 Main,主线程从标准输入读取浏览器的操作。 /// 这个类不能引用 Roslyn 的任何类型,否则运行模式也会加载编译器。 /// 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(书上的截图也是);长日期后面多了星期(2024年1月16日星期二), // 中文 Windows 的长日期不带星期(日期选择框的长日期格式窄,带上星期会被截掉)。这里都改成 Windows 的写法 var zhCN = (CultureInfo)CultureInfo.GetCultureInfo("zh-CN").Clone(); zhCN.DateTimeFormat.LongTimePattern = "H:mm:ss"; zhCN.DateTimeFormat.ShortTimePattern = "H:mm"; zhCN.DateTimeFormat.LongDatePattern = "yyyy'年'M'月'd'日'"; zhCN.DateTimeFormat.FullDateTimePattern = "yyyy'年'M'月'd'日' H:mm:ss"; 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() }; 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; } /// /// 学生程序的"程序所在目录":和在 VS 里按 F5 一样是工程下的 bin/Debug/,也是当前目录,可以写文件 /// (教材把摄像头照片存到 AppDomain.CurrentDomain.BaseDirectory + "Camera/")。 /// 沙箱里工程目录只读,改用沙箱自己的临时目录;两种情况下文件都在程序结束后清掉。 /// 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; } /// 读取浏览器发来的操作,排队到界面线程执行 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; } } } }