项目 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>
This commit is contained in:
1 parent
6b088240da
commit
670abe5921
72 files changed
+7253
-51
No files matched your search
@@ -41,11 +41,16 @@ internal static class ApiPolicy
|
||||
["System.Runtime.InteropServices.NativeMemory"] = "调用本机代码",
|
||||
};
|
||||
|
||||
/// <summary>不允许的类型里单独放行的成员:教材用 AppDomain.CurrentDomain.BaseDirectory 拼出保存照片的路径</summary>
|
||||
/// <summary>
|
||||
/// 不允许的类型里单独放行的成员:教材用 AppDomain.CurrentDomain.BaseDirectory 拼出保存照片的路径;
|
||||
/// 类型的名字(GetType().Name、DataColumn.DataType.Name)只是读名字,没有风险
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> AllowedMembers = new()
|
||||
{
|
||||
"System.AppDomain.CurrentDomain",
|
||||
"System.AppDomain.BaseDirectory",
|
||||
"System.Reflection.MemberInfo.Name",
|
||||
"System.Type.FullName",
|
||||
};
|
||||
|
||||
public static IEnumerable<Diagnostic> Check(CSharpCompilation compilation)
|
||||
|
||||
@@ -5,6 +5,7 @@ using Sim.Runner;
|
||||
// 三种模式,都通过标准输入输出与 Bun 服务器交换 JSON 消息(每行一条):
|
||||
// dotnet Sim.Runner.dll compile-server 常驻编译服务,全服务器共用一个,编译学生工程并把程序集写回工程目录
|
||||
// dotnet Sim.Runner.dll language-server 常驻语言服务,全服务器共用一个:代码补全、悬停提示、参数提示、边写边检查
|
||||
// dotnet Sim.Runner.dll sql-server 常驻 SQL 服务:数据库页面的查询、对象资源管理器,评分前准备实验数据库
|
||||
// dotnet Sim.Runner.dll run <工程目录> 运行已编译的学生程序,每次运行一个进程
|
||||
// 运行模式不加载 Roslyn,单个学生程序的内存占用因此小得多。
|
||||
|
||||
@@ -12,12 +13,13 @@ return args switch
|
||||
{
|
||||
["compile-server"] => CompileServer.Run(),
|
||||
["language-server"] => LanguageServer.Run(),
|
||||
["sql-server"] => SqlService.Run(),
|
||||
["run", var dir] when Directory.Exists(dir) => StudentProgramHost.Run(dir),
|
||||
_ => Usage(),
|
||||
};
|
||||
|
||||
static int Usage()
|
||||
{
|
||||
Console.Error.WriteLine("用法:Sim.Runner compile-server | Sim.Runner language-server | Sim.Runner run <工程目录>");
|
||||
Console.Error.WriteLine("用法:Sim.Runner compile-server | Sim.Runner language-server | Sim.Runner sql-server | Sim.Runner run <工程目录>");
|
||||
return 2;
|
||||
}
|
||||
@@ -17,5 +17,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Sim.WinForms/Sim.WinForms.csproj" />
|
||||
<ProjectReference Include="../Sim.Devices/Sim.Devices.csproj" />
|
||||
<ProjectReference Include="../Sim.Data/Sim.Data.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SqlClient;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Sim.Data.Engine;
|
||||
|
||||
namespace Sim.Runner;
|
||||
|
||||
/// <summary>
|
||||
/// 常驻 SQL 服务:数据库页面(代替 SSMS)的查询、对象资源管理器,以及评分前按实验预置脚本准备数据库。
|
||||
/// 请求里的目录由服务器给出(学生自己的数据库目录或评分用的临时目录),学生的 SQL 经转写后只能操作这个目录里的库。
|
||||
/// 每行一个请求 {"id":…,"type":"exec"|"script"|"explorer",…},回复带同样的 id;请求可以并发处理。
|
||||
/// </summary>
|
||||
internal static class SqlService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
|
||||
private static readonly TimeSpan TimeLimit = TimeSpan.FromSeconds(5);
|
||||
/// <summary>查询窗口最多显示的行数</summary>
|
||||
private const int MaxRows = 1000;
|
||||
|
||||
public static int Run()
|
||||
{
|
||||
// 日期等按中文 Windows 上 SSMS 的样子显示
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.GetCultureInfo("zh-CN");
|
||||
using var input = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false));
|
||||
var output = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)) { AutoFlush = true };
|
||||
var writeLock = new object();
|
||||
using var slots = new SemaphoreSlim(4);
|
||||
|
||||
output.WriteLine(new JsonObject { ["type"] = "ready" }.ToJsonString(JsonOptions));
|
||||
while (input.ReadLine() is { } line)
|
||||
{
|
||||
slots.Wait();
|
||||
Task.Run(() =>
|
||||
{
|
||||
JsonObject reply;
|
||||
JsonNode? id = null;
|
||||
try
|
||||
{
|
||||
var request = JsonNode.Parse(line)!.AsObject();
|
||||
id = request["id"]?.DeepClone();
|
||||
reply = Handle(request);
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
reply = Error(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reply = new JsonObject { ["type"] = "error", ["number"] = 0, ["message"] = ex.Message, ["line"] = 1 };
|
||||
}
|
||||
finally
|
||||
{
|
||||
slots.Release();
|
||||
}
|
||||
reply["id"] = id;
|
||||
lock (writeLock) output.WriteLine(reply.ToJsonString(JsonOptions));
|
||||
});
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static JsonObject Handle(JsonObject request)
|
||||
{
|
||||
var dir = (string?)request["dir"] ?? throw new ArgumentException("缺少 dir");
|
||||
var store = new DatabaseStore(dir);
|
||||
return (string?)request["type"] switch
|
||||
{
|
||||
"exec" => Exec(store, (string?)request["database"] ?? DatabaseStore.Master, (string?)request["sql"] ?? ""),
|
||||
"script" => Script(store, (string?)request["sql"] ?? ""),
|
||||
"explorer" => Explorer(store),
|
||||
var type => throw new ArgumentException($"不认识的请求 {type}"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>查询窗口执行一批语句:返回各个结果集(最多 1000 行)、受影响的行数,以及执行后的当前库(USE 可能切换了)</summary>
|
||||
private static JsonObject Exec(DatabaseStore store, string database, string sql)
|
||||
{
|
||||
var started = Environment.TickCount64;
|
||||
using var session = new Session(store, database, TimeLimit);
|
||||
var batch = session.Execute(sql, name => (false, null));
|
||||
var results = new JsonArray();
|
||||
foreach (var result in batch.Results)
|
||||
{
|
||||
var columns = new JsonArray();
|
||||
foreach (var column in result.Columns) columns.Add(new JsonObject { ["name"] = column.Name, ["type"] = column.Type?.ToString() });
|
||||
var rows = new JsonArray();
|
||||
foreach (var row in result.Rows.Take(MaxRows))
|
||||
{
|
||||
var cells = new JsonArray();
|
||||
for (var i = 0; i < row.Length; i++) cells.Add(Display(row[i], result.Columns[i].Type?.IsDate == true));
|
||||
rows.Add(cells);
|
||||
}
|
||||
results.Add(new JsonObject { ["columns"] = columns, ["rows"] = rows, ["total"] = result.Rows.Count });
|
||||
}
|
||||
return new JsonObject
|
||||
{
|
||||
["type"] = "result",
|
||||
["database"] = session.Database,
|
||||
["results"] = results,
|
||||
["affected"] = batch.RecordsAffected,
|
||||
["ms"] = Environment.TickCount64 - started,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>执行一个脚本(多批语句,用单独一行的 GO 分隔),从 master 开始,USE 对后面的批次有效</summary>
|
||||
private static JsonObject Script(DatabaseStore store, string sql)
|
||||
{
|
||||
var batches = Regex.Split(sql, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase).Where(b => !string.IsNullOrWhiteSpace(b));
|
||||
using var session = new Session(store, DatabaseStore.Master, TimeSpan.FromSeconds(30));
|
||||
foreach (var batch in batches) session.Execute(batch, name => (false, null));
|
||||
return new JsonObject { ["type"] = "done" };
|
||||
}
|
||||
|
||||
/// <summary>对象资源管理器:所有库、库里的表、表的列</summary>
|
||||
private static JsonObject Explorer(DatabaseStore store)
|
||||
{
|
||||
var databases = new JsonArray();
|
||||
foreach (var name in store.List())
|
||||
{
|
||||
var tables = new JsonArray();
|
||||
using (var connection = store.Open(name))
|
||||
{
|
||||
foreach (var table in DatabaseStore.ListTables(connection))
|
||||
{
|
||||
var info = DatabaseStore.GetTable(connection, table);
|
||||
var columns = new JsonArray();
|
||||
foreach (var column in info?.Columns ?? Array.Empty<ColumnInfo>())
|
||||
{
|
||||
columns.Add(new JsonObject
|
||||
{
|
||||
["name"] = column.Name,
|
||||
["type"] = column.Type.ToString(),
|
||||
["nullable"] = column.Nullable,
|
||||
["identity"] = column.Identity,
|
||||
["primaryKey"] = column.PrimaryKey,
|
||||
});
|
||||
}
|
||||
tables.Add(new JsonObject { ["name"] = table, ["columns"] = columns });
|
||||
}
|
||||
}
|
||||
databases.Add(new JsonObject { ["name"] = name, ["tables"] = tables });
|
||||
}
|
||||
return new JsonObject { ["type"] = "explorer", ["databases"] = databases };
|
||||
}
|
||||
|
||||
/// <summary>结果网格里显示的样子,和 SSMS 一致:日期 2024-01-16 09:05:03.000,bit 显示 1 / 0,二进制显示 0x…</summary>
|
||||
private static JsonNode? Display(object value, bool dateOnly) => value switch
|
||||
{
|
||||
DBNull => null,
|
||||
DateTime d when dateOnly => d.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
DateTime d => d.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture),
|
||||
bool b => b ? 1 : 0,
|
||||
byte[] bytes => "0x" + Convert.ToHexString(bytes.AsSpan(0, Math.Min(bytes.Length, 32))) + (bytes.Length > 32 ? "…" : ""),
|
||||
int i => i,
|
||||
long l => l,
|
||||
double x => x,
|
||||
decimal m => m,
|
||||
IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
|
||||
_ => value.ToString(),
|
||||
};
|
||||
|
||||
private static JsonObject Error(SqlException ex) =>
|
||||
new() { ["type"] = "error", ["number"] = ex.Number, ["message"] = ex.Message, ["line"] = ex.LineNumber };
|
||||
}
|
||||
@@ -27,7 +27,7 @@ internal static class StudentCompiler
|
||||
"System.Drawing.dll",
|
||||
};
|
||||
|
||||
private static readonly string[] SimAssemblies = { "Sim.WinForms.dll", "Sim.Devices.dll" };
|
||||
private static readonly string[] SimAssemblies = { "Sim.WinForms.dll", "Sim.Devices.dll", "Sim.Data.dll" };
|
||||
|
||||
public sealed record Result(bool Success, byte[] Assembly, byte[] Pdb, JsonArray Diagnostics);
|
||||
|
||||
|
||||
@@ -27,8 +27,12 @@ internal static class StudentProgramHost
|
||||
Console.SetOut(console);
|
||||
Console.SetIn(TextReader.Null); // 标准输入用于协议,学生的 Console.ReadLine 读不到内容
|
||||
|
||||
// 和中文 Windows 一致:DateTime.ToString() 等按 zh-CN 格式输出
|
||||
var zhCN = CultureInfo.GetCultureInfo("zh-CN");
|
||||
// 和中文 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;
|
||||
|
||||
Reference in new issue
Block a user