diff --git a/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs b/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs index 90586818b..01906d6cd 100644 --- a/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs +++ b/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs @@ -139,7 +139,7 @@ public class LanguageServerWebSocketConnection : EditorServiceWebSocketConnectio { public LanguageServerWebSocketConnection() { - Server = new LanguageServer(Channel); + Server = new LanguageServer(null, Channel); } } @@ -150,7 +150,7 @@ public class DebugAdapterWebSocketConnection : EditorServiceWebSocketConnection { public DebugAdapterWebSocketConnection() { - Server = new DebugAdapter(Channel); + Server = new DebugAdapter(null, Channel); } } diff --git a/src/PowerShellEditorServices.Host/Program.cs b/src/PowerShellEditorServices.Host/Program.cs index 95b146c6d..fc2cd06ff 100644 --- a/src/PowerShellEditorServices.Host/Program.cs +++ b/src/PowerShellEditorServices.Host/Program.cs @@ -5,11 +5,11 @@ using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.Server; +using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Diagnostics; using System.Linq; -using System.Threading; namespace Microsoft.PowerShell.EditorServices.Host { @@ -78,21 +78,63 @@ static void Main(string[] args) "/debugAdapter", StringComparison.InvariantCultureIgnoreCase)); - // Catch unhandled exceptions for logging purposes - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + string hostProfileId = null; + string hostProfileIdArgument = + args.FirstOrDefault( + arg => + arg.StartsWith( + "/hostProfileId:", + StringComparison.InvariantCultureIgnoreCase)); + + if (!string.IsNullOrEmpty(hostProfileIdArgument)) + { + hostProfileId = hostProfileIdArgument.Substring(15).Trim('"'); + } + + string hostName = null; + string hostNameArgument = + args.FirstOrDefault( + arg => + arg.StartsWith( + "/hostName:", + StringComparison.InvariantCultureIgnoreCase)); - ProtocolEndpoint server = null; - if (runDebugAdapter) + if (!string.IsNullOrEmpty(hostNameArgument)) { - logPath = logPath ?? "DebugAdapter.log"; - server = new DebugAdapter(); + hostName = hostNameArgument.Substring(10).Trim('"'); } - else + + Version hostVersion = null; + string hostVersionArgument = + args.FirstOrDefault( + arg => + arg.StartsWith( + "/hostVersion:", + StringComparison.InvariantCultureIgnoreCase)); + + if (!string.IsNullOrEmpty(hostVersionArgument)) { - logPath = logPath ?? "EditorServices.log"; - server = new LanguageServer(); + hostVersion = + new Version( + hostVersionArgument.Substring(13).Trim('"')); } + // Create the host details from parameters + HostDetails hostDetails = + new HostDetails( + hostName, + hostProfileId, + hostVersion); + + // Catch unhandled exceptions for logging purposes + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + + // Use a default log path filename if one isn't specified + logPath = + runDebugAdapter + ? logPath ?? "DebugAdapter.log" + : logPath ?? "EditorServices.log"; + // Start the logger with the specified log path and level Logger.Initialize(logPath, logLevel); @@ -103,9 +145,20 @@ static void Main(string[] args) Logger.Write( LogLevel.Normal, string.Format( - "PowerShell Editor Services Host v{0} starting (pid {1})...", + "PowerShell Editor Services Host v{0} starting (pid {1})...\r\n\r\n" + + " Host application details:\r\n\r\n" + + " Name: {2}\r\n ProfileId: {3}\r\n Version: {4}", fileVersionInfo.FileVersion, - Process.GetCurrentProcess().Id)); + Process.GetCurrentProcess().Id, + hostDetails.Name, + hostDetails.ProfileId, + hostDetails.Version)); + + // Create the appropriate server type + ProtocolEndpoint server = + runDebugAdapter + ? (ProtocolEndpoint) new DebugAdapter(hostDetails) + : (ProtocolEndpoint) new LanguageServer(hostDetails); // Start the server server.Start().Wait(); diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index a357535e9..7a3b58b79 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -6,6 +6,7 @@ using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; @@ -25,14 +26,16 @@ public class DebugAdapter : DebugAdapterBase private string scriptPathToLaunch; private string arguments; - public DebugAdapter() : this(new StdioServerChannel()) + public DebugAdapter(HostDetails hostDetails) + : this(hostDetails, new StdioServerChannel()) { } - public DebugAdapter(ChannelBase serverChannel) : base(serverChannel) + public DebugAdapter(HostDetails hostDetails, ChannelBase serverChannel) + : base(serverChannel) { this.editorSession = new EditorSession(); - this.editorSession.StartSession(); + this.editorSession.StartSession(hostDetails); this.editorSession.DebugService.DebuggerStopped += this.DebugService_DebuggerStopped; this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten; diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 689b64b83..f62f59ed1 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -6,7 +6,7 @@ using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using Microsoft.PowerShell.EditorServices.Protocol.Messages; +using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; @@ -24,18 +24,27 @@ public class LanguageServer : LanguageServerBase { private static CancellationTokenSource existingRequestCancellation; + private bool profilesLoaded; private EditorSession editorSession; private OutputDebouncer outputDebouncer; private LanguageServerSettings currentSettings = new LanguageServerSettings(); - public LanguageServer() : this(new StdioServerChannel()) + /// + /// Provides details about the host application. + /// + public LanguageServer(HostDetails hostDetails) + : this(hostDetails, new StdioServerChannel()) { } - public LanguageServer(ChannelBase serverChannel) : base(serverChannel) + /// + /// Provides details about the host application. + /// + public LanguageServer(HostDetails hostDetails, ChannelBase serverChannel) + : base(serverChannel) { this.editorSession = new EditorSession(); - this.editorSession.StartSession(); + this.editorSession.StartSession(hostDetails); this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten; // Always send console prompts through the UI in the language service @@ -59,7 +68,7 @@ protected override void Initialize() this.SetEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotification); this.SetEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotification); this.SetEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotification); - this.SetEventHandler(DidChangeConfigurationNotification.Type, this.HandleDidChangeConfigurationNotification); + this.SetEventHandler(DidChangeConfigurationNotification.Type, this.HandleDidChangeConfigurationNotification); this.SetRequestHandler(DefinitionRequest.Type, this.HandleDefinitionRequest); this.SetRequestHandler(ReferencesRequest.Type, this.HandleReferencesRequest); @@ -287,15 +296,24 @@ protected Task HandleDidChangeTextDocumentNotification( } protected async Task HandleDidChangeConfigurationNotification( - DidChangeConfigurationParams configChangeParams, + DidChangeConfigurationParams configChangeParams, EventContext eventContext) { + bool oldLoadProfiles = this.currentSettings.EnableProfileLoading; bool oldScriptAnalysisEnabled = this.currentSettings.ScriptAnalysis.Enable.HasValue; this.currentSettings.Update( configChangeParams.Settings.Powershell); + if (!this.profilesLoaded && + this.currentSettings.EnableProfileLoading && + oldLoadProfiles != this.currentSettings.EnableProfileLoading) + { + await this.editorSession.PowerShellContext.LoadHostProfiles(); + this.profilesLoaded = true; + } + if (oldScriptAnalysisEnabled != this.currentSettings.ScriptAnalysis.Enable) { // If the user just turned off script analysis, send a diagnostics diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs index 256a81c99..ae4970687 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs @@ -7,6 +7,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.Server { public class LanguageServerSettings { + public bool EnableProfileLoading { get; set; } + public ScriptAnalysisSettings ScriptAnalysis { get; set; } public LanguageServerSettings() @@ -18,6 +20,7 @@ public void Update(LanguageServerSettings settings) { if (settings != null) { + this.EnableProfileLoading = settings.EnableProfileLoading; this.ScriptAnalysis.Update(settings.ScriptAnalysis); } } @@ -41,7 +44,7 @@ public void Update(ScriptAnalysisSettings settings) } } - public class SettingsWrapper + public class LanguageServerSettingsWrapper { // NOTE: This property is capitalized as 'Powershell' because the // mode name sent from the client is written as 'powershell' and diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 914047a31..ef6e2f1fa 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -98,6 +98,7 @@ + @@ -107,6 +108,7 @@ + diff --git a/src/PowerShellEditorServices/Session/EditorSession.cs b/src/PowerShellEditorServices/Session/EditorSession.cs index a06c10e64..6392d96c7 100644 --- a/src/PowerShellEditorServices/Session/EditorSession.cs +++ b/src/PowerShellEditorServices/Session/EditorSession.cs @@ -4,13 +4,9 @@ // using Microsoft.PowerShell.EditorServices.Console; +using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; -using System; using System.IO; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Reflection; -using System.Threading; namespace Microsoft.PowerShell.EditorServices { @@ -61,9 +57,21 @@ public class EditorSession /// for the ConsoleService. /// public void StartSession() + { + this.StartSession(null); + } + + /// + /// Starts the session using the provided IConsoleHost implementation + /// for the ConsoleService. + /// + /// + /// Provides details about the host application. + /// + public void StartSession(HostDetails hostDetails) { // Initialize all services - this.PowerShellContext = new PowerShellContext(); + this.PowerShellContext = new PowerShellContext(hostDetails); this.LanguageService = new LanguageService(this.PowerShellContext); this.DebugService = new DebugService(this.PowerShellContext); this.ConsoleService = new ConsoleService(this.PowerShellContext); diff --git a/src/PowerShellEditorServices/Session/HostDetails.cs b/src/PowerShellEditorServices/Session/HostDetails.cs new file mode 100644 index 000000000..a20bc8cf3 --- /dev/null +++ b/src/PowerShellEditorServices/Session/HostDetails.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Contains details about the current host application (most + /// likely the editor which is using the host process). + /// + public class HostDetails + { + #region Constants + + /// + /// The default host name for PowerShell Editor Services. Used + /// if no host name is specified by the host application. + /// + public const string DefaultHostName = "PowerShell Editor Services Host"; + + /// + /// The default host ID for PowerShell Editor Services. Used + /// for the host-specific profile path if no host ID is specified. + /// + public const string DefaultHostProfileId = "Microsoft.PowerShellEditorServices"; + + /// + /// The default host version for PowerShell Editor Services. If + /// no version is specified by the host application, we use 0.0.0 + /// to indicate a lack of version. + /// + public static readonly Version DefaultHostVersion = new Version("0.0.0"); + + /// + /// The default host details in a HostDetails object. + /// + public static readonly HostDetails Default = new HostDetails(null, null, null); + + #endregion + + #region Properties + + /// + /// Gets the name of the host. + /// + public string Name { get; private set; } + + /// + /// Gets the profile ID of the host, used to determine the + /// host-specific profile path. + /// + public string ProfileId { get; private set; } + + /// + /// Gets the version of the host. + /// + public Version Version { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the HostDetails class. + /// + /// + /// The display name for the host, typically in the form of + /// "[Application Name] Host". + /// + /// + /// The identifier of the PowerShell host to use for its profile path. + /// loaded. Used to resolve a profile path of the form 'X_profile.ps1' + /// where 'X' represents the value of hostProfileId. If null, a default + /// will be used. + /// + /// The host application's version. + public HostDetails( + string name, + string profileId, + Version version) + { + this.Name = name ?? DefaultHostName; + this.ProfileId = profileId ?? DefaultHostProfileId; + this.Version = version ?? DefaultHostVersion; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index dd90c951b..6771183b1 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -37,6 +37,7 @@ public class PowerShellContext : IDisposable private bool ownsInitialRunspace; private Runspace initialRunspace; private Runspace currentRunspace; + private ProfilePaths profilePaths; private ConsoleServicePSHost psHost; private InitialSessionState initialSessionState; private IVersionSpecificOperations versionSpecificOperations; @@ -104,9 +105,20 @@ internal IConsoleHost ConsoleHost /// Initializes a new instance of the PowerShellContext class and /// opens a runspace to be used for the session. /// - public PowerShellContext() + public PowerShellContext() : this(null) { - this.psHost = new ConsoleServicePSHost(); + } + + /// + /// Initializes a new instance of the PowerShellContext class and + /// opens a runspace to be used for the session. + /// + /// Provides details about the host application. + public PowerShellContext(HostDetails hostDetails) + { + hostDetails = hostDetails ?? HostDetails.Default; + + this.psHost = new ConsoleServicePSHost(hostDetails); this.initialSessionState = InitialSessionState.CreateDefault2(); Runspace runspace = RunspaceFactory.CreateRunspace(psHost, this.initialSessionState); @@ -116,7 +128,7 @@ public PowerShellContext() this.ownsInitialRunspace = true; - this.Initialize(runspace); + this.Initialize(hostDetails, runspace); // Use reflection to execute ConsoleVisibility.AlwaysCaptureApplicationIO = true; Type consoleVisibilityType = @@ -141,13 +153,14 @@ public PowerShellContext() /// Initializes a new instance of the PowerShellContext class using /// an existing runspace for the session. /// - /// - public PowerShellContext(Runspace initialRunspace) + /// The initial runspace to use for this instance. + /// Provides details about the host application. + public PowerShellContext(HostDetails hostDetails, Runspace initialRunspace) { - this.Initialize(initialRunspace); + this.Initialize(hostDetails, initialRunspace); } - private void Initialize(Runspace initialRunspace) + private void Initialize(HostDetails hostDetails, Runspace initialRunspace) { Validate.IsNotNull("initialRunspace", initialRunspace); @@ -160,7 +173,6 @@ private void Initialize(Runspace initialRunspace) this.currentRunspace.Debugger.DebuggerStop += OnDebuggerStop; this.powerShell = PowerShell.Create(); - this.powerShell.InvocationStateChanged += powerShell_InvocationStateChanged; this.powerShell.Runspace = this.currentRunspace; // TODO: Should this be configurable? @@ -199,6 +211,14 @@ private void Initialize(Runspace initialRunspace) this.versionSpecificOperations.ConfigureDebugger( this.currentRunspace); + // Set the $profile variable in the runspace + this.profilePaths = + this.SetProfileVariableInCurrentRunspace( + hostDetails); + + // Now that initialization is complete we can watch for InvocationStateChanged + this.powerShell.InvocationStateChanged += powerShell_InvocationStateChanged; + this.SessionState = PowerShellContextState.Ready; // Now that the runspace is ready, enqueue it for first use @@ -493,6 +513,24 @@ public async Task ExecuteScriptAtPath(string scriptPath, string arguments = null await this.ExecuteCommand(command, true); } + /// + /// Loads PowerShell profiles for the host from the + /// standard system locations. Only the profile paths which + /// exist are loaded. + /// + /// A Task that can be awaited for completion. + public async Task LoadHostProfiles() + { + // Load any of the profile paths that exist + PSCommand command = null; + foreach (var profilePath in this.profilePaths.GetLoadableProfilePaths()) + { + command = new PSCommand(); + command.AddCommand(profilePath, false); + await this.ExecuteCommand(command); + } + } + /// /// Causes the current execution to be aborted no matter what state /// it is currently in. @@ -970,6 +1008,56 @@ private void WritePromptWithNestedPipeline() }); } } + + private ProfilePaths SetProfileVariableInCurrentRunspace(HostDetails hostDetails) + { + // Get the profile paths for the host name + ProfilePaths profilePaths = + new ProfilePaths( + hostDetails.ProfileId, + this.currentRunspace); + + // Create the $profile variable + PSObject profile = new PSObject(profilePaths.CurrentUserCurrentHost); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.AllUsersAllHosts), + profilePaths.AllUsersAllHosts)); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.AllUsersCurrentHost), + profilePaths.AllUsersCurrentHost)); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.CurrentUserAllHosts), + profilePaths.CurrentUserAllHosts)); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.CurrentUserCurrentHost), + profilePaths.CurrentUserCurrentHost)); + + Logger.Write( + LogLevel.Verbose, + string.Format( + "Setting $profile variable in runspace. Current user host profile path: {0}", + profilePaths.CurrentUserCurrentHost)); + + // Set the variable in the runspace + this.powerShell.Commands.Clear(); + this.powerShell + .AddCommand("Set-Variable") + .AddParameter("Name", "profile") + .AddParameter("Value", profile) + .AddParameter("Option", "None"); + this.powerShell.Invoke(); + this.powerShell.Commands.Clear(); + + return profilePaths; + } #endregion diff --git a/src/PowerShellEditorServices/Session/ProfilePaths.cs b/src/PowerShellEditorServices/Session/ProfilePaths.cs new file mode 100644 index 000000000..7014d7686 --- /dev/null +++ b/src/PowerShellEditorServices/Session/ProfilePaths.cs @@ -0,0 +1,111 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides profile path resolution behavior relative to the name + /// of a particular PowerShell host. + /// + public class ProfilePaths + { + #region Constants + + /// + /// The file name for the "all hosts" profile. Also used as the + /// suffix for the host-specific profile filenames. + /// + public const string AllHostsProfileName = "profile.ps1"; + + #endregion + + #region Properties + + /// + /// Gets the profile path for all users, all hosts. + /// + public string AllUsersAllHosts { get; private set; } + + /// + /// Gets the profile path for all users, current host. + /// + public string AllUsersCurrentHost { get; private set; } + + /// + /// Gets the profile path for the current user, all hosts. + /// + public string CurrentUserAllHosts { get; private set; } + + /// + /// Gets the profile path for the current user and host. + /// + public string CurrentUserCurrentHost { get; private set; } + + #endregion + + #region Public Methods + + /// + /// Creates a new instance of the ProfilePaths class. + /// + /// + /// The identifier of the host used in the host-specific X_profile.ps1 filename. + /// A runspace used to gather profile path locations. + public ProfilePaths( + string hostProfileId, + Runspace runspace) + { + string allUsersPath = + (string)runspace + .SessionStateProxy + .PSVariable + .Get("PsHome") + .Value; + + string currentUserPath = + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + "WindowsPowerShell"); + + string currentHostProfileName = + string.Format( + "{0}_{1}", + hostProfileId, + AllHostsProfileName); + + this.AllUsersCurrentHost = Path.Combine(allUsersPath, currentHostProfileName); + this.CurrentUserCurrentHost = Path.Combine(currentUserPath, currentHostProfileName); + this.AllUsersAllHosts = Path.Combine(allUsersPath, AllHostsProfileName); + this.CurrentUserAllHosts = Path.Combine(currentUserPath, AllHostsProfileName); + } + + /// + /// Gets the list of profile paths that exist on the filesystem. + /// + /// An IEnumerable of profile path strings to be loaded. + public IEnumerable GetLoadableProfilePaths() + { + var profilePaths = + new string[] + { + this.AllUsersAllHosts, + this.AllUsersCurrentHost, + this.CurrentUserAllHosts, + this.CurrentUserCurrentHost + }; + + return profilePaths.Where(p => File.Exists(p)); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices/Session/SessionPSHost.cs b/src/PowerShellEditorServices/Session/SessionPSHost.cs index ca69c6ae8..fa5e49e7e 100644 --- a/src/PowerShellEditorServices/Session/SessionPSHost.cs +++ b/src/PowerShellEditorServices/Session/SessionPSHost.cs @@ -4,6 +4,7 @@ // using Microsoft.PowerShell.EditorServices.Console; +using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Management.Automation.Host; @@ -19,6 +20,7 @@ internal class ConsoleServicePSHost : PSHost { #region Private Fields + private HostDetails hostDetails; private IConsoleHost consoleHost; private Guid instanceId = Guid.NewGuid(); private ConsoleServicePSHostUserInterface hostUserInterface; @@ -40,12 +42,17 @@ internal IConsoleHost ConsoleHost #endregion #region Constructors + /// /// Creates a new instance of the ConsoleServicePSHost class /// with the given IConsoleHost implementation. /// - public ConsoleServicePSHost() + /// + /// Provides details about the host application. + /// + public ConsoleServicePSHost(HostDetails hostDetails) { + this.hostDetails = hostDetails; this.hostUserInterface = new ConsoleServicePSHostUserInterface(); } @@ -60,16 +67,12 @@ public override Guid InstanceId public override string Name { - // TODO: Change this based on proper naming! - get { return "PowerShell Editor Services"; } + get { return this.hostDetails.Name; } } public override Version Version { - get - { - return this.GetType().Assembly.GetName().Version; - } + get { return this.hostDetails.Version; } } // TODO: Pull these from IConsoleHost diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index a73e32ab5..84a7cd1b2 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -9,6 +9,8 @@ using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; using Microsoft.PowerShell.EditorServices.Protocol.Messages; +using Microsoft.PowerShell.EditorServices.Protocol.Server; +using Microsoft.PowerShell.EditorServices.Session; using System; using System.IO; using System.Linq; @@ -37,6 +39,10 @@ public Task InitializeAsync() new LanguageServiceClient( new StdioClientChannel( "Microsoft.PowerShell.EditorServices.Host.exe", + //"/waitForDebugger", + "/hostName:\"PowerShell Editor Services Test Host\"", + "/hostProfileId:Test.PowerShellEditorServices", + "/hostVersion:1.0.0", "/logPath:\"" + testLogPath + "\"", "/logLevel:Verbose")); @@ -610,6 +616,76 @@ public async Task ServiceExecutesNativeCommandAndReceivesCommand() Assert.Equal("Test Output", await outputReader.ReadLine()); } + [Fact] + public async Task ServiceLoadsProfilesOnDemand() + { + // Send the configuration change to cause profiles to be loaded + await this.languageServiceClient.SendEvent( + DidChangeConfigurationNotification.Type, + new DidChangeConfigurationParams + { + Settings = new LanguageServerSettingsWrapper + { + Powershell = new LanguageServerSettings + { + EnableProfileLoading = true, + ScriptAnalysis = null + } + } + }); + + string testProfilePath = + Path.GetFullPath( + @"..\..\..\PowerShellEditorServices.Test.Shared\Profile\Profile.ps1"); + + string testHostName = "Test.PowerShellEditorServices"; + string profileName = + string.Format( + "{0}_{1}", + testHostName, + ProfilePaths.AllHostsProfileName); + + string currentUserCurrentHostPath = + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + "WindowsPowerShell", + profileName); + + // Copy the test profile to the current user's host profile path + File.Copy(testProfilePath, currentUserCurrentHostPath, true); + + OutputReader outputReader = new OutputReader(this.protocolClient); + + Task evaluateTask = + this.SendRequest( + EvaluateRequest.Type, + new EvaluateRequestArguments + { + Expression = "\"PROFILE: $(Assert-ProfileLoaded)\"", + Context = "repl" + }); + + // Try reading up to 10 lines to find the + string outputString = null; + for (int i = 0; i < 10; i++) + { + outputString = await outputReader.ReadLine(); + + if (outputString.StartsWith("PROFILE")) + { + break; + } + } + + // Delete the test profile before any assert failures + // cause the function to exit + File.Delete(currentUserCurrentHostPath); + + // Wait for the selection to appear as output + await evaluateTask; + Assert.Equal("PROFILE: True", outputString); + } + private async Task SendOpenFileEvent(string filePath, bool waitForDiagnostics = true) { string fileContents = string.Join(Environment.NewLine, File.ReadAllLines(filePath)); diff --git a/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj b/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj index 7ab2946cd..e9165d021 100644 --- a/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj +++ b/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj @@ -66,6 +66,8 @@ + + diff --git a/test/PowerShellEditorServices.Test.Shared/Profile/Profile.ps1 b/test/PowerShellEditorServices.Test.Shared/Profile/Profile.ps1 new file mode 100644 index 000000000..ef37455fa --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Profile/Profile.ps1 @@ -0,0 +1,3 @@ +function Assert-ProfileLoaded { + return $true +} \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/Profile/ProfileTest.ps1 b/test/PowerShellEditorServices.Test.Shared/Profile/ProfileTest.ps1 new file mode 100644 index 000000000..b7229c261 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Profile/ProfileTest.ps1 @@ -0,0 +1 @@ +Assert-ProfileLoaded \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs index d00e5f94e..18a7e931a 100644 --- a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs @@ -3,9 +3,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Management.Automation; using System.Threading.Tasks; @@ -21,9 +23,15 @@ public class PowerShellContextTests : IDisposable private const string DebugTestFilePath = @"..\..\..\PowerShellEditorServices.Test.Shared\Debugging\DebugTest.ps1"; + private static readonly HostDetails TestHostDetails = + new HostDetails( + "PowerShell Editor Services Test Host", + "Test.PowerShellEditorServices", + new Version("1.0.0")); + public PowerShellContextTests() { - this.powerShellContext = new PowerShellContext(); + this.powerShellContext = new PowerShellContext(TestHostDetails); this.powerShellContext.SessionStateChanged += OnSessionStateChanged; this.stateChangeQueue = new AsyncQueue(); } @@ -94,6 +102,79 @@ public async Task CanAbortExecution() await executeTask; } + [Fact] + public async Task CanResolveAndLoadProfilesForHostId() + { + string testProfilePath = + Path.GetFullPath( + @"..\..\..\PowerShellEditorServices.Test.Shared\Profile\Profile.ps1"); + + string profileName = + string.Format( + "{0}_{1}", + TestHostDetails.ProfileId, + ProfilePaths.AllHostsProfileName); + + string currentUserPath = + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + "WindowsPowerShell"); + string allUsersPath = null; // To be set later + + using (RunspaceHandle runspaceHandle = await this.powerShellContext.GetRunspaceHandle()) + { + allUsersPath = + (string)runspaceHandle + .Runspace + .SessionStateProxy + .PSVariable + .Get("PsHome") + .Value; + } + + string[] expectedProfilePaths = + new string[] + { + Path.Combine(allUsersPath, ProfilePaths.AllHostsProfileName), + Path.Combine(allUsersPath, profileName), + Path.Combine(currentUserPath, ProfilePaths.AllHostsProfileName), + Path.Combine(currentUserPath, profileName) + }; + + // Copy the test profile to the current user's host profile path + File.Copy(testProfilePath, expectedProfilePaths[3], true); + + // Load the profiles for the test host name + await this.powerShellContext.LoadHostProfiles(); + + // Delete the test profile before any assert failures + // cause the function to exit + File.Delete(expectedProfilePaths[3]); + + // Ensure that all the paths are set in the correct variables + // and that the current user's host profile got loaded + PSCommand psCommand = new PSCommand(); + psCommand.AddScript( + "\"$($profile.AllUsersAllHosts) " + + "$($profile.AllUsersCurrentHost) " + + "$($profile.CurrentUserAllHosts) " + + "$($profile.CurrentUserCurrentHost) " + + "$(Assert-ProfileLoaded)\""); + + var result = + await this.powerShellContext.ExecuteCommand( + psCommand); + + string expectedString = + string.Format( + "{0} True", + string.Join( + " ", + expectedProfilePaths)); + + Assert.Equal(expectedString, result.FirstOrDefault(), true); + } + #region Helper Methods private async Task AssertStateChange(PowerShellContextState expectedState)