Add bounded-wait timeout support to hosting API - #27027
Open
Ahmed Taha (SufficientDaikon) wants to merge 4 commits into
Open
Add bounded-wait timeout support to hosting API#27027Ahmed Taha (SufficientDaikon) wants to merge 4 commits into
Ahmed Taha (SufficientDaikon) wants to merge 4 commits into
Conversation
Ahmed Taha (SufficientDaikon)
force-pushed
the
fix/hosting-api-bounded-waits
branch
2 times, most recently
from
March 13, 2026 20:20
f659713 to
5d18264
Compare
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).
Ahmed Taha (SufficientDaikon)
force-pushed
the
fix/hosting-api-bounded-waits
branch
from
March 24, 2026 15:40
d079276 to
8ca0cd9
Compare
This was referenced Mar 24, 2026
Ahmed Taha (SufficientDaikon)
marked this pull request as draft
March 27, 2026 12:18
Ahmed Taha (SufficientDaikon)
marked this pull request as ready for review
April 15, 2026 20:05
|
Azure Pipelines: 5 pipeline(s) require an authorized user to comment /azp run to run. |
1 similar comment
|
Azure Pipelines: 5 pipeline(s) require an authorized user to comment /azp run to run. |
Contributor
|
This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days. |
Copilot started reviewing on behalf of
Ahmed Taha (SufficientDaikon)
July 22, 2026 14:24
View session
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces opt-in bounded waits for PowerShell hosting scenarios by adding a new PSInvocationSettings.Timeout property and a PowerShell.Stop(TimeSpan) overload, and by bounding several internal waits so hosts can avoid indefinite hangs when scripts or cleanup operations stall.
Changes:
- Add public hosting API surface:
PSInvocationSettings.TimeoutandPowerShell.Stop(TimeSpan). - Add bounded wait behavior to synchronous invoke paths (including runspace-pool acquisition and batch invocation waits).
- Add/adjust internal timeout behaviors for pipeline stopping and runspace lifecycle operations, plus new localized timeout strings and new xUnit/Pester coverage.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/System.Management.Automation/engine/hostifaces/PowerShell.cs |
Adds PSInvocationSettings.Timeout, Stop(TimeSpan), and bounded invoke/pool waits. |
src/System.Management.Automation/engine/hostifaces/ConnectionBase.cs |
Adds timeout/parallelism logic in close/stop pipeline coordination paths. |
src/System.Management.Automation/engine/hostifaces/LocalConnection.cs |
Adds bounded waits for remote runspace/job cleanup and Broken-state fallback on dispose timeout. |
src/System.Management.Automation/engine/hostifaces/LocalPipeline.cs |
Adds a bounded wait for pipeline-finished signaling during stop. |
src/System.Management.Automation/resources/PowerShellStrings.resx |
Adds timeout-related resource strings used by hosting APIs. |
src/System.Management.Automation/resources/RunspaceStrings.resx |
Adds timeout-related resource strings used by runspace/pipeline shutdown paths. |
test/xUnit/csharp/TimeoutTests.cs |
Adds xUnit coverage for invoke/stop timeouts, pool exhaustion, and lifecycle bounded waits. |
test/powershell/engine/Api/Timeout.Tests.ps1 |
Adds Pester coverage for the new API surface and lifecycle bounded wait behavior. |
Comment on lines
+416
to
+420
| /// <summary> | ||
| /// Maximum time to wait for synchronous operations (Invoke, Stop, Close). | ||
| /// Default is Timeout.InfiniteTimeSpan which preserves backwards compatibility. | ||
| /// </summary> | ||
| public TimeSpan Timeout { get; set; } = System.Threading.Timeout.InfiniteTimeSpan; |
Comment on lines
+3785
to
+3801
| public void Stop(TimeSpan timeout) | ||
| { | ||
| try | ||
| { | ||
| IAsyncResult asyncResult = CoreStop(true, null, null); | ||
| if (!asyncResult.AsyncWaitHandle.WaitOne(timeout)) | ||
| { | ||
| throw new TimeoutException( | ||
| StringUtil.Format(PowerShellStrings.StopTimedOut, timeout)); | ||
| } | ||
|
|
||
| ResetOutputBufferAsNeeded(); | ||
| } | ||
| catch (ObjectDisposedException) | ||
| { | ||
| } | ||
| } |
Comment on lines
4568
to
+4574
| _worker.GetRunspaceAsyncResult = pool.BeginGetRunspace(null, null); | ||
| _worker.GetRunspaceAsyncResult.AsyncWaitHandle.WaitOne(); | ||
| TimeSpan poolTimeout = settings?.Timeout ?? System.Threading.Timeout.InfiniteTimeSpan; | ||
| if (!_worker.GetRunspaceAsyncResult.AsyncWaitHandle.WaitOne(poolTimeout)) | ||
| { | ||
| throw new TimeoutException( | ||
| StringUtil.Format(PowerShellStrings.OperationTimedOut, poolTimeout)); | ||
| } |
Comment on lines
+4627
to
+4632
| if (!invokeCompleted) | ||
| { | ||
| CoreStop(true, null, null); // best-effort: signal pipeline to stop | ||
| throw new TimeoutException( | ||
| StringUtil.Format(PowerShellStrings.OperationTimedOut, invokeTimeout)); | ||
| } |
Comment on lines
+392
to
+399
| if (!opened) | ||
| { | ||
| SetRunspaceState(RunspaceState.Broken, | ||
| new TimeoutException( | ||
| StringUtil.Format(RunspaceStrings.StopPipelinesTimedOut, | ||
| TimeSpan.FromSeconds(30)))); | ||
| RaiseRunspaceStateEvents(); | ||
| return; |
Comment on lines
966
to
+979
| ThreadPool.QueueUserWorkItem(new WaitCallback( | ||
| (object state) => | ||
| { | ||
| var tuple = (Tuple<WaitHandle[], ManualResetEvent>)state; | ||
| WaitHandle.WaitAll(tuple.Item1); | ||
| tuple.Item2.Set(); | ||
| }), | ||
| stateInfo); | ||
| return waitAllIsDone.WaitOne(); | ||
| if (!waitAllIsDone.WaitOne(TimeSpan.FromSeconds(30))) | ||
| { | ||
| throw new TimeoutException( | ||
| StringUtil.Format(RunspaceStrings.StopPipelinesTimedOut, | ||
| TimeSpan.FromSeconds(30))); | ||
| } |
Comment on lines
+956
to
+961
| if (!remoteRunspaceCloseCompleted.WaitOne(TimeSpan.FromSeconds(30))) | ||
| { | ||
| throw new TimeoutException( | ||
| StringUtil.Format(RunspaceStrings.StopPipelinesTimedOut, | ||
| TimeSpan.FromSeconds(30))); | ||
| } |
Comment on lines
+1010
to
+1015
| if (!jobsStopCompleted.WaitOne(TimeSpan.FromSeconds(30))) | ||
| { | ||
| throw new TimeoutException( | ||
| StringUtil.Format(RunspaceStrings.StopPipelinesTimedOut, | ||
| TimeSpan.FromSeconds(30))); | ||
| } |
Comment on lines
+797
to
+802
| if (!PipelineFinishedEvent.WaitOne(TimeSpan.FromSeconds(30))) | ||
| { | ||
| throw new TimeoutException( | ||
| StringUtil.Format(RunspaceStrings.StopPipelinesTimedOut, | ||
| TimeSpan.FromSeconds(30))); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Summary
Hosting applications can now set a timeout on
PowerShell.Invoke()andPowerShell.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:PSInvocationSettings.TimeoutTimeSpan { get; set; }Timeout.InfiniteTimeSpanPowerShell.Stop(TimeSpan)void Stop(TimeSpan timeout)Backwards compatibility: The default
InfiniteTimeSpanpreserves existing behavior — code that does not setTimeouttakes the original same-thread code path with no extra allocations or thread switches.Fixes #26594. Addresses #24289. Foundation for #19685.
What Changed
PowerShell.csPSInvocationSettings.Timeout,Stop(TimeSpan), boundedInvokeviaTask.Run+Wait(timeout), pool acquisition + batch timeoutsConnectionBase.csStopPipelines(TimeSpan)viaTask.Run+Task.WaitAll, 30s runspace-open waitLocalConnection.csDispose()catchesTimeoutException→ forcesBrokenstateLocalPipeline.csPipelineFinishedEvent.WaitOnePowerShellStrings.resxOperationTimedOut,StopTimedOutresource stringsRunspaceStrings.resxStopPipelinesTimedOutresource stringExecution Flow
When
Timeoutis 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 toBrokenstate to release resources:stateDiagram-v2 [*] --> Open Open --> Closing : Close() Closing --> Closed : Completes within 30s Closing --> Broken : TimeoutException Open --> Broken : Dispose() timeout Closed --> [*] Broken --> [*] : Resources releasedTests
Test inventory
test/xUnit/csharp/test_Timeout.cstest/powershell/engine/Api/Timeout.Tests.ps1Coverage: 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
Timeoutis finite,Invoke()dispatches work to a ThreadPool (MTA) thread viaTask.Run. Scripts that depend on STA COM apartment state should leaveTimeoutat its defaultInfiniteTimeSpan, 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, everyWaitOne()call blocks indefinitely — the host has no way to recover short of killing the process.Process.Kill()when the integrated console hangsTimeoutExceptionand recover gracefullyThread.Abortdoes not exist in .NET Core — no timeout mechanismPSInvocationSettings.TimeoutsupportWhy not experimental feature gating? The feature is inherently opt-in. Code that does not set
Timeouttakes the identical code path as before. The[Experimental]attribute system targets cmdlet parameters, not POCO properties. Happy to add aPSHostingAPITimeoutfeature 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
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright header