Skip to content

Add bounded-wait timeout support to hosting API#27027

Open
SufficientDaikon wants to merge 1 commit intoPowerShell:masterfrom
SufficientDaikon:fix/hosting-api-bounded-waits
Open

Add bounded-wait timeout support to hosting API#27027
SufficientDaikon wants to merge 1 commit intoPowerShell:masterfrom
SufficientDaikon:fix/hosting-api-bounded-waits

Conversation

@SufficientDaikon
Copy link

@SufficientDaikon SufficientDaikon commented Mar 13, 2026

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

Add bounded alternatives to critical unbounded WaitOne()/Wait()
calls in the hosting API (engine/hostifaces/). Add two opt-in
public API members: PSInvocationSettings.Timeout (TimeSpan,
defaults to InfiniteTimeSpan) and PowerShell.Stop(TimeSpan).

Internal waits (runspace open, pipeline stop, runspace close,
dispose) bounded to 30 seconds. Default InfiniteTimeSpan
preserves existing behavior with zero overhead.

34 tests added (19 xUnit C#, 15 Pester).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET SDK's InvokeAsync hangs forever when command contains multiple statements

1 participant