◐ Shell
clean mode source ↗

Add bounded-wait timeout support to hosting API by SufficientDaikon · Pull Request #27027 · PowerShell/PowerShell

PR Summary

Hosting applications can now set a timeout on PowerShell.Invoke() and PowerShell.Stop() so that a runaway script cannot hang the host process indefinitely.

xUnit Pester breaking RFC

Important

This PR adds new public API surface. An RFC has been filed at PowerShell-RFC#409. Not ready to merge until the RFC is accepted.

This PR adds bounded alternatives to the critical unbounded WaitOne()/Wait() calls in the hosting API and introduces two opt-in public API members:

New Member Signature Default
PSInvocationSettings.Timeout TimeSpan { get; set; } Timeout.InfiniteTimeSpan
PowerShell.Stop(TimeSpan) void Stop(TimeSpan timeout)

Backwards compatibility: The default InfiniteTimeSpan preserves existing behavior — code that does not set Timeout takes the original same-thread code path with no extra allocations or thread switches.

Fixes #26594. Addresses #24289. Foundation for #19685.

What Changed

File Lines Change
PowerShell.cs +91 / -5 PSInvocationSettings.Timeout, Stop(TimeSpan), bounded Invoke via Task.Run + Wait(timeout), pool acquisition + batch timeouts
ConnectionBase.cs +37 / -5 Parallel StopPipelines(TimeSpan) via Task.Run + Task.WaitAll, 30s runspace-open wait
LocalConnection.cs +28 / -2 30s close/job waits, Dispose() catches TimeoutException → forces Broken state
LocalPipeline.cs +6 / -1 30s PipelineFinishedEvent.WaitOne
PowerShellStrings.resx +7 / -0 OperationTimedOut, StopTimedOut resource strings
RunspaceStrings.resx +4 / -0 StopPipelinesTimedOut resource string

Execution Flow

When Timeout is set to a finite value, Invoke() dispatches execution to a thread pool thread and joins with a bounded wait:

flowchart TD
    A["ps.Invoke()"] --> B{"Timeout\nset?"}
    B -->|"InfiniteTimeSpan\n(default)"| C["Same-thread path\n— original code, unchanged"]
    B -->|"Finite timeout"| D["Task.Run(worker)"]
    D --> E{"invokeTask\n.Wait(timeout)"}
    E -->|Completed| F["Return results"]
    E -->|Expired| G["CoreStop()"]
    G --> H["throw TimeoutException"]
Loading

Runspace Lifecycle

Internal waits (Close(), StopPipelines(), Dispose()) are now bounded to 30 seconds. If cleanup times out, the runspace transitions to Broken state to release resources:

stateDiagram-v2
    [*] --> Open
    Open --> Closing : Close()
    Closing --> Closed : Completes within 30s
    Closing --> Broken : TimeoutException
    Open --> Broken : Dispose() timeout
    Closed --> [*]
    Broken --> [*] : Resources released
Loading

Tests

xUnit Pester scenarios

Test inventory
Suite Count Location
xUnit C# 19 facts test/xUnit/csharp/test_Timeout.cs
Pester (CI tagged) 15 tests test/powershell/engine/Api/Timeout.Tests.ps1
Adversarial scenarios 8 scripts Real-world hang conditions — sleep loops, nested invocations, concurrent stop+invoke, pool exhaustion

Coverage: REQ-01 (basic timeout) through REQ-10 (pool exhaustion), including edge cases for double-dispose, broken runspace recovery, and nested timeout propagation.

Caution

STA COM caveat: When Timeout is finite, Invoke() dispatches work to a ThreadPool (MTA) thread via Task.Run. Scripts that depend on STA COM apartment state should leave Timeout at its default InfiniteTimeSpan, which uses the original same-thread path unchanged.


PR Context

The PowerShell hosting API (System.Management.Automation.PowerShell) is used by VS Code, Azure Functions, Azure Automation, Jupyter notebooks, and thousands of custom C# applications. When a script hangs or deadlocks, every WaitOne() call blocks indefinitely — the host has no way to recover short of killing the process.

Consumer Problem today With this PR
VS Code PowerShell Extension Must Process.Kill() when the integrated console hangs Set a finite timeout; get a TimeoutException and recover gracefully
Azure Functions Stuck scripts hold pool slots forever, requiring app pool recycle Pool acquisition timeout prevents total resource exhaustion
Custom C# hosts Thread.Abort does not exist in .NET Core — no timeout mechanism First-class PSInvocationSettings.Timeout support
Jupyter / Polyglot Notebooks Hung cell means killing the kernel Bounded cell execution without kernel restart

Why not experimental feature gating? The feature is inherently opt-in. Code that does not set Timeout takes the identical code path as before. The [Experimental] attribute system targets cmdlet parameters, not POCO properties. Happy to add a PSHostingAPITimeout feature flag if the Committee prefers.

RFC: PowerShell-RFC#409
Docs issue: MicrosoftDocs/PowerShell-Docs#12852

Prefer a rendered documentation site?

Bounded-Wait API Documentation — full specification, test matrix, scenario walkthroughs, and annotated source diffs in a browsable format.


PR Checklist