Skip to content

Commit 1979e51

Browse files
Fix Windows PowerShell host-start hang in SetCorrectExecutionPolicy
On recent in-box Windows PowerShell 5.1 servicing builds, the language and debug servers intermittently hang on startup and ride the CI job timeout (#2323). The wedge is `SetCorrectExecutionPolicy`: it calls `Microsoft.PowerShell.Security\Get-ExecutionPolicy -List`, which autoloads `Microsoft.PowerShell.Security` into the freshly created runspace. That runspace's `InitialSessionState` is reused from the host runspace and already carries the module's `ObjectSecurity` type data, so re-binding it throws "The member `AuditToString` is already present". `PsesInternalHost.Run()` catches it, faults `_started`, and the pipeline thread exits — but `TryStartAsync` is still awaiting queued startup tasks that now never run, so it hangs forever. Configuring the execution policy is best-effort, so we wrap the `Get-ExecutionPolicy` query in try/catch (mirroring the existing `Set-ExecutionPolicy` handling just below it) and skip policy configuration when it fails, rather than letting a type-data hiccup abort host startup. I also guard the subsequent indexing against a short result list. With the hang fixed we no longer need the in-box Windows PowerShell E2E skips added in #2318, so this reverts them: the `SkippableFactOnWindowsPowerShell` attributes, the `LSPTestsFixture` early-return, and the two attribute files. The DAP suite passes clean under `powershell.exe`. Three LSP help tests (`Expand-Archive` synopsis) still fail locally on my older servicing build (.8655); they pass under `pwsh`, so I'm letting CI judge them on its build rather than re-skipping. The `ci-test.yml` job timeout from #2318 stays as a backstop. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0515a5f commit 1979e51

6 files changed

Lines changed: 76 additions & 187 deletions

File tree

src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,31 @@ public static void SetCorrectExecutionPolicy(this PowerShell pwsh, ILogger logge
138138
{
139139
// We want to get the list hierarchy of execution policies
140140
// Calling the cmdlet is the simplest way to do that
141-
IReadOnlyList<PSObject> policies = pwsh
142-
.AddCommand(@"Microsoft.PowerShell.Security\Get-ExecutionPolicy")
143-
.AddParameter("List")
144-
.InvokeAndClear<PSObject>();
141+
IReadOnlyList<PSObject> policies;
142+
try
143+
{
144+
policies = pwsh
145+
.AddCommand(@"Microsoft.PowerShell.Security\Get-ExecutionPolicy")
146+
.AddParameter("List")
147+
.InvokeAndClear<PSObject>();
148+
}
149+
catch (Exception e)
150+
{
151+
// Some Windows PowerShell servicing builds throw a type-data conflict
152+
// ("The member ... is already present" on ObjectSecurity) when
153+
// autoloading Microsoft.PowerShell.Security into a runspace whose
154+
// InitialSessionState already carries that module's type data.
155+
// Configuring the execution policy is best-effort, so log and skip
156+
// rather than letting it abort host startup (which manifests as a hang).
157+
logger.LogError(e, "Failed to query the execution policy; skipping execution policy configuration.");
158+
return;
159+
}
160+
161+
// We need at least the CurrentUser and LocalMachine scopes to proceed.
162+
if (policies is null || policies.Count < 2)
163+
{
164+
return;
165+
}
145166

146167
// The policies come out in the following order:
147168
// - MachinePolicy

test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,6 @@
2424

2525
namespace PowerShellEditorServices.Test.E2E
2626
{
27-
/// <remarks>
28-
/// Every test in this class is skipped at discovery time on in-box Windows
29-
/// PowerShell (via <see cref="SkippableFactOnWindowsPowerShellAttribute"/> and
30-
/// <see cref="SkippableTheoryOnWindowsPowerShellAttribute"/>) because the shared
31-
/// <see cref="InitializeAsync"/> debug-adapter startup can wedge there since the
32-
/// 20260614 runner image, riding the job timeout. See
33-
/// https://github.com/PowerShell/PowerShellEditorServices/issues/2323.
34-
/// </remarks>
3527
[Trait("Category", "DAP")]
3628
// ITestOutputHelper is injected by XUnit
3729
// https://xunit.net/docs/capturing-output
@@ -246,25 +238,16 @@ private async Task<string> ReadScriptLogLineAsync()
246238
}
247239
}
248240

249-
// Tail the log until a non-empty line is available. The awaited
250-
// delay between reads matters: at EOF ReadLineAsync completes
251-
// synchronously with null, so without it this is a tight loop that
252-
// never releases its thread-pool thread and needlessly pressures
253-
// the pool on constrained CI runners. Yielding keeps the tail loop
254-
// cheap while we wait for the script to write.
255-
while (true)
241+
// return valid lines only
242+
string nextLine = string.Empty;
243+
while (nextLine is null || nextLine.Length == 0)
256244
{
257-
string nextLine = await scriptLogReader.ReadLineAsync();
258-
if (!string.IsNullOrEmpty(nextLine))
259-
{
260-
return nextLine;
261-
}
262-
263-
await Task.Delay(100);
245+
nextLine = await scriptLogReader.ReadLineAsync(); //Might return null if at EOF because we created it above but the script hasn't written to it yet
264246
}
247+
return nextLine;
265248
}
266249

267-
[SkippableFactOnWindowsPowerShell]
250+
[Fact]
268251
public void CanInitializeWithCorrectServerSettings()
269252
{
270253
Assert.True(client.ServerSettings.SupportsConditionalBreakpoints);
@@ -276,7 +259,7 @@ public void CanInitializeWithCorrectServerSettings()
276259
Assert.True(client.ServerSettings.SupportsDelayedStackTraceLoading);
277260
}
278261

279-
[SkippableFactOnWindowsPowerShell]
262+
[Fact]
280263
public async Task UsesDotSourceOperatorAndQuotesAsync()
281264
{
282265
string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)"));
@@ -288,7 +271,7 @@ public async Task UsesDotSourceOperatorAndQuotesAsync()
288271
Assert.StartsWith(". '", actual);
289272
}
290273

291-
[SkippableFactOnWindowsPowerShell]
274+
[Fact]
292275
public async Task UsesCallOperatorWithSettingAsync()
293276
{
294277
string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)"));
@@ -300,7 +283,7 @@ public async Task UsesCallOperatorWithSettingAsync()
300283
Assert.StartsWith("& '", actual);
301284
}
302285

303-
[SkippableFactOnWindowsPowerShell]
286+
[Fact]
304287
public async Task CanLaunchScriptWithNoBreakpointsAsync()
305288
{
306289
string filePath = NewTestFile(GenerateLoggingScript("works"));
@@ -314,7 +297,7 @@ public async Task CanLaunchScriptWithNoBreakpointsAsync()
314297
Assert.Equal("works", actual);
315298
}
316299

317-
[SkippableFactOnWindowsPowerShell]
300+
[SkippableFact]
318301
public async Task CanSetBreakpointsAsync()
319302
{
320303
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
@@ -365,7 +348,7 @@ public async Task CanSetBreakpointsAsync()
365348
Assert.Equal("after breakpoint", afterBreakpointActual);
366349
}
367350

368-
[SkippableFactOnWindowsPowerShell]
351+
[SkippableFact]
369352
public async Task FailsIfStacktraceRequestedWhenNotPaused()
370353
{
371354
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
@@ -396,7 +379,7 @@ await Assert.ThrowsAsync<JsonRpcException>(() => client.RequestStackTrace(
396379
));
397380
}
398381

399-
[SkippableFactOnWindowsPowerShell]
382+
[SkippableFact]
400383
public async Task SendsInitialLabelBreakpointForPerformanceReasons()
401384
{
402385
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
@@ -454,7 +437,7 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons()
454437
// PowerShell, we avoid all issues with our test project (and the xUnit executable) not
455438
// having System.Windows.Forms deployed, and can instead rely on the Windows Global Assembly
456439
// Cache (GAC) to find it.
457-
[SkippableFactOnWindowsPowerShell]
440+
[SkippableFact]
458441
public async Task CanStepPastSystemWindowsForms()
459442
{
460443
Skip.IfNot(PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
@@ -497,7 +480,7 @@ public async Task CanStepPastSystemWindowsForms()
497480
// commented. Since in some cases (such as Windows PowerShell, or the script not having a
498481
// backing ScriptFile) we just wrap the script with braces, we had a bug where the last
499482
// brace would be after the comment. We had to ensure we wrapped with newlines instead.
500-
[SkippableFactOnWindowsPowerShell]
483+
[Fact]
501484
public async Task CanLaunchScriptWithCommentedLastLineAsync()
502485
{
503486
string script = GenerateLoggingScript("$($MyInvocation.Line)", "$(1+1)") + "# a comment at the end";
@@ -521,7 +504,7 @@ public async Task CanLaunchScriptWithCommentedLastLineAsync()
521504
Assert.Equal("2", await ReadScriptLogLineAsync());
522505
}
523506

524-
[SkippableFactOnWindowsPowerShell]
507+
[SkippableFact]
525508
public async Task CanRunPesterTestFile()
526509
{
527510
Skip.If(true, "Pester test is broken.");
@@ -566,7 +549,7 @@ public async Task CanRunPesterTestFile()
566549
[InlineData("-ProcessId 1234 -RunspaceId 5678", null, null, 1234, 5678, null)]
567550
[InlineData("-ProcessId 1234 -RunspaceId 5678 -ComputerName comp", "comp", null, 1234, 5678, null)]
568551
[InlineData("-CustomPipeName testpipe -RunspaceName rs-name", null, "testpipe", 0, 0, "rs-name")]
569-
[SkippableTheoryOnWindowsPowerShell]
552+
[SkippableTheory]
570553
public async Task CanLaunchScriptWithNewChildAttachSession(
571554
string paramString,
572555
string? expectedComputerName,
@@ -604,7 +587,7 @@ public async Task CanLaunchScriptWithNewChildAttachSession(
604587
await terminatedTcs.Task;
605588
}
606589

607-
[SkippableFactOnWindowsPowerShell]
590+
[SkippableFact]
608591
public async Task CanLaunchScriptWithNewChildAttachSessionAsJob()
609592
{
610593
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
@@ -638,9 +621,7 @@ public async Task CanLaunchScriptWithNewChildAttachSessionAsJob()
638621
await terminatedTcs.Task;
639622
}
640623

641-
// Timeout is a per-test backstop; the Windows PowerShell skip happens at
642-
// discovery time via the attribute (see the class remarks).
643-
[SkippableFactOnWindowsPowerShell(Timeout = 15000)]
624+
[SkippableFact(Timeout = 15000)]
644625
public async Task CanAttachScriptWithPathMappings()
645626
{
646627
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
@@ -781,10 +762,6 @@ WinPS will always need this.
781762
if (((Get-Date) - $start).TotalSeconds -gt 10) {
782763
throw 'Timeout waiting for Debug-Runspace to be subscribed.'
783764
}
784-
785-
# Yield a slice so this poll doesn't peg a core while the
786-
# runner is also servicing the attach handshake.
787-
Start-Sleep -Milliseconds 100
788765
}
789766
790767
$ps.Invoke()

test/PowerShellEditorServices.Test.E2E/Hosts/SkippableFactOnWindowsPowerShellAttribute.cs

Lines changed: 0 additions & 56 deletions
This file was deleted.

test/PowerShellEditorServices.Test.E2E/Hosts/SkippableTheoryOnWindowsPowerShellAttribute.cs

Lines changed: 0 additions & 25 deletions
This file was deleted.

test/PowerShellEditorServices.Test.E2E/LSPTestsFixtures.cs

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,6 @@ public class LSPTestsFixture : IAsyncLifetime
4242

4343
public async Task InitializeAsync()
4444
{
45-
// All LSP end-to-end tests are skipped at discovery time on Windows
46-
// PowerShell (see SkippableFactOnWindowsPowerShell), but xUnit still
47-
// creates this class fixture even when every test method is skipped.
48-
// The in-box Windows PowerShell server can wedge during startup on the
49-
// current windows-latest runner image (a runner-image regression, not
50-
// our code); see https://github.com/PowerShell/PowerShellEditorServices/issues/2323.
51-
// So we must not start the server here on Windows PowerShell.
52-
if (PsesStdioLanguageServerProcessHost.IsWindowsPowerShell)
53-
{
54-
return;
55-
}
56-
5745
(StreamReader stdout, StreamWriter stdin) = await _psesHost.Start();
5846

5947
// Splice the streams together and enable debug logging of all messages sent and received
@@ -112,16 +100,9 @@ public async Task InitializeAsync()
112100

113101
public async Task DisposeAsync()
114102
{
115-
// The server is never started on Windows PowerShell (see
116-
// InitializeAsync), so there is nothing to shut down there.
117-
if (PsesLanguageClient is null)
118-
{
119-
return;
120-
}
121-
122103
await PsesLanguageClient.Shutdown();
123104
await _psesHost.Stop();
124-
PsesLanguageClient.Dispose();
105+
PsesLanguageClient?.Dispose();
125106
}
126107
}
127108
}

0 commit comments

Comments
 (0)