using System.Text;
namespace ScadaLink.CentralUI.ScriptAnalysis;
///
/// Per-call console capture for the Test Run sandbox.
///
/// Sandbox scripts use System.Console.WriteLine for ad-hoc output. The
/// sandbox needs to capture that output per execution. Console.Out is,
/// however, process-global: redirecting it with Console.SetOut for
/// the duration of one run corrupts any other run executing concurrently —
/// outputs interleave, and whichever run finishes first restores
/// Console.Out while the others are still writing (CentralUI-003).
///
///
/// This writer is installed into Console.Out/Console.Error
/// exactly once (see ) and never removed. Each
/// concurrent run pushes its own buffer onto an
/// scope via ; 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.
///
///
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 _current = new();
private SandboxConsoleCapture(TextWriter fallback) => _fallback = fallback;
public override Encoding Encoding => _fallback.Encoding;
///
/// Installs the routing writers into and
/// once for the process. Idempotent and
/// thread-safe. Subsequent calls return the already-installed instances.
///
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);
}
///
/// 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 instead of the original writer.
/// The scope is restored on dispose, so nesting and concurrent scopes on
/// other call-trees are unaffected.
///
public CaptureScope BeginCapture(StringWriter buffer)
{
var previous = _current.Value;
_current.Value = buffer;
return new CaptureScope(this, previous);
}
public override void Write(char value) => Target.Write(value);
public override void Write(string? value) => Target.Write(value);
public override void Write(char[] buffer, int index, int count) =>
Target.Write(buffer, index, count);
public override void WriteLine() => Target.WriteLine();
public override void WriteLine(string? value) => Target.WriteLine(value);
private TextWriter Target => _current.Value ?? _fallback;
internal readonly struct CaptureScope : IDisposable
{
private readonly SandboxConsoleCapture _owner;
private readonly StringWriter? _previous;
internal CaptureScope(SandboxConsoleCapture owner, StringWriter? previous)
{
_owner = owner;
_previous = previous;
}
public void Dispose() => _owner._current.Value = _previous;
}
}