Debugger/SRxDebugger.psm1
|
<#
================================================================================ SRxDebugger.psm1 (Monaco Editor - Phase 2: debugger runtime engine) rev 5 ----------------------------------------------------------------------------- UI-FREE debugger runtime for Rules Library objects (ts_Rule / ts_Script). No WinForms / WebView2 types: imports anywhere, testable headless (Tests\SRxDebugger.Tests.ps1, .Busy.Tests.ps1, .Pump.Tests.ps1). ARCHITECTURE (see 'PowerShell debugger oveview.txt' / '... prototype.txt') The debugger runtime is a MANAGED RUNSPACE inside Explorer's process (the same model as the Phase 6 IT3.1 runspace tasks - hence Initialize-SRxEnv -NoHostUI): Explorer UI thread debug runspace thread ------------------ --------------------- SRxDebugSession.Poll() (UI timer) setup script (param = arguments): <- C# queues (stops / streams) 1) Initialize-SRxEnv -NoHostUI Resume(action) / Stop() 2) $global:SRxEnv.Pipeline = copy -> C# request queue 3) Set-PSBreakpoint (first line + user) 4) Start-Rule -Name <rule> (as <start>) DebuggerStop -> SRxDebug7.DebugHost.OnStop snapshot, then WAITS for a resume request - pumping sent messages end of the run: teardown script (rev 5), then the runspace is disposed WHY C# FOR THE STOP HANDLER DebuggerStop must be handled SYNCHRONOUSLY on the runspace's pipeline thread: the handler sets e.ResumeAction before it returns. A Register-ObjectEvent -Action only QUEUES the event, and a PowerShell scriptblock delegate on that thread is GOTCHA #4 of SRxTerminalBoxControl. rev 2 Local variables: Get-Variable -Scope Local in the scope of the stop. rev 3 Own minimal host (DebugPSHost): Write-Host is lost on 5.1's DefaultHost. rev 4 No SessionStateProxy (arguments = script parameter); the start waits for RunspaceAvailability 'Available' (SRxCore keeps it busy at open). rev 5 First real debug session: * Explorer frozen while stopped (cannot move / activate the window, tab pictograms dead, Monaco - another process - responsive; frees itself after minutes). The debug runspace thread OWNS WINDOWS (created by SRxCore / the rule code on that STA thread). Windows delivers activation / z-order / broadcast messages with SendMessage to every top-level window of the process and the SENDER WAITS: while the stop handler blocked in a plain wait, Explorer's UI thread hung inside SendMessage. The stop handler now waits with MsgWaitForMultipleObjectsEx and dispatches INCOMING SENT messages (PeekMessage PM_QS_SENDMESSAGE) - posted messages are not pumped, so no rule code (window event handlers) runs while stopped. Diagnostics: 'DBG debug thread ... owns N window(s): <classes>' at every stop where the count changed, and 'DBG ... sent message(s) dispatched while stopped' on resume. * Endless 'onJobsWatchTick / [IPC] PublishAllControl: Object reference not set' after the session ended: whatever SRxCore registered from inside the debug runspace (event subscriptions, jobs, the jobs-child connection) outlived the disposed runspace. Before the runspace is disposed a TEARDOWN script runs in it: Disconnect-SRxJobsChild (when that command exists), all event subscribers unregistered, all PS jobs removed. Its result is traced: '[DBG] session ...: teardown: ...'. C# namespace SRxDebug5. rev 6 r5.1 real run: still frozen; the debug thread owns 0 windows, so the r5 sent-message theory is ruled out. Diagnostics instead of a guess: UI-THREAD WATCHDOG (SRxDebug7.UiWatchdog). Started by StartSession on the UI thread (WindowsForms SynchronizationContext only - headless tests are unaffected). A background thread posts a heartbeat to the UI thread every 500 ms; when it is not executed within HangMs (3 s) the watchdog captures the MANAGED STACKS of the UI thread and of the debug thread (thread suspend + StackTrace, .NET Framework) and writes them to %TEMP%\SRxDebugger-hang-<pid>.log (+ a console line) then again every 30 s while the hang lasts, and a 'resumed after N s' line. The UI stack names the exact call Explorer is blocked in. C# namespace SRxDebug7. rev 6.1 Module failed to load: a [SRxDebug7.UiWatchdog] type literal inside the SRxDebugger class is resolved at PARSE time, before Add-Type runs. All C# types are now looked up at run time inside classes. rev 7.1 class parse error: a local '$path' in SRxDebugSession clashed with the property 'Path' (class locals may not shadow properties) - renamed. rev 7 ROOT CAUSE of the frozen Explorer and of the endless 'onJobsWatchTick / [IPC] PublishAllControl: Object reference not set' after the session: SHARED POWERSHELL CLASSES. The debug runspace imports SRxCore from the SAME file as Explorer. Windows PowerShell 5.1 caches compiled script files by path + content, so the debug runspace gets the very SAME class types (SRxJobsEngine, SRxBasePipeParentAPI ...) and re-binds their methods to ITS session state (the effect noted in SRxJobsEngine 'PHASE 6 IT3.1 r6'). From then on every method Explorer's UI thread calls on its own engine objects (Pump -> PublishAllControl, the jobs watch -> CheckProcesses ...) EXECUTES IN THE DEBUG RUNSPACE'S ENGINE: * while that engine is stopped / stepping, the debugger stops the UI thread inside SRxCore (frozen Explorer; a resume posted for the debug thread sometimes lets it go for a few seconds); * after the session the runspace is disposed - the methods run on a dead session state -> NullReferenceException forever. Fix 1 (cause) MODULE ISOLATION: every module imported into the debug runspace is loaded from a private copy of its module folder (%TEMP%\SRxDebugger\modules\<hash>\<name>\...). Other path = other cache entry = OWN class types; the types of Explorer stay bound to Explorer. Trace: 'module <name> isolated: <copy>'. $engine.IsolateModules = $false switches it off. Fix 2 (guard) a debugger stop on a thread that is NOT the debug runspace's thread is never held: it continues at once (stepping mode kept) and is logged once per script: 'DBG foreign-thread stop ignored ...'. C# namespace SRxDebug7. PUBLIC API $dbg = New-SRxDebugger $s = $dbg.StartSession(@{ Path; Kind = Rule|Script; RuleName; RootPath; Pipeline; Modules; SourceId; Title; Command }) $dbg.Pump() -> [object[]] events (UI timer) { Type = opened|stopped|running|output|ended; ... } $s.Resume('Continue'|'StepOver'|'StepInto'|'StepOut') / $s.Stop() $dbg.ToggleBreakpoint($path, $line) -> [int[]] lines of the file $dbg.SetBreakpoints($path, [int[]]$lines) (editor -> line drift sync) $dbg.GetBreakpoints($path) $dbg.Shutdown($waitMs) (Explorer FormClosed) [SRxDebugger]::FirstExecutableLine($path) automatic first breakpoint LIMITS (documented, by design) * Breakpoints toggled while the script RUNS are applied at the next stop. * The runspace has no interactive host: Read-Host / Get-Credential / choice prompts fail (same as the runspace tasks). * While stopped, windows owned by the debug thread do not repaint (posted messages are not pumped) - they are not the Explorer UI. ================================================================================ #> Set-StrictMode -Off #region ---- native code (C#, compiled once per process) ----------------------- if (-not ('SRxDebug7.DebugHost' -as [type])) { Add-Type -ReferencedAssemblies @('System.Core', [psobject].Assembly.Location) -TypeDefinition @' using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Management.Automation; using System.Management.Automation.Host; using System.Management.Automation.Runspaces; using System.Runtime.InteropServices; using System.Security; using System.Text; using System.Threading; namespace SRxDebug7 { // ---- rev 5: waits that keep cross-thread SendMessage callers alive ------- public static class NativeWait { public const uint QS_SENDMESSAGE = 0x0040; public const uint PM_NOREMOVE = 0x0000; public const uint PM_QS_SENDMESSAGE = QS_SENDMESSAGE << 16; public const uint WAIT_TIMEOUT = 0x102; [StructLayout(LayoutKind.Sequential)] public struct MSG { public IntPtr hwnd; public uint message; public IntPtr wParam; public IntPtr lParam; public uint time; public int ptX; public int ptY; } public delegate bool EnumThreadWndProc(IntPtr hWnd, IntPtr lParam); [DllImport("user32.dll", SetLastError = true)] public static extern uint MsgWaitForMultipleObjectsEx(uint nCount, IntPtr[] pHandles, uint dwMilliseconds, uint dwWakeMask, uint dwFlags); [DllImport("user32.dll")] public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); [DllImport("user32.dll")] public static extern bool EnumThreadWindows(uint dwThreadId, EnumThreadWndProc lpfn, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId(); // Waits up to ms for the handle; incoming SENT messages of this thread // are dispatched meanwhile. Returns the number of dispatch passes. public static int Wait(WaitHandle h, int ms) { IntPtr[] handles = new IntPtr[] { h.SafeWaitHandle.DangerousGetHandle() }; int passes = 0; DateTime end = DateTime.UtcNow.AddMilliseconds(ms); while (true) { int left = (int)(end - DateTime.UtcNow).TotalMilliseconds; if (left < 0) left = 0; uint r = MsgWaitForMultipleObjectsEx(1, handles, (uint)left, QS_SENDMESSAGE, 0); GC.KeepAlive(h); if (r == 0) return passes; // signalled if (r == 1) // sent message(s) waiting { MSG m; PeekMessage(out m, IntPtr.Zero, 0, 0, PM_NOREMOVE | PM_QS_SENDMESSAGE); // dispatches them passes++; if (left == 0) return passes; continue; } return passes; // timeout / failure } } public static string[] ThreadWindows() { List<string> list = new List<string>(); uint tid = GetCurrentThreadId(); EnumThreadWindows(tid, delegate (IntPtr hWnd, IntPtr lp) { StringBuilder sb = new StringBuilder(256); GetClassName(hWnd, sb, sb.Capacity); list.Add(sb.ToString()); return true; }, IntPtr.Zero); return list.ToArray(); } } // ---- rev 6: UI-thread hang watchdog ------------------------------------ public static class UiWatchdog { private static readonly object _lock = new object(); private static Thread _ui; private static SynchronizationContext _ctx; private static Thread _watcher; private static volatile bool _run; private static long _beatTicks; // UTC ticks of the last executed heartbeat private static volatile bool _posted; private static volatile string _note = ""; public static int HangMs = 3000; public static int RepeatMs = 30000; public static int IntervalMs = 500; public static string LogPath = ""; public static volatile int HangCount; // tests / diagnostics public static volatile string LastReport = ""; public static volatile bool EchoToConsole = true; public static bool IsRunning { get { return _run; } } public static void Note(string text) { _note = DateTime.Now.ToString("HH:mm:ss.fff") + " " + (text ?? ""); } // Call ON the UI thread. public static void Start(string logPath) { lock (_lock) { SynchronizationContext ctx = SynchronizationContext.Current; if (ctx == null) throw new InvalidOperationException("UiWatchdog.Start: no SynchronizationContext on this thread (call it on the UI thread)."); if (_run && _ui == Thread.CurrentThread) return; Stop(); _ui = Thread.CurrentThread; _ctx = ctx; LogPath = string.IsNullOrEmpty(logPath) ? System.IO.Path.Combine(System.IO.Path.GetTempPath(), "SRxDebugger-hang-" + System.Diagnostics.Process.GetCurrentProcess().Id + ".log") : logPath; Interlocked.Exchange(ref _beatTicks, DateTime.UtcNow.Ticks); _posted = false; _run = true; _watcher = new Thread(Loop); _watcher.IsBackground = true; _watcher.Name = "SRxDebugger UI watchdog"; _watcher.Start(); Write(string.Format("{0:HH:mm:ss.fff} watchdog started for UI thread {1} (hang threshold {2} ms)", DateTime.Now, _ui.ManagedThreadId, HangMs), false); } } public static void Stop() { _run = false; Thread w = _watcher; _watcher = null; if (w != null && w != Thread.CurrentThread) { try { w.Join(2000); } catch { } } } private static void Beat(object state) { Interlocked.Exchange(ref _beatTicks, DateTime.UtcNow.Ticks); _posted = false; } private static void Loop() { DateTime hangStart = DateTime.MinValue; DateTime lastReport = DateTime.MinValue; while (_run) { try { if (!_posted) { _posted = true; _ctx.Post(Beat, null); } Thread.Sleep(IntervalMs); DateTime beat = new DateTime(Interlocked.Read(ref _beatTicks), DateTimeKind.Utc); double ms = (DateTime.UtcNow - beat).TotalMilliseconds; if (ms >= HangMs) { if (hangStart == DateTime.MinValue) { hangStart = beat; HangCount++; Report(ms, true); lastReport = DateTime.UtcNow; } else if ((DateTime.UtcNow - lastReport).TotalMilliseconds >= RepeatMs) { Report(ms, false); lastReport = DateTime.UtcNow; } } else if (hangStart != DateTime.MinValue) { Write(string.Format("{0:HH:mm:ss.fff} UI thread resumed after {1:N1} s", DateTime.Now, (DateTime.UtcNow - hangStart).TotalSeconds), true); hangStart = DateTime.MinValue; } } catch (Exception ex) { try { Write("watchdog error: " + ex.Message, false); } catch { } Thread.Sleep(1000); } } } private static void Report(double ms, bool first) { StringBuilder sb = new StringBuilder(); sb.AppendLine("=============================================================================="); sb.AppendFormat("{0:HH:mm:ss.fff} UI THREAD NOT RESPONDING for {1:N1} s{2}\r\n", DateTime.Now, ms / 1000.0, first ? "" : " (still)"); sb.AppendFormat("last debugger event: {0}\r\n", _note); sb.AppendLine("---- UI thread managed stack (innermost first) ----"); sb.AppendLine(StackOf(_ui)); Thread d = DebugHost.LastDebugThread; if (d != null && d.IsAlive) { sb.AppendLine("---- debug runspace thread managed stack ----"); sb.AppendLine(StackOf(d)); } string text = sb.ToString(); LastReport = text; Write(text, false); if (EchoToConsole) { try { Console.WriteLine("[SRxDebugger] UI thread not responding {0:N1} s - stacks in {1}", ms / 1000.0, LogPath); } catch { } } } #pragma warning disable 618 public static string StackOf(Thread t) { if (t == null) return "(no thread)"; if (t == Thread.CurrentThread) return new System.Diagnostics.StackTrace(true).ToString(); bool suspended = false; try { t.Suspend(); suspended = true; return new System.Diagnostics.StackTrace(t, false).ToString(); } catch (Exception ex) { return "(stack not available: " + ex.GetType().Name + ": " + ex.Message + ")"; } finally { if (suspended) { try { t.Resume(); } catch { } } } } #pragma warning restore 618 private static void Write(string text, bool echo) { try { lock (_lock) { System.IO.File.AppendAllText(LogPath, text + "\r\n"); } } catch { } if (echo && EchoToConsole) { try { Console.WriteLine("[SRxDebugger] " + text); } catch { } } } } // ---- minimal host of the debug runspace (rev 3) --------------------------- public sealed class DebugPSHost : PSHost { private readonly Guid _id = Guid.NewGuid(); private readonly DebugPSHostUI _ui; internal readonly ConcurrentQueue<string> Pending = new ConcurrentQueue<string>(); internal volatile DebugHost Target; public DebugPSHost() { _ui = new DebugPSHostUI(this); } internal void Emit(string line) { DebugHost t = Target; if (t != null) t.Log("INF " + line); else Pending.Enqueue("INF " + line); } public void Flush() { _ui.Flush(); } public override string Name { get { return "SRxDebuggerHost"; } } public override Version Version { get { return new Version(1, 0); } } public override Guid InstanceId { get { return _id; } } public override PSHostUserInterface UI { get { return _ui; } } public override CultureInfo CurrentCulture { get { return Thread.CurrentThread.CurrentCulture; } } public override CultureInfo CurrentUICulture { get { return Thread.CurrentThread.CurrentUICulture; } } public override void SetShouldExit(int exitCode) { } public override void EnterNestedPrompt() { throw new NotSupportedException("Nested prompts are not supported by the SRx debugger host."); } public override void ExitNestedPrompt() { throw new NotSupportedException("Nested prompts are not supported by the SRx debugger host."); } public override void NotifyBeginApplication() { } public override void NotifyEndApplication() { } } internal sealed class DebugPSHostUI : PSHostUserInterface { private readonly DebugPSHost _host; private readonly DebugRawUI _raw = new DebugRawUI(); private readonly StringBuilder _line = new StringBuilder(); private readonly object _lock = new object(); public DebugPSHostUI(DebugPSHost host) { _host = host; } public override PSHostRawUserInterface RawUI { get { return _raw; } } private static Exception NoPrompt() { return new PSInvalidOperationException("The SRx debugger has no interactive host: Read-Host / Get-Credential / choice prompts are not supported while debugging."); } public override string ReadLine() { throw NoPrompt(); } public override SecureString ReadLineAsSecureString() { throw NoPrompt(); } public override Dictionary<string, PSObject> Prompt(string caption, string message, Collection<FieldDescription> descriptions) { throw NoPrompt(); } public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) { throw NoPrompt(); } public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) { throw NoPrompt(); } public override int PromptForChoice(string caption, string message, Collection<ChoiceDescription> choices, int defaultChoice) { throw NoPrompt(); } public override void Write(string value) { Append(value); } public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) { Append(value); } public override void WriteLine(string value) { Append((value ?? "") + "\n"); } public override void WriteErrorLine(string value) { } public override void WriteDebugLine(string message) { } public override void WriteVerboseLine(string message) { } public override void WriteWarningLine(string message) { } public override void WriteProgress(long sourceId, ProgressRecord record) { } private void Append(string value) { if (string.IsNullOrEmpty(value)) return; List<string> done = new List<string>(); lock (_lock) { foreach (char c in value) { if (c == '\r') continue; if (c == '\n') { done.Add(_line.ToString()); _line.Length = 0; continue; } _line.Append(c); } } foreach (string l in done) _host.Emit(l); } public void Flush() { string rest = null; lock (_lock) { if (_line.Length > 0) { rest = _line.ToString(); _line.Length = 0; } } if (rest != null) _host.Emit(rest); } } internal sealed class DebugRawUI : PSHostRawUserInterface { private Size _buffer = new Size(200, 3000); private Size _window = new Size(200, 50); private Coordinates _cursor = new Coordinates(0, 0); private ConsoleColor _fg = ConsoleColor.Gray; private ConsoleColor _bg = ConsoleColor.Black; private string _title = "SRx Debugger"; public override ConsoleColor BackgroundColor { get { return _bg; } set { _bg = value; } } public override ConsoleColor ForegroundColor { get { return _fg; } set { _fg = value; } } public override Size BufferSize { get { return _buffer; } set { _buffer = value; } } public override Coordinates CursorPosition { get { return _cursor; } set { _cursor = value; } } public override int CursorSize { get { return 25; } set { } } public override bool KeyAvailable { get { return false; } } public override Size MaxPhysicalWindowSize { get { return _buffer; } } public override Size MaxWindowSize { get { return _buffer; } } public override Coordinates WindowPosition { get { return new Coordinates(0, 0); } set { } } public override Size WindowSize { get { return _window; } set { _window = value; } } public override string WindowTitle { get { return _title; } set { _title = value ?? ""; } } public override void FlushInputBuffer() { } public override BufferCell[,] GetBufferContents(Rectangle rectangle) { throw new NotSupportedException("GetBufferContents is not supported by the SRx debugger host."); } public override KeyInfo ReadKey(ReadKeyOptions options) { throw new PSInvalidOperationException("The SRx debugger has no interactive host: ReadKey is not supported while debugging."); } public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) { } public override void SetBufferContents(Coordinates origin, BufferCell[,] contents) { } public override void SetBufferContents(Rectangle rectangle, BufferCell fill) { } } public sealed class VarInfo { public string Name; public string Scope; public string Type; public string Value; } public sealed class StopInfo { public int Seq; public DateTime Utc; public string Script; public int Line; public int Column; public string Position; public string Reason; // breakpoint | step public string[] Breakpoints; public string[] CallStack; public VarInfo[] Locals; public VarInfo[] Globals; public string Error; } internal sealed class Req { public string Kind; // resume | setbp | removebp | refresh | eval public int Seq; public string Action; public string Script; public int Line; public string Text; } public sealed class DebugHost : IDisposable { public static readonly string[] DefaultHidden = new string[] { "$", "?", "^", "_", "args", "ConfirmPreference", "ConsoleFileName", "DebugPreference", "Error", "ErrorActionPreference", "ErrorView", "ExecutionContext", "false", "FormatEnumerationLimit", "HOME", "Host", "InformationPreference", "input", "MaximumAliasCount", "MaximumDriveCount", "MaximumErrorCount", "MaximumFunctionCount", "MaximumHistoryCount", "MaximumVariableCount", "MyInvocation", "NestedPromptLevel", "null", "OutputEncoding", "PID", "PROFILE", "ProgressPreference", "PSBoundParameters", "PSCommandPath", "PSCulture", "PSDebugContext", "PSDefaultParameterValues", "PSEmailServer", "PSHOME", "PSItem", "PSScriptRoot", "PSSessionApplicationName", "PSSessionConfigurationName", "PSSessionOption", "PSUICulture", "PSVersionTable", "PWD", "ShellId", "StackTrace", "true", "VerbosePreference", "WarningPreference", "WhatIfPreference", "this", "foreach", "LogCommandHealthEvent", "LogCommandLifecycleEvent", "LogEngineHealthEvent", "LogEngineLifecycleEvent", "LogProviderHealthEvent", "LogProviderLifecycleEvent", "Transcript", "EnabledExperimentalFeatures" }; private readonly Runspace _rs; private readonly ConcurrentQueue<Req> _req = new ConcurrentQueue<Req>(); private readonly AutoResetEvent _signal = new AutoResetEvent(false); // rev 5: request arrived private readonly ConcurrentQueue<StopInfo> _stops = new ConcurrentQueue<StopInfo>(); private readonly ConcurrentQueue<string> _log = new ConcurrentQueue<string>(); private readonly HashSet<string> _hidden = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private volatile bool _stopped; private volatile bool _disposed; private volatile bool _abort; private volatile StopInfo _current; private volatile DebugPSHost _psHost; private int _seq; private int _lastWindowCount = -1; public static volatile Thread LastDebugThread; // rev 6: watchdog dumps its stack public int MaxValueLength = 400; public int MaxVariables = 500; public int MaxLog = 5000; public bool HideAutomatic = true; public bool SkipScriptless = true; public bool PumpSentMessages = true; // rev 5: keep SendMessage callers (Explorer UI) alive while stopped public string HiddenPrefix = "__srxdbg"; public volatile string LastResume = ""; public volatile string LastError = ""; public volatile int SentMessagePasses = 0; // rev 5: diagnostics / tests public volatile string ThreadWindowClasses = ""; // rev 5: diagnostics / tests public DebugHost(Runspace rs) { if (rs == null) throw new ArgumentNullException("rs"); if (rs.Debugger == null) throw new InvalidOperationException("The runspace has no debugger (open it first)."); _rs = rs; foreach (string n in DefaultHidden) _hidden.Add(n); _rs.Debugger.DebuggerStop += OnStop; } public bool IsStopped { get { return _stopped; } } public StopInfo Current { get { return _current; } } public int StopCount { get { return Thread.VolatileRead(ref _seq); } } public int PendingRequests { get { return _req.Count; } } public void AttachPSHost(DebugPSHost h) { if (h == null) return; _psHost = h; string x; while (h.Pending.TryDequeue(out x)) Log(x); h.Target = this; } public void FlushHost() { DebugPSHost h = _psHost; if (h != null) h.Flush(); } public void Hide(string name) { lock (_hidden) { _hidden.Add(name); } } public bool TryDequeueStop(out StopInfo s) { return _stops.TryDequeue(out s); } public bool TryDequeueLog(out string s) { return _log.TryDequeue(out s); } public void Log(string line) { if (_log.Count >= MaxLog) { string x; _log.TryDequeue(out x); } _log.Enqueue(line ?? ""); } // ---- UI thread requests --------------------------------------------- public bool Resume(string action) { if (!_stopped) return false; StopInfo cur = _current; Req r = new Req(); r.Kind = "resume"; r.Action = action; r.Seq = cur == null ? 0 : cur.Seq; Enqueue(r); return true; } public void SetBreakpoint(string script, int line) { Add("setbp", script, line, null); } public void RemoveBreakpoint(string script, int line) { Add("removebp", script, line, null); } public void Refresh() { Add("refresh", null, 0, null); } public void Evaluate(string text) { Add("eval", null, 0, text); } public void Abort() { _abort = true; try { _signal.Set(); } catch { } } private void Add(string kind, string script, int line, string text) { Req r = new Req(); r.Kind = kind; r.Script = script; r.Line = line; r.Text = text; Enqueue(r); } private void Enqueue(Req r) { _req.Enqueue(r); try { _signal.Set(); } catch { } } // ---- streams of the PowerShell instance (writer thread, C# only) ----- public void AttachStreams(PowerShell ps, PSDataCollection<PSObject> output) { if (output != null) { output.DataAdded += delegate (object sender, DataAddedEventArgs e) { try { PSDataCollection<PSObject> c = (PSDataCollection<PSObject>)sender; Log("OUT " + Text(c[e.Index])); } catch { } }; } if (ps == null) return; ps.Streams.Error.DataAdded += delegate (object sender, DataAddedEventArgs e) { try { PSDataCollection<ErrorRecord> c = (PSDataCollection<ErrorRecord>)sender; ErrorRecord r = c[e.Index]; string pos = ""; if (r.InvocationInfo != null && !string.IsNullOrEmpty(r.InvocationInfo.ScriptName)) pos = string.Format(" ({0}:{1})", r.InvocationInfo.ScriptName, r.InvocationInfo.ScriptLineNumber); Log("ERR " + r.ToString() + pos); } catch { } }; ps.Streams.Warning.DataAdded += delegate (object sender, DataAddedEventArgs e) { try { PSDataCollection<WarningRecord> c = (PSDataCollection<WarningRecord>)sender; Log("WRN " + c[e.Index].Message); } catch { } }; ps.Streams.Verbose.DataAdded += delegate (object sender, DataAddedEventArgs e) { try { PSDataCollection<VerboseRecord> c = (PSDataCollection<VerboseRecord>)sender; Log("VRB " + c[e.Index].Message); } catch { } }; ps.Streams.Information.DataAdded += delegate (object sender, DataAddedEventArgs e) { try { PSDataCollection<InformationRecord> c = (PSDataCollection<InformationRecord>)sender; InformationRecord rec = c[e.Index]; if (_psHost != null && rec.Tags != null && rec.Tags.Contains("PSHOST")) return; object md = rec.MessageData; HostInformationMessage him = md as HostInformationMessage; Log("INF " + (him != null ? him.Message : Text(md))); } catch (Exception ex) { Log("INF <" + ex.Message + ">"); } }; } // ---- the stop (pipeline thread) ------------------------------------- private Req WaitRequest(int ms) { Req r; if (_req.TryDequeue(out r)) return r; if (PumpSentMessages) { try { int p = NativeWait.Wait(_signal, ms); if (p > 0) SentMessagePasses += p; } catch (Exception ex) { PumpSentMessages = false; Log("DBG sent-message pump disabled: " + ex.Message); _signal.WaitOne(ms); } } else _signal.WaitOne(ms); if (_req.TryDequeue(out r)) return r; return null; } private void ReportThreadWindows(int seq) { try { string[] w = NativeWait.ThreadWindows(); ThreadWindowClasses = string.Join(", ", w); if (w.Length == _lastWindowCount) return; _lastWindowCount = w.Length; Log(string.Format("DBG debug thread {0} ({1}) owns {2} window(s){3}{4}", NativeWait.GetCurrentThreadId(), Thread.CurrentThread.GetApartmentState(), w.Length, w.Length > 0 ? ": " : "", string.Join(", ", w))); } catch (Exception ex) { Log("DBG thread window check failed: " + ex.Message); } } // rev 7: stops raised on a thread whose runspace is not the debug // runspace (UI thread executing a class method bound to this engine) public volatile int ForeignStops = 0; private readonly HashSet<string> _foreignLogged = new HashSet<string>(StringComparer.OrdinalIgnoreCase); public bool HoldForeignStops = false; private void OnStop(object sender, DebuggerStopEventArgs e) { if (!HoldForeignStops && !object.ReferenceEquals(Runspace.DefaultRunspace, _rs)) { ForeignStops++; DebuggerResumeAction keep = ParseAction(LastResume); if (keep == DebuggerResumeAction.Stop) keep = DebuggerResumeAction.Continue; e.ResumeAction = keep; string where = "?"; try { if (e.InvocationInfo != null) where = string.Format("{0}:{1}", e.InvocationInfo.ScriptName, e.InvocationInfo.ScriptLineNumber); } catch { } string key = where; int colon = where.LastIndexOf(':'); if (colon > 0) key = where.Substring(0, colon); bool log; lock (_foreignLogged) { log = _foreignLogged.Add(key); } if (log) Log(string.Format("DBG foreign-thread stop ignored (managed thread {0}, at {1}) - a class of a module imported into the debug runspace is shared with Explorer", Thread.CurrentThread.ManagedThreadId, where)); return; } if (_disposed || _abort) { e.ResumeAction = DebuggerResumeAction.Stop; return; } Debugger dbg = sender as Debugger; if (dbg == null) dbg = _rs.Debugger; InvocationInfo ii = e.InvocationInfo; string script = ii == null ? null : ii.ScriptName; bool hasBp = e.Breakpoints != null && e.Breakpoints.Count > 0; if (SkipScriptless && string.IsNullOrEmpty(script) && !hasBp) { e.ResumeAction = DebuggerResumeAction.Continue; Log("DBG stop outside any script file - continued"); return; } int seq = Interlocked.Increment(ref _seq); LastDebugThread = Thread.CurrentThread; UiWatchdog.Note(string.Format("stop #{0} at {1}:{2}", seq, script, ii == null ? 0 : ii.ScriptLineNumber)); ReportThreadWindows(seq); int passesBefore = SentMessagePasses; StopInfo info = Snapshot(dbg, e, seq); _current = info; _stopped = true; _stops.Enqueue(info); DebuggerResumeAction act = DebuggerResumeAction.Continue; try { while (true) { Req r = WaitRequest(200); if (r == null) { if (_disposed || _abort) { act = DebuggerResumeAction.Stop; break; } continue; } if (r.Kind == "resume") { if (r.Seq != 0 && r.Seq != seq) continue; // stale resume act = ParseAction(r.Action); break; } try { Handle(dbg, r, e, seq); } catch (Exception ex) { LastError = ex.Message; Log("DBG " + r.Kind + " failed: " + ex.Message); } } } finally { LastResume = act.ToString(); _stopped = false; UiWatchdog.Note("resumed: " + act); int n = SentMessagePasses - passesBefore; if (n > 0) Log(string.Format("DBG stop #{0}: {1} sent-message dispatch pass(es) while stopped", seq, n)); } e.ResumeAction = act; } private static DebuggerResumeAction ParseAction(string a) { if (string.IsNullOrEmpty(a)) return DebuggerResumeAction.Continue; try { return (DebuggerResumeAction)Enum.Parse(typeof(DebuggerResumeAction), a, true); } catch { return DebuggerResumeAction.Continue; } } private void Handle(Debugger dbg, Req r, DebuggerStopEventArgs e, int seq) { PSDataCollection<PSObject> output = new PSDataCollection<PSObject>(); PSCommand cmd = null; switch (r.Kind) { case "setbp": cmd = new PSCommand(); cmd.AddCommand("Set-PSBreakpoint").AddParameter("Script", r.Script).AddParameter("Line", r.Line); dbg.ProcessCommand(cmd, output); Log(string.Format("DBG breakpoint set {0}:{1}", r.Script, r.Line)); break; case "removebp": cmd = new PSCommand(); cmd.AddScript("param($s, $l) Get-PSBreakpoint -Script $s | Where-Object { $_.Line -eq $l } | Remove-PSBreakpoint").AddArgument(r.Script).AddArgument(r.Line); dbg.ProcessCommand(cmd, output); Log(string.Format("DBG breakpoint removed {0}:{1}", r.Script, r.Line)); break; case "refresh": StopInfo s = Snapshot(dbg, e, seq); _current = s; _stops.Enqueue(s); break; case "eval": cmd = new PSCommand(); cmd.AddScript(r.Text ?? "").AddCommand("Out-String"); dbg.ProcessCommand(cmd, output); foreach (PSObject o in output) Log("EVL " + Text(o)); break; } } private StopInfo Snapshot(Debugger dbg, DebuggerStopEventArgs e, int seq) { StopInfo s = new StopInfo(); s.Seq = seq; s.Utc = DateTime.UtcNow; s.Error = ""; InvocationInfo ii = e.InvocationInfo; if (ii != null) { s.Script = ii.ScriptName; s.Line = ii.ScriptLineNumber; s.Column = ii.OffsetInLine; try { s.Position = ii.PositionMessage; } catch { s.Position = ""; } } List<string> bps = new List<string>(); if (e.Breakpoints != null) foreach (Breakpoint b in e.Breakpoints) bps.Add(b.ToString()); s.Breakpoints = bps.ToArray(); s.Reason = bps.Count > 0 ? "breakpoint" : "step"; List<string> stack = new List<string>(); List<VarInfo> locals = new List<VarInfo>(); List<VarInfo> globals = new List<VarInfo>(); HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); StringBuilder err = new StringBuilder(); try { PSDataCollection<PSObject> frames = new PSDataCollection<PSObject>(); PSCommand c1 = new PSCommand(); c1.AddCommand("Get-PSCallStack"); dbg.ProcessCommand(c1, frames); CallStackFrame top = null; foreach (PSObject o in frames) { CallStackFrame f = o == null ? null : o.BaseObject as CallStackFrame; if (f == null) continue; if (top == null) top = f; stack.Add(f.ToString()); } if (top != null) { try { Dictionary<string, PSVariable> fv = top.GetFrameVariables(); foreach (KeyValuePair<string, PSVariable> kv in fv) { if (kv.Value == null || seen.Contains(kv.Value.Name)) continue; seen.Add(kv.Value.Name); AddVar(locals, kv.Value, "Local"); } } catch { } } } catch (Exception ex) { err.Append("stack: " + ex.Message + " "); } try { PSDataCollection<PSObject> lv = new PSDataCollection<PSObject>(); PSCommand c3 = new PSCommand(); c3.AddCommand("Get-Variable").AddParameter("Scope", "Local"); dbg.ProcessCommand(c3, lv); foreach (PSObject o in lv) { PSVariable v = o == null ? null : o.BaseObject as PSVariable; if (v == null || seen.Contains(v.Name)) continue; seen.Add(v.Name); AddVar(locals, v, "Local"); } } catch (Exception ex) { err.Append("locals: " + ex.Message + " "); } try { PSDataCollection<PSObject> gv = new PSDataCollection<PSObject>(); PSCommand c2 = new PSCommand(); c2.AddCommand("Get-Variable").AddParameter("Scope", "Global"); dbg.ProcessCommand(c2, gv); foreach (PSObject o in gv) { PSVariable v = o == null ? null : o.BaseObject as PSVariable; if (v != null) AddVar(globals, v, "Global"); } } catch (Exception ex) { err.Append("globals: " + ex.Message); } locals.Sort(delegate (VarInfo a, VarInfo b) { return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); }); globals.Sort(delegate (VarInfo a, VarInfo b) { return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); }); s.CallStack = stack.ToArray(); s.Locals = locals.ToArray(); s.Globals = globals.ToArray(); s.Error = err.ToString().Trim(); return s; } private void AddVar(List<VarInfo> list, PSVariable v, string scope) { if (v == null || list.Count >= MaxVariables) return; string n = v.Name ?? ""; if (n.StartsWith(HiddenPrefix, StringComparison.OrdinalIgnoreCase)) return; if (HideAutomatic) { lock (_hidden) { if (_hidden.Contains(n)) return; } } VarInfo x = new VarInfo(); x.Name = n; x.Scope = scope; object val = null; try { val = v.Value; } catch (Exception ex) { x.Type = "?"; x.Value = "<" + ex.Message + ">"; list.Add(x); return; } x.Type = TypeOf(val); x.Value = Format(val, MaxValueLength); list.Add(x); } public static string TypeOf(object v) { if (v == null) return "null"; PSObject p = v as PSObject; object b = p != null ? p.BaseObject : v; if (b is PSCustomObject) return "PSCustomObject"; if (p != null && p.TypeNames.Count > 0 && p.TypeNames[0].StartsWith("Deserialized.")) return p.TypeNames[0]; return b.GetType().Name; } public static string Format(object v, int max) { string s; try { s = FormatCore(v); } catch (Exception ex) { s = "<" + ex.GetType().Name + ": " + ex.Message + ">"; } if (s == null) s = ""; s = s.Replace("\r", " ").Replace("\n", " "); if (max > 3 && s.Length > max) s = s.Substring(0, max - 3) + "..."; return s; } private static string FormatCore(object v) { if (v == null) return "$null"; PSObject p = v as PSObject; object b = p != null ? p.BaseObject : v; if (b is string) return "\"" + (string)b + "\""; if (b is bool) return ((bool)b) ? "$true" : "$false"; if (b is ScriptBlock) return "{" + b.ToString() + "}"; IDictionary d = b as IDictionary; if (d != null) { StringBuilder sb = new StringBuilder(); sb.Append(b.GetType().Name).Append(" (").Append(d.Count).Append(") {"); int i = 0; foreach (DictionaryEntry de in d) { if (i == 5) { sb.Append(" ..."); break; } if (i > 0) sb.Append(";"); sb.Append(" ").Append(Text(de.Key)).Append(" = ").Append(Text(de.Value)); i++; } return sb.Append(" }").ToString(); } ICollection c = b as ICollection; if (c != null) { StringBuilder sb = new StringBuilder(); sb.Append(b.GetType().Name).Append(" (").Append(c.Count).Append(")"); int i = 0; foreach (object o in c) { sb.Append(i == 0 ? ": " : ", "); if (i == 5) { sb.Append("..."); break; } sb.Append(Text(o)); i++; } return sb.ToString(); } return Text(v); } public static string Text(object v) { if (v == null) return "$null"; try { string s = LanguagePrimitives.ConvertTo<string>(v); return s ?? ""; } catch { try { return v.ToString(); } catch { return "?"; } } } public void Dispose() { if (_disposed) return; _disposed = true; _abort = true; try { _signal.Set(); } catch { } try { _rs.Debugger.DebuggerStop -= OnStop; } catch { } // _signal is not disposed: a stopped handler may still wait on it. } } } '@ } #endregion #region ---- scripts that run in the debug runspace ---------------------------- function Get-SRxDebugSetupScript { <# .SYNOPSIS Script the debug runspace runs. The same three steps as the production runtime, then the code fragment is loaded by Start-Rule (= <start>). The arguments are a PARAMETER (no SessionStateProxy call). #> [CmdletBinding()] [OutputType([string])] param() return @' param($__srxdbg) # ---- 1) Initialize-SRx: the production runtime environment if (-not [string]::IsNullOrEmpty([string]$__srxdbg.Bootstrap)) { . ([scriptblock]::Create([string]$__srxdbg.Bootstrap)) } elseif ($null -ne (Get-Command -Name Initialize-SRxEnv -ErrorAction SilentlyContinue)) { $__srxdbgInit = @{ NoHostUI = $true } if (-not [string]::IsNullOrEmpty([string]$__srxdbg.RootPath)) { $__srxdbgInit['RootPath'] = [string]$__srxdbg.RootPath } if (-not [string]::IsNullOrEmpty([string]$__srxdbg.SourceId)) { $__srxdbgInit['SourceID'] = [string]$__srxdbg.SourceId } $null = Initialize-SRxEnv @__srxdbgInit } else { Write-Warning 'SRx debugger: Initialize-SRxEnv not found - the code runs without the SRx environment' } if ($null -eq $global:SRxEnv) { $global:SRxEnv = [pscustomobject]@{ Pipeline = $null } } # ---- 2) copy of the pipeline $__srxdbgPipe = @() if (-not [string]::IsNullOrEmpty([string]$__srxdbg.PipelineXml)) { $__srxdbgPipe = @([System.Management.Automation.PSSerializer]::Deserialize([string]$__srxdbg.PipelineXml)) } if ($null -ne $global:SRxEnv.PSObject.Methods['SetCustomProperty']) { $global:SRxEnv.SetCustomProperty('Pipeline', $__srxdbgPipe) } else { $global:SRxEnv | Add-Member -NotePropertyName Pipeline -NotePropertyValue $__srxdbgPipe -Force } # ---- 3) breakpoints: automatic first line + the user's foreach ($__srxdbgBp in @($__srxdbg.Breakpoints)) { if ($null -eq $__srxdbgBp) { continue } $null = Set-PSBreakpoint -Script ([string]$__srxdbgBp.Path) -Line ([int]$__srxdbgBp.Line) } # ---- 4) load the code fragment the way <start> does if (-not [string]::IsNullOrEmpty([string]$__srxdbg.Command)) { . ([scriptblock]::Create([string]$__srxdbg.Command)) } elseif (-not [string]::IsNullOrEmpty([string]$__srxdbg.RuleName)) { Start-Rule -Name ([string]$__srxdbg.RuleName) } else { . ([string]$__srxdbg.Path) } '@ } function Get-SRxDebugTeardownScript { <# .SYNOPSIS rev 5: runs in the debug runspace before it is disposed. Releases what SRxCore / the rule registered from inside the runspace, so nothing in Explorer keeps calling into a disposed runspace. Returns one summary line. #> [CmdletBinding()] [OutputType([string])] param() return @' $__srxdbgR = New-Object System.Collections.Generic.List[string] foreach ($__srxdbgCmd in @('Disconnect-SRxJobsChild')) { if ($null -eq (Get-Command -Name $__srxdbgCmd -ErrorAction SilentlyContinue)) { $__srxdbgR.Add("$__srxdbgCmd not available"); continue } try { $null = & $__srxdbgCmd -ErrorAction Stop; $__srxdbgR.Add("$__srxdbgCmd done") } catch { $__srxdbgR.Add("$__srxdbgCmd failed: $($_.Exception.Message)") } } $__srxdbgSubs = @(Get-EventSubscriber -Force -ErrorAction SilentlyContinue) $__srxdbgNames = New-Object System.Collections.Generic.List[string] foreach ($__srxdbgS in $__srxdbgSubs) { $__srxdbgSrc = '?' try { if ($null -ne $__srxdbgS.SourceObject) { $__srxdbgSrc = $__srxdbgS.SourceObject.GetType().Name } else { $__srxdbgSrc = 'engine' } } catch { } $__srxdbgEvt = [string]$__srxdbgS.EventName if ([string]::IsNullOrEmpty($__srxdbgEvt)) { $__srxdbgEvt = [string]$__srxdbgS.SourceIdentifier } $__srxdbgNames.Add(('{0}.{1}' -f $__srxdbgSrc, $__srxdbgEvt)) try { Unregister-Event -SubscriptionId $__srxdbgS.SubscriptionId -Force -ErrorAction Stop } catch { } } $__srxdbgR.Add(('{0} event subscriber(s) removed{1}' -f $__srxdbgSubs.Count, $(if ($__srxdbgNames.Count -gt 0) { ': ' + ($__srxdbgNames -join ', ') } else { '' }))) $__srxdbgJobs = @(Get-Job -ErrorAction SilentlyContinue) foreach ($__srxdbgJ in $__srxdbgJobs) { try { Stop-Job -Job $__srxdbgJ -ErrorAction SilentlyContinue; Remove-Job -Job $__srxdbgJ -Force -ErrorAction SilentlyContinue } catch { } } $__srxdbgR.Add(('{0} PS job(s) removed' -f $__srxdbgJobs.Count)) $__srxdbgR -join '; ' '@ } #endregion #region ---- SRxDebugSession (one run of one file) ----------------------------- class SRxDebugSession { [string] $Id = '' [string] $Path = '' [string] $Kind = '' [string] $RuleName = '' [string] $Title = '' [string] $State = 'Created' # Created | Opening | Running | Stopped | Stopping | Ended [string] $EndState = '' # Completed | Failed | Stopped [string] $EndReason = '' [string] $TeardownResult = '' # rev 5 [int] $FirstLine = 1 [int] $Stops = 0 [bool] $HitFile = $false [object] $LastStop = $null # SRxDebug7.StopInfo [hashtable]$Spec = @{} [int] $StopTimeoutMs = 5000 [int] $ReadyTimeoutMs = 30000 [int] $TeardownTimeoutMs = 3000 # rev 5 [int] $StartAttempts = 0 [int] $MaxEventsPerPoll = 300 [int] $ErrorCount = 0 [string] $LastErrorText = '' [System.Collections.Generic.List[string]] $Output [scriptblock] $OnTrace = $null [object] $Orphan = $null hidden [object] $_owner = $null hidden [object] $_rs = $null hidden [object] $_ps = $null hidden [object] $_host = $null # SRxDebug7.DebugHost hidden [object] $_psHost = $null # SRxDebug7.DebugPSHost hidden [object] $_async = $null hidden [object] $_out = $null hidden [hashtable] $_args = $null hidden [bool] $_invoked = $false hidden [bool] $_stopRequested = $false hidden [datetime] $_stopRequestedUtc = [datetime]::MinValue hidden [datetime] $_openedUtc = [datetime]::MinValue hidden [string] $_lastBusy = '' hidden [bool] $_endReported = $false hidden [System.Collections.ArrayList] $_pendingEvents SRxDebugSession([object]$owner, [hashtable]$spec) { $this._owner = $owner $this.Spec = $spec $this.Id = [guid]::NewGuid().ToString('N') $this.Output = New-Object 'System.Collections.Generic.List[string]' $this._pendingEvents = New-Object System.Collections.ArrayList $this.Path = [System.IO.Path]::GetFullPath([string]$spec['Path']) $this.Kind = [string]$spec['Kind'] $this.RuleName = [string]$spec['RuleName'] $this.Title = [string]$spec['Title'] if ([string]::IsNullOrEmpty($this.Title)) { $this.Title = [System.IO.Path]::GetFileName($this.Path) } } [void] Start() { if ($this.State -ne 'Created') { throw 'SRxDebugSession.Start: already started.' } if (-not [System.IO.File]::Exists($this.Path)) { throw ("SRxDebugSession.Start: file not found: {0}" -f $this.Path) } $this.FirstLine = [SRxDebugger]::FirstExecutableLine($this.Path) $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() try { $iss.ExecutionPolicy = 'Bypass' } catch { } $isolate = $true if ($null -ne $this._owner) { $isolate = [bool]$this._owner.IsolateModules } foreach ($m in @($this.Spec['Modules'])) { if ([string]::IsNullOrWhiteSpace([string]$m)) { continue } $modPath = [string]$m if ($isolate) { try { $modPath = [SRxDebugger]::IsolatedModulePath($modPath, [string]$this._owner.ModuleCacheRoot) ; $this.trace(("module {0} isolated: {1}" -f [System.IO.Path]::GetFileNameWithoutExtension([string]$m), $modPath)) } catch { $modPath = [string]$m; $this.trace(("module '{0}' NOT isolated ({1}) - Explorer's classes may be shared with the debug runspace" -f $m, $_.Exception.Message)) } } try { [void]$iss.ImportPSModule([string[]]@($modPath)) } catch { $this.trace("module '$modPath' not queued: $($_.Exception.Message)") } } $this._psHost = New-Object -TypeName 'SRxDebug7.DebugPSHost' $rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($this._psHost, $iss) try { $rs.ApartmentState = [System.Threading.ApartmentState]::STA } catch { } try { $rs.ThreadOptions = [System.Management.Automation.Runspaces.PSThreadOptions]::ReuseThread } catch { } try { $rs.Name = 'SRxDebugger' } catch { } $this._rs = $rs $this.State = 'Opening' $rs.OpenAsync() $this.trace(("session {0}: opening the debug runspace for '{1}' (first line {2}, {3} module(s))" -f $this.Id, $this.Path, $this.FirstLine, @($this.Spec['Modules']).Count)) } hidden [void] prepare() { if ($null -ne $this._host) { return } $rs = $this._rs try { $rs.Debugger.SetDebugMode([System.Management.Automation.DebugModes]::LocalScript) } catch { } $h = New-Object -TypeName 'SRxDebug7.DebugHost' -ArgumentList $rs if ($null -ne $this._owner) { $h.HideAutomatic = [bool]$this._owner.HideAutomaticVariables $h.MaxValueLength = [int]$this._owner.MaxValueLength $h.PumpSentMessages = [bool]$this._owner.PumpSentMessages } $h.AttachPSHost($this._psHost) $this._host = $h $bps = New-Object System.Collections.ArrayList [void]$bps.Add(@{ Path = $this.Path; Line = $this.FirstLine }) if ($null -ne $this._owner) { foreach ($b in @($this._owner.AllBreakpoints())) { if ([string]::Equals([string]$b.Path, $this.Path, [System.StringComparison]::OrdinalIgnoreCase) -and [int]$b.Line -eq $this.FirstLine) { continue } [void]$bps.Add($b) } } $xml = '' if ($null -ne $this.Spec['Pipeline']) { $xml = [System.Management.Automation.PSSerializer]::Serialize([object[]]@($this.Spec['Pipeline']), 6) } $this._args = @{ Path = $this.Path Kind = $this.Kind RuleName = $this.RuleName RootPath = [string]$this.Spec['RootPath'] SourceId = [string]$this.Spec['SourceId'] PipelineXml = $xml Breakpoints = [object[]]$bps.ToArray() Bootstrap = [string]$this.Spec['Bootstrap'] Command = [string]$this.Spec['Command'] } } hidden [bool] tryInvoke() { $this.prepare() $rs = $this._rs $avail = '' try { $avail = [string]$rs.RunspaceAvailability } catch { $avail = '' } if (-not [string]::IsNullOrEmpty($avail) -and $avail -ne 'Available') { if ($avail -ne $this._lastBusy) { $this.trace(("session {0}: runspace {1} after open - waiting" -f $this.Id, $avail)) } $this._lastBusy = $avail return $false } $this.StartAttempts++ $ps = [System.Management.Automation.PowerShell]::Create() $ps.Runspace = $rs [void]$ps.AddScript((Get-SRxDebugSetupScript)).AddArgument($this._args) $in = New-Object 'System.Management.Automation.PSDataCollection[psobject]' $in.Complete() $out = New-Object 'System.Management.Automation.PSDataCollection[psobject]' $this._host.AttachStreams($ps, $out) try { $this._async = $ps.BeginInvoke($in, $out) } catch { $msg = [string]$_.Exception.Message if ($null -ne $_.Exception.InnerException) { $msg += ' ' + [string]$_.Exception.InnerException.Message } try { $ps.Dispose() } catch { } if ($msg -match 'already running|concurrent|cannot be run concurrently|busy') { $this.trace(("session {0}: start attempt {1} - runspace busy ({2}) - retrying" -f $this.Id, $this.StartAttempts, $msg)) return $false } throw } $this._ps = $ps $this._out = $out $this._invoked = $true $this.State = 'Running' $this.trace(("session {0}: running ({1} breakpoint(s), start attempt {2}, {3:N0} ms after open)" -f $this.Id, @($this._args.Breakpoints).Count, $this.StartAttempts, ([datetime]::UtcNow - $this._openedUtc).TotalMilliseconds)) return $true } [object[]] Poll() { $ev = New-Object System.Collections.ArrayList foreach ($p in @($this._pendingEvents)) { [void]$ev.Add($p) } $this._pendingEvents.Clear() if ($this._endReported) { return [object[]]$ev.ToArray() } if ($this.State -eq 'Opening') { $st = [string]$this._rs.RunspaceStateInfo.State if ($st -eq 'Opened') { if ($this._openedUtc -eq [datetime]::MinValue) { $this._openedUtc = [datetime]::UtcNow } $started = $false try { $started = $this.tryInvoke() } catch { $this.finish('Failed', ("debugger start failed: {0}" -f $_.Exception.Message)) [void]$ev.Add($this.endedEvent()) return [object[]]$ev.ToArray() } if ($started) { [void]$ev.Add([pscustomobject]@{ Type = 'opened'; Text = ''; Stream = ''; Stop = $null; State = ''; Reason = '' }) } elseif (([datetime]::UtcNow - $this._openedUtc).TotalMilliseconds -gt $this.ReadyTimeoutMs) { $this.finish('Failed', ("debugger start failed: the debug runspace stayed busy ({0}) for {1} s after it opened - a module imported into it keeps running pipelines" -f $this._lastBusy, [int]($this.ReadyTimeoutMs / 1000))) [void]$ev.Add($this.endedEvent()) return [object[]]$ev.ToArray() } else { $this.drainLog($ev) return [object[]]$ev.ToArray() } } elseif ($st -eq 'Broken' -or $st -eq 'Closed') { $why = '' try { $why = [string]$this._rs.RunspaceStateInfo.Reason.Message } catch { $why = '' } $this.finish('Failed', ("the debug runspace could not be opened: {0}" -f $why)) [void]$ev.Add($this.endedEvent()) return [object[]]$ev.ToArray() } else { return [object[]]$ev.ToArray() } } if ($null -eq $this._host) { return [object[]]$ev.ToArray() } $this.drainLog($ev) $s = $null $gotStop = $false while ($this._host.TryDequeueStop([ref]$s)) { $gotStop = $true $this.LastStop = $s $this.Stops++ if ([string]::Equals([string]$s.Script, $this.Path, [System.StringComparison]::OrdinalIgnoreCase)) { $this.HitFile = $true } $this.State = 'Stopped' if ($this._stopRequested) { $this.State = 'Stopping' } [void]$ev.Add([pscustomobject]@{ Type = 'stopped'; Text = ''; Stream = ''; Stop = $s; State = 'Stopped'; Reason = [string]$s.Reason }) } if (-not $gotStop -and $this.State -eq 'Stopped' -and -not $this._host.IsStopped) { $this.State = 'Running' [void]$ev.Add([pscustomobject]@{ Type = 'running'; Text = [string]$this._host.LastResume; Stream = ''; Stop = $null; State = 'Running'; Reason = '' }) } $done = $false if ($null -ne $this._async) { try { $done = [bool]$this._async.IsCompleted } catch { $done = $false } } if ($done) { $err = '' try { [void]$this._ps.EndInvoke($this._async) } catch { $err = $_.Exception.Message } try { $this._host.FlushHost() } catch { } $this.drainLog($ev) $inv = '' $why = '' try { $inv = [string]$this._ps.InvocationStateInfo.State } catch { $inv = '' } try { if ($null -ne $this._ps.InvocationStateInfo.Reason) { $why = [string]$this._ps.InvocationStateInfo.Reason.Message } } catch { $why = '' } if ([string]::IsNullOrEmpty($why)) { $why = $err } $end = 'Completed' if ($inv -eq 'Failed') { $end = 'Failed' } elseif ($inv -eq 'Stopped' -or $this._stopRequested) { $end = 'Stopped'; $why = 'stopped by the user' } $this.finish($end, $why) [void]$ev.Add($this.endedEvent()) } elseif ($this._stopRequested -and ([datetime]::UtcNow - $this._stopRequestedUtc).TotalMilliseconds -gt $this.StopTimeoutMs) { $this.finish('Stopped', ("the run did not stop within {0} ms - debug runspace closed" -f $this.StopTimeoutMs)) [void]$ev.Add($this.endedEvent()) } return [object[]]$ev.ToArray() } hidden [void] drainLog([System.Collections.ArrayList]$ev) { if ($null -eq $this._host) { return } $line = $null $n = 0 while ($n -lt $this.MaxEventsPerPoll -and $this._host.TryDequeueLog([ref]$line)) { $n++ $text = [string]$line $stream = 'OUT' if ($text.Length -ge 4 -and $text[3] -eq ' ') { $stream = $text.Substring(0, 3); $text = $text.Substring(4) } if ($stream -eq 'ERR') { $this.ErrorCount++; $this.LastErrorText = $text } $this.Output.Add(('{0} {1}' -f $stream, $text)) while ($this.Output.Count -gt 2000) { $this.Output.RemoveAt(0) } [void]$ev.Add([pscustomobject]@{ Type = 'output'; Text = $text; Stream = $stream; Stop = $null; State = ''; Reason = '' }) } } hidden [object] endedEvent() { $this._endReported = $true return [pscustomobject]@{ Type = 'ended'; Text = ''; Stream = ''; Stop = $this.LastStop; State = $this.EndState; Reason = $this.EndReason } } hidden [void] finish([string]$endState, [string]$reason) { if ($this.State -eq 'Ended') { return } $this.State = 'Ended' $this.EndState = $endState $this.EndReason = $reason $this.TeardownResult = $this.teardown() if (-not [string]::IsNullOrEmpty($this.TeardownResult)) { $this.trace(("session {0}: teardown: {1}" -f $this.Id, $this.TeardownResult)) } $this.dispose($endState -ne 'Completed' -and -not ($null -ne $this._async -and [bool]$this._async.IsCompleted)) $this.trace(("session {0}: ended {1} ({2} stop(s)) {3}" -f $this.Id, $endState, $this.Stops, $reason)) } # rev 5: release what was registered from inside the runspace. Only when # the runspace is idle (a stopped / busy one is abandoned as before). hidden [string] teardown() { $rs = $this._rs if ($null -eq $rs) { return '' } $st = '' $avail = '' try { $st = [string]$rs.RunspaceStateInfo.State; $avail = [string]$rs.RunspaceAvailability } catch { return '' } if ($st -ne 'Opened') { return '' } if ($avail -ne 'Available') { return ("skipped - runspace {0}" -f $avail) } $ps = $null try { $ps = [System.Management.Automation.PowerShell]::Create() $ps.Runspace = $rs [void]$ps.AddScript((Get-SRxDebugTeardownScript)) $a = $ps.BeginInvoke() if (-not $a.AsyncWaitHandle.WaitOne($this.TeardownTimeoutMs)) { try { [void]$ps.BeginStop($null, $null) } catch { } $ps = $null return ("timed out after {0} ms" -f $this.TeardownTimeoutMs) } return (@($ps.EndInvoke($a)) -join '; ') } catch { return ("failed: {0}" -f $_.Exception.Message) } finally { if ($null -ne $ps) { try { $ps.Dispose() } catch { } } } } hidden [void] dispose([bool]$abandon) { if ($null -ne $this._host) { try { $this._host.Dispose() } catch { } } if (-not $abandon -and $null -ne $this._ps) { try { $this._ps.Dispose() } catch { } } if ($null -ne $this._rs) { if ($abandon) { $closing = $false try { $this._rs.CloseAsync(); $closing = $true } catch { $closing = $false } if (-not $closing) { $this.Orphan = $this._rs } } else { try { $this._rs.Dispose() } catch { } } } $this._ps = $null $this._rs = $null } [bool] IsActive() { return ($this.State -ne 'Ended' -and $this.State -ne 'Created') } [bool] IsStopped() { return ($this.State -eq 'Stopped') } [bool] Resume([string]$action) { if ($action -eq 'Stop') { $this.Stop(); return $true } if ($this.State -ne 'Stopped' -or $null -eq $this._host) { return $false } if ($action -notin @('Continue', 'StepOver', 'StepInto', 'StepOut')) { throw "SRxDebugSession.Resume: unknown action '$action'." } $ok = [bool]$this._host.Resume($action) if ($ok) { $this.trace(("session {0}: {1}" -f $this.Id, $action)) } return $ok } [void] Stop() { if ($this.State -eq 'Ended' -or $this.State -eq 'Created') { return } if ($this.State -eq 'Opening') { $this._stopRequested = $true $this.finish('Stopped', 'cancelled before the code started') [void]$this._pendingEvents.Add($this.endedEvent()) return } if ($this._stopRequested) { return } $this._stopRequested = $true $this._stopRequestedUtc = [datetime]::UtcNow $this.State = 'Stopping' if ($null -ne $this._host) { $this._host.Abort() } if ($null -ne $this._ps) { try { [void]$this._ps.BeginStop($null, $null) } catch { } } $this.trace(("session {0}: stop requested" -f $this.Id)) } [void] ApplyBreakpoint([string]$path, [int]$line, [bool]$on) { if ($this.State -eq 'Ended' -or $null -eq $this._host) { return } if (-not $this._invoked) { if ($null -ne $this._args) { $list = New-Object System.Collections.ArrayList foreach ($b in @($this._args.Breakpoints)) { if ([string]::Equals([string]$b.Path, $path, [System.StringComparison]::OrdinalIgnoreCase) -and [int]$b.Line -eq $line) { continue } [void]$list.Add($b) } if ($on) { [void]$list.Add(@{ Path = $path; Line = $line }) } $this._args.Breakpoints = [object[]]$list.ToArray() } return } if ($on) { $this._host.SetBreakpoint($path, $line) } else { $this._host.RemoveBreakpoint($path, $line) } } [void] Refresh() { if ($this.State -eq 'Stopped' -and $null -ne $this._host) { $this._host.Refresh() } } # diagnostics / tests (rev 5) [int] SentMessagePasses() { if ($null -eq $this._host) { return 0 }; return [int]$this._host.SentMessagePasses } [string] ThreadWindowClasses() { if ($null -eq $this._host) { return '' }; return [string]$this._host.ThreadWindowClasses } [int] ForeignStops() { if ($null -eq $this._host) { return 0 }; return [int]$this._host.ForeignStops } hidden [void] trace([string]$line) { if ($null -ne $this.OnTrace) { try { & $this.OnTrace $line } catch { } } } } #endregion #region ---- SRxDebugger (engine: one session at a time + breakpoint store) ----- class SRxDebugger { [object] $Session = $null [hashtable] $Breakpoints = @{} [scriptblock] $OnTrace = $null [bool] $HideAutomaticVariables = $true [bool] $PumpSentMessages = $true # rev 5 [bool] $WatchUiThread = $true # rev 6: UI-thread hang watchdog (WinForms UI thread only) [bool] $IsolateModules = $true # rev 7: modules loaded from a private copy (own class types) [string] $ModuleCacheRoot = '' # rev 7: '' = %TEMP%\SRxDebugger\modules [string] $WatchdogLog = '' # rev 6: '' = %TEMP%\SRxDebugger-hang-<pid>.log [int] $MaxValueLength = 400 [int] $ReadyTimeoutMs = 30000 [string] $Bootstrap = '' [string[]] $KeyCommands = @('Initialize-SRxEnv', 'Start-Rule', 'Set-SRxPipeline') hidden [System.Collections.ArrayList] $_orphans = (New-Object System.Collections.ArrayList) [bool] IsActive() { return ($null -ne $this.Session -and $this.Session.IsActive()) } [object] StartSession([hashtable]$spec) { if ($null -eq $spec -or [string]::IsNullOrWhiteSpace([string]$spec['Path'])) { throw 'StartSession: Path is required.' } if ($this.IsActive()) { throw ("StartSession: '{0}' is being debugged - stop that session first." -f $this.Session.Title) } $s2 = @{} foreach ($k in $spec.Keys) { $s2[[string]$k] = $spec[$k] } if (-not $s2.ContainsKey('Modules') -or $null -eq $s2['Modules']) { $s2['Modules'] = $this.DefaultModules() } if ([string]::IsNullOrEmpty([string]$s2['Bootstrap'])) { $s2['Bootstrap'] = $this.Bootstrap } $this.ensureWatchdog() $s = [SRxDebugSession]::new($this, $s2) $s.OnTrace = $this.OnTrace $s.ReadyTimeoutMs = $this.ReadyTimeoutMs $this.Session = $s try { $s.Start() } catch { $this.Session = $null; throw } return $s } [object[]] Pump() { $s = $this.Session if ($null -eq $s) { $this.closeOrphans(); return @() } $ev = @($s.Poll()) if ($s.State -eq 'Ended') { if ($null -ne $s.Orphan) { [void]$this._orphans.Add($s.Orphan); $s.Orphan = $null } $this.Session = $null } $this.closeOrphans() return [object[]]$ev } [void] Shutdown([int]$waitMs) { $s = $this.Session if ($null -eq $s) { return } $s.Stop() $sw = [System.Diagnostics.Stopwatch]::StartNew() while ($s.State -ne 'Ended' -and $sw.ElapsedMilliseconds -lt $waitMs) { [void]$s.Poll() Start-Sleep -Milliseconds 50 } if ($s.State -ne 'Ended') { [void]$s.Poll() } $this.Session = $null } # rev 6: the watchdog needs the UI thread's WindowsForms context. hidden [void] ensureWatchdog() { if (-not $this.WatchUiThread) { return } $ctx = [System.Threading.SynchronizationContext]::Current if ($null -eq $ctx -or $ctx.GetType().Name -ne 'WindowsFormsSynchronizationContext') { return } try { # rev 6.1: no [SRxDebug7.*] type literal inside a class - class bodies are # resolved when the module is PARSED, before Add-Type has compiled the type. $wd = 'SRxDebug7.UiWatchdog' -as [type] if ($null -eq $wd) { $this.trace('UI watchdog type not loaded'); return } $wd::Start($this.WatchdogLog) $this.trace(("UI watchdog active - hang reports go to {0}" -f $wd::LogPath)) } catch { $this.trace(("UI watchdog not started: {0}" -f $_.Exception.Message)) } } hidden [void] closeOrphans() { for ($i = $this._orphans.Count - 1; $i -ge 0; $i--) { $rs = $this._orphans[$i] $st = '' try { $st = [string]$rs.RunspaceStateInfo.State } catch { $st = 'Closed' } if ($st -eq 'Opening' -or $st -eq 'BeforeOpen') { continue } if ($st -eq 'Opened') { try { $rs.CloseAsync() } catch { } } $this._orphans.RemoveAt($i) } } [string[]] DefaultModules() { $list = New-Object System.Collections.Generic.List[string] foreach ($n in @($this.KeyCommands)) { $c = $null try { $c = Get-Command -Name $n -ErrorAction SilentlyContinue | Select-Object -First 1 } catch { $c = $null } if ($null -eq $c -or $null -eq $c.Module) { continue } $m = $c.Module $p = '' try { $psd = [System.IO.Path]::Combine([string]$m.ModuleBase, ([string]$m.Name + '.psd1')) if ([System.IO.File]::Exists($psd)) { $p = $psd } else { $p = [string]$m.Path } } catch { $p = [string]$m.Path } if (-not [string]::IsNullOrEmpty($p) -and -not $list.Contains($p)) { $list.Add($p) } } return $list.ToArray() } hidden [string] bpKey([string]$path) { return ([System.IO.Path]::GetFullPath($path)).ToLowerInvariant() } [int[]] GetBreakpoints([string]$path) { if ([string]::IsNullOrWhiteSpace($path)) { return @() } $e = $this.Breakpoints[$this.bpKey($path)] if ($null -eq $e) { return @() } return [int[]]@($e['Lines'] | Sort-Object) } [object[]] AllBreakpoints() { $list = New-Object System.Collections.ArrayList foreach ($e in @($this.Breakpoints.Values)) { foreach ($l in @($e['Lines'])) { [void]$list.Add(@{ Path = [string]$e['Path']; Line = [int]$l }) } } return [object[]]$list.ToArray() } [int[]] ToggleBreakpoint([string]$path, [int]$line) { if ([string]::IsNullOrWhiteSpace($path) -or $line -le 0) { return $this.GetBreakpoints($path) } $full = [System.IO.Path]::GetFullPath($path) $key = $full.ToLowerInvariant() $e = $this.Breakpoints[$key] if ($null -eq $e) { $e = @{ Path = $full; Lines = (New-Object System.Collections.Generic.List[int]) }; $this.Breakpoints[$key] = $e } $on = -not $e['Lines'].Contains($line) if ($on) { $e['Lines'].Add($line) } else { [void]$e['Lines'].Remove($line) } if ($e['Lines'].Count -eq 0) { $this.Breakpoints.Remove($key) } if ($this.IsActive()) { $this.Session.ApplyBreakpoint($full, $line, $on) } $this.trace(("breakpoint {0} {1}:{2}" -f $(if ($on) { 'set' } else { 'removed' }), $full, $line)) return $this.GetBreakpoints($full) } [int[]] SetBreakpoints([string]$path, [int[]]$lines) { if ([string]::IsNullOrWhiteSpace($path)) { return @() } $full = [System.IO.Path]::GetFullPath($path) $old = @($this.GetBreakpoints($full)) $new = @(@($lines) | Where-Object { [int]$_ -gt 0 } | ForEach-Object { [int]$_ } | Sort-Object -Unique) foreach ($l in $old) { if ($new -notcontains $l) { [void]$this.ToggleBreakpoint($full, [int]$l) } } foreach ($l in $new) { if ($old -notcontains $l) { [void]$this.ToggleBreakpoint($full, [int]$l) } } return $this.GetBreakpoints($full) } [void] ClearBreakpoints([string]$path) { foreach ($l in @($this.GetBreakpoints($path))) { [void]$this.ToggleBreakpoint($path, [int]$l) } } # rev 7: private copy of a module's folder, so the debug runspace compiles # its OWN class types. The copy is keyed by the folder path and the newest # file write time - reused while the module is unchanged. Returns the path # of the same file inside the copy. static [string] IsolatedModulePath([string]$modulePath, [string]$cacheRoot) { $full = [System.IO.Path]::GetFullPath($modulePath) if (-not [System.IO.File]::Exists($full)) { throw "module file not found: $full" } $base = [System.IO.Path]::GetDirectoryName($full) $files = @([System.IO.Directory]::GetFiles($base, '*', [System.IO.SearchOption]::AllDirectories)) if ($files.Count -gt 5000) { throw ("module folder too large to copy ({0} files): {1}" -f $files.Count, $base) } $newest = [long]0 foreach ($f in $files) { $t = [System.IO.File]::GetLastWriteTimeUtc($f).Ticks; if ($t -gt $newest) { $newest = $t } } $sha = [System.Security.Cryptography.SHA1]::Create() $bytes = $sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes(($base.ToLowerInvariant() + '|' + [string]$newest + '|' + [string]$files.Count))) $sha.Dispose() $hash = [System.BitConverter]::ToString($bytes).Replace('-', '').Substring(0, 16).ToLowerInvariant() $root = $cacheRoot if ([string]::IsNullOrWhiteSpace($root)) { $root = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'SRxDebugger\modules') } $leafName = [System.IO.Path]::GetFileName($base) $target = [System.IO.Path]::Combine([System.IO.Path]::Combine($root, $hash), $leafName) $done = [System.IO.Path]::Combine($target, '.srxdbg-complete') if (-not [System.IO.File]::Exists($done)) { if ([System.IO.Directory]::Exists($target)) { [System.IO.Directory]::Delete($target, $true) } foreach ($f in $files) { $rel = $f.Substring($base.Length).TrimStart('\') $dst = [System.IO.Path]::Combine($target, $rel) [void][System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($dst)) [System.IO.File]::Copy($f, $dst, $true) } [System.IO.File]::WriteAllText($done, $full) } return [System.IO.Path]::Combine($target, $full.Substring($base.Length).TrimStart('\')) } static [int] FirstExecutableLine([string]$path) { $tokens = $null $errors = $null $ast = $null try { $ast = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$errors) } catch { $ast = $null } if ($null -eq $ast) { return 1 } foreach ($blk in @($ast.BeginBlock, $ast.ProcessBlock, $ast.EndBlock)) { if ($null -eq $blk) { continue } foreach ($st in @($blk.Statements)) { if ($null -eq $st) { continue } return [int]$st.Extent.StartLineNumber } } return 1 } [string] NodeId([string]$kind, [string]$key) { $sha = [System.Security.Cryptography.SHA1]::Create() $bytes = $sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes(($kind + '|' + $key).ToLowerInvariant())) $sha.Dispose() $hex = [System.BitConverter]::ToString($bytes).Replace('-', '').Substring(0, 16).ToLowerInvariant() return ('srxdbg-{0}-{1}' -f $kind.ToLowerInvariant(), $hex) } hidden [void] trace([string]$line) { if ($null -ne $this.OnTrace) { try { & $this.OnTrace $line } catch { } } } } #endregion function New-SRxDebugger { [CmdletBinding()] [OutputType([object])] param() return [SRxDebugger]::new() } function Get-SRxFirstExecutableLine { [CmdletBinding()] [OutputType([int])] param([Parameter(Mandatory)][string]$Path) return [SRxDebugger]::FirstExecutableLine($Path) } function Start-SRxUiWatchdog { <# .SYNOPSIS rev 6: start the UI-thread hang watchdog manually (call on the UI thread). #> [CmdletBinding()] param([string]$LogPath = '', [int]$HangMs = 3000) $wd = 'SRxDebug7.UiWatchdog' -as [type] $wd::HangMs = $HangMs $wd::Start($LogPath) return $wd::LogPath } Export-ModuleMember -Function New-SRxDebugger, Get-SRxFirstExecutableLine, Get-SRxDebugSetupScript, Get-SRxDebugTeardownScript, Start-SRxUiWatchdog |