Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions src/PowerShellEditorServices/Server/PsesDebugServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.PowerShell.EditorServices.Handlers;
using Microsoft.PowerShell.EditorServices.Logging;
using Microsoft.PowerShell.EditorServices.Services;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
using OmniSharp.Extensions.DebugAdapter.Server;
Expand Down Expand Up @@ -89,23 +91,40 @@ public async Task StartAsync()
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
.OnInitialize(async (server, _, cancellationToken) =>
{
// Start the host if not already started, and enable debug mode (required
// for remote debugging).
//
// TODO: We might need to fill in HostStartOptions here.
_startedPses = !await _psesHost.TryStartAsync(new HostStartOptions(), cancellationToken).ConfigureAwait(false);
_psesHost.DebugContext.EnableDebugMode();

// We need to give the host a handle to the DAP so it can register
// notifications (specifically for sendKeyPress).
if (_isTemp)
try
{
_psesHost.DebugServer = server;
// Start the host if not already started, and enable debug mode (required
// for remote debugging).
//
// TODO: We might need to fill in HostStartOptions here.
_startedPses = !await _psesHost.TryStartAsync(new HostStartOptions(), cancellationToken).ConfigureAwait(false);
_psesHost.DebugContext.EnableDebugMode();

// We need to give the host a handle to the DAP so it can register
// notifications (specifically for sendKeyPress).
if (_isTemp)
{
_psesHost.DebugServer = server;
}

// Clear any existing breakpoints before proceeding.
BreakpointService breakpointService = server.GetService<BreakpointService>();
await breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false);
}
catch (Exception e)
{
// Never let an exception escape this delegate. OmniSharp's
// DebugAdapterServer only signals its internal initialize-complete
// subject on the success path of the InitializeRequest handler, so a
// throw here leaves DebugAdapterServer.From() (and thus StartAsync)
// awaiting that subject forever -- wedging startup and riding the job
// timeout instead of failing fast. Log the failure and signal shutdown
// so WaitForShutdown unblocks and the process can exit cleanly.
ServiceProvider.GetService<ILoggerFactory>()?
.CreateLogger<PsesDebugServer>()
.LogException("Failed to start the debug server; terminating the debug session.", e);
_serverStopped.TrySetResult(true);
}

// Clear any existing breakpoints before proceeding.
BreakpointService breakpointService = server.GetService<BreakpointService>();
await breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false);
})
// The OnInitialized delegate gets run right before the server responds to the _Initialize_ request:
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
Expand Down Expand Up @@ -134,7 +153,7 @@ public void Dispose()
_debugAdapterServer?.Dispose();
_inputStream.Dispose();
_outputStream.Dispose();
_serverStopped.SetResult(true);
_serverStopped.TrySetResult(true);
// TODO: If the debugger has stopped, should we clear the breakpoints?
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,31 @@ public static void SetCorrectExecutionPolicy(this PowerShell pwsh, ILogger logge
{
// We want to get the list hierarchy of execution policies
// Calling the cmdlet is the simplest way to do that
IReadOnlyList<PSObject> policies = pwsh
.AddCommand(@"Microsoft.PowerShell.Security\Get-ExecutionPolicy")
.AddParameter("List")
.InvokeAndClear<PSObject>();
IReadOnlyList<PSObject> policies;
try
{
policies = pwsh
.AddCommand(@"Microsoft.PowerShell.Security\Get-ExecutionPolicy")
.AddParameter("List")
.InvokeAndClear<PSObject>();
}
catch (Exception e)
{
// Some Windows PowerShell servicing builds throw a type-data conflict
// ("The member ... is already present" on ObjectSecurity) when
// autoloading Microsoft.PowerShell.Security into a runspace whose
// InitialSessionState already carries that module's type data.
// Configuring the execution policy is best-effort, so log and skip
// rather than letting it abort host startup (which manifests as a hang).
logger.LogError(e, "Failed to query the execution policy; skipping execution policy configuration.");
return;
}

// We need at least the CurrentUser and LocalMachine scopes to proceed.
if (policies is null || policies.Count < 2)
{
return;
}
Comment on lines +141 to +165

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@JustinGrote I got this whole issue to repro locally on a Windows box and apparently this was the issue that was hanging all the end-to-end tests on the updated image.


// The policies come out in the following order:
// - MachinePolicy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,6 @@

namespace PowerShellEditorServices.Test.E2E
{
/// <remarks>
/// Every test in this class is skipped at discovery time on in-box Windows
/// PowerShell (via <see cref="SkippableFactOnWindowsPowerShellAttribute"/> and
/// <see cref="SkippableTheoryOnWindowsPowerShellAttribute"/>) because the shared
/// <see cref="InitializeAsync"/> debug-adapter startup can wedge there since the
/// 20260614 runner image, riding the job timeout. See
/// https://github.com/PowerShell/PowerShellEditorServices/issues/2323.
/// </remarks>
[Trait("Category", "DAP")]
// ITestOutputHelper is injected by XUnit
// https://xunit.net/docs/capturing-output
Expand Down Expand Up @@ -264,7 +256,7 @@ private async Task<string> ReadScriptLogLineAsync()
}
}

[SkippableFactOnWindowsPowerShell]
[Fact]
public void CanInitializeWithCorrectServerSettings()
{
Assert.True(client.ServerSettings.SupportsConditionalBreakpoints);
Expand All @@ -276,7 +268,7 @@ public void CanInitializeWithCorrectServerSettings()
Assert.True(client.ServerSettings.SupportsDelayedStackTraceLoading);
}

[SkippableFactOnWindowsPowerShell]
[Fact]
public async Task UsesDotSourceOperatorAndQuotesAsync()
{
string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)"));
Expand All @@ -288,7 +280,7 @@ public async Task UsesDotSourceOperatorAndQuotesAsync()
Assert.StartsWith(". '", actual);
}

[SkippableFactOnWindowsPowerShell]
[Fact]
public async Task UsesCallOperatorWithSettingAsync()
{
string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)"));
Expand All @@ -300,7 +292,7 @@ public async Task UsesCallOperatorWithSettingAsync()
Assert.StartsWith("& '", actual);
}

[SkippableFactOnWindowsPowerShell]
[Fact]
public async Task CanLaunchScriptWithNoBreakpointsAsync()
{
string filePath = NewTestFile(GenerateLoggingScript("works"));
Expand All @@ -314,7 +306,7 @@ public async Task CanLaunchScriptWithNoBreakpointsAsync()
Assert.Equal("works", actual);
}

[SkippableFactOnWindowsPowerShell]
[SkippableFact]
public async Task CanSetBreakpointsAsync()
{
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
Expand Down Expand Up @@ -365,7 +357,7 @@ public async Task CanSetBreakpointsAsync()
Assert.Equal("after breakpoint", afterBreakpointActual);
}

[SkippableFactOnWindowsPowerShell]
[SkippableFact]
public async Task FailsIfStacktraceRequestedWhenNotPaused()
{
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
Expand Down Expand Up @@ -396,7 +388,7 @@ await Assert.ThrowsAsync<JsonRpcException>(() => client.RequestStackTrace(
));
}

[SkippableFactOnWindowsPowerShell]
[SkippableFact]
public async Task SendsInitialLabelBreakpointForPerformanceReasons()
{
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
Expand Down Expand Up @@ -454,7 +446,7 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons()
// PowerShell, we avoid all issues with our test project (and the xUnit executable) not
// having System.Windows.Forms deployed, and can instead rely on the Windows Global Assembly
// Cache (GAC) to find it.
[SkippableFactOnWindowsPowerShell]
[SkippableFact]
public async Task CanStepPastSystemWindowsForms()
{
Skip.IfNot(PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
Expand Down Expand Up @@ -497,7 +489,7 @@ public async Task CanStepPastSystemWindowsForms()
// commented. Since in some cases (such as Windows PowerShell, or the script not having a
// backing ScriptFile) we just wrap the script with braces, we had a bug where the last
// brace would be after the comment. We had to ensure we wrapped with newlines instead.
[SkippableFactOnWindowsPowerShell]
[Fact]
public async Task CanLaunchScriptWithCommentedLastLineAsync()
{
string script = GenerateLoggingScript("$($MyInvocation.Line)", "$(1+1)") + "# a comment at the end";
Expand All @@ -521,7 +513,7 @@ public async Task CanLaunchScriptWithCommentedLastLineAsync()
Assert.Equal("2", await ReadScriptLogLineAsync());
}

[SkippableFactOnWindowsPowerShell]
[SkippableFact]
public async Task CanRunPesterTestFile()
{
Skip.If(true, "Pester test is broken.");
Expand Down Expand Up @@ -566,7 +558,7 @@ public async Task CanRunPesterTestFile()
[InlineData("-ProcessId 1234 -RunspaceId 5678", null, null, 1234, 5678, null)]
[InlineData("-ProcessId 1234 -RunspaceId 5678 -ComputerName comp", "comp", null, 1234, 5678, null)]
[InlineData("-CustomPipeName testpipe -RunspaceName rs-name", null, "testpipe", 0, 0, "rs-name")]
[SkippableTheoryOnWindowsPowerShell]
[SkippableTheory]
public async Task CanLaunchScriptWithNewChildAttachSession(
string paramString,
string? expectedComputerName,
Expand All @@ -578,6 +570,9 @@ public async Task CanLaunchScriptWithNewChildAttachSession(
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
"PowerShellEditorServices.Command is not signed to run FLM in Constrained Language Mode.");

Skip.If(PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
"The Start-DebugAttachSession reverse-request round-trip is flaky on in-box Windows PowerShell CI runners (see #2323).");

string script = NewTestFile($"Start-DebugAttachSession {paramString}");

using CancellationTokenSource timeoutCts = new(30000);
Expand All @@ -604,7 +599,7 @@ public async Task CanLaunchScriptWithNewChildAttachSession(
await terminatedTcs.Task;
}

[SkippableFactOnWindowsPowerShell]
[SkippableFact]
public async Task CanLaunchScriptWithNewChildAttachSessionAsJob()
{
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
Expand Down Expand Up @@ -638,14 +633,15 @@ public async Task CanLaunchScriptWithNewChildAttachSessionAsJob()
await terminatedTcs.Task;
}

// Timeout is a per-test backstop; the Windows PowerShell skip happens at
// discovery time via the attribute (see the class remarks).
[SkippableFactOnWindowsPowerShell(Timeout = 15000)]
[SkippableFact(Timeout = 15000)]
public async Task CanAttachScriptWithPathMappings()
{
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
"Breakpoints can't be set in Constrained Language Mode.");

Skip.If(PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
"In-box Windows PowerShell wedges on the cross-process Debug-Runspace attach handshake (see #2323).");

string[] logStatements = ["$PSCommandPath", "after breakpoint"];

await RunWithAttachableProcess(logStatements, async (filePath, processId, runspaceId) =>
Expand Down

This file was deleted.

This file was deleted.

19 changes: 5 additions & 14 deletions test/PowerShellEditorServices.Test.E2E/LSPTestsFixtures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,6 @@ public class LSPTestsFixture : IAsyncLifetime

public async Task InitializeAsync()
{
// All LSP end-to-end tests are skipped at discovery time on Windows
// PowerShell (see SkippableFactOnWindowsPowerShell), but xUnit still
// creates this class fixture even when every test method is skipped.
// The in-box Windows PowerShell server can wedge during startup on the
// current windows-latest runner image (a runner-image regression, not
// our code); see https://github.com/PowerShell/PowerShellEditorServices/issues/2323.
// So we must not start the server here on Windows PowerShell.
if (PsesStdioLanguageServerProcessHost.IsWindowsPowerShell)
{
return;
}

(StreamReader stdout, StreamWriter stdin) = await _psesHost.Start();

// Splice the streams together and enable debug logging of all messages sent and received
Expand Down Expand Up @@ -112,8 +100,11 @@ public async Task InitializeAsync()

public async Task DisposeAsync()
{
// The server is never started on Windows PowerShell (see
// InitializeAsync), so there is nothing to shut down there.
// If InitializeAsync failed before the client connected (e.g. the
// server never finished starting), PsesLanguageClient was never
// assigned, so there is nothing to shut down. Guarding here keeps a
// startup failure from being masked by a NullReferenceException
// during teardown.
if (PsesLanguageClient is null)
{
return;
Expand Down
Loading