5919a71b8b
Advisory, not a trust-policy violation: every policy entry blocks deploy, and an error here would fail the next redeploy of already-deployed scripts. ValidationService surfaces it as a non-blocking ScriptCompilation warning.
94 lines
4.2 KiB
C#
94 lines
4.2 KiB
C#
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
|
|
|
/// <summary>
|
|
/// Advisory (non-blocking) analysis that finds scripts synchronously blocking on a
|
|
/// <see cref="Task"/> — <c>.Result</c>, <c>.Wait(...)</c>, <c>Task.WaitAll/WaitAny</c>
|
|
/// and <c>.GetAwaiter().GetResult()</c> (review 2026-10-21 01/SR-U2).
|
|
///
|
|
/// <para>
|
|
/// Site scripts run on the dedicated <c>ScriptExecutionScheduler</c> pool, and not
|
|
/// every runtime helper completes off that pool, so a script that blocks on a helper's
|
|
/// task can hold a worker while the helper's continuation waits for a worker — enough
|
|
/// concurrent runs and the pool starves until the stuck-script watchdog detaches
|
|
/// workers. <c>await</c> is always the right form. This is an advisory rather than a
|
|
/// <see cref="ScriptTrustPolicy"/> violation: the trust policy is a deny-list over API
|
|
/// roots whose every entry blocks deploy, and turning this into a deploy-blocking
|
|
/// error would break already-deployed scripts on their next redeploy.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Resolution is semantic (the script is bound against <c>globalsType</c>), so a
|
|
/// member that merely happens to be named <c>Result</c> or <c>Wait</c> on a non-Task
|
|
/// type is not reported. Unresolved code is silently skipped — compile errors are the
|
|
/// compile gate's job.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class BlockingWaitAnalyzer
|
|
{
|
|
private const string TasksNamespace = "System.Threading.Tasks";
|
|
private const string CompilerServicesNamespace = "System.Runtime.CompilerServices";
|
|
|
|
/// <summary>
|
|
/// Returns one advisory message per blocking wait found in <paramref name="code"/>.
|
|
/// Empty when the script never blocks on a task (or is blank).
|
|
/// </summary>
|
|
/// <param name="code">The C# script source.</param>
|
|
/// <param name="globalsType">The globals type the script binds against (e.g. <see cref="ScriptCompileSurface"/>).</param>
|
|
/// <returns>Advisory messages naming the member and its line.</returns>
|
|
public static IReadOnlyList<string> Find(string code, Type? globalsType)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(code))
|
|
return Array.Empty<string>();
|
|
|
|
var compilation = RoslynScriptCompiler.CreateScript(code, globalsType).GetCompilation();
|
|
var tree = compilation.SyntaxTrees.Single();
|
|
var model = compilation.GetSemanticModel(tree);
|
|
var advisories = new List<string>();
|
|
|
|
foreach (var access in tree.GetRoot().DescendantNodes().OfType<MemberAccessExpressionSyntax>())
|
|
{
|
|
var symbol = model.GetSymbolInfo(access).Symbol;
|
|
var form = symbol switch
|
|
{
|
|
IPropertySymbol { Name: "Result" } p when IsTaskType(p.ContainingType) => ".Result",
|
|
IMethodSymbol { Name: "Wait" } m when IsTaskType(m.ContainingType) => ".Wait()",
|
|
IMethodSymbol { Name: "WaitAll" or "WaitAny" } m when IsTaskType(m.ContainingType) => $"Task.{m.Name}()",
|
|
IMethodSymbol { Name: "GetResult" } m when IsAwaiterType(m.ContainingType) => ".GetAwaiter().GetResult()",
|
|
_ => null,
|
|
};
|
|
if (form == null)
|
|
continue;
|
|
|
|
var line = access.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
|
|
advisories.Add(
|
|
$"blocks on a Task with '{form}' (line {line}) — use 'await' instead; a blocked " +
|
|
"script-pool worker can starve the pool the awaited work needs to complete.");
|
|
}
|
|
|
|
return advisories;
|
|
}
|
|
|
|
private static bool IsTaskType(INamedTypeSymbol? type)
|
|
{
|
|
for (var t = type; t != null; t = t.BaseType)
|
|
{
|
|
if (t.Name is "Task" or "ValueTask"
|
|
&& t.ContainingNamespace?.ToDisplayString() == TasksNamespace)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsAwaiterType(INamedTypeSymbol? type)
|
|
=> type != null
|
|
&& type.Name.EndsWith("Awaiter", StringComparison.Ordinal)
|
|
&& (type.ContainingNamespace?.ToDisplayString() == CompilerServicesNamespace
|
|
|| type.ContainingType?.ContainingNamespace?.ToDisplayString() == CompilerServicesNamespace);
|
|
}
|