-
Notifications
You must be signed in to change notification settings - Fork 234
/
Copy pathEditorServicesLoader.cs
419 lines (349 loc) · 17.1 KB
/
EditorServicesLoader.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
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using SMA = System.Management.Automation;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
#if ASSEMBLY_LOAD_STACKTRACE
using System.Diagnostics;
#endif
#if CoreCLR
using System.Runtime.Loader;
#else
using Microsoft.Win32;
#endif
namespace Microsoft.PowerShell.EditorServices.Hosting
{
/// <summary>
/// Class to contain the loading behavior of Editor Services.
/// In particular, this class wraps the point where Editor Services is safely loaded
/// in a way that separates its dependencies from the calling context.
/// </summary>
public sealed class EditorServicesLoader : IDisposable
{
#if !CoreCLR
// See https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed
private const int Net462Version = 394802;
private static readonly string s_psesBaseDirPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
#endif
private static readonly string s_psesDependencyDirPath = Path.GetFullPath(
Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
"..",
"Common"));
/// <summary>
/// Create a new Editor Services loader.
/// </summary>
/// <param name="logger">The host logger to use.</param>
/// <param name="hostConfig">The host configuration to start editor services with.</param>
/// <param name="sessionFileWriter">The session file writer to write the session file with.</param>
/// <returns></returns>
public static EditorServicesLoader Create(
HostLogger logger,
EditorServicesConfig hostConfig,
ISessionFileWriter sessionFileWriter) => Create(logger, hostConfig, sessionFileWriter, loggersToUnsubscribe: null);
/// <summary>
/// Create a new Editor Services loader.
/// </summary>
/// <param name="logger">The host logger to use.</param>
/// <param name="hostConfig">The host configuration to start editor services with.</param>
/// <param name="sessionFileWriter">The session file writer to write the session file with.</param>
/// <param name="loggersToUnsubscribe">The loggers to unsubscribe form writing to the terminal.</param>
/// <returns></returns>
public static EditorServicesLoader Create(
HostLogger logger,
EditorServicesConfig hostConfig,
ISessionFileWriter sessionFileWriter,
IReadOnlyCollection<IDisposable> loggersToUnsubscribe)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
if (hostConfig == null)
{
throw new ArgumentNullException(nameof(hostConfig));
}
#if CoreCLR
// In .NET Core, we add an event here to redirect dependency loading to the new AssemblyLoadContext we load PSES' dependencies into
logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for new AssemblyLoadContext dependency loading");
PsesLoadContext psesLoadContext = new(s_psesDependencyDirPath);
if (hostConfig.LogLevel == PsesLogLevel.Diagnostic)
{
AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) =>
{
logger.Log(
PsesLogLevel.Diagnostic,
$"Loaded into load context {AssemblyLoadContext.GetLoadContext(args.LoadedAssembly)}: {args.LoadedAssembly}");
};
}
AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext _, AssemblyName asmName) =>
{
#if ASSEMBLY_LOAD_STACKTRACE
logger.Log(PsesLogLevel.Diagnostic, $"Assembly resolve event fired for {asmName}. Stacktrace:\n{new StackTrace()}");
#else
logger.Log(PsesLogLevel.Diagnostic, $"Assembly resolve event fired for {asmName}");
#endif
// We only want the Editor Services DLL; the new ALC will lazily load its dependencies automatically
if (!string.Equals(asmName.Name, "Microsoft.PowerShell.EditorServices", StringComparison.Ordinal))
{
return null;
}
string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll");
logger.Log(PsesLogLevel.Verbose, "Loading PSES DLL using new assembly load context");
return psesLoadContext.LoadFromAssemblyPath(asmPath);
};
#else
// In .NET Framework we add an event here to redirect dependency loading in the current AppDomain for PSES' dependencies
logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for dependency loading");
if (hostConfig.LogLevel == PsesLogLevel.Diagnostic)
{
AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) =>
{
if (args.LoadedAssembly.IsDynamic)
{
return;
}
logger.Log(
PsesLogLevel.Diagnostic,
$"Loaded '{args.LoadedAssembly.GetName()}' from '{args.LoadedAssembly.Location}'");
};
}
// Unlike in .NET Core, we need to be look for all dependencies in .NET Framework, not just PSES.dll
AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs args) =>
{
#if ASSEMBLY_LOAD_STACKTRACE
logger.Log(PsesLogLevel.Diagnostic, $"Assembly resolve event fired for {args.Name}. Stacktrace:\n{new StackTrace()}");
#else
logger.Log(PsesLogLevel.Diagnostic, $"Assembly resolve event fired for {args.Name}");
#endif
AssemblyName asmName = new(args.Name);
string dllName = $"{asmName.Name}.dll";
// First look for the required assembly in the .NET Framework DLL dir
string baseDirAsmPath = Path.Combine(s_psesBaseDirPath, dllName);
if (File.Exists(baseDirAsmPath))
{
logger.Log(PsesLogLevel.Diagnostic, $"Loading {args.Name} from PSES base dir into LoadFile context");
return Assembly.LoadFile(baseDirAsmPath);
}
// Then look in the shared .NET Standard directory
string asmPath = Path.Combine(s_psesDependencyDirPath, dllName);
if (File.Exists(asmPath))
{
logger.Log(PsesLogLevel.Diagnostic, $"Loading {args.Name} from PSES dependency dir into LoadFile context");
return Assembly.LoadFile(asmPath);
}
return null;
};
#endif
return new EditorServicesLoader(logger, hostConfig, sessionFileWriter, loggersToUnsubscribe);
}
private readonly EditorServicesConfig _hostConfig;
private readonly ISessionFileWriter _sessionFileWriter;
private readonly HostLogger _logger;
private readonly IReadOnlyCollection<IDisposable> _loggersToUnsubscribe;
private EditorServicesRunner _editorServicesRunner;
private EditorServicesLoader(
HostLogger logger,
EditorServicesConfig hostConfig,
ISessionFileWriter sessionFileWriter,
IReadOnlyCollection<IDisposable> loggersToUnsubscribe)
{
_logger = logger;
_hostConfig = hostConfig;
_sessionFileWriter = sessionFileWriter;
_loggersToUnsubscribe = loggersToUnsubscribe;
}
/// <summary>
/// Load Editor Services and its dependencies in an isolated way and start it.
/// This method's returned task will end when Editor Services shuts down.
/// </summary>
/// <returns></returns>
public Task LoadAndRunEditorServicesAsync()
{
// Log important host information here
LogHostInformation();
#if !CoreCLR
// Make sure the .NET Framework version supports .NET Standard 2.0
CheckNetFxVersion();
#endif
// Add the bundled modules to the PSModulePath
// TODO: Why do we do this in addition to passing the bundled module path to the host?
UpdatePSModulePath();
// Check to see if the configuration we have is valid
ValidateConfiguration();
// Method with no implementation that forces the PSES assembly to load, triggering an AssemblyResolve event
_logger.Log(PsesLogLevel.Verbose, "Loading PowerShell Editor Services");
LoadEditorServices();
_logger.Log(PsesLogLevel.Verbose, "Starting EditorServices");
_editorServicesRunner = new EditorServicesRunner(_logger, _hostConfig, _sessionFileWriter, _loggersToUnsubscribe);
// The trigger method for Editor Services
return Task.Run(_editorServicesRunner.RunUntilShutdown);
}
public void Dispose()
{
_logger.Log(PsesLogLevel.Diagnostic, "Loader disposed");
_editorServicesRunner?.Dispose();
// TODO:
// Remove assembly resolve events
// This is not high priority, since the PSES process shouldn't be reused
}
private static void LoadEditorServices() =>
// This must be in its own method, since the actual load happens when the calling method is called
// The call within this method is therefore a total no-op
EditorServicesLoading.LoadEditorServicesForHost();
#if !CoreCLR
private void CheckNetFxVersion()
{
_logger.Log(PsesLogLevel.Diagnostic, "Checking that .NET Framework version is at least 4.6.2");
using RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Net Framework Setup\NDP\v4\Full");
object netFxValue = key?.GetValue("Release");
if (netFxValue == null || netFxValue is not int netFxVersion)
{
return;
}
_logger.Log(PsesLogLevel.Verbose, $".NET registry version: {netFxVersion}");
if (netFxVersion < Net462Version)
{
_logger.Log(PsesLogLevel.Warning, $".NET Framework version {netFxVersion} lower than .NET 4.6.2. This runtime is not supported and you may experience errors. Please update your .NET runtime version.");
}
}
#endif
private void UpdatePSModulePath()
{
if (string.IsNullOrEmpty(_hostConfig.BundledModulePath))
{
_logger.Log(PsesLogLevel.Diagnostic, "BundledModulePath not set, skipping");
return;
}
string psModulePath = Environment.GetEnvironmentVariable("PSModulePath").TrimEnd(Path.PathSeparator);
if ($"{psModulePath}{Path.PathSeparator}".Contains($"{_hostConfig.BundledModulePath}{Path.PathSeparator}"))
{
_logger.Log(PsesLogLevel.Diagnostic, "BundledModulePath already set, skipping");
return;
}
psModulePath = $"{psModulePath}{Path.PathSeparator}{_hostConfig.BundledModulePath}";
Environment.SetEnvironmentVariable("PSModulePath", psModulePath);
_logger.Log(PsesLogLevel.Verbose, $"Updated PSModulePath to: '{psModulePath}'");
}
private void LogHostInformation()
{
_logger.Log(PsesLogLevel.Verbose, $"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}");
_logger.Log(PsesLogLevel.Verbose, $@"
== Build Details ==
- Editor Services version: {BuildInfo.BuildVersion}
- Build origin: {BuildInfo.BuildOrigin}
- Build commit: {BuildInfo.BuildCommit}
- Build time: {BuildInfo.BuildTime}
");
_logger.Log(PsesLogLevel.Verbose, $@"
== Host Startup Configuration Details ==
- Host name: {_hostConfig.HostInfo.Name}
- Host version: {_hostConfig.HostInfo.Version}
- Host profile ID: {_hostConfig.HostInfo.ProfileId}
- PowerShell host type: {_hostConfig.PSHost.GetType()}
- REPL setting: {_hostConfig.ConsoleRepl}
- Session details path: {_hostConfig.SessionDetailsPath}
- Bundled modules path: {_hostConfig.BundledModulePath}
- Additional modules: {(_hostConfig.AdditionalModules == null ? "<null>" : string.Join(", ", _hostConfig.AdditionalModules))}
- Feature flags: {(_hostConfig.FeatureFlags == null ? "<null>" : string.Join(", ", _hostConfig.FeatureFlags))}
- Log path: {_hostConfig.LogPath}
- Minimum log level: {_hostConfig.LogLevel}
- Profile paths:
+ AllUsersAllHosts: {_hostConfig.ProfilePaths.AllUsersAllHosts ?? "<null>"}
+ AllUsersCurrentHost: {_hostConfig.ProfilePaths.AllUsersCurrentHost ?? "<null>"}
+ CurrentUserAllHosts: {_hostConfig.ProfilePaths.CurrentUserAllHosts ?? "<null>"}
+ CurrentUserCurrentHost: {_hostConfig.ProfilePaths.CurrentUserCurrentHost ?? "<null>"}
");
_logger.Log(PsesLogLevel.Verbose, $@"
== Console Details ==
- Console input encoding: {Console.InputEncoding.EncodingName}
- Console output encoding: {Console.OutputEncoding.EncodingName}
- PowerShell output encoding: {GetPSOutputEncoding()}
");
LogPowerShellDetails();
LogOperatingSystemDetails();
}
private static string GetPSOutputEncoding()
{
using SMA.PowerShell pwsh = SMA.PowerShell.Create();
return pwsh.AddScript("$OutputEncoding.EncodingName", useLocalScope: true).Invoke<string>()[0];
}
private void LogPowerShellDetails()
{
PSLanguageMode languageMode = Runspace.DefaultRunspace.SessionStateProxy.LanguageMode;
_logger.Log(PsesLogLevel.Verbose, $@"
== PowerShell Details ==
- PowerShell version: {GetPSVersion()}
- Language mode: {languageMode}
");
}
private void LogOperatingSystemDetails()
{
_logger.Log(PsesLogLevel.Verbose, $@"
== Environment Details ==
- OS description: {RuntimeInformation.OSDescription}
- OS architecture: {GetOSArchitecture()}
- Process bitness: {(Environment.Is64BitProcess ? "64" : "32")}
");
}
// TODO: Deduplicate this with VersionUtils.
private static string GetOSArchitecture()
{
#if CoreCLR
if (Environment.OSVersion.Platform != PlatformID.Win32NT)
{
return RuntimeInformation.OSArchitecture.ToString();
}
#endif
// If on win7 (version 6.1.x), avoid System.Runtime.InteropServices.RuntimeInformation
if (Environment.OSVersion.Version < new Version(6, 2))
{
return Environment.Is64BitProcess
? "X64"
: "X86";
}
return RuntimeInformation.OSArchitecture.ToString();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "Checking user-defined configuration")]
private void ValidateConfiguration()
{
_logger.Log(PsesLogLevel.Diagnostic, "Validating configuration");
bool lspUsesStdio = _hostConfig.LanguageServiceTransport is StdioTransportConfig;
bool debugUsesStdio = _hostConfig.DebugServiceTransport is StdioTransportConfig;
// Ensure LSP and Debug are not both Stdio
if (lspUsesStdio && debugUsesStdio)
{
throw new ArgumentException("LSP and Debug transports cannot both use Stdio");
}
if (_hostConfig.ConsoleRepl != ConsoleReplKind.None
&& (lspUsesStdio || debugUsesStdio))
{
throw new ArgumentException("Cannot use the REPL with a Stdio protocol transport");
}
if (_hostConfig.PSHost == null)
{
throw new ArgumentNullException(nameof(_hostConfig.PSHost));
}
}
private static object GetPSVersion()
{
// In order to read the $PSVersionTable variable,
// we are forced to create a new runspace to avoid concurrency issues,
// which is expensive.
// Rather than do that, we instead go straight to the source,
// which is a static property, internal in WinPS and public in PS 6+
#pragma warning disable CA1825
return typeof(PSObject).Assembly
.GetType("System.Management.Automation.PSVersionInfo")
.GetMethod("get_PSVersion", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
.Invoke(null, new object[0] /* Cannot use Array.Empty, since it must work in net452 */);
#pragma warning restore CA1825
}
}
}