- 编译时把 Thread 的 Suspend / Resume / Abort、ThreadState、Sleep、new Thread 改写成平台的 协作式实现:被挂起或终止的线程在 Sleep、后台改控件、调设备类库、弹提示框时停下或结束; ThreadStateException 等行为与 .NET Framework 一致;编辑器不再提示"已过时" - ListBox:列表项、单选、SelectedIndexChanged 先于 Click、删除选中项后取消选中; ComboBox 与 ListBox 共用 ListControl 和列表项集合;设计器工具箱加"列表框" - 评分规则的"包含"对列表框检查所有列表项 - 示例:项目 5 任务 2(烟雾,线程)、任务 3(推送灾情,二维数组、两个窗体、ListBox), 参考答案修正书上直接 Abort 挂起线程、烟雾记成"发现火情"的问题 - 一直不换行的 Console 输出攒满 4096 字符先发出,避免占满内存 - 测试:旧版线程 API、ListBox 事件顺序、两个示例的端到端和评分测试 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" };
|
|
|
|
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; // 框架目录里的原生库
|
|
}
|
|
}
|
|
}
|