-
Notifications
You must be signed in to change notification settings - Fork 234
/
Copy pathEditorServicesHost.cs
493 lines (410 loc) · 18.5 KB
/
EditorServicesHost.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Components;
using Microsoft.PowerShell.EditorServices.CodeLenses;
using Microsoft.PowerShell.EditorServices.Extensions;
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
using Microsoft.PowerShell.EditorServices.Protocol.Server;
using Microsoft.PowerShell.EditorServices.Session;
using Microsoft.PowerShell.EditorServices.Symbols;
using Microsoft.PowerShell.EditorServices.Utility;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace Microsoft.PowerShell.EditorServices.Host
{
public enum EditorServicesHostStatus
{
Started,
Failed,
Ended
}
public enum EditorServiceTransportType
{
NamedPipe,
Stdio
}
public class EditorServiceTransportConfig
{
public EditorServiceTransportType TransportType { get; set; }
/// <summary>
/// Configures the endpoint of the transport.
/// For Stdio it's ignored.
/// For NamedPipe it's the pipe name.
/// </summary>
public string InOutPipeName { get; set; }
public string OutPipeName { get; set; }
public string InPipeName { get; set; }
internal string Endpoint => OutPipeName != null && InPipeName != null ? $"In pipe: {InPipeName} Out pipe: {OutPipeName}" : $" InOut pipe: {InOutPipeName}";
}
/// <summary>
/// Provides a simplified interface for hosting the language and debug services
/// over the named pipe server protocol.
/// </summary>
public class EditorServicesHost
{
#region Private Fields
private string[] additionalModules;
private string bundledModulesPath;
private DebugAdapter debugAdapter;
private EditorSession editorSession;
private bool enableConsoleRepl;
private HashSet<string> featureFlags;
private HostDetails hostDetails;
private LanguageServer languageServer;
private ILogger logger;
private ProfilePaths profilePaths;
private TaskCompletionSource<bool> serverCompletedTask;
private IServerListener languageServiceListener;
private IServerListener debugServiceListener;
#endregion
#region Properties
public EditorServicesHostStatus Status { get; private set; }
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the EditorServicesHost class and waits for
/// the debugger to attach if waitForDebugger is true.
/// </summary>
/// <param name="hostDetails">The details of the host which is launching PowerShell Editor Services.</param>
/// <param name="bundledModulesPath">Provides a path to PowerShell modules bundled with the host, if any. Null otherwise.</param>
/// <param name="waitForDebugger">If true, causes the host to wait for the debugger to attach before proceeding.</param>
public EditorServicesHost(
HostDetails hostDetails,
string bundledModulesPath,
bool enableConsoleRepl,
bool waitForDebugger,
string[] additionalModules,
string[] featureFlags)
{
Validate.IsNotNull(nameof(hostDetails), hostDetails);
this.hostDetails = hostDetails;
this.enableConsoleRepl = enableConsoleRepl;
this.bundledModulesPath = bundledModulesPath;
this.additionalModules = additionalModules ?? new string[0];
this.featureFlags = new HashSet<string>(featureFlags ?? new string[0]);
this.serverCompletedTask = new TaskCompletionSource<bool>();
#if DEBUG
if (waitForDebugger)
{
if (Debugger.IsAttached)
{
Debugger.Break();
}
else
{
Debugger.Launch();
}
}
#endif
// Catch unhandled exceptions for logging purposes
#if !CoreCLR
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
#endif
}
#endregion
#region Public Methods
/// <summary>
/// Starts the Logger for the specified file path and log level.
/// </summary>
/// <param name="logFilePath">The path of the log file to be written.</param>
/// <param name="logLevel">The minimum level of log messages to be written.</param>
public void StartLogging(string logFilePath, LogLevel logLevel)
{
this.logger = Logging.CreateLogger()
.LogLevel(logLevel)
.AddLogFile(logFilePath)
.Build();
#if CoreCLR
FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location);
#else
FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(this.GetType().Assembly.Location);
#endif
string osVersion = RuntimeInformation.OSDescription;
string buildTime = BuildInfo.BuildTime?.ToString("s", System.Globalization.CultureInfo.InvariantCulture) ?? "<unspecified>";
string logHeader = $@"
PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (PID {Process.GetCurrentProcess().Id}
Host application details:
Name: {this.hostDetails.Name}
Version: {this.hostDetails.Version}
ProfileId: {this.hostDetails.ProfileId}
Arch: {RuntimeInformation.OSArchitecture}
Operating system details:
Version: {osVersion}
Arch: {RuntimeInformation.OSArchitecture}
Build information:
Version: {BuildInfo.BuildVersion}
Origin: {BuildInfo.BuildOrigin}
Date: {buildTime}
";
this.logger.Write(LogLevel.Normal, logHeader);
}
/// <summary>
/// Starts the language service with the specified config.
/// </summary>
/// <param name="config">The config that contains information on the communication protocol that will be used.</param>
/// <param name="profilePaths">The profiles that will be loaded in the session.</param>
public void StartLanguageService(
EditorServiceTransportConfig config,
ProfilePaths profilePaths)
{
this.profilePaths = profilePaths;
this.languageServiceListener = CreateServiceListener(MessageProtocolType.LanguageServer, config);
this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnect;
this.languageServiceListener.Start();
this.logger.Write(
LogLevel.Normal,
string.Format(
"Language service started, type = {0}, endpoint = {1}",
config.TransportType, config.Endpoint));
}
private async void OnLanguageServiceClientConnect(
object sender,
ChannelBase serverChannel)
{
MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger);
ProtocolEndpoint protocolEndpoint =
new ProtocolEndpoint(
serverChannel,
messageDispatcher,
this.logger);
protocolEndpoint.UnhandledException += ProtocolEndpoint_UnhandledException;
this.editorSession =
CreateSession(
this.hostDetails,
this.profilePaths,
protocolEndpoint,
messageDispatcher,
this.enableConsoleRepl);
this.languageServer =
new LanguageServer(
this.editorSession,
messageDispatcher,
protocolEndpoint,
this.serverCompletedTask,
this.logger);
await this.editorSession.PowerShellContext.ImportCommandsModule(
Path.Combine(
Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location),
@"..\..\Commands"));
this.languageServer.Start();
// TODO: This can be moved to the point after the $psEditor object
// gets initialized when that is done earlier than LanguageServer.Initialize
foreach (string module in this.additionalModules)
{
await this.editorSession.PowerShellContext.ExecuteCommand<System.Management.Automation.PSObject>(
new System.Management.Automation.PSCommand().AddCommand("Microsoft.PowerShell.Core\\Import-Module").AddArgument(module),
false,
true);
}
protocolEndpoint.Start();
}
/// <summary>
/// Starts the debug service with the specified config.
/// </summary>
/// <param name="config">The config that contains information on the communication protocol that will be used.</param>
/// <param name="profilePaths">The profiles that will be loaded in the session.</param>
/// <param name="useExistingSession">Determines if we will reuse the session that we have.</param>
public void StartDebugService(
EditorServiceTransportConfig config,
ProfilePaths profilePaths,
bool useExistingSession)
{
this.debugServiceListener = CreateServiceListener(MessageProtocolType.DebugAdapter, config);
this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect;
this.debugServiceListener.Start();
this.logger.Write(
LogLevel.Normal,
string.Format(
"Debug service started, type = {0}, endpoint = {1}",
config.TransportType, config.Endpoint));
}
private void OnDebugServiceClientConnect(object sender, ChannelBase serverChannel)
{
MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger);
ProtocolEndpoint protocolEndpoint =
new ProtocolEndpoint(
serverChannel,
messageDispatcher,
this.logger);
protocolEndpoint.UnhandledException += ProtocolEndpoint_UnhandledException;
bool ownsEditorSession = this.editorSession == null;
if (ownsEditorSession)
{
this.editorSession =
this.CreateDebugSession(
this.hostDetails,
profilePaths,
protocolEndpoint,
messageDispatcher,
this.languageServer?.EditorOperations,
this.enableConsoleRepl);
}
this.debugAdapter =
new DebugAdapter(
this.editorSession,
ownsEditorSession,
messageDispatcher,
protocolEndpoint,
this.logger);
this.debugAdapter.SessionEnded +=
(obj, args) =>
{
if (!ownsEditorSession)
{
this.logger.Write(
LogLevel.Normal,
"Previous debug session ended, restarting debug service listener...");
this.debugServiceListener.Stop();
this.debugServiceListener.Start();
}
else if (this.debugAdapter.IsUsingTempIntegratedConsole)
{
this.logger.Write(
LogLevel.Normal,
"Previous temp debug session ended");
}
else
{
// Exit the host process
this.serverCompletedTask.SetResult(true);
}
};
this.debugAdapter.Start();
protocolEndpoint.Start();
}
/// <summary>
/// Stops the language or debug services if either were started.
/// </summary>
public void StopServices()
{
// TODO: Need a new way to shut down the services
this.languageServer = null;
this.debugAdapter = null;
}
/// <summary>
/// Waits for either the language or debug service to shut down.
/// </summary>
public void WaitForCompletion()
{
// TODO: We need a way to know when to complete this task!
this.serverCompletedTask.Task.Wait();
}
#endregion
#region Private Methods
private EditorSession CreateSession(
HostDetails hostDetails,
ProfilePaths profilePaths,
IMessageSender messageSender,
IMessageHandlers messageHandlers,
bool enableConsoleRepl)
{
EditorSession editorSession = new EditorSession(this.logger);
PowerShellContext powerShellContext = new PowerShellContext(this.logger);
EditorServicesPSHostUserInterface hostUserInterface =
enableConsoleRepl
? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger)
: new ProtocolPSHostUserInterface(powerShellContext, messageSender, this.logger);
EditorServicesPSHost psHost =
new EditorServicesPSHost(
powerShellContext,
hostDetails,
hostUserInterface,
this.logger);
Runspace initialRunspace = PowerShellContext.CreateRunspace(psHost);
powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface);
editorSession.StartSession(powerShellContext, hostUserInterface);
// TODO: Move component registrations elsewhere!
editorSession.Components.Register(this.logger);
editorSession.Components.Register(messageHandlers);
editorSession.Components.Register(messageSender);
editorSession.Components.Register(powerShellContext);
CodeLensFeature.Create(editorSession.Components, editorSession);
DocumentSymbolFeature.Create(editorSession.Components, editorSession);
return editorSession;
}
private EditorSession CreateDebugSession(
HostDetails hostDetails,
ProfilePaths profilePaths,
IMessageSender messageSender,
IMessageHandlers messageHandlers,
IEditorOperations editorOperations,
bool enableConsoleRepl)
{
EditorSession editorSession = new EditorSession(this.logger);
PowerShellContext powerShellContext = new PowerShellContext(this.logger);
EditorServicesPSHostUserInterface hostUserInterface =
enableConsoleRepl
? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger)
: new ProtocolPSHostUserInterface(powerShellContext, messageSender, this.logger);
EditorServicesPSHost psHost =
new EditorServicesPSHost(
powerShellContext,
hostDetails,
hostUserInterface,
this.logger);
Runspace initialRunspace = PowerShellContext.CreateRunspace(psHost);
powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface);
editorSession.StartDebugSession(
powerShellContext,
hostUserInterface,
editorOperations);
return editorSession;
}
private void ProtocolEndpoint_UnhandledException(object sender, Exception e)
{
this.logger.Write(
LogLevel.Error,
"PowerShell Editor Services is terminating due to an unhandled exception, see previous logs for details.");
this.serverCompletedTask.SetException(e);
}
#if !CoreCLR
private void CurrentDomain_UnhandledException(
object sender,
UnhandledExceptionEventArgs e)
{
// Log the exception
this.logger.Write(
LogLevel.Error,
string.Format(
"FATAL UNHANDLED EXCEPTION:\r\n\r\n{0}",
e.ExceptionObject.ToString()));
}
#endif
private IServerListener CreateServiceListener(MessageProtocolType protocol, EditorServiceTransportConfig config)
{
switch (config.TransportType)
{
case EditorServiceTransportType.Stdio:
{
return new StdioServerListener(protocol, this.logger);
}
case EditorServiceTransportType.NamedPipe:
{
if ((config.OutPipeName != null) && (config.InPipeName != null))
{
this.logger.Write(LogLevel.Verbose, $"Creating NamedPipeServerListener for ${protocol} protocol with two pipes: In: '{config.InPipeName}'. Out: '{config.OutPipeName}'");
return new NamedPipeServerListener(protocol, config.InPipeName, config.OutPipeName, this.logger);
}
else
{
return new NamedPipeServerListener(protocol, config.InOutPipeName, this.logger);
}
}
default:
{
throw new NotSupportedException();
}
}
}
#endregion
}
}