Skip to content

Commit d1acc64

Browse files
committed
Route startup exceptions through and ensure startup has completed before proceeding
1 parent d5aebd6 commit d1acc64

File tree

10 files changed

+178
-32
lines changed

10 files changed

+178
-32
lines changed

src/PowerShellEditorServices/Server/PsesDebugServer.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.IO;
6+
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.Extensions.DependencyInjection;
89
using Microsoft.Extensions.Logging;
@@ -28,7 +29,9 @@ internal class PsesDebugServer : IDisposable
2829

2930
private DebugAdapterServer _debugAdapterServer;
3031

31-
private PowerShellDebugContext _debugContext;
32+
private PsesInternalHost _psesHost;
33+
34+
private bool _startedPses;
3235

3336
protected readonly ILoggerFactory _loggerFactory;
3437

@@ -61,8 +64,8 @@ public async Task StartAsync()
6164
{
6265
// We need to let the PowerShell Context Service know that we are in a debug session
6366
// so that it doesn't send the powerShell/startDebugger message.
64-
_debugContext = ServiceProvider.GetService<PsesInternalHost>().DebugContext;
65-
_debugContext.IsDebugServerActive = true;
67+
_psesHost = ServiceProvider.GetService<PsesInternalHost>();
68+
_psesHost.DebugContext.IsDebugServerActive = true;
6669

6770
options
6871
.WithInput(_inputStream)
@@ -88,8 +91,11 @@ public async Task StartAsync()
8891
// The OnInitialize delegate gets run when we first receive the _Initialize_ request:
8992
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
9093
.OnInitialize(async (server, request, cancellationToken) => {
94+
// We need to make sure the host has been started
95+
_startedPses = !(await _psesHost.TryStartAsync(new HostStartOptions(), CancellationToken.None).ConfigureAwait(false));
96+
9197
// Ensure the debugger mode is set correctly - this is required for remote debugging to work
92-
_debugContext.EnableDebugMode();
98+
_psesHost.DebugContext.EnableDebugMode();
9399

94100
var breakpointService = server.GetService<BreakpointService>();
95101
// Clear any existing breakpoints before proceeding
@@ -115,7 +121,7 @@ public void Dispose()
115121
// Note that the lifetime of the DebugContext is longer than the debug server;
116122
// It represents the debugger on the PowerShell process we're in,
117123
// while a new debug server is spun up for every debugging session
118-
_debugContext.IsDebugServerActive = false;
124+
_psesHost.DebugContext.IsDebugServerActive = false;
119125
_debugAdapterServer.Dispose();
120126
_inputStream.Dispose();
121127
_outputStream.Dispose();
@@ -126,6 +132,13 @@ public void Dispose()
126132
public async Task WaitForShutdown()
127133
{
128134
await _serverStopped.Task.ConfigureAwait(false);
135+
136+
// If we started the host, we need to ensure any errors are marshalled back to us like this
137+
if (_startedPses)
138+
{
139+
_psesHost.TriggerShutdown();
140+
await _psesHost.Shutdown.ConfigureAwait(false);
141+
}
129142
}
130143

131144
#region Events

src/PowerShellEditorServices/Server/PsesLanguageServer.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.PowerShell.EditorServices.Hosting;
1111
using Microsoft.PowerShell.EditorServices.Services;
1212
using Microsoft.PowerShell.EditorServices.Services.Extension;
13+
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
1314
using Microsoft.PowerShell.EditorServices.Services.Template;
1415
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
1516
using OmniSharp.Extensions.LanguageServer.Server;
@@ -32,6 +33,8 @@ internal class PsesLanguageServer
3233
private readonly HostStartupInfo _hostDetails;
3334
private readonly TaskCompletionSource<bool> _serverStart;
3435

36+
private PsesInternalHost _psesHost;
37+
3538
/// <summary>
3639
/// Create a new language server instance.
3740
/// </summary>
@@ -75,8 +78,12 @@ public async Task StartAsync()
7578
options
7679
.WithInput(_inputStream)
7780
.WithOutput(_outputStream)
78-
.WithServices(serviceCollection => serviceCollection
79-
.AddPsesLanguageServices(_hostDetails)) // NOTE: This adds a lot of services!
81+
.WithServices(serviceCollection =>
82+
{
83+
84+
// NOTE: This adds a lot of services!
85+
serviceCollection.AddPsesLanguageServices(_hostDetails);
86+
})
8087
.ConfigureLogging(builder => builder
8188
.AddSerilog(Log.Logger) // TODO: Set dispose to true?
8289
.AddLanguageProtocolLogging()
@@ -116,6 +123,8 @@ public async Task StartAsync()
116123

117124
IServiceProvider serviceProvider = languageServer.Services;
118125

126+
_psesHost = serviceProvider.GetService<PsesInternalHost>();
127+
119128
var workspaceService = serviceProvider.GetService<WorkspaceService>();
120129

121130
// Grab the workspace path from the parameters
@@ -150,6 +159,10 @@ public async Task WaitForShutdown()
150159
Log.Logger.Debug("Shutting down OmniSharp Language Server");
151160
await _serverStart.Task.ConfigureAwait(false);
152161
await LanguageServer.WaitForExit.ConfigureAwait(false);
162+
163+
// Doing this means we're able to route through any exceptions experienced on the pipeline thread
164+
_psesHost.TriggerShutdown();
165+
await _psesHost.Shutdown.ConfigureAwait(false);
153166
}
154167
}
155168
}

src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,12 @@ public void StepOver()
107107
public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction)
108108
{
109109
_psesHost.SetExit();
110-
LastStopEventArgs.ResumeAction = debuggerResumeAction;
110+
111+
if (LastStopEventArgs is not null)
112+
{
113+
LastStopEventArgs.ResumeAction = debuggerResumeAction;
114+
}
115+
111116
// We need to tell whatever is happening right now in the debug prompt to wrap up so we can continue
112117
_psesHost.CancelCurrentTask();
113118
}

src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,16 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns
5757

5858
private readonly IdempotentLatch _isRunningLatch = new();
5959

60+
private readonly TaskCompletionSource<bool> _started = new();
61+
62+
private readonly TaskCompletionSource<bool> _stopped = new();
63+
6064
private EngineIntrinsics _mainRunspaceEngineIntrinsics;
6165

6266
private bool _shouldExit = false;
6367

68+
private int _shuttingDown = 0;
69+
6470
private string _localComputerName;
6571

6672
private ConsoleKeyInfo? _lastKey;
@@ -128,12 +134,16 @@ public PsesInternalHost(
128134

129135
public string InitialWorkingDirectory { get; private set; }
130136

137+
public Task Shutdown => _stopped.Task;
138+
131139
IRunspaceInfo IRunspaceContext.CurrentRunspace => CurrentRunspace;
132140

133141
private PowerShellContextFrame CurrentFrame => _psFrameStack.Peek();
134142

135143
public event Action<object, RunspaceChangedEventArgs> RunspaceChanged;
136144

145+
private bool ShouldExitExecutionLoop => _shouldExit || _shuttingDown != 0;
146+
137147
public override void EnterNestedPrompt()
138148
{
139149
PushPowerShellAndRunLoop(CreateNestedPowerShell(CurrentRunspace), PowerShellFrameType.Nested);
@@ -172,13 +182,22 @@ public override void SetShouldExit(int exitCode)
172182
SetExit();
173183
}
174184

175-
public async Task StartAsync(HostStartOptions startOptions, CancellationToken cancellationToken)
185+
/// <summary>
186+
/// Try to start the PowerShell loop in the host.
187+
/// If the host is already started, this is idempotent.
188+
/// Returns when the host is in a valid initialized state.
189+
/// </summary>
190+
/// <param name="startOptions">Options to configure host startup.</param>
191+
/// <param name="cancellationToken">A token to cancel startup.</param>
192+
/// <returns>A task that resolves when the host has finished startup, with the value true if the caller started the host, and false otherwise.</returns>
193+
public async Task<bool> TryStartAsync(HostStartOptions startOptions, CancellationToken cancellationToken)
176194
{
177195
_logger.LogInformation("Host starting");
178196
if (!_isRunningLatch.TryEnter())
179197
{
180-
_logger.LogDebug("Host start requested after already started");
181-
return;
198+
_logger.LogDebug("Host start requested after already started.");
199+
await _started.Task.ConfigureAwait(false);
200+
return false;
182201
}
183202

184203
_pipelineThread.Start();
@@ -198,6 +217,15 @@ await ExecuteDelegateAsync(
198217
{
199218
await SetInitialWorkingDirectoryAsync(startOptions.InitialWorkingDirectory, CancellationToken.None).ConfigureAwait(false);
200219
}
220+
221+
await _started.Task.ConfigureAwait(false);
222+
return true;
223+
}
224+
225+
public void TriggerShutdown()
226+
{
227+
Interlocked.Exchange(ref _shuttingDown, 1);
228+
_cancellationContext.CancelCurrentTaskStack();
201229
}
202230

203231
public void SetExit()
@@ -349,11 +377,19 @@ public Task SetInitialWorkingDirectoryAsync(string path, CancellationToken cance
349377

350378
private void Run()
351379
{
352-
(PowerShell pwsh, RunspaceInfo localRunspaceInfo, EngineIntrinsics engineIntrinsics) = CreateInitialPowerShellSession();
353-
_mainRunspaceEngineIntrinsics = engineIntrinsics;
354-
_localComputerName = localRunspaceInfo.SessionDetails.ComputerName;
355-
_runspaceStack.Push(new RunspaceFrame(pwsh.Runspace, localRunspaceInfo));
356-
PushPowerShellAndRunLoop(pwsh, PowerShellFrameType.Normal, localRunspaceInfo);
380+
try
381+
{
382+
(PowerShell pwsh, RunspaceInfo localRunspaceInfo, EngineIntrinsics engineIntrinsics) = CreateInitialPowerShellSession();
383+
_mainRunspaceEngineIntrinsics = engineIntrinsics;
384+
_localComputerName = localRunspaceInfo.SessionDetails.ComputerName;
385+
_runspaceStack.Push(new RunspaceFrame(pwsh.Runspace, localRunspaceInfo));
386+
PushPowerShellAndRunLoop(pwsh, PowerShellFrameType.Normal, localRunspaceInfo);
387+
}
388+
catch (Exception e)
389+
{
390+
_started.TrySetException(e);
391+
_stopped.TrySetException(e);
392+
}
357393
}
358394

359395
private (PowerShell, RunspaceInfo, EngineIntrinsics) CreateInitialPowerShellSession()
@@ -465,25 +501,40 @@ private void PopPowerShell(RunspaceChangeAction runspaceChangeAction = RunspaceC
465501

466502
private void RunTopLevelExecutionLoop()
467503
{
468-
// Make sure we execute any startup tasks first
469-
while (_taskQueue.TryTake(out ISynchronousTask task))
504+
try
470505
{
471-
task.ExecuteSynchronously(CancellationToken.None);
472-
}
506+
// Make sure we execute any startup tasks first
507+
while (_taskQueue.TryTake(out ISynchronousTask task))
508+
{
509+
task.ExecuteSynchronously(CancellationToken.None);
510+
}
473511

474-
if (_hostInfo.ConsoleReplEnabled)
475-
{
476-
RunExecutionLoop();
512+
// Signal that we are ready for outside services to use
513+
_started.TrySetResult(true);
514+
515+
if (_hostInfo.ConsoleReplEnabled)
516+
{
517+
RunExecutionLoop();
518+
}
519+
else
520+
{
521+
RunNoPromptExecutionLoop();
522+
}
477523
}
478-
else
524+
catch (Exception e)
479525
{
480-
RunNoPromptExecutionLoop();
526+
_logger.LogError(e, "PSES pipeline thread loop experienced an unexpected top-level exception");
527+
_stopped.TrySetException(e);
528+
return;
481529
}
530+
531+
_logger.LogInformation("PSES pipeline thread loop shutting down");
532+
_stopped.SetResult(true);
482533
}
483534

484535
private void RunNoPromptExecutionLoop()
485536
{
486-
while (!_shouldExit)
537+
while (!ShouldExitExecutionLoop)
487538
{
488539
using (CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: false))
489540
{
@@ -521,13 +572,13 @@ private void RunDebugExecutionLoop()
521572

522573
private void RunExecutionLoop()
523574
{
524-
while (!_shouldExit)
575+
while (!ShouldExitExecutionLoop)
525576
{
526577
using (CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: false))
527578
{
528579
DoOneRepl(cancellationScope.CancellationToken);
529580

530-
while (!_shouldExit
581+
while (!ShouldExitExecutionLoop
531582
&& !cancellationScope.CancellationToken.IsCancellationRequested
532583
&& _taskQueue.TryTake(out ISynchronousTask task))
533584
{
@@ -806,7 +857,7 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs break
806857

807858
private void OnRunspaceStateChanged(object sender, RunspaceStateEventArgs runspaceStateEventArgs)
808859
{
809-
if (!_shouldExit && !_resettingRunspace && !runspaceStateEventArgs.RunspaceStateInfo.IsUsable())
860+
if (!ShouldExitExecutionLoop && !_resettingRunspace && !runspaceStateEventArgs.RunspaceStateInfo.IsUsable())
810861
{
811862
_resettingRunspace = true;
812863
PopOrReinitializeRunspaceAsync().HandleErrorsAsync(_logger);

src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public override async Task<Unit> Handle(DidChangeConfigurationParams request, Ca
9292
_logger.LogTrace("Loading profiles...");
9393
}
9494

95-
await _psesHost.StartAsync(new HostStartOptions(), CancellationToken.None).ConfigureAwait(false);
95+
await _psesHost.TryStartAsync(new HostStartOptions { LoadProfiles = loadProfiles }, CancellationToken.None).ConfigureAwait(false);
9696

9797
if (loadProfiles)
9898
{

test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Diagnostics;
56
using System.IO;
67
using System.Linq;
78
using System.Reflection;
@@ -44,6 +45,13 @@ public async Task InitializeAsync()
4445
await _psesProcess.Start().ConfigureAwait(false);
4546

4647
var initialized = new TaskCompletionSource<bool>();
48+
49+
_psesProcess.ProcessExited += (sender, args) =>
50+
{
51+
initialized.TrySetException(new ProcessExitedException("Initialization failed due to process failure", args.ExitCode, args.ErrorMessage));
52+
Started.TrySetException(new ProcessExitedException("Startup failed due to process failure", args.ExitCode, args.ErrorMessage));
53+
};
54+
4755
PsesDebugAdapterClient = DebugAdapterClient.Create(options =>
4856
{
4957
options
@@ -61,6 +69,11 @@ public async Task InitializeAsync()
6169
initialized.SetResult(true);
6270
return Task.CompletedTask;
6371
});
72+
73+
options.OnUnhandledException = (exception) => {
74+
initialized.SetException(exception);
75+
Started.SetException(exception);
76+
};
6477
});
6578

6679
// PSES follows the following flow:

test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.IO;
78
using System.Linq;
89
using System.Reflection;
@@ -44,6 +45,7 @@ public async Task InitializeAsync()
4445
var factory = new LoggerFactory();
4546
_psesProcess = new PsesStdioProcess(factory, IsDebugAdapterTests);
4647
await _psesProcess.Start().ConfigureAwait(false);
48+
//Debugger.Launch();
4749

4850
Diagnostics = new List<Diagnostic>();
4951
TelemetryEvents = new List<PsesTelemetryEvent>();

test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class LanguageServerProtocolMessageTests : IClassFixture<LSPTestsFixture>
4141
private readonly List<Diagnostic> Diagnostics;
4242
private readonly List<PsesTelemetryEvent> TelemetryEvents;
4343
private readonly string PwshExe;
44+
private readonly LSPTestsFixture _fixture;
4445

4546
public LanguageServerProtocolMessageTests(ITestOutputHelper output, LSPTestsFixture data)
4647
{
@@ -50,6 +51,7 @@ public LanguageServerProtocolMessageTests(ITestOutputHelper output, LSPTestsFixt
5051
Diagnostics.Clear();
5152
TelemetryEvents = data.TelemetryEvents;
5253
TelemetryEvents.Clear();
54+
_fixture = data;
5355

5456
PwshExe = PsesStdioProcess.PwshExe;
5557
}

0 commit comments

Comments
 (0)