9cff87fe85
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
152 lines
6.2 KiB
C#
152 lines
6.2 KiB
C#
using System.Text;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
|
|
|
/// <summary>
|
|
/// Per-call console capture for the Test Run sandbox.
|
|
/// <para>
|
|
/// Sandbox scripts use <c>System.Console.WriteLine</c> for ad-hoc output. The
|
|
/// sandbox needs to capture that output per execution. <c>Console.Out</c> is,
|
|
/// however, <b>process-global</b>: redirecting it with <c>Console.SetOut</c> for
|
|
/// the duration of one run corrupts any other run executing concurrently —
|
|
/// outputs interleave, and whichever run finishes first restores
|
|
/// <c>Console.Out</c> while the others are still writing.
|
|
/// </para>
|
|
/// <para>
|
|
/// This writer is installed into <c>Console.Out</c>/<c>Console.Error</c>
|
|
/// <b>exactly once</b> (see <see cref="Install"/>) and never removed. Each
|
|
/// concurrent run pushes its own buffer onto an <see cref="AsyncLocal{T}"/>
|
|
/// scope via <see cref="BeginCapture"/>; writes on that run's logical call-tree
|
|
/// land in that run's buffer only. Writes made on threads with no active
|
|
/// capture scope (i.e. genuine host-process console output) fall through to the
|
|
/// original writer. No process-global mutation happens per run.
|
|
/// </para>
|
|
/// </summary>
|
|
internal sealed class SandboxConsoleCapture : TextWriter
|
|
{
|
|
private static readonly object InstallLock = new();
|
|
private static SandboxConsoleCapture? _outInstance;
|
|
private static SandboxConsoleCapture? _errorInstance;
|
|
|
|
private readonly TextWriter _fallback;
|
|
private readonly AsyncLocal<StringWriter?> _current = new();
|
|
|
|
private SandboxConsoleCapture(TextWriter fallback) => _fallback = fallback;
|
|
|
|
/// <inheritdoc />
|
|
public override Encoding Encoding => _fallback.Encoding;
|
|
|
|
/// <summary>
|
|
/// Installs the routing writers into <see cref="Console.Out"/> and
|
|
/// <see cref="Console.Error"/> once for the process. Idempotent and
|
|
/// thread-safe. Subsequent calls return the already-installed instances.
|
|
/// </summary>
|
|
/// <returns>The installed <see cref="SandboxConsoleCapture"/> instances for stdout and stderr.</returns>
|
|
public static (SandboxConsoleCapture Out, SandboxConsoleCapture Error) Install()
|
|
{
|
|
if (_outInstance != null && _errorInstance != null)
|
|
return (_outInstance, _errorInstance);
|
|
|
|
lock (InstallLock)
|
|
{
|
|
if (_outInstance == null)
|
|
{
|
|
_outInstance = new SandboxConsoleCapture(Console.Out);
|
|
Console.SetOut(_outInstance);
|
|
}
|
|
|
|
if (_errorInstance == null)
|
|
{
|
|
_errorInstance = new SandboxConsoleCapture(Console.Error);
|
|
Console.SetError(_errorInstance);
|
|
}
|
|
}
|
|
|
|
return (_outInstance, _errorInstance);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Begins a capture scope on the current logical (async) call-tree. All
|
|
/// console writes from this point until the returned scope is disposed are
|
|
/// routed into <paramref name="buffer"/> instead of the original writer.
|
|
/// The scope is restored on dispose, so nesting and concurrent scopes on
|
|
/// other call-trees are unaffected.
|
|
/// </summary>
|
|
/// <param name="buffer">The writer that receives console output for this scope.</param>
|
|
/// <returns>A <see cref="CaptureScope"/> that, when disposed, restores the previous capture state.</returns>
|
|
public CaptureScope BeginCapture(StringWriter buffer)
|
|
{
|
|
var previous = _current.Value;
|
|
_current.Value = buffer;
|
|
return new CaptureScope(this, previous);
|
|
}
|
|
|
|
// Intra-script concurrency hardening. A sandboxed script
|
|
// can fan out work with `Task.WhenAll` / `Task.Run`; `AsyncLocal` flows
|
|
// the capture `StringWriter` into every child task, so two tasks can
|
|
// race the *same* buffer. `StringWriter` is not thread-safe — concurrent
|
|
// `Write`/`WriteLine` calls can corrupt the underlying `StringBuilder`
|
|
// and either throw or interleave at the character level. We lock on the
|
|
// captured writer itself so writes from one capture scope serialise;
|
|
// fall-through to the original `_fallback` (host-process console) is
|
|
// unlocked because the BCL's process-wide `Console.Out` is already
|
|
// synchronised by its TextWriter wrapper.
|
|
/// <inheritdoc />
|
|
public override void Write(char value) => WriteSynchronized(t => t.Write(value));
|
|
|
|
/// <inheritdoc />
|
|
public override void Write(string? value) => WriteSynchronized(t => t.Write(value));
|
|
|
|
/// <inheritdoc />
|
|
public override void Write(char[] buffer, int index, int count) =>
|
|
WriteSynchronized(t => t.Write(buffer, index, count));
|
|
|
|
/// <inheritdoc />
|
|
public override void WriteLine() => WriteSynchronized(t => t.WriteLine());
|
|
|
|
/// <inheritdoc />
|
|
public override void WriteLine(string? value) => WriteSynchronized(t => t.WriteLine(value));
|
|
|
|
/// <summary>
|
|
/// Routes a single write through the currently-active capture buffer
|
|
/// under a lock on that buffer, or to the unwrapped fallback writer when
|
|
/// no capture scope is active. The lock target is the `StringWriter`
|
|
/// instance itself — different capture scopes have different writers,
|
|
/// so two unrelated scopes never block each other.
|
|
/// </summary>
|
|
private void WriteSynchronized(Action<TextWriter> write)
|
|
{
|
|
var captured = _current.Value;
|
|
if (captured is null)
|
|
{
|
|
write(_fallback);
|
|
return;
|
|
}
|
|
|
|
lock (captured)
|
|
{
|
|
write(captured);
|
|
}
|
|
}
|
|
|
|
internal readonly struct CaptureScope : IDisposable
|
|
{
|
|
private readonly SandboxConsoleCapture _owner;
|
|
private readonly StringWriter? _previous;
|
|
|
|
/// <summary>
|
|
/// Initializes a capture scope that restores the previous writer on dispose.
|
|
/// </summary>
|
|
/// <param name="owner">The owning <see cref="SandboxConsoleCapture"/> instance.</param>
|
|
/// <param name="previous">The writer that was active before this scope was opened.</param>
|
|
internal CaptureScope(SandboxConsoleCapture owner, StringWriter? previous)
|
|
{
|
|
_owner = owner;
|
|
_previous = previous;
|
|
}
|
|
|
|
/// <summary>Restores the previous capture scope.</summary>
|
|
public void Dispose() => _owner._current.Value = _previous;
|
|
}
|
|
}
|