fix(central-ui): resolve CentralUI-002/003/004 — site-scope enforcement, per-circuit console capture, cached auth state
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
using System.Text;
|
||||
|
||||
namespace ScadaLink.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 (CentralUI-003).
|
||||
/// </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;
|
||||
|
||||
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>
|
||||
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>
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user