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.
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"]
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
Tests
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
- PR has a meaningful title
- Use the present tense and imperative mood when describing your changes
- Summarized changes
- Make sure all
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright header - This PR is ready to merge. If this PR is a work in progress, please open this as a Draft Pull Request and mark it as Ready to Review when it is ready to merge.
- Breaking changes
- None
- User-facing changes
- Documentation needed
- Issue filed: MicrosoftDocs/PowerShell-Docs#12852
- Documentation needed
- Testing - New and feature