Private/Invoke-QAFrameworkDotNet.ps1

function Invoke-QAFrameworkDotNet {
    <# Inferred helper: the single mockable seam for dotnet. Uses a C# runner so redirected stream callbacks never execute PowerShell script blocks on reader threads. #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)][string[]]$Arguments,
        [Parameter()][string]$WorkingDirectory = (Get-Location).ProviderPath,
        [Parameter()][hashtable]$Environment,
        [Parameter()][int]$TimeoutSeconds = 600,
        [Parameter()][int]$MaxStreamCharacters = 1048576
    )
    if (-not ('Skyline.QAOps.ProcessRunner' -as [type])) {
        Add-Type -TypeDefinition @'
using System;
using System.Collections;
using System.Diagnostics;
using System.Text;
namespace Skyline.QAOps {
  public sealed class ProcessResult { public int ExitCode; public string StdOut; public string StdErr; public bool TimedOut; public bool OutputTruncated; }
  public static class ProcessRunner {
    public static ProcessResult Run(string fileName, string[] args, string workingDirectory, IDictionary environment, int timeoutSeconds, int maxChars) {
      if (maxChars <= 0) maxChars = 1048576;
      var psi = new ProcessStartInfo(); psi.FileName = fileName; psi.Arguments = QuoteArguments(args ?? new string[0]); psi.WorkingDirectory = workingDirectory;
      psi.UseShellExecute = false; psi.RedirectStandardOutput = true; psi.RedirectStandardError = true; psi.CreateNoWindow = true;
      if (environment != null) { foreach (DictionaryEntry entry in environment) { string key = (string)entry.Key; object value = entry.Value; if (value == null) psi.EnvironmentVariables.Remove(key); else psi.EnvironmentVariables[key] = Convert.ToString(value); } }
      var result = new ProcessResult();
      using (var process = new Process()) {
        process.StartInfo = psi; var stdout = new BoundedBuffer(maxChars); var stderr = new BoundedBuffer(maxChars);
        try {
          if (!process.Start()) throw new InvalidOperationException("Failed to start process.");
          var outTask = process.StandardOutput.ReadToEndAsync(); var errTask = process.StandardError.ReadToEndAsync();
          int waitMs = timeoutSeconds > 0 ? checked(timeoutSeconds * 1000) : int.MaxValue;
          if (!process.WaitForExit(waitMs)) { result.TimedOut = true; KillTree(process); process.WaitForExit(5000); }
          try { outTask.Wait(5000); } catch { } try { errTask.Wait(5000); } catch { }
          stdout.Append(SafeTaskResult(outTask)); stderr.Append(SafeTaskResult(errTask));
          result.ExitCode = result.TimedOut ? -1 : (process.HasExited ? process.ExitCode : -1);
        } finally { try { if (!process.HasExited) KillTree(process); } catch { } }
        result.StdOut = stdout.Text; result.StdErr = stderr.Text; result.OutputTruncated = stdout.Truncated || stderr.Truncated; return result;
      }
    }
    static string SafeTaskResult(System.Threading.Tasks.Task<string> task) { try { return task.IsCompleted ? task.Result : String.Empty; } catch { return String.Empty; } }
    static void KillTree(Process process) {
      try { var method = typeof(Process).GetMethod("Kill", new Type[] { typeof(bool) }); if (method != null) { method.Invoke(process, new object[] { true }); return; } } catch { }
      try { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { using (var killer = Process.Start(new ProcessStartInfo("taskkill", "/T /F /PID " + process.Id) { UseShellExecute = false, CreateNoWindow = true })) { if (killer != null) killer.WaitForExit(5000); } } else { process.Kill(); } } catch { try { process.Kill(); } catch { } }
    }
    static string QuoteArguments(string[] args) { var sb = new StringBuilder(); for (int i = 0; i < args.Length; i++) { if (i > 0) sb.Append(' '); sb.Append(Quote(args[i] ?? String.Empty)); } return sb.ToString(); }
    static string Quote(string arg) { if (arg.Length == 0) return "\"\""; bool quote = arg.IndexOfAny(new char[] { ' ', '\t', '\n', '\v', '"' }) >= 0; if (!quote) return arg; var sb = new StringBuilder(); sb.Append('"'); int slashCount = 0; foreach (char c in arg) { if (c == '\\') { slashCount++; continue; } if (c == '"') { sb.Append('\\', slashCount * 2 + 1); sb.Append('"'); slashCount = 0; continue; } if (slashCount > 0) { sb.Append('\\', slashCount); slashCount = 0; } sb.Append(c); } if (slashCount > 0) sb.Append('\\', slashCount * 2); sb.Append('"'); return sb.ToString(); }
    sealed class BoundedBuffer { readonly int max; readonly StringBuilder builder = new StringBuilder(); public bool Truncated; public string Text { get { return builder.ToString(); } } public BoundedBuffer(int max) { this.max = max; } public void Append(string value) { if (String.IsNullOrEmpty(value) || builder.Length >= max) { if (!String.IsNullOrEmpty(value)) Truncated = true; return; } int remaining = max - builder.Length; if (value.Length <= remaining) builder.Append(value); else { builder.Append(value.Substring(0, remaining)); Truncated = true; } } }
  }
}
'@
 -ErrorAction Stop
    }
    try {
        $r = [Skyline.QAOps.ProcessRunner]::Run('dotnet', [string[]]@($Arguments), $WorkingDirectory, $Environment, $TimeoutSeconds, $MaxStreamCharacters)
        [pscustomobject]@{ ExitCode = $r.ExitCode; StdOut = $r.StdOut; StdErr = $r.StdErr; TimedOut = $r.TimedOut; OutputTruncated = $r.OutputTruncated }
    }
    catch { throw [System.InvalidOperationException]::new("Failed to invoke dotnet: $($_.Exception.Message)", $_.Exception) }
}