Sitelet https://github.com/PowerShell/PowerShell/pull/27027
Skip to content

Add bounded-wait timeout support to hosting API - #27027

Open
Ahmed Taha (SufficientDaikon) wants to merge 4 commits into
PowerShell:masterfrom
SufficientDaikon:fix/hosting-api-bounded-waits
Open

Add bounded-wait timeout support to hosting API#27027
Ahmed Taha (SufficientDaikon) wants to merge 4 commits into
PowerShell:masterfrom
SufficientDaikon:fix/hosting-api-bounded-waits

Conversation

@SufficientDaikon

@SufficientDaikon Ahmed Taha (SufficientDaikon) commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

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).
@microsoft-github-policy-service microsoft-github-policy-service Bot added the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Mar 31, 2026
@SufficientDaikon
Ahmed Taha (SufficientDaikon) marked this pull request as ready for review April 15, 2026 20:05
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
5 pipeline(s) require an authorized user to comment /azp run to run.

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
5 pipeline(s) require an authorized user to comment /azp run to run.

@microsoft-github-policy-service microsoft-github-policy-service Bot removed Stale Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept labels Apr 15, 2026
@microsoft-github-policy-service microsoft-github-policy-service Bot added the Review - Needed The PR is being reviewed label Apr 23, 2026
@microsoft-github-policy-service

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

Copilot AI review requested due to automatic review settings July 22, 2026 14:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Timeout and PowerShell.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)));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review - Needed The PR is being reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants