Skip to content

Commit 9d56268

Browse files
committed
Add support for loading host profile scripts
This change adds support for loading both host-agnostic and host-specific profile scripts for the current user and all users depending on which of those scripts are available on the system. Profile loading behavior is opt-in and not enabled by default. Regardless of whether profiles get loaded, a $profile variable is inserted into the PowerShellContext's runspace upon initialization. Resolves #111.
1 parent 9130e75 commit 9d56268

File tree

13 files changed

+452
-34
lines changed

13 files changed

+452
-34
lines changed

src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public class LanguageServerWebSocketConnection : EditorServiceWebSocketConnectio
139139
{
140140
public LanguageServerWebSocketConnection()
141141
{
142-
Server = new LanguageServer(Channel);
142+
Server = new LanguageServer(null, Channel);
143143
}
144144
}
145145

src/PowerShellEditorServices.Host/Program.cs

+27-14
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using System;
1010
using System.Diagnostics;
1111
using System.Linq;
12-
using System.Threading;
1312

1413
namespace Microsoft.PowerShell.EditorServices.Host
1514
{
@@ -78,21 +77,28 @@ static void Main(string[] args)
7877
"/debugAdapter",
7978
StringComparison.InvariantCultureIgnoreCase));
8079

81-
// Catch unhandled exceptions for logging purposes
82-
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
80+
string hostProfileId = null;
81+
string hostProfileIdArgument =
82+
args.FirstOrDefault(
83+
arg =>
84+
arg.StartsWith(
85+
"/hostProfileId:",
86+
StringComparison.InvariantCultureIgnoreCase));
8387

84-
ProtocolEndpoint server = null;
85-
if (runDebugAdapter)
88+
if (!string.IsNullOrEmpty(hostProfileIdArgument))
8689
{
87-
logPath = logPath ?? "DebugAdapter.log";
88-
server = new DebugAdapter();
89-
}
90-
else
91-
{
92-
logPath = logPath ?? "EditorServices.log";
93-
server = new LanguageServer();
90+
hostProfileId = hostProfileIdArgument.Substring(15).Trim('"');
9491
}
9592

93+
// Catch unhandled exceptions for logging purposes
94+
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
95+
96+
// Use a default log path filename if one isn't specified
97+
logPath =
98+
runDebugAdapter
99+
? logPath ?? "DebugAdapter.log"
100+
: logPath ?? "EditorServices.log";
101+
96102
// Start the logger with the specified log path and level
97103
Logger.Initialize(logPath, logLevel);
98104

@@ -103,9 +109,16 @@ static void Main(string[] args)
103109
Logger.Write(
104110
LogLevel.Normal,
105111
string.Format(
106-
"PowerShell Editor Services Host v{0} starting (pid {1})...",
112+
"PowerShell Editor Services Host v{0} starting (pid {1}, hostProfileId '{2}')...",
107113
fileVersionInfo.FileVersion,
108-
Process.GetCurrentProcess().Id));
114+
Process.GetCurrentProcess().Id,
115+
hostProfileId));
116+
117+
// Create the appropriate server type
118+
ProtocolEndpoint server =
119+
runDebugAdapter
120+
? (ProtocolEndpoint) new DebugAdapter()
121+
: (ProtocolEndpoint) new LanguageServer(hostProfileId);
109122

110123
// Start the server
111124
server.Start().Wait();

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+29-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer;
77
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
88
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
9-
using Microsoft.PowerShell.EditorServices.Protocol.Messages;
109
using Microsoft.PowerShell.EditorServices.Utility;
1110
using System;
1211
using System.Collections.Generic;
@@ -24,18 +23,33 @@ public class LanguageServer : LanguageServerBase
2423
{
2524
private static CancellationTokenSource existingRequestCancellation;
2625

26+
private bool profilesLoaded;
2727
private EditorSession editorSession;
2828
private OutputDebouncer outputDebouncer;
2929
private LanguageServerSettings currentSettings = new LanguageServerSettings();
3030

31-
public LanguageServer() : this(new StdioServerChannel())
31+
/// <param name="hostProfileId">
32+
/// The identifier of the PowerShell host to use for its profile path.
33+
/// loaded. Used to resolve a profile path of the form 'X_profile.ps1'
34+
/// where 'X' represents the value of hostProfileId. If null, a default
35+
/// will be used.
36+
/// </param>
37+
public LanguageServer(string hostProfileId)
38+
: this(hostProfileId, new StdioServerChannel())
3239
{
3340
}
3441

35-
public LanguageServer(ChannelBase serverChannel) : base(serverChannel)
42+
/// <param name="hostProfileId">
43+
/// The identifier of the PowerShell host to use for its profile path.
44+
/// loaded. Used to resolve a profile path of the form 'X_profile.ps1'
45+
/// where 'X' represents the value of hostProfileId. If null, a default
46+
/// will be used.
47+
/// </param>
48+
public LanguageServer(string hostProfileId, ChannelBase serverChannel)
49+
: base(serverChannel)
3650
{
3751
this.editorSession = new EditorSession();
38-
this.editorSession.StartSession();
52+
this.editorSession.StartSession(hostProfileId);
3953
this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten;
4054

4155
// Always send console prompts through the UI in the language service
@@ -59,7 +73,7 @@ protected override void Initialize()
5973
this.SetEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotification);
6074
this.SetEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotification);
6175
this.SetEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotification);
62-
this.SetEventHandler(DidChangeConfigurationNotification<SettingsWrapper>.Type, this.HandleDidChangeConfigurationNotification);
76+
this.SetEventHandler(DidChangeConfigurationNotification<LanguageServerSettingsWrapper>.Type, this.HandleDidChangeConfigurationNotification);
6377

6478
this.SetRequestHandler(DefinitionRequest.Type, this.HandleDefinitionRequest);
6579
this.SetRequestHandler(ReferencesRequest.Type, this.HandleReferencesRequest);
@@ -287,15 +301,24 @@ protected Task HandleDidChangeTextDocumentNotification(
287301
}
288302

289303
protected async Task HandleDidChangeConfigurationNotification(
290-
DidChangeConfigurationParams<SettingsWrapper> configChangeParams,
304+
DidChangeConfigurationParams<LanguageServerSettingsWrapper> configChangeParams,
291305
EventContext eventContext)
292306
{
307+
bool oldLoadProfiles = this.currentSettings.LoadProfiles;
293308
bool oldScriptAnalysisEnabled =
294309
this.currentSettings.ScriptAnalysis.Enable.HasValue;
295310

296311
this.currentSettings.Update(
297312
configChangeParams.Settings.Powershell);
298313

314+
if (!this.profilesLoaded &&
315+
this.currentSettings.LoadProfiles &&
316+
oldLoadProfiles != this.currentSettings.LoadProfiles)
317+
{
318+
await this.editorSession.PowerShellContext.LoadProfilesForHost();
319+
this.profilesLoaded = true;
320+
}
321+
299322
if (oldScriptAnalysisEnabled != this.currentSettings.ScriptAnalysis.Enable)
300323
{
301324
// If the user just turned off script analysis, send a diagnostics

src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.Server
77
{
88
public class LanguageServerSettings
99
{
10+
public bool LoadProfiles { get; set; }
11+
1012
public ScriptAnalysisSettings ScriptAnalysis { get; set; }
1113

1214
public LanguageServerSettings()
@@ -18,6 +20,7 @@ public void Update(LanguageServerSettings settings)
1820
{
1921
if (settings != null)
2022
{
23+
this.LoadProfiles = settings.LoadProfiles;
2124
this.ScriptAnalysis.Update(settings.ScriptAnalysis);
2225
}
2326
}
@@ -41,7 +44,7 @@ public void Update(ScriptAnalysisSettings settings)
4144
}
4245
}
4346

44-
public class SettingsWrapper
47+
public class LanguageServerSettingsWrapper
4548
{
4649
// NOTE: This property is capitalized as 'Powershell' because the
4750
// mode name sent from the client is written as 'powershell' and

src/PowerShellEditorServices/PowerShellEditorServices.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
<Compile Include="Session\PowerShellExecutionResult.cs" />
108108
<Compile Include="Session\PowerShellContext.cs" />
109109
<Compile Include="Session\PowerShellContextState.cs" />
110+
<Compile Include="Session\ProfilePaths.cs" />
110111
<Compile Include="Session\ProgressDetails.cs" />
111112
<Compile Include="Session\RunspaceHandle.cs" />
112113
<Compile Include="Session\SessionPSHost.cs" />

src/PowerShellEditorServices/Session/EditorSession.cs

+16-6
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@
55

66
using Microsoft.PowerShell.EditorServices.Console;
77
using Microsoft.PowerShell.EditorServices.Utility;
8-
using System;
98
using System.IO;
10-
using System.Management.Automation;
11-
using System.Management.Automation.Runspaces;
12-
using System.Reflection;
13-
using System.Threading;
149

1510
namespace Microsoft.PowerShell.EditorServices
1611
{
@@ -61,9 +56,24 @@ public class EditorSession
6156
/// for the ConsoleService.
6257
/// </summary>
6358
public void StartSession()
59+
{
60+
this.StartSession(null);
61+
}
62+
63+
/// <summary>
64+
/// Starts the session using the provided IConsoleHost implementation
65+
/// for the ConsoleService.
66+
/// </summary>
67+
/// <param name="hostProfileId">
68+
/// The identifier of the PowerShell host to use for its profile path.
69+
/// loaded. Used to resolve a profile path of the form 'X_profile.ps1'
70+
/// where 'X' represents the value of hostProfileId. If null, a default
71+
/// will be used.
72+
/// </param>
73+
public void StartSession(string hostProfileId)
6474
{
6575
// Initialize all services
66-
this.PowerShellContext = new PowerShellContext();
76+
this.PowerShellContext = new PowerShellContext(hostProfileId);
6777
this.LanguageService = new LanguageService(this.PowerShellContext);
6878
this.DebugService = new DebugService(this.PowerShellContext);
6979
this.ConsoleService = new ConsoleService(this.PowerShellContext);

src/PowerShellEditorServices/Session/PowerShellContext.cs

+106-5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class PowerShellContext : IDisposable
3737
private bool ownsInitialRunspace;
3838
private Runspace initialRunspace;
3939
private Runspace currentRunspace;
40+
private ProfilePaths profilePaths;
4041
private ConsoleServicePSHost psHost;
4142
private InitialSessionState initialSessionState;
4243
private IVersionSpecificOperations versionSpecificOperations;
@@ -104,7 +105,21 @@ internal IConsoleHost ConsoleHost
104105
/// Initializes a new instance of the PowerShellContext class and
105106
/// opens a runspace to be used for the session.
106107
/// </summary>
107-
public PowerShellContext()
108+
public PowerShellContext() : this(null)
109+
{
110+
}
111+
112+
/// <summary>
113+
/// Initializes a new instance of the PowerShellContext class and
114+
/// opens a runspace to be used for the session.
115+
/// </summary>
116+
/// <param name="hostProfileId">
117+
/// The identifier of the PowerShell host to use for its profile path.
118+
/// loaded. Used to resolve a profile path of the form 'X_profile.ps1'
119+
/// where 'X' represents the value of hostProfileId. If null, a default
120+
/// will be used.
121+
/// </param>
122+
public PowerShellContext(string hostProfileId)
108123
{
109124
this.psHost = new ConsoleServicePSHost();
110125
this.initialSessionState = InitialSessionState.CreateDefault2();
@@ -116,7 +131,7 @@ public PowerShellContext()
116131

117132
this.ownsInitialRunspace = true;
118133

119-
this.Initialize(runspace);
134+
this.Initialize(hostProfileId, runspace);
120135

121136
// Use reflection to execute ConsoleVisibility.AlwaysCaptureApplicationIO = true;
122137
Type consoleVisibilityType =
@@ -142,12 +157,23 @@ public PowerShellContext()
142157
/// an existing runspace for the session.
143158
/// </summary>
144159
/// <param name="initialRunspace"></param>
145-
public PowerShellContext(Runspace initialRunspace)
160+
/// <param name="hostProfileId">
161+
/// The identifier of the PowerShell host to use for its profile path.
162+
/// loaded. Used to resolve a profile path of the form 'X_profile.ps1'
163+
/// where 'X' represents the value of hostProfileId. If null, a default
164+
/// will be used.
165+
/// </param>
166+
public PowerShellContext(string hostProfileId, Runspace initialRunspace)
146167
{
147-
this.Initialize(initialRunspace);
168+
this.Initialize(hostProfileId, initialRunspace);
148169
}
149170

150171
private void Initialize(Runspace initialRunspace)
172+
{
173+
this.Initialize(null, initialRunspace);
174+
}
175+
176+
private void Initialize(string hostProfileId, Runspace initialRunspace)
151177
{
152178
Validate.IsNotNull("initialRunspace", initialRunspace);
153179

@@ -160,7 +186,6 @@ private void Initialize(Runspace initialRunspace)
160186
this.currentRunspace.Debugger.DebuggerStop += OnDebuggerStop;
161187

162188
this.powerShell = PowerShell.Create();
163-
this.powerShell.InvocationStateChanged += powerShell_InvocationStateChanged;
164189
this.powerShell.Runspace = this.currentRunspace;
165190

166191
// TODO: Should this be configurable?
@@ -199,6 +224,14 @@ private void Initialize(Runspace initialRunspace)
199224
this.versionSpecificOperations.ConfigureDebugger(
200225
this.currentRunspace);
201226

227+
// Set the $profile variable in the runspace
228+
this.profilePaths =
229+
this.SetProfileVariableInCurrentRunspace(
230+
hostProfileId ?? ProfilePaths.DefaultHostProfileId);
231+
232+
// Now that initialization is complete we can watch for InvocationStateChanged
233+
this.powerShell.InvocationStateChanged += powerShell_InvocationStateChanged;
234+
202235
this.SessionState = PowerShellContextState.Ready;
203236

204237
// Now that the runspace is ready, enqueue it for first use
@@ -493,6 +526,24 @@ public async Task ExecuteScriptAtPath(string scriptPath, string arguments = null
493526
await this.ExecuteCommand<object>(command, true);
494527
}
495528

529+
/// <summary>
530+
/// Loads PowerShell profiles for the host from the
531+
/// standard system locations. Only the profile paths which
532+
/// exist are loaded.
533+
/// </summary>
534+
/// <returns>A Task that can be awaited for completion.</returns>
535+
public async Task LoadProfilesForHost()
536+
{
537+
// Load any of the profile paths that exist
538+
PSCommand command = null;
539+
foreach (var profilePath in this.profilePaths.GetLoadableProfilePaths())
540+
{
541+
command = new PSCommand();
542+
command.AddCommand(profilePath, false);
543+
await this.ExecuteCommand(command);
544+
}
545+
}
546+
496547
/// <summary>
497548
/// Causes the current execution to be aborted no matter what state
498549
/// it is currently in.
@@ -970,6 +1021,56 @@ private void WritePromptWithNestedPipeline()
9701021
});
9711022
}
9721023
}
1024+
1025+
private ProfilePaths SetProfileVariableInCurrentRunspace(string hostProfileId)
1026+
{
1027+
// Get the profile paths for the host name
1028+
ProfilePaths profilePaths =
1029+
new ProfilePaths(
1030+
hostProfileId,
1031+
this.currentRunspace);
1032+
1033+
// Create the $profile variable
1034+
PSObject profile = new PSObject(profilePaths.CurrentUserCurrentHost);
1035+
1036+
profile.Members.Add(
1037+
new PSNoteProperty(
1038+
nameof(profilePaths.AllUsersAllHosts),
1039+
profilePaths.AllUsersAllHosts));
1040+
1041+
profile.Members.Add(
1042+
new PSNoteProperty(
1043+
nameof(profilePaths.AllUsersCurrentHost),
1044+
profilePaths.AllUsersCurrentHost));
1045+
1046+
profile.Members.Add(
1047+
new PSNoteProperty(
1048+
nameof(profilePaths.CurrentUserAllHosts),
1049+
profilePaths.CurrentUserAllHosts));
1050+
1051+
profile.Members.Add(
1052+
new PSNoteProperty(
1053+
nameof(profilePaths.CurrentUserCurrentHost),
1054+
profilePaths.CurrentUserCurrentHost));
1055+
1056+
Logger.Write(
1057+
LogLevel.Verbose,
1058+
string.Format(
1059+
"Setting $profile variable in runspace. Current user host profile path: {0}",
1060+
profilePaths.CurrentUserCurrentHost));
1061+
1062+
// Set the variable in the runspace
1063+
this.powerShell.Commands.Clear();
1064+
this.powerShell
1065+
.AddCommand("Set-Variable")
1066+
.AddParameter("Name", "profile")
1067+
.AddParameter("Value", profile)
1068+
.AddParameter("Option", "None");
1069+
this.powerShell.Invoke();
1070+
this.powerShell.Commands.Clear();
1071+
1072+
return profilePaths;
1073+
}
9731074

9741075
#endregion
9751076

0 commit comments

Comments
 (0)