项目 5 补齐:旧版线程 API(Suspend / Resume / Abort)与 ListBox
- 编译时把 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>
This commit is contained in:
1 parent
8a92354fe4
commit
91b39603a2
36 files changed
+1653
-105
No files matched your search
@@ -7,6 +7,9 @@ namespace Sim.Runner;
|
||||
/// <summary>把学生程序的 Console 输出按行转发给浏览器(标准输出本身留给协议消息使用)</summary>
|
||||
internal sealed class ConsoleForwarder : TextWriter
|
||||
{
|
||||
/// <summary>一直不换行的输出(如教材的 while (true) Console.Write("A"))攒到这么多字符就先发出去</summary>
|
||||
private const int MaxBuffered = 4096;
|
||||
|
||||
private readonly StringBuilder _buffer = new();
|
||||
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
@@ -16,7 +19,7 @@ internal sealed class ConsoleForwarder : TextWriter
|
||||
lock (_buffer)
|
||||
{
|
||||
_buffer.Append(value);
|
||||
if (value == '\n') FlushBuffer();
|
||||
if (value == '\n' || _buffer.Length >= MaxBuffered) FlushBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +29,7 @@ internal sealed class ConsoleForwarder : TextWriter
|
||||
lock (_buffer)
|
||||
{
|
||||
_buffer.Append(value);
|
||||
if (value.Contains('\n')) FlushBuffer();
|
||||
if (value.Contains('\n') || _buffer.Length >= MaxBuffered) FlushBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,7 @@ internal static class LanguageServer
|
||||
foreach (var diagnostic in compilation.GetDiagnostics()
|
||||
.Concat(ApiPolicy.Check(compilation))
|
||||
.Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning && d.Location.IsInSource)
|
||||
.Where(d => !LegacyThreadRewriter.IsSupportedObsoleteWarning(d))
|
||||
.OrderBy(d => d.Severity == DiagnosticSeverity.Error ? 0 : 1))
|
||||
{
|
||||
diagnostics.Add(StudentCompiler.ToJson(diagnostic));
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Sim.Runner;
|
||||
|
||||
/// <summary>
|
||||
/// 编译前改写学生代码里的旧版线程 API,让教材项目 5 的 Suspend / Resume / Abort 能在新版 .NET 上运行:
|
||||
/// <code>
|
||||
/// new Thread(GetData) → new Thread(LegacyThreads.Wrap(new ThreadStart(GetData)))
|
||||
/// thd.Suspend() / Resume() / Abort() → LegacyThreads.Suspend(thd) …
|
||||
/// thd.ThreadState → LegacyThreads.GetThreadState(thd)
|
||||
/// Thread.Sleep(2000) → LegacyThreads.Sleep(2000)
|
||||
/// </code>
|
||||
/// 只改写语义上确实是 System.Threading.Thread 成员的调用;改写不换行,报错和异常堆栈里的行号不变。
|
||||
/// 学生在编辑器里看到的仍是原来的代码。
|
||||
/// </summary>
|
||||
internal static class LegacyThreadRewriter
|
||||
{
|
||||
private const string Target = "global::Sim.Host.LegacyThreads";
|
||||
|
||||
/// <summary>改写整个编译;没有需要改写的地方时原样返回</summary>
|
||||
public static CSharpCompilation Rewrite(CSharpCompilation compilation)
|
||||
{
|
||||
var result = compilation;
|
||||
foreach (var tree in compilation.SyntaxTrees)
|
||||
{
|
||||
// 用到线程的文件一定会出现 Thread 这个词,其他文件不用做语义分析
|
||||
if (!tree.GetText().ToString().Contains("Thread")) continue;
|
||||
var rewriter = new Rewriter(compilation.GetSemanticModel(tree));
|
||||
var root = rewriter.Visit(tree.GetRoot());
|
||||
if (!rewriter.Changed) continue;
|
||||
var text = SourceText.From(root.ToFullString(), Encoding.UTF8);
|
||||
result = result.ReplaceSyntaxTree(tree, CSharpSyntaxTree.ParseText(text, (CSharpParseOptions)tree.Options, tree.FilePath));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>平台支持的过时线程 API 不再提示"已过时"(边写边检查时用)</summary>
|
||||
public static bool IsSupportedObsoleteWarning(Diagnostic diagnostic)
|
||||
{
|
||||
if (diagnostic.Id is not ("CS0618" or "SYSLIB0006")) return false;
|
||||
// 警告标在整个调用上,如 thd.Suspend()
|
||||
var source = diagnostic.Location.SourceTree?.GetText().ToString(diagnostic.Location.SourceSpan) ?? "";
|
||||
return Regex.IsMatch(source, @"\b(Suspend|Resume|Abort)\b");
|
||||
}
|
||||
|
||||
private static bool IsThread(ITypeSymbol? type) =>
|
||||
type is { Name: "Thread", ContainingNamespace: { Name: "Threading", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } } };
|
||||
|
||||
private sealed class Rewriter(SemanticModel model) : CSharpSyntaxRewriter
|
||||
{
|
||||
public bool Changed { get; private set; }
|
||||
|
||||
public override SyntaxNode? VisitInvocationExpression(InvocationExpressionSyntax node)
|
||||
{
|
||||
var method = model.GetSymbolInfo(node).Symbol as IMethodSymbol;
|
||||
var visited = (InvocationExpressionSyntax)base.VisitInvocationExpression(node)!;
|
||||
if (method == null || !IsThread(method.ContainingType)) return visited;
|
||||
|
||||
switch (method.Name)
|
||||
{
|
||||
case "Suspend" or "Resume" or "Abort" when !method.IsStatic && visited.Expression is MemberAccessExpressionSyntax access:
|
||||
// thd.Abort(x) → LegacyThreads.Abort(thd, x)
|
||||
var arguments = visited.ArgumentList.Arguments.Insert(0, SyntaxFactory.Argument(access.Expression.WithoutTrivia()));
|
||||
Changed = true;
|
||||
return SyntaxFactory.InvocationExpression(Member(method.Name), SyntaxFactory.ArgumentList(arguments))
|
||||
.WithTriviaFrom(visited);
|
||||
case "Sleep" when method.IsStatic:
|
||||
Changed = true;
|
||||
return visited.WithExpression(Member("Sleep").WithTriviaFrom(visited.Expression));
|
||||
default:
|
||||
return visited;
|
||||
}
|
||||
}
|
||||
|
||||
public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
|
||||
{
|
||||
var symbol = model.GetSymbolInfo(node).Symbol;
|
||||
var visited = (MemberAccessExpressionSyntax)base.VisitMemberAccessExpression(node)!;
|
||||
if (symbol is not IPropertySymbol { Name: "ThreadState", IsStatic: false } property || !IsThread(property.ContainingType)) return visited;
|
||||
Changed = true;
|
||||
return SyntaxFactory.InvocationExpression(
|
||||
Member("GetThreadState"),
|
||||
SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(visited.Expression.WithoutTrivia()))))
|
||||
.WithTriviaFrom(visited);
|
||||
}
|
||||
|
||||
public override SyntaxNode? VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) =>
|
||||
WrapThreadStart(node, base.VisitObjectCreationExpression(node));
|
||||
|
||||
public override SyntaxNode? VisitImplicitObjectCreationExpression(ImplicitObjectCreationExpressionSyntax node) =>
|
||||
WrapThreadStart(node, base.VisitImplicitObjectCreationExpression(node));
|
||||
|
||||
/// <summary>new Thread(start) 的第一个参数包一层 LegacyThreads.Wrap,线程被 Abort 时安静地结束</summary>
|
||||
private SyntaxNode? WrapThreadStart(BaseObjectCreationExpressionSyntax original, SyntaxNode? visitedNode)
|
||||
{
|
||||
if (visitedNode is not BaseObjectCreationExpressionSyntax visited
|
||||
|| model.GetSymbolInfo(original).Symbol is not IMethodSymbol { MethodKind: MethodKind.Constructor } constructor
|
||||
|| !IsThread(constructor.ContainingType)
|
||||
|| constructor.Parameters.Length == 0
|
||||
|| visited.ArgumentList is not { Arguments.Count: > 0 } argumentList)
|
||||
{
|
||||
return visitedNode;
|
||||
}
|
||||
var first = argumentList.Arguments[0];
|
||||
if (first.Expression.IsKind(SyntaxKind.NullLiteralExpression)) return visited;
|
||||
var delegateType = constructor.Parameters[0].Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
// new ThreadStart(方法名或 lambda):明确委托类型,Wrap 的重载就不会有歧义
|
||||
var wrapped = SyntaxFactory.ParseExpression($"{Target}.Wrap(new {delegateType}({first.Expression.WithoutTrivia().ToFullString()}))");
|
||||
Changed = true;
|
||||
return visited.WithArgumentList(argumentList.WithArguments(
|
||||
argumentList.Arguments.Replace(first, first.WithExpression(wrapped.WithTriviaFrom(first.Expression)))));
|
||||
}
|
||||
|
||||
private static ExpressionSyntax Member(string name) => SyntaxFactory.ParseExpression($"{Target}.{name}");
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,8 @@ internal static class StudentCompiler
|
||||
|
||||
using var peStream = new MemoryStream();
|
||||
using var pdbStream = new MemoryStream();
|
||||
var emit = compilation.Emit(peStream, pdbStream,
|
||||
// 编译改写后的代码(旧版线程 API 换成平台实现),API 检查仍针对学生写的原始代码
|
||||
var emit = LegacyThreadRewriter.Rewrite(compilation).Emit(peStream, pdbStream,
|
||||
options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb));
|
||||
var denied = ApiPolicy.Check(compilation).ToList();
|
||||
|
||||
|
||||
Reference in new issue
Block a user