- 每个学生一套数据库,长期保存(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>
122 lines
5.2 KiB
C#
122 lines
5.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.Emit;
|
|
using Microsoft.CodeAnalysis.Text;
|
|
|
|
namespace Sim.Runner;
|
|
|
|
/// <summary>用 Roslyn 在内存里编译学生工程,报错信息用简体中文</summary>
|
|
internal static class StudentCompiler
|
|
{
|
|
private static readonly CultureInfo Chinese = CultureInfo.GetCultureInfo("zh-Hans");
|
|
|
|
/// <summary>
|
|
/// 框架里不能给学生代码引用的程序集:System.Drawing.dll 门面把 Font、Image 等转发到 Linux 不支持的
|
|
/// System.Drawing.Common,会和仿真库里的同名类型冲突。
|
|
/// </summary>
|
|
private static readonly HashSet<string> ExcludedFrameworkAssemblies = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"System.Drawing.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);
|
|
|
|
/// <summary>编译和语言服务(代码补全等)共用同一套选项</summary>
|
|
internal static readonly CSharpParseOptions ParseOptions = new(LanguageVersion.Latest);
|
|
|
|
internal static readonly CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions(OutputKind.WindowsApplication)
|
|
.WithOptimizationLevel(OptimizationLevel.Debug)
|
|
.WithNullableContextOptions(NullableContextOptions.Disable)
|
|
.WithAllowUnsafe(false)
|
|
.WithConcurrentBuild(true);
|
|
|
|
public static Result Compile(string projectDir)
|
|
{
|
|
var parseOptions = ParseOptions;
|
|
var trees = Directory.EnumerateFiles(projectDir, "*.cs", SearchOption.AllDirectories)
|
|
.Where(path => !IsBuildOutput(projectDir, path))
|
|
.OrderBy(path => path, StringComparer.Ordinal)
|
|
.Select(path => CSharpSyntaxTree.ParseText(
|
|
SourceText.From(File.ReadAllText(path), Encoding.UTF8),
|
|
parseOptions,
|
|
path: Path.GetRelativePath(projectDir, path)))
|
|
.ToList();
|
|
|
|
var compilation = CSharpCompilation.Create("StudentApp", trees, References.Value, CompilationOptions);
|
|
|
|
using var peStream = new MemoryStream();
|
|
using var pdbStream = new MemoryStream();
|
|
// 编译改写后的代码(旧版线程 API 换成平台实现),API 检查仍针对学生写的原始代码
|
|
var emit = LegacyThreadRewriter.Rewrite(compilation).Emit(peStream, pdbStream,
|
|
options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb));
|
|
var denied = ApiPolicy.Check(compilation).ToList();
|
|
|
|
var diagnostics = new JsonArray();
|
|
foreach (var diagnostic in emit.Diagnostics.Concat(denied)
|
|
.Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning)
|
|
.OrderBy(d => d.Severity == DiagnosticSeverity.Error ? 0 : 1))
|
|
{
|
|
diagnostics.Add(ToJson(diagnostic));
|
|
}
|
|
return new Result(emit.Success && denied.Count == 0, peStream.ToArray(), pdbStream.ToArray(), diagnostics);
|
|
}
|
|
|
|
internal static JsonObject ToJson(Diagnostic diagnostic)
|
|
{
|
|
var span = diagnostic.Location.GetMappedLineSpan();
|
|
var message = diagnostic.GetMessage(Chinese);
|
|
var json = new JsonObject
|
|
{
|
|
["file"] = span.Path,
|
|
["line"] = span.StartLinePosition.Line + 1,
|
|
["column"] = span.StartLinePosition.Character + 1,
|
|
["severity"] = diagnostic.Severity == DiagnosticSeverity.Error ? "error" : "warning",
|
|
["code"] = diagnostic.Id,
|
|
["message"] = message,
|
|
};
|
|
var hint = CompileHints.For(diagnostic);
|
|
if (hint != null) json["hint"] = hint;
|
|
return json;
|
|
}
|
|
|
|
private static bool IsBuildOutput(string projectDir, string path)
|
|
{
|
|
var relative = Path.GetRelativePath(projectDir, path).Replace('\\', '/');
|
|
return relative.StartsWith("bin/") || relative.StartsWith("obj/") || relative.Contains("/bin/") || relative.Contains("/obj/");
|
|
}
|
|
|
|
/// <summary>学生代码可引用的程序集:.NET 框架(去掉冲突的门面)+ 仿真库</summary>
|
|
internal static readonly Lazy<IReadOnlyList<MetadataReference>> References = new(() =>
|
|
{
|
|
var frameworkDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
|
|
var framework = Directory.EnumerateFiles(frameworkDir, "*.dll")
|
|
.Where(path => !ExcludedFrameworkAssemblies.Contains(Path.GetFileName(path)))
|
|
.Where(IsManagedAssembly);
|
|
var sim = SimAssemblies.Select(name => Path.Combine(AppContext.BaseDirectory, name));
|
|
return framework.Concat(sim).Select(path => (MetadataReference)MetadataReference.CreateFromFile(path)).ToList();
|
|
});
|
|
|
|
private static bool IsManagedAssembly(string path)
|
|
{
|
|
try
|
|
{
|
|
AssemblyName.GetAssemblyName(path);
|
|
return true;
|
|
}
|
|
catch (BadImageFormatException)
|
|
{
|
|
return false; // 框架目录里的原生库
|
|
}
|
|
}
|
|
}
|