Files
IOT_SIM/runtime/Sim.Runner/StudentProgramHost.cs
T
xuyueandClaude Opus 5.5 670abe5921
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
项目 6 数据存储:仿冒 SqlClient(T-SQL 转写成 SQLite)、数据库页面、DataGridView、DateTimePicker
- 每个学生一套数据库,长期保存(data/databases/<用户编号>/库名.db);学生程序运行时挂进沙箱可读写
- Sim.Data:SqlConnection / SqlCommand / SqlDataReader / SqlDataAdapter / SqlCommandBuilder / SqlParameter;
  连接字符串只按库名打开学生自己的库;ScriptDom 解析 T-SQL 后按白名单转写成 SQLite 语句,
  STRICT 表 + 元数据表还原 SQL Server 的列类型,报错编号和中文信息与 SQL Server 一致;
  INSERT 不写列名时跳过自增列,日期统一格式(书上 DateTime.ToString() 拼接的日期能正确比较)
- 常驻 SQL 服务(Sim.Runner sql-server):数据库页面的查询、对象资源管理器,评分前按实验 database.sql 建库
- 数据库页面(代替 SSMS):对象资源管理器、查询窗口、编辑前 200 行、表设计器、恢复实验初始数据
- DataGridView(绑定 DataView/DataTable/DataSet,单元格修改写回 DataTable)、DateTimePicker;设计器工具箱加入
- 评分:每个检查项一份新建的实验库;新增点 / 修改表格单元格、选日期、表格里有 / 没有某行的检查
- 示例:项目 6 任务 2、3;时间格式改成和中文 Windows 一致(9:05:03);异常堆栈不暴露服务器路径
- 测试:数据库端到端、接口(每人只看到自己的库、恢复实验数据)、评分出错情况、T-SQL 生成

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:18:56 +08:00

149 lines
5.9 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;
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;
}
}
}
}