diff --git a/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs b/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs index f3c0741cc..07acf953a 100644 --- a/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs +++ b/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs @@ -151,7 +151,7 @@ public class DebugAdapterWebSocketConnection : EditorServiceWebSocketConnection { public DebugAdapterWebSocketConnection() { - Server = new DebugAdapter(null, null, Channel); + Server = new DebugAdapter(null, null, Channel, null); } } } diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 4fb304e07..081ec2b2d 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -164,7 +164,8 @@ public void StartDebugService(int debugServicePort, ProfilePaths profilePaths) new DebugAdapter( hostDetails, profilePaths, - new TcpSocketServerChannel(debugServicePort)); + new TcpSocketServerChannel(debugServicePort), + this.languageServer?.EditorOperations); this.debugAdapter.SessionEnded += (obj, args) => diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs index 4b8faa9c3..902abd990 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs @@ -16,8 +16,10 @@ public static readonly public class AttachRequestArguments { - public string Address { get; set; } + public string ComputerName { get; set; } - public int Port { get; set; } + public int ProcessId { get; set; } + + public int RunspaceId { get; set; } } } diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinuedEvent.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinuedEvent.cs new file mode 100644 index 000000000..8e307500a --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinuedEvent.cs @@ -0,0 +1,20 @@ +// +// 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.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter +{ + public class ContinuedEvent + { + public static readonly + EventType Type = + EventType.Create("continued"); + + public int ThreadId { get; set; } + + public bool AllThreadsContinued { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/TerminatedEvent.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/TerminatedEvent.cs index 2756899fd..39202676d 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/TerminatedEvent.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/TerminatedEvent.cs @@ -10,8 +10,10 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class TerminatedEvent { public static readonly - EventType Type = - EventType.Create("terminated"); + EventType Type = + EventType.Create("terminated"); + + public bool Restart { get; set; } } } diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs index f97b26ee1..ee39820bd 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs @@ -108,6 +108,13 @@ public static readonly RequestType.Create("editor/openFile"); } + public class CloseFileRequest + { + public static readonly + RequestType Type = + RequestType.Create("editor/closeFile"); + } + public class ShowInformationMessageRequest { public static readonly diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs index 37f7ce1b0..87a7ac20e 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs @@ -11,11 +11,11 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer public class PowerShellVersionRequest { public static readonly - RequestType Type = - RequestType.Create("powerShell/getVersion"); + RequestType Type = + RequestType.Create("powerShell/getVersion"); } - public class PowerShellVersionResponse + public class PowerShellVersion { public string Version { get; set; } @@ -25,11 +25,11 @@ public class PowerShellVersionResponse public string Architecture { get; set; } - public PowerShellVersionResponse() + public PowerShellVersion() { } - public PowerShellVersionResponse(PowerShellVersionDetails versionDetails) + public PowerShellVersion(PowerShellVersionDetails versionDetails) { this.Version = versionDetails.VersionString; this.DisplayVersion = $"{versionDetails.Version.Major}.{versionDetails.Version.Minor}"; diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/RunspaceChanged.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/RunspaceChanged.cs new file mode 100644 index 000000000..77a0148dc --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/RunspaceChanged.cs @@ -0,0 +1,37 @@ +// +// 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.Session; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + public class RunspaceChangedEvent + { + public static readonly + EventType Type = + EventType.Create("powerShell/runspaceChanged"); + } + + public class RunspaceDetails + { + public PowerShellVersion PowerShellVersion { get; set; } + + public RunspaceLocation RunspaceType { get; set; } + + public string ConnectionString { get; set; } + + public RunspaceDetails() + { + } + + public RunspaceDetails(Session.RunspaceDetails eventArgs) + { + this.PowerShellVersion = new PowerShellVersion(eventArgs.PowerShellVersion); + this.RunspaceType = eventArgs.Location; + this.ConnectionString = eventArgs.ConnectionString; + } + } +} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerChannel.cs index 483a90976..6f79c28c2 100755 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerChannel.cs @@ -51,6 +51,7 @@ protected override void Shutdown() { if (this.tcpListener != null) { + this.networkStream.Dispose(); this.tcpListener.Stop(); this.tcpListener = null; diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs index 964488ecc..40f494fd6 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs @@ -89,6 +89,7 @@ public void Stop() { this.messageLoopCancellationToken.Cancel(); this.messageLoopThread.Stop(); + SynchronizationContext.SetSynchronizationContext(null); } } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs index 1993cf899..92aed5ecb 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs @@ -335,7 +335,6 @@ private void MessageDispatcher_UnhandledException(object sender, Exception e) { this.endpointExitedTask.SetException(e); } - else if (this.originalSynchronizationContext != null) { this.originalSynchronizationContext.Post(o => { throw e; }, null); diff --git a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj index d5e97d08f..3bb8e254e 100644 --- a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj +++ b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj @@ -52,6 +52,7 @@ + @@ -61,6 +62,7 @@ + diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index 86a87cc9b..15bd5e671 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -4,6 +4,7 @@ // using Microsoft.PowerShell.EditorServices.Debugging; +using Microsoft.PowerShell.EditorServices.Extensions; using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; @@ -14,6 +15,7 @@ using System.IO; using System.Linq; using System.Management.Automation; +using System.Text; using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Protocol.Server @@ -22,22 +24,27 @@ public class DebugAdapter : DebugAdapterBase { private EditorSession editorSession; private OutputDebouncer outputDebouncer; - private bool isConfigurationDoneRequestComplete; - private bool isLaunchRequestComplete; + private bool noDebug; + private bool waitingForAttach; private string scriptPathToLaunch; private string arguments; public DebugAdapter(HostDetails hostDetails, ProfilePaths profilePaths) - : this(hostDetails, profilePaths, new StdioServerChannel()) + : this(hostDetails, profilePaths, new StdioServerChannel(), null) { } - public DebugAdapter(HostDetails hostDetails, ProfilePaths profilePaths, ChannelBase serverChannel) + public DebugAdapter( + HostDetails hostDetails, + ProfilePaths profilePaths, + ChannelBase serverChannel, + IEditorOperations editorOperations) : base(serverChannel) { this.editorSession = new EditorSession(); - this.editorSession.StartDebugSession(hostDetails, profilePaths); + this.editorSession.StartDebugSession(hostDetails, profilePaths, editorOperations); + this.editorSession.PowerShellContext.RunspaceChanged += this.powerShellContext_RunspaceChanged; this.editorSession.DebugService.DebuggerStopped += this.DebugService_DebuggerStopped; this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten; @@ -86,7 +93,7 @@ protected Task LaunchScript(RequestContext requestContext) await requestContext.SendEvent( TerminatedEvent.Type, - null); + new TerminatedEvent()); // Stop the server await this.Stop(); @@ -113,16 +120,14 @@ protected async Task HandleConfigurationDoneRequest( object args, RequestContext requestContext) { - // The order of debug protocol messages apparently isn't as guaranteed as we might like. - // Need to be able to handle the case where we get the configurationDone request after the - // launch request. - if (this.isLaunchRequestComplete) + if (!string.IsNullOrEmpty(this.scriptPathToLaunch)) { - this.LaunchScript(requestContext); + // Configuration is done, launch the script + var nonAwaitedTask = + this.LaunchScript(requestContext) + .ConfigureAwait(false); } - this.isConfigurationDoneRequestComplete = true; - await requestContext.SendResult(null); } @@ -133,19 +138,31 @@ protected async Task HandleLaunchRequest( // Set the working directory for the PowerShell runspace to the cwd passed in via launch.json. // In case that is null, use the the folder of the script to be executed. If the resulting // working dir path is a file path then extract the directory and use that. - string workingDir = launchParams.Cwd ?? launchParams.Script ?? launchParams.Program; - workingDir = PowerShellContext.UnescapePath(workingDir); - try + string workingDir = + launchParams.Cwd ?? + launchParams.Script ?? + launchParams.Program; + + if (workingDir != null) { - if ((File.GetAttributes(workingDir) & FileAttributes.Directory) != FileAttributes.Directory) + workingDir = PowerShellContext.UnescapePath(workingDir); + try + { + if ((File.GetAttributes(workingDir) & FileAttributes.Directory) != FileAttributes.Directory) + { + workingDir = Path.GetDirectoryName(workingDir); + } + } + catch (Exception ex) { - workingDir = Path.GetDirectoryName(workingDir); + Logger.Write(LogLevel.Error, "cwd path is invalid: " + ex.Message); + + workingDir = null; } } - catch (Exception ex) - { - Logger.Write(LogLevel.Error, "cwd path is invalid: " + ex.Message); + if (workingDir == null) + { #if NanoServer workingDir = AppContext.BaseDirectory; #else @@ -164,37 +181,107 @@ protected async Task HandleLaunchRequest( Logger.Write(LogLevel.Verbose, "Script arguments are: " + arguments); } - // We may not actually launch the script in response to this - // request unless it comes after the configurationDone request. - // If the launch request comes first, then stash the launch - // params so that the subsequent configurationDone request handler - // can launch the script. + // Store the launch parameters so that they can be used later this.noDebug = launchParams.NoDebug; this.scriptPathToLaunch = launchParams.Script ?? launchParams.Program; this.arguments = arguments; - // The order of debug protocol messages apparently isn't as guaranteed as we might like. - // Need to be able to handle the case where we get the launch request after the - // configurationDone request. - if (this.isConfigurationDoneRequestComplete) + await requestContext.SendResult(null); + + // If no script is being launched, execute an empty script to + // cause the prompt string to be evaluated and displayed + if (string.IsNullOrEmpty(this.scriptPathToLaunch)) { - this.LaunchScript(requestContext); + await this.editorSession.PowerShellContext.ExecuteScriptString( + "", false, true); } - this.isLaunchRequestComplete = true; - - await requestContext.SendResult(null); + // Send the InitializedEvent so that the debugger will continue + // sending configuration requests + await requestContext.SendEvent( + InitializedEvent.Type, + null); } - protected Task HandleAttachRequest( + protected async Task HandleAttachRequest( AttachRequestArguments attachParams, RequestContext requestContext) { - // TODO: Implement this once we support attaching to processes - throw new NotImplementedException(); + StringBuilder errorMessages = new StringBuilder(); + + if (attachParams.ComputerName != null) + { + PowerShellVersionDetails runspaceVersion = + this.editorSession.PowerShellContext.CurrentRunspace.PowerShellVersion; + + if (runspaceVersion.Version.Major < 4) + { + await requestContext.SendError( + $"Remote sessions are only available with PowerShell 4 and higher (current session is {runspaceVersion.Version})."); + + return; + } + + await this.editorSession.PowerShellContext.ExecuteScriptString( + $"Enter-PSSession -ComputerName \"{attachParams.ComputerName}\"", + errorMessages); + + if (errorMessages.Length > 0) + { + await requestContext.SendError( + $"Could not establish remote session to computer '{attachParams.ComputerName}'"); + + return; + } + } + + if (attachParams.ProcessId > 0) + { + PowerShellVersionDetails runspaceVersion = + this.editorSession.PowerShellContext.CurrentRunspace.PowerShellVersion; + + if (runspaceVersion.Version.Major < 5) + { + await requestContext.SendError( + $"Attaching to a process is only available with PowerShell 5 and higher (current session is {runspaceVersion.Version})."); + + return; + } + + await this.editorSession.PowerShellContext.ExecuteScriptString( + $"Enter-PSHostProcess -Id {attachParams.ProcessId}", + errorMessages); + + if (errorMessages.Length > 0) + { + await requestContext.SendError( + $"Could not attach to process '{attachParams.ProcessId}'"); + + return; + } + + // Execute the Debug-Runspace command but don't await it because it + // will block the debug adapter initialization process. The + // InitializedEvent will be sent as soon as the RunspaceChanged + // event gets fired with the attached runspace. + int runspaceId = attachParams.RunspaceId > 0 ? attachParams.RunspaceId : 1; + this.waitingForAttach = true; + Task nonAwaitedTask = + this.editorSession.PowerShellContext.ExecuteScriptString( + $"\nDebug-Runspace -Id {runspaceId}"); + } + else + { + await requestContext.SendError( + "A positive integer must be specified for the processId field."); + + return; + } + + await requestContext.SendResult(null); } - protected Task HandleDisconnectRequest( + protected async Task HandleDisconnectRequest( object disconnectParams, RequestContext requestContext) { @@ -217,11 +304,18 @@ protected Task HandleDisconnectRequest( // so we shouldn't try to abort because PowerShellContext will be null if (this.editorSession != null && this.editorSession.PowerShellContext != null) { - this.editorSession.PowerShellContext.SessionStateChanged += handler; - this.editorSession.PowerShellContext.AbortExecution(); + if (this.editorSession.PowerShellContext.SessionState == PowerShellContextState.Running || + this.editorSession.PowerShellContext.IsDebuggerStopped) + { + this.editorSession.PowerShellContext.SessionStateChanged += handler; + this.editorSession.PowerShellContext.AbortExecution(); + } + else + { + await requestContext.SendResult(null); + await this.Stop(); + } } - - return Task.FromResult(true); } protected async Task HandleSetBreakpointsRequest( @@ -568,7 +662,7 @@ await requestContext.SendResult( #region Event Handlers - async void DebugService_DebuggerStopped(object sender, DebuggerStopEventArgs e) + async void DebugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) { // Flush pending output before sending the event await this.outputDebouncer.Flush(); @@ -580,10 +674,10 @@ async void DebugService_DebuggerStopped(object sender, DebuggerStopEventArgs e) // We don't support exception breakpoints and for "pause", we can't distinguish // between stepping and the user pressing the pause/break button in the debug toolbar. string debuggerStoppedReason = "step"; - if (e.Breakpoints.Count > 0) + if (e.OriginalEvent.Breakpoints.Count > 0) { debuggerStoppedReason = - e.Breakpoints[0] is CommandBreakpoint + e.OriginalEvent.Breakpoints[0] is CommandBreakpoint ? "function breakpoint" : "breakpoint"; } @@ -594,15 +688,39 @@ await this.SendEvent( { Source = new Source { - Path = e.InvocationInfo.ScriptName, + Path = e.ScriptPath, }, - Line = e.InvocationInfo.ScriptLineNumber, - Column = e.InvocationInfo.OffsetInLine, - ThreadId = 1, // TODO: Change this based on context + Line = e.LineNumber, + Column = e.ColumnNumber, + ThreadId = 1, Reason = debuggerStoppedReason }); } + async void powerShellContext_RunspaceChanged(object sender, RunspaceChangedEventArgs e) + { + if (this.waitingForAttach && e.ChangeAction == RunspaceChangeAction.Enter && e.NewRunspace.IsAttached) + { + // Send the InitializedEvent so that the debugger will continue + // sending configuration requests + this.waitingForAttach = false; + await this.SendEvent(InitializedEvent.Type, null); + } + else if (e.ChangeAction == RunspaceChangeAction.Exit && this.editorSession.PowerShellContext.IsDebuggerStopped) + { + // Exited the session while the debugger is stopped, + // send a ContinuedEvent so that the client changes the + // UI to appear to be running again + await this.SendEvent( + ContinuedEvent.Type, + new ContinuedEvent + { + ThreadId = 1, + AllThreadsContinued = true + }); + } + } + async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e) { // Queue the output for writing diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs index 4f324d269..d2b7c5e6a 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs @@ -63,11 +63,6 @@ await requestContext.SendResult( SupportsHitConditionalBreakpoints = true, SupportsSetVariable = true }); - - // Send the Initialized event so that we get breakpoints - await requestContext.SendEvent( - InitializedEvent.Type, - null); } } } diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 6a7e81d81..9301d9a18 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -32,9 +32,15 @@ public class LanguageServer : LanguageServerBase private OutputDebouncer outputDebouncer; private LanguageServerEditorOperations editorOperations; private LanguageServerSettings currentSettings = new LanguageServerSettings(); + private Dictionary> codeActionsPerFile = new Dictionary>(); + public IEditorOperations EditorOperations + { + get { return this.editorOperations; } + } + /// /// Provides details about the host application. /// @@ -52,6 +58,7 @@ public LanguageServer(HostDetails hostDetails, ProfilePaths profilePaths, Channe this.editorSession = new EditorSession(); this.editorSession.StartSession(hostDetails, profilePaths); this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten; + this.editorSession.PowerShellContext.RunspaceChanged += PowerShellContext_RunspaceChanged; // Attach to ExtensionService events this.editorSession.ExtensionService.CommandAdded += ExtensionService_ExtensionAdded; @@ -910,11 +917,11 @@ protected async Task HandleWorkspaceSymbolRequest( protected async Task HandlePowerShellVersionRequest( object noParams, - RequestContext requestContext) + RequestContext requestContext) { await requestContext.SendResult( - new PowerShellVersionResponse( - this.editorSession.PowerShellContext.PowerShellVersionDetails)); + new PowerShellVersion( + this.editorSession.PowerShellContext.LocalPowerShellVersion)); } private bool IsQueryMatch(string query, string symbolName) @@ -988,6 +995,13 @@ protected Task HandleEvaluateRequest( #region Event Handlers + private async void PowerShellContext_RunspaceChanged(object sender, Session.RunspaceChangedEventArgs e) + { + await this.SendEvent( + RunspaceChangedEvent.Type, + new Protocol.LanguageServer.RunspaceDetails(e.NewRunspace)); + } + private async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e) { // Queue the output for writing diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs index b8c1da92c..c8648660d 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs @@ -110,6 +110,15 @@ public Task OpenFile(string filePath) true); } + public Task CloseFile(string filePath) + { + return + this.messageSender.SendRequest( + CloseFileRequest.Type, + filePath, + true); + } + public string GetWorkspacePath() { return this.editorSession.Workspace.WorkspacePath; diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index 9b7df6e61..220d9f2bc 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -12,6 +12,8 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Debugging; using Microsoft.PowerShell.EditorServices.Utility; +using System.IO; +using Microsoft.PowerShell.EditorServices.Session; namespace Microsoft.PowerShell.EditorServices { @@ -23,7 +25,10 @@ public class DebugService { #region Fields + private const string PsesGlobalVariableNamePrefix = "__psEditorServices_"; + private PowerShellContext powerShellContext; + private RemoteFileManager remoteFileManager; // TODO: This needs to be managed per nested session private Dictionary> breakpointsPerFile = @@ -49,12 +54,31 @@ public class DebugService /// The PowerShellContext to use for all debugging operations. /// public DebugService(PowerShellContext powerShellContext) + : this(powerShellContext, null) + { + } + + /// + /// Initializes a new instance of the DebugService class and uses + /// the given PowerShellContext for all future operations. + /// + /// + /// The PowerShellContext to use for all debugging operations. + /// + /// + /// A RemoteFileManager instance to use for accessing files in remote sessions. + /// + public DebugService( + PowerShellContext powerShellContext, + RemoteFileManager remoteFileManager) { - Validate.IsNotNull("powerShellContext", powerShellContext); + Validate.IsNotNull(nameof(powerShellContext), powerShellContext); this.powerShellContext = powerShellContext; this.powerShellContext.DebuggerStop += this.OnDebuggerStop; this.powerShellContext.BreakpointUpdated += this.OnBreakpointUpdated; + + this.remoteFileManager = remoteFileManager; } #endregion @@ -82,10 +106,32 @@ public async Task SetLineBreakpoints( if (breakpoints.Length > 0) { + // Make sure we're using the remote script path + string scriptPath = scriptFile.FilePath; + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && + this.remoteFileManager != null) + { + string mappedPath = + this.remoteFileManager.GetMappedPath( + scriptPath, + this.powerShellContext.CurrentRunspace); + + if (mappedPath == null) + { + Logger.Write( + LogLevel.Error, + $"Could not map local path '{scriptPath}' to a remote path."); + + return resultBreakpointDetails.ToArray(); + } + + scriptPath = mappedPath; + } + // Fix for issue #123 - file paths that contain wildcard chars [ and ] need to // quoted and have those wildcard chars escaped. string escapedScriptPath = - PowerShellContext.EscapePath(scriptFile.FilePath, escapeSpaces: false); + PowerShellContext.EscapePath(scriptPath, escapeSpaces: false); foreach (BreakpointDetails breakpoint in breakpoints) { @@ -556,7 +602,7 @@ private async Task ClearBreakpointsInFile(ScriptFile scriptFile) { PSCommand psCommand = new PSCommand(); psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - psCommand.AddParameter("Breakpoint", breakpoints.ToArray()); + psCommand.AddParameter("Id", breakpoints.Select(b => b.Id).ToArray()); await this.powerShellContext.ExecuteCommand(psCommand); @@ -612,16 +658,16 @@ private async Task FetchVariableContainer( new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); this.variables.Add(scopeVariableContainer); - var results = await this.powerShellContext.ExecuteCommand(psCommand, sendErrorToHost: false); + var results = await this.powerShellContext.ExecuteCommand(psCommand, sendErrorToHost: false); if (results != null) { - foreach (PSVariable psvariable in results) + foreach (PSObject psVariableObject in results) { - var variableDetails = new VariableDetails(psvariable) {Id = this.nextVariableId++}; + var variableDetails = new VariableDetails(psVariableObject) {Id = this.nextVariableId++}; this.variables.Add(variableDetails); scopeVariableContainer.Children.Add(variableDetails.Name, variableDetails); - if ((autoVariables != null) && AddToAutoVariables(psvariable, scope)) + if ((autoVariables != null) && AddToAutoVariables(psVariableObject, scope)) { autoVariables.Children.Add(variableDetails.Name, variableDetails); } @@ -631,9 +677,30 @@ private async Task FetchVariableContainer( return scopeVariableContainer; } - private bool AddToAutoVariables(PSVariable psvariable, string scope) + private bool AddToAutoVariables(PSObject psvariable, string scope) { - if ((scope == VariableContainerDetails.GlobalScopeName) || + string variableName = psvariable.Properties["Name"].Value as string; + object variableValue = psvariable.Properties["Value"].Value; + + ScopedItemOptions variableScope = ScopedItemOptions.None; + PSPropertyInfo optionsProperty = psvariable.Properties["Options"]; + if (string.Equals(optionsProperty.TypeNameOfValue, "System.String")) + { + if (!Enum.TryParse( + optionsProperty.Value as string, + out variableScope)) + { + Logger.Write( + LogLevel.Warning, + $"Could not parse a variable's ScopedItemOptions value of '{optionsProperty.Value}'"); + } + } + else if (optionsProperty.Value is ScopedItemOptions) + { + variableScope = (ScopedItemOptions)optionsProperty.Value; + } + + if ((scope == VariableContainerDetails.GlobalScopeName) || (scope == VariableContainerDetails.ScriptScopeName)) { // We don't A) have a good way of distinguishing built-in from user created variables @@ -646,36 +713,36 @@ private bool AddToAutoVariables(PSVariable psvariable, string scope) var readonlyAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.ReadOnly; // Some local variables, if they exist, should be displayed by default - if (psvariable.GetType().Name == "LocalVariable") + if (psvariable.TypeNames.Any(typeName => typeName.EndsWith("LocalVariable"))) { - if (psvariable.Name.Equals("_")) + if (variableName.Equals("_")) { return true; } - else if (psvariable.Name.Equals("args", StringComparison.OrdinalIgnoreCase)) + else if (variableName.Equals("args", StringComparison.OrdinalIgnoreCase)) { - var array = psvariable.Value as Array; + var array = variableValue as Array; return array != null ? array.Length > 0 : false; } return false; } - else if (psvariable.GetType() != typeof(PSVariable)) + else if (!psvariable.TypeNames.Any(typeName => typeName.EndsWith("PSVariable"))) { return false; } - if (((psvariable.Options | constantAllScope) == constantAllScope) || - ((psvariable.Options | readonlyAllScope) == readonlyAllScope)) + if (((variableScope | constantAllScope) == constantAllScope) || + ((variableScope | readonlyAllScope) == readonlyAllScope)) { - string prefixedVariableName = VariableDetails.DollarPrefix + psvariable.Name; + string prefixedVariableName = VariableDetails.DollarPrefix + variableName; if (this.globalScopeVariables.Children.ContainsKey(prefixedVariableName)) { return false; } } - if ((psvariable.Value != null) && (psvariable.Value.GetType() == typeof(PSDebugContext))) + if (variableValue != null && variableValue.GetType().Name.EndsWith(nameof(PSDebugContext))) { return false; } @@ -686,9 +753,15 @@ private bool AddToAutoVariables(PSVariable psvariable, string scope) private async Task FetchStackFrames() { PSCommand psCommand = new PSCommand(); - psCommand.AddCommand("Get-PSCallStack"); - var results = await this.powerShellContext.ExecuteCommand(psCommand); + // This glorious hack ensures that Get-PSCallStack returns a list of CallStackFrame + // objects (or "deserialized" CallStackFrames) when attached to a runspace in another + // process. Without the intermediate variable Get-PSCallStack inexplicably returns + // an array of strings containing the formatted output of the CallStackFrame list. + var callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; + psCommand.AddScript($"{callStackVarName} = Get-PSCallStack; {callStackVarName}"); + + var results = await this.powerShellContext.ExecuteCommand(psCommand); var callStackFrames = results.ToArray(); this.stackFrameDetails = new StackFrameDetails[callStackFrames.Length]; @@ -707,6 +780,17 @@ private async Task FetchStackFrames() this.stackFrameDetails[i] = StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables); + + string stackFrameScriptPath = this.stackFrameDetails[i].ScriptPath; + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && + this.remoteFileManager != null && + !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) + { + this.stackFrameDetails[i].ScriptPath = + this.remoteFileManager.GetMappedPath( + stackFrameScriptPath, + this.powerShellContext.CurrentRunspace); + } } } @@ -784,7 +868,7 @@ private ScriptBlock GetBreakpointActionScriptBlock( if (hitCount.HasValue) { string globalHitCountVarName = - $"$global:__psEditorServices_BreakHitCounter_{breakpointHitCounter++}"; + $"$global:{PsesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter++}"; wrappedCondition = $"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}"; @@ -902,18 +986,31 @@ private string FormatInvalidBreakpointConditionMessage(string condition, string /// /// Raised when the debugger stops execution at a breakpoint or when paused. /// - public event EventHandler DebuggerStopped; + public event EventHandler DebuggerStopped; private async void OnDebuggerStop(object sender, DebuggerStopEventArgs e) { // Get call stack and variables. await this.FetchStackFramesAndVariables(); - // Notify the host that the debugger is stopped - if (this.DebuggerStopped != null) + // If this is a remote connection, get the file content + string localScriptPath = e.InvocationInfo.ScriptName; + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && + this.remoteFileManager != null) { - this.DebuggerStopped(sender, e); + localScriptPath = + await this.remoteFileManager.FetchRemoteFile( + e.InvocationInfo.ScriptName, + this.powerShellContext.CurrentRunspace); } + + // Notify the host that the debugger is stopped + this.DebuggerStopped?.Invoke( + sender, + new DebuggerStoppedEventArgs( + e, + this.powerShellContext.CurrentRunspace, + localScriptPath)); } /// @@ -933,8 +1030,29 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) { List breakpoints; + string scriptPath = lineBreakpoint.Script; + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && + this.remoteFileManager != null) + { + string mappedPath = + this.remoteFileManager.GetMappedPath( + scriptPath, + this.powerShellContext.CurrentRunspace); + + if (mappedPath == null) + { + Logger.Write( + LogLevel.Error, + $"Could not map remote path '{scriptPath}' to a local path."); + + return; + } + + scriptPath = mappedPath; + } + // Normalize the script filename for proper indexing - string normalizedScriptName = lineBreakpoint.Script.ToLower(); + string normalizedScriptName = scriptPath.ToLower(); // Get the list of breakpoints for this file if (!this.breakpointsPerFile.TryGetValue(normalizedScriptName, out breakpoints)) diff --git a/src/PowerShellEditorServices/Debugging/DebuggerStoppedEventArgs.cs b/src/PowerShellEditorServices/Debugging/DebuggerStoppedEventArgs.cs new file mode 100644 index 000000000..f53ab8323 --- /dev/null +++ b/src/PowerShellEditorServices/Debugging/DebuggerStoppedEventArgs.cs @@ -0,0 +1,117 @@ +// +// 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.Session; +using Microsoft.PowerShell.EditorServices.Utility; +using System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Debugging +{ + /// + /// Provides event arguments for the DebugService.DebuggerStopped event. + /// + public class DebuggerStoppedEventArgs + { + #region Properties + + /// + /// Gets the path of the script where the debugger has stopped execution. + /// If 'IsRemoteSession' returns true, this path will be a local filesystem + /// path containing the contents of the script that is executing remotely. + /// + public string ScriptPath { get; private set; } + + /// + /// Returns true if the breakpoint was raised from a remote debugging session. + /// + public bool IsRemoteSession + { + get { return this.RunspaceDetails.Location == RunspaceLocation.Remote; } + } + + /// + /// Gets the original script path if 'IsRemoteSession' returns true. + /// + public string RemoteScriptPath { get; private set; } + + /// + /// Gets the RunspaceDetails for the current runspace. + /// + public RunspaceDetails RunspaceDetails { get; private set; } + + /// + /// Gets the line number at which the debugger stopped execution. + /// + public int LineNumber + { + get + { + return this.OriginalEvent.InvocationInfo.ScriptLineNumber; + } + } + + /// + /// Gets the column number at which the debugger stopped execution. + /// + public int ColumnNumber + { + get + { + return this.OriginalEvent.InvocationInfo.OffsetInLine; + } + } + + /// + /// Gets the original DebuggerStopEventArgs from the PowerShell engine. + /// + public DebuggerStopEventArgs OriginalEvent { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the DebuggerStoppedEventArgs class. + /// + /// The original DebuggerStopEventArgs instance from which this instance is based. + /// The RunspaceDetails of the runspace which raised this event. + public DebuggerStoppedEventArgs( + DebuggerStopEventArgs originalEvent, + RunspaceDetails runspaceDetails) + : this(originalEvent, runspaceDetails, null) + { + } + + /// + /// Creates a new instance of the DebuggerStoppedEventArgs class. + /// + /// The original DebuggerStopEventArgs instance from which this instance is based. + /// The RunspaceDetails of the runspace which raised this event. + /// The local path of the remote script being debugged. + public DebuggerStoppedEventArgs( + DebuggerStopEventArgs originalEvent, + RunspaceDetails runspaceDetails, + string localScriptPath) + { + Validate.IsNotNull(nameof(originalEvent), originalEvent); + Validate.IsNotNull(nameof(runspaceDetails), runspaceDetails); + + if (!string.IsNullOrEmpty(localScriptPath)) + { + this.ScriptPath = localScriptPath; + this.RemoteScriptPath = originalEvent.InvocationInfo.ScriptName; + } + else + { + this.ScriptPath = originalEvent.InvocationInfo.ScriptName; + } + + this.OriginalEvent = originalEvent; + this.RunspaceDetails = runspaceDetails; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs b/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs index fe4a24f32..8b8a07eed 100644 --- a/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs +++ b/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs @@ -13,10 +13,22 @@ namespace Microsoft.PowerShell.EditorServices /// public class StackFrameDetails { + #region Fields + + /// + /// A constant string used in the ScriptPath field to represent a + /// stack frame with no associated script file. + /// + public const string NoFileScriptPath = ""; + + #endregion + + #region Properties + /// /// Gets the path to the script where the stack frame occurred. /// - public string ScriptPath { get; private set; } + public string ScriptPath { get; internal set; } /// /// Gets the name of the function where the stack frame occurred. @@ -43,12 +55,16 @@ public class StackFrameDetails /// public VariableContainerDetails LocalVariables { get; private set; } + #endregion + + #region Constructors + /// /// Creates an instance of the StackFrameDetails class from a /// CallStackFrame instance provided by the PowerShell engine. /// - /// - /// The original CallStackFrame instance from which details will be obtained. + /// + /// A PSObject representing the CallStackFrame instance from which details will be obtained. /// /// /// A variable container with all the filtered, auto variables for this stack frame. @@ -58,19 +74,21 @@ public class StackFrameDetails /// /// A new instance of the StackFrameDetails class. static internal StackFrameDetails Create( - CallStackFrame callStackFrame, + PSObject callStackFrameObject, VariableContainerDetails autoVariables, VariableContainerDetails localVariables) { return new StackFrameDetails { - ScriptPath = callStackFrame.ScriptName ?? "", - FunctionName = callStackFrame.FunctionName, - LineNumber = callStackFrame.Position.StartLineNumber, - ColumnNumber = callStackFrame.Position.StartColumnNumber, + ScriptPath = (callStackFrameObject.Properties["ScriptName"].Value as string) ?? NoFileScriptPath, + FunctionName = callStackFrameObject.Properties["FunctionName"].Value as string, + LineNumber = (int)(callStackFrameObject.Properties["ScriptLineNumber"].Value ?? 0), + ColumnNumber = 0, // Column number isn't given in PowerShell stack frames AutoVariables = autoVariables, LocalVariables = localVariables }; } + + #endregion } } diff --git a/src/PowerShellEditorServices/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Debugging/VariableDetails.cs index 8a76e0899..5c3d34c74 100644 --- a/src/PowerShellEditorServices/Debugging/VariableDetails.cs +++ b/src/PowerShellEditorServices/Debugging/VariableDetails.cs @@ -47,6 +47,21 @@ public VariableDetails(PSVariable psVariable) { } + /// + /// Initializes an instance of the VariableDetails class from + /// the name and value pair stored inside of a PSObject which + /// represents a PSVariable. + /// + /// + /// The PSObject which represents a PSVariable. + /// + public VariableDetails(PSObject psVariableObject) + : this( + DollarPrefix + psVariableObject.Properties["Name"].Value as string, + psVariableObject.Properties["Value"].Value) + { + } + /// /// Initializes an instance of the VariableDetails class from /// the details contained in a PSPropertyInfo instance. diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index 8e9358e1b..9471aec80 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -41,6 +41,13 @@ public interface IEditorOperations /// A Task that can be tracked for completion. Task OpenFile(string filePath); + /// + /// Causes a file to be closed in the editor. + /// + /// The path of the file to be closed. + /// A Task that can be tracked for completion. + Task CloseFile(string filePath); + /// /// Inserts text into the specified range for the file at the specified path. /// diff --git a/src/PowerShellEditorServices/Language/CommandHelpers.cs b/src/PowerShellEditorServices/Language/CommandHelpers.cs index d3976dcab..527fc3ac8 100644 --- a/src/PowerShellEditorServices/Language/CommandHelpers.cs +++ b/src/PowerShellEditorServices/Language/CommandHelpers.cs @@ -28,8 +28,12 @@ public static async Task GetCommandInfo( command.AddCommand(@"Microsoft.PowerShell.Core\Get-Command"); command.AddArgument(commandName); - var results = await powerShellContext.ExecuteCommand(command, false, false); - return results.FirstOrDefault(); + return + (await powerShellContext + .ExecuteCommand(command, false, false)) + .Select(o => o.BaseObject) + .OfType() + .FirstOrDefault(); } /// diff --git a/src/PowerShellEditorServices/Language/LanguageService.cs b/src/PowerShellEditorServices/Language/LanguageService.cs index e7e0a3769..0679ba429 100644 --- a/src/PowerShellEditorServices/Language/LanguageService.cs +++ b/src/PowerShellEditorServices/Language/LanguageService.cs @@ -231,7 +231,7 @@ public FindOccurrencesResult FindSymbolsInFile(ScriptFile scriptFile) IEnumerable symbolReferencesinFile = AstOperations - .FindSymbolsInDocument(scriptFile.ScriptAst, this.powerShellContext.PowerShellVersion) + .FindSymbolsInDocument(scriptFile.ScriptAst, this.powerShellContext.LocalPowerShellVersion.Version) .Select( reference => { reference.SourceLine = @@ -346,7 +346,7 @@ await CommandHelpers.GetCommandInfo( this.powerShellContext); foundDefinition = - await FindDeclarationForBuiltinCommand( + FindDeclarationForBuiltinCommand( cmdInfo, foundSymbol, workspace); @@ -532,22 +532,17 @@ private ScriptFile[] GetBuiltinCommandScriptFiles( return new List().ToArray(); } - private async Task FindDeclarationForBuiltinCommand( - CommandInfo cmdInfo, + private SymbolReference FindDeclarationForBuiltinCommand( + CommandInfo commandInfo, SymbolReference foundSymbol, Workspace workspace) { SymbolReference foundDefinition = null; - if (cmdInfo != null) + if (commandInfo != null) { int index = 0; ScriptFile[] nestedModuleFiles; - CommandInfo commandInfo = - await CommandHelpers.GetCommandInfo( - foundSymbol.SymbolName, - this.powerShellContext); - nestedModuleFiles = GetBuiltinCommandScriptFiles( commandInfo.Module, diff --git a/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj b/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj index 62c2244b5..15100124c 100644 --- a/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj @@ -56,6 +56,7 @@ + @@ -109,10 +110,13 @@ + + + @@ -143,12 +147,14 @@ + + diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 62103ef88..0d388e23c 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -69,6 +69,7 @@ + @@ -121,6 +122,8 @@ + + @@ -141,6 +144,7 @@ + diff --git a/src/PowerShellEditorServices/Session/EditorSession.cs b/src/PowerShellEditorServices/Session/EditorSession.cs index d2fb0c447..ad9df1451 100644 --- a/src/PowerShellEditorServices/Session/EditorSession.cs +++ b/src/PowerShellEditorServices/Session/EditorSession.cs @@ -87,7 +87,7 @@ public void StartSession(HostDetails hostDetails, ProfilePaths profilePaths) this.InstantiateAnalysisService(); // Create a workspace to contain open files - this.Workspace = new Workspace(this.PowerShellContext.PowerShellVersion); + this.Workspace = new Workspace(this.PowerShellContext.LocalPowerShellVersion.Version); } /// @@ -100,15 +100,24 @@ public void StartSession(HostDetails hostDetails, ProfilePaths profilePaths) /// /// An object containing the profile paths for the session. /// - public void StartDebugSession(HostDetails hostDetails, ProfilePaths profilePaths) + /// + /// An IEditorOperations implementation used to interact with the editor. + /// + public void StartDebugSession( + HostDetails hostDetails, + ProfilePaths profilePaths, + IEditorOperations editorOperations) { // Initialize all services this.PowerShellContext = new PowerShellContext(hostDetails, profilePaths); - this.DebugService = new DebugService(this.PowerShellContext); this.ConsoleService = new ConsoleService(this.PowerShellContext); + this.DebugService = + new DebugService( + this.PowerShellContext, + new RemoteFileManager(this.PowerShellContext, editorOperations)); // Create a workspace to contain open files - this.Workspace = new Workspace(this.PowerShellContext.PowerShellVersion); + this.Workspace = new Workspace(this.PowerShellContext.LocalPowerShellVersion.Version); } internal void InstantiateAnalysisService(string settingsPath = null) diff --git a/src/PowerShellEditorServices/Session/PowerShell3Operations.cs b/src/PowerShellEditorServices/Session/PowerShell3Operations.cs index faa4996c9..7b4ac4a9c 100644 --- a/src/PowerShellEditorServices/Session/PowerShell3Operations.cs +++ b/src/PowerShellEditorServices/Session/PowerShell3Operations.cs @@ -39,11 +39,19 @@ public IEnumerable ExecuteCommandInDebugger( nestedPipeline.Commands.Add(command); } - executionResult = - nestedPipeline - .Invoke() - .Select(pso => pso.BaseObject) - .Cast(); + var results = nestedPipeline.Invoke(); + + if (typeof(TResult) != typeof(PSObject)) + { + executionResult = + results + .Select(pso => pso.BaseObject) + .Cast(); + } + else + { + executionResult = results.Cast(); + } } // Write the output to the host if necessary diff --git a/src/PowerShellEditorServices/Session/PowerShell4Operations.cs b/src/PowerShellEditorServices/Session/PowerShell4Operations.cs index fd582f4bd..4551ef996 100644 --- a/src/PowerShellEditorServices/Session/PowerShell4Operations.cs +++ b/src/PowerShellEditorServices/Session/PowerShell4Operations.cs @@ -16,7 +16,10 @@ internal class PowerShell4Operations : IVersionSpecificOperations public void ConfigureDebugger(Runspace runspace) { #if !PowerShellv3 - runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); + if (runspace.Debugger != null) + { + runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); + } #endif } @@ -55,10 +58,20 @@ public IEnumerable ExecuteCommandInDebugger( outputCollection); #endif - return - outputCollection - .Select(pso => pso.BaseObject) - .Cast(); + IEnumerable results = null; + if (typeof(TResult) != typeof(PSObject)) + { + results = + outputCollection + .Select(pso => pso.BaseObject) + .Cast(); + } + else + { + results = outputCollection.Cast(); + } + + return results; } } } diff --git a/src/PowerShellEditorServices/Session/PowerShell5Operations.cs b/src/PowerShellEditorServices/Session/PowerShell5Operations.cs index 65c7f4527..54f434cb8 100644 --- a/src/PowerShellEditorServices/Session/PowerShell5Operations.cs +++ b/src/PowerShellEditorServices/Session/PowerShell5Operations.cs @@ -12,7 +12,10 @@ internal class PowerShell5Operations : PowerShell4Operations public override void PauseDebugger(Runspace runspace) { #if !PowerShellv3 && !PowerShellv4 - runspace.Debugger.SetDebuggerStepMode(true); + if (runspace.Debugger != null) + { + runspace.Debugger.SetDebuggerStepMode(true); + } #endif } } diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index 280220372..b4f7bed7d 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -6,7 +6,6 @@ using Microsoft.PowerShell.EditorServices.Console; using Microsoft.PowerShell.EditorServices.Utility; using System; -using System.Collections; using System.Globalization; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -20,6 +19,7 @@ namespace Microsoft.PowerShell.EditorServices { using Session; using System.Management.Automation; + using System.Management.Automation.Host; using System.Management.Automation.Runspaces; using System.Reflection; @@ -28,21 +28,21 @@ namespace Microsoft.PowerShell.EditorServices /// Handles nested PowerShell prompts and also manages execution of /// commands whether inside or outside of the debugger. /// - public class PowerShellContext : IDisposable + public class PowerShellContext : IDisposable, IHostSupportsInteractiveSession { #region Fields private PowerShell powerShell; - private IConsoleHost consoleHost; private bool ownsInitialRunspace; - private Runspace initialRunspace; - private Runspace currentRunspace; + private RunspaceDetails initialRunspace; + + private IConsoleHost consoleHost; private ProfilePaths profilePaths; private ConsoleServicePSHost psHost; - private InitialSessionState initialSessionState; + private IVersionSpecificOperations versionSpecificOperations; - private int pipelineThreadId; + private int pipelineThreadId; private TaskCompletionSource debuggerStoppedTask; private TaskCompletionSource pipelineExecutionTask; private TaskCompletionSource pipelineResultTask; @@ -50,6 +50,8 @@ public class PowerShellContext : IDisposable private object runspaceMutex = new object(); private AsyncQueue runspaceWaitQueue = new AsyncQueue(); + private Stack runspaceStack = new Stack(); + #endregion #region Properties @@ -76,27 +78,12 @@ public PowerShellContextState SessionState } /// - /// Gets the PowerShell version details for the current runspace. - /// - public PowerShellVersionDetails PowerShellVersionDetails - { - get; private set; - } - - /// - /// Gets the PowerShell version of the current runspace. - /// - public Version PowerShellVersion - { - get { return this.PowerShellVersionDetails.Version; } - } - - /// - /// Gets the PowerShell edition of the current runspace. + /// Gets the PowerShell version details for the initial local runspace. /// - public string PowerShellEdition + public PowerShellVersionDetails LocalPowerShellVersion { - get { return this.PowerShellVersionDetails.Edition; } + get; + private set; } /// @@ -113,6 +100,15 @@ internal IConsoleHost ConsoleHost } } + /// + /// Gets details pertaining to the current runspace. + /// + public RunspaceDetails CurrentRunspace + { + get; + private set; + } + #endregion #region Constructors @@ -135,10 +131,10 @@ public PowerShellContext(HostDetails hostDetails, ProfilePaths profilePaths) { hostDetails = hostDetails ?? HostDetails.Default; - this.psHost = new ConsoleServicePSHost(hostDetails); - this.initialSessionState = InitialSessionState.CreateDefault2(); + this.psHost = new ConsoleServicePSHost(hostDetails, this); + var initialSessionState = InitialSessionState.CreateDefault2(); - Runspace runspace = RunspaceFactory.CreateRunspace(psHost, this.initialSessionState); + Runspace runspace = RunspaceFactory.CreateRunspace(psHost, initialSessionState); #if !NanoServer runspace.ApartmentState = ApartmentState.STA; #endif @@ -185,36 +181,40 @@ private void Initialize(ProfilePaths profilePaths, Runspace initialRunspace) this.SessionState = PowerShellContextState.NotStarted; - this.initialRunspace = initialRunspace; - this.currentRunspace = initialRunspace; - this.psHost.Runspace = initialRunspace; - - this.currentRunspace.Debugger.BreakpointUpdated += OnBreakpointUpdated; - this.currentRunspace.Debugger.DebuggerStop += OnDebuggerStop; + // Get the PowerShell runtime version + this.LocalPowerShellVersion = + PowerShellVersionDetails.GetVersionDetails( + initialRunspace); + + this.initialRunspace = + new RunspaceDetails( + initialRunspace, + this.LocalPowerShellVersion, + RunspaceLocation.Local, + null); + this.CurrentRunspace = this.initialRunspace; this.powerShell = PowerShell.Create(); - this.powerShell.Runspace = this.currentRunspace; - - // Get the PowerShell runtime version - this.PowerShellVersionDetails = GetPowerShellVersion(); + this.powerShell.Runspace = initialRunspace; // Write out the PowerShell version for tracking purposes Logger.Write( LogLevel.Normal, string.Format( "PowerShell runtime version: {0}, edition: {1}", - this.PowerShellVersion, - this.PowerShellEdition)); + this.LocalPowerShellVersion.Version, + this.LocalPowerShellVersion.Edition)); - if (PowerShellVersion >= new Version(5,0)) + Version powerShellVersion = this.LocalPowerShellVersion.Version; + if (powerShellVersion >= new Version(5,0)) { this.versionSpecificOperations = new PowerShell5Operations(); } - else if (PowerShellVersion.Major == 4) + else if (powerShellVersion.Major == 4) { this.versionSpecificOperations = new PowerShell4Operations(); } - else if (PowerShellVersion.Major == 3) + else if (powerShellVersion.Major == 3) { this.versionSpecificOperations = new PowerShell3Operations(); } @@ -222,18 +222,17 @@ private void Initialize(ProfilePaths profilePaths, Runspace initialRunspace) { throw new NotSupportedException( "This computer has an unsupported version of PowerShell installed: " + - PowerShellVersion.ToString()); + powerShellVersion.ToString()); } - if (this.PowerShellEdition != "Linux") + if (this.LocalPowerShellVersion.Edition != "Linux") { // TODO: Should this be configurable? this.SetExecutionPolicy(ExecutionPolicy.RemoteSigned); } - // Configure the runspace's debugger - this.versionSpecificOperations.ConfigureDebugger( - this.currentRunspace); + // Set up the runspace + this.ConfigureRunspace(this.CurrentRunspace); // Set the $profile variable in the runspace this.profilePaths = profilePaths; @@ -248,73 +247,36 @@ private void Initialize(ProfilePaths profilePaths, Runspace initialRunspace) this.SessionState = PowerShellContextState.Ready; // Now that the runspace is ready, enqueue it for first use - RunspaceHandle runspaceHandle = new RunspaceHandle(this.currentRunspace, this); + RunspaceHandle runspaceHandle = new RunspaceHandle(this); this.runspaceWaitQueue.EnqueueAsync(runspaceHandle).Wait(); } - private PowerShellVersionDetails GetPowerShellVersion() + private void ConfigureRunspace(RunspaceDetails runspaceDetails) { - Version powerShellVersion = new Version(5, 0); - string versionString = null; - string powerShellEdition = "Desktop"; - var architecture = PowerShellProcessArchitecture.Unknown; - - try + if (!runspaceDetails.IsAttached) { - var psVersionTable = this.currentRunspace.SessionStateProxy.GetVariable("PSVersionTable") as Hashtable; - if (psVersionTable != null) + runspaceDetails.Runspace.StateChanged += this.HandleRunspaceStateChanged; + if (runspaceDetails.Runspace.Debugger != null) { - var edition = psVersionTable["PSEdition"] as string; - if (edition != null) - { - powerShellEdition = edition; - } - - // The PSVersion value will either be of Version or SemanticVersion. - // In the former case, take the value directly. In the latter case, - // generate a Version from its string representation. - var version = psVersionTable["PSVersion"]; - if (version is Version) - { - powerShellVersion = (Version)version; - } - else if (string.Equals(powerShellEdition, "Core", StringComparison.CurrentCultureIgnoreCase)) - { - // Expected version string format is 6.0.0-alpha so build a simpler version from that - powerShellVersion = new Version(version.ToString().Split('-')[0]); - } - - var gitCommitId = psVersionTable["GitCommitId"] as string; - if (gitCommitId != null) - { - versionString = gitCommitId; - } - else - { - versionString = powerShellVersion.ToString(); - } - - var arch = this.currentRunspace.SessionStateProxy.GetVariable("env:PROCESSOR_ARCHITECTURE") as string; - if (string.Equals(arch, "AMD64", StringComparison.CurrentCultureIgnoreCase)) - { - architecture = PowerShellProcessArchitecture.X64; - } - else if (string.Equals(arch, "x86", StringComparison.CurrentCultureIgnoreCase)) - { - architecture = PowerShellProcessArchitecture.X86; - } + runspaceDetails.Runspace.Debugger.BreakpointUpdated += OnBreakpointUpdated; + runspaceDetails.Runspace.Debugger.DebuggerStop += OnDebuggerStop; } + + this.versionSpecificOperations.ConfigureDebugger(runspaceDetails.Runspace); } - catch (Exception ex) + } + + private void CleanupRunspace(RunspaceDetails runspaceDetails) + { + if (!runspaceDetails.IsAttached) { - Logger.Write(LogLevel.Warning, "Failed to look up PowerShell version. Defaulting to version 5. " + ex.Message); + runspaceDetails.Runspace.StateChanged -= this.HandleRunspaceStateChanged; + if (runspaceDetails.Runspace.Debugger != null) + { + runspaceDetails.Runspace.Debugger.BreakpointUpdated -= OnBreakpointUpdated; + runspaceDetails.Runspace.Debugger.DebuggerStop -= OnDebuggerStop; + } } - - return new PowerShellVersionDetails( - powerShellVersion, - versionString, - powerShellEdition, - architecture); } #endregion @@ -374,8 +336,7 @@ public async Task> ExecuteCommand( /// /// The expected result type. /// The PSCommand to be executed. - /// Error messages from PowerShell will be written to the StringBuilder. - /// You must set sendErrorToHost to false for errors to be written to the StringBuilder. This value can be null. + /// Error messages from PowerShell will be written to the StringBuilder. /// /// If true, causes any output written during command execution to be written to the host. /// @@ -429,15 +390,9 @@ public async Task> ExecuteCommand( endOfStatement: false)); } - if (this.currentRunspace.RunspaceAvailability == RunspaceAvailability.AvailableForNestedCommand || + if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.AvailableForNestedCommand || this.debuggerStoppedTask != null) { - Logger.Write( - LogLevel.Verbose, - string.Format( - "Attempting to execute nested pipeline command(s):\r\n\r\n{0}", - GetStringForPSCommand(psCommand))); - executionResult = this.ExecuteCommandInDebugger( psCommand, @@ -464,8 +419,21 @@ public async Task> ExecuteCommand( await Task.Factory.StartNew>( () => { - this.powerShell.Commands = psCommand; - Collection result = this.powerShell.Invoke(); + Collection result = null; + try + { + this.powerShell.Commands = psCommand; + result = this.powerShell.Invoke(); + } + catch (RemoteException e) + { + if (!e.SerializedRemoteException.TypeNames[0].EndsWith("PipelineStoppedException")) + { + // Rethrow anything that isn't a PipelineStoppedException + throw e; + } + } + return result; }, CancellationToken.None, // Might need a cancellation token @@ -495,6 +463,14 @@ await Task.Factory.StartNew>( return executionResult; } } + catch (PipelineStoppedException e) + { + Logger.Write( + LogLevel.Error, + "Popeline stopped while executing command:\r\n\r\n" + e.ToString()); + + errorMessages?.Append(e.Message); + } catch (RuntimeException e) { Logger.Write( @@ -519,6 +495,12 @@ await Task.Factory.StartNew>( { this.WritePromptWithRunspace(runspaceHandle.Runspace); } + else if (this.IsDebuggerStopped) + { + // Check for RunspaceAvailability.RemoteDebug (4) in a way that's safe in PSv3 + //if ((int)this.currentRunspace.RunspaceAvailability == 4) + this.WritePromptInDebugger(); + } else { this.WritePromptWithNestedPipeline(); @@ -565,11 +547,41 @@ public Task> ExecuteScriptString( /// Executes a script string in the session's runspace. /// /// The script string to execute. + /// Error messages from PowerShell will be written to the StringBuilder. + /// A Task that can be awaited for the script completion. + public Task> ExecuteScriptString( + string scriptString, + StringBuilder errorMessages) + { + return this.ExecuteScriptString(scriptString, errorMessages, false, true); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// If true, causes the script string to be written to the host. + /// If true, causes the script output to be written to the host. + /// A Task that can be awaited for the script completion. + public Task> ExecuteScriptString( + string scriptString, + bool writeInputToHost, + bool writeOutputToHost) + { + return this.ExecuteScriptString(scriptString, null, writeInputToHost, writeOutputToHost); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// Error messages from PowerShell will be written to the StringBuilder. /// If true, causes the script string to be written to the host. /// If true, causes the script output to be written to the host. /// A Task that can be awaited for the script completion. public async Task> ExecuteScriptString( string scriptString, + StringBuilder errorMessages, bool writeInputToHost, bool writeOutputToHost) { @@ -583,7 +595,11 @@ public async Task> ExecuteScriptString( PSCommand psCommand = new PSCommand(); psCommand.AddScript(scriptString); - return await this.ExecuteCommand(psCommand, writeOutputToHost); + return + await this.ExecuteCommand( + psCommand, + errorMessages, + writeOutputToHost); } /// @@ -614,6 +630,50 @@ public async Task ExecuteScriptAtPath(string scriptPath, string arguments = null await this.ExecuteCommand(command, true); } + internal static TResult ExecuteScriptAndGetItem(string scriptToExecute, Runspace runspace, TResult defaultValue = default(TResult)) + { + Pipeline pipeline = null; + + try + { + if (runspace.RunspaceAvailability == RunspaceAvailability.AvailableForNestedCommand) + { + pipeline = runspace.CreateNestedPipeline(scriptToExecute, false); + } + else + { + pipeline = runspace.CreatePipeline(scriptToExecute, false); + } + + Collection results = pipeline.Invoke(); + + if (results.Count == 0) + { + return defaultValue; + } + + if (typeof(TResult) != typeof(PSObject)) + { + return + results + .Select(pso => pso.BaseObject) + .OfType() + .FirstOrDefault(); + } + else + { + return + results + .OfType() + .FirstOrDefault(); + } + } + finally + { + pipeline.Dispose(); + } + } + /// /// Loads PowerShell profiles for the host from the specified /// profile locations. Only the profile paths which exist are @@ -674,7 +734,7 @@ internal void BreakExecution() // Pause the debugger this.versionSpecificOperations.PauseDebugger( - this.currentRunspace); + this.CurrentRunspace.Runspace); } internal void ResumeDebugger(DebuggerResumeAction resumeAction) @@ -713,22 +773,99 @@ public void Dispose() this.powerShell = null; } + // Clean up the active runspace + this.CleanupRunspace(this.CurrentRunspace); + + // Drain the runspace stack if any have been pushed + if (this.runspaceStack.Count > 0) + { + // Push the active runspace so it will be included in the loop + this.runspaceStack.Push(this.CurrentRunspace); + + while (this.runspaceStack.Count > 1) + { + RunspaceDetails poppedRunspace = this.runspaceStack.Pop(); + this.CloseRunspace(poppedRunspace); + + this.OnRunspaceChanged( + this, + new RunspaceChangedEventArgs( + RunspaceChangeAction.Shutdown, + poppedRunspace, + null)); + } + } + if (this.ownsInitialRunspace && this.initialRunspace != null) { - // TODO: Detach from events - this.initialRunspace.Close(); - this.initialRunspace.Dispose(); + this.CloseRunspace(this.initialRunspace); this.initialRunspace = null; } } + private void CloseRunspace(RunspaceDetails runspaceDetails) + { + // An attached runspace will be detached when the + // running pipeline is aborted + if (!runspaceDetails.IsAttached) + { + string exitCommand = null; + + switch (runspaceDetails.Location) + { + case RunspaceLocation.Local: + runspaceDetails.Runspace.Close(); + runspaceDetails.Runspace.Dispose(); + break; + + case RunspaceLocation.LocalProcess: + exitCommand = "Exit-PSHostProcess"; + break; + + case RunspaceLocation.Remote: + exitCommand = "Exit-PSSession"; + break; + } + + if (exitCommand != null) + { + Exception exitException = null; + + try + { + using (PowerShell ps = PowerShell.Create()) + { + ps.Runspace = runspaceDetails.Runspace; + ps.AddCommand(exitCommand); + ps.Invoke(); + } + } + catch (RemoteException e) + { + exitException = e; + } + catch (RuntimeException e) + { + exitException = e; + } + + if (exitException != null) + { + Logger.Write( + LogLevel.Error, + $"Caught {exitException.GetType().Name} while exiting {runspaceDetails.Location} runspace:\r\n{exitException.ToString()}"); + } + } + } + } + internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) { Validate.IsNotNull("runspaceHandle", runspaceHandle); if (this.runspaceWaitQueue.IsEmpty) { - var newRunspaceHandle = new RunspaceHandle(this.currentRunspace, this); + var newRunspaceHandle = new RunspaceHandle(this); this.runspaceWaitQueue.EnqueueAsync(newRunspaceHandle).Wait(); } else @@ -747,7 +884,7 @@ internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) /// public void SetWorkingDirectory(string path) { - this.currentRunspace.SessionStateProxy.Path.SetLocation(path); + this.CurrentRunspace.Runspace.SessionStateProxy.Path.SetLocation(path); } /// @@ -796,31 +933,52 @@ public static string UnescapePath(string path) private void OnSessionStateChanged(object sender, SessionStateChangedEventArgs e) { - Logger.Write( - LogLevel.Verbose, - string.Format( - "Session state changed --\r\n\r\n Old state: {0}\r\n New state: {1}\r\n Result: {2}", - this.SessionState.ToString(), - e.NewSessionState.ToString(), - e.ExecutionResult)); - - this.SessionState = e.NewSessionState; + if (this.SessionState != PowerShellContextState.Disposed) + { + Logger.Write( + LogLevel.Verbose, + string.Format( + "Session state changed --\r\n\r\n Old state: {0}\r\n New state: {1}\r\n Result: {2}", + this.SessionState.ToString(), + e.NewSessionState.ToString(), + e.ExecutionResult)); - if (this.SessionStateChanged != null) + this.SessionState = e.NewSessionState; + this.SessionStateChanged?.Invoke(sender, e); + } + else { - this.SessionStateChanged(sender, e); + Logger.Write( + LogLevel.Warning, + $"Received session state change to {e.NewSessionState} when already disposed"); } } + /// + /// Raised when the runspace changes by entering a remote session or one in a different process. + /// + public event EventHandler RunspaceChanged; + + private void OnRunspaceChanged(object sender, RunspaceChangedEventArgs e) + { + this.RunspaceChanged?.Invoke(sender, e); + } + #endregion #region Private Methods private IEnumerable ExecuteCommandInDebugger(PSCommand psCommand, bool sendOutputToHost) { + Logger.Write( + LogLevel.Verbose, + string.Format( + "Attempting to execute command(s) in the debugger:\r\n\r\n{0}", + GetStringForPSCommand(psCommand))); + return this.versionSpecificOperations.ExecuteCommandInDebugger( this, - this.currentRunspace, + this.CurrentRunspace.Runspace, psCommand, sendOutputToHost); } @@ -1097,18 +1255,42 @@ private void WritePromptWithRunspace(Runspace runspace) this.WritePromptToHost( command => { + this.powerShell.Runspace = runspace; this.powerShell.Commands = command; - return + var promptString = this.powerShell .Invoke() .FirstOrDefault(); + + if (this.CurrentRunspace.Location == RunspaceLocation.Remote) + { + promptString = + string.Format( + CultureInfo.InvariantCulture, + "[{0}]: {1}", + runspace.ConnectionInfo.ComputerName, + promptString); + } + + return promptString; + }); + } + + private void WritePromptInDebugger() + { + this.WritePromptToHost( + command => + { + return + this.ExecuteCommandInDebugger(command, false) + .FirstOrDefault(); }); } private void WritePromptWithNestedPipeline() { - using (var pipeline = this.currentRunspace.CreateNestedPipeline()) + using (var pipeline = this.CurrentRunspace.Runspace.CreateNestedPipeline()) { this.WritePromptToHost( command => @@ -1168,6 +1350,24 @@ private void SetProfileVariableInCurrentRunspace(ProfilePaths profilePaths) this.powerShell.Commands.Clear(); } + private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs args) + { + switch (args.RunspaceStateInfo.State) + { + case RunspaceState.Opening: + case RunspaceState.Opened: + // These cases don't matter, just return + return; + + case RunspaceState.Closing: + case RunspaceState.Closed: + case RunspaceState.Broken: + // If the runspace closes or fails, pop the runspace + ((IHostSupportsInteractiveSession)this).PopRunspace(); + break; + } + } + #endregion #region Events @@ -1196,15 +1396,34 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) PowerShellExecutionResult.Stopped, null)); + // Did we attach to a runspace? + if (this.CurrentRunspace.Location != RunspaceLocation.Local && + this.CurrentRunspace.IsAttached == false) + { + // Check the current runspace ID + PSCommand command = new PSCommand(); + command.AddScript("$host.Runspace.InstanceId"); + Guid currentRunspaceId = + this.ExecuteCommandInDebugger(command, false) + .FirstOrDefault(); + + // If the reported unspace ID is different it means we're + // attached to a different runspace in the process + if (currentRunspaceId != Guid.Empty && currentRunspaceId != this.CurrentRunspace.Id) + { + // Push the details about the attached runspace + this.PushRunspace( + RunspaceDetails.CreateAttached( + this.CurrentRunspace, + currentRunspaceId)); + } + } + // Write out the debugger prompt - // TODO: Eventually re-enable this and put it behind a setting, #133 - //this.WritePromptWithNestedPipeline(); + this.WritePromptInDebugger(); // Raise the event for the debugger service - if (this.DebuggerStop != null) - { - this.DebuggerStop(sender, e); - } + this.DebuggerStop?.Invoke(sender, e); Logger.Write(LogLevel.Verbose, "Starting pipeline thread message loop..."); @@ -1218,8 +1437,7 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) if (taskIndex == 0) { // Write a new output line before continuing - // TODO: Re-enable this with fix for #133 - //this.WriteOutput("", true); + this.WriteOutput("", true); e.ResumeAction = this.debuggerStoppedTask.Task.Result; Logger.Write(LogLevel.Verbose, "Received debugger resume action " + e.ResumeAction.ToString()); @@ -1240,6 +1458,19 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) Logger.Write(LogLevel.Verbose, "Pipeline thread execution completed."); this.pipelineResultTask.SetResult(executionRequest); + + if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.Available) + { + if (this.CurrentRunspace.IsAttached) + { + // We're detached from the runspace now, send a runspace update. + this.PopRunspace(); + } + + // If the executed command caused the debugger to exit, break + // from the pipeline loop + break; + } } else { @@ -1249,6 +1480,7 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) // Clear the task so that it won't be used again this.debuggerStoppedTask = null; + this.pipelineExecutionTask = null; } // NOTE: This event is 'internal' because the DebugService provides @@ -1257,10 +1489,7 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) { - if (this.BreakpointUpdated != null) - { - this.BreakpointUpdated(sender, e); - } + this.BreakpointUpdated?.Invoke(sender, e); } #endregion @@ -1310,6 +1539,105 @@ await this.powerShellContext.ExecuteCommand( } } + private void PushRunspace(RunspaceDetails newRunspaceDetails) + { + Logger.Write( + LogLevel.Verbose, + $"Pushing {this.CurrentRunspace.Location} runspace {this.CurrentRunspace.Id}, new runspace is {newRunspaceDetails.Id} (connection: {newRunspaceDetails.ConnectionString})"); + + RunspaceDetails previousRunspace = this.CurrentRunspace; + + // If the new runspace is just attaching to another runspace in + // the same process, don't clean up the previous runspace because + // we need to maintain its event handler registrations. + if (!newRunspaceDetails.IsAttached) + { + this.CleanupRunspace(previousRunspace); + } + + this.runspaceStack.Push(previousRunspace); + + this.CurrentRunspace = newRunspaceDetails; + this.ConfigureRunspace(newRunspaceDetails); + + this.OnRunspaceChanged( + this, + new RunspaceChangedEventArgs( + RunspaceChangeAction.Enter, + previousRunspace, + this.CurrentRunspace)); + } + + private void PopRunspace() + { + if (this.SessionState != PowerShellContextState.Disposed) + { + if (this.runspaceStack.Count > 0) + { + RunspaceDetails previousRunspace = this.CurrentRunspace; + this.CurrentRunspace = this.runspaceStack.Pop(); + + Logger.Write( + LogLevel.Verbose, + $"Popping {previousRunspace.Location} runspace {previousRunspace.Id}, new runspace is {this.CurrentRunspace.Id} (connection: {this.CurrentRunspace.ConnectionString})"); + + this.CleanupRunspace(previousRunspace); + + // If the old runspace is just attached to another runspace in + // the same process, don't configure the popped runspace because + // its event handlers are already registered. + if (!previousRunspace.IsAttached) + { + this.ConfigureRunspace(this.CurrentRunspace); + } + + this.OnRunspaceChanged( + this, + new RunspaceChangedEventArgs( + RunspaceChangeAction.Exit, + previousRunspace, + this.CurrentRunspace)); + } + else + { + Logger.Write( + LogLevel.Error, + "Caller attempted to pop a runspace when no runspaces are on the stack."); + } + } + } + + #endregion + + #region IHostSupportsInteractiveSession Implementation + + bool IHostSupportsInteractiveSession.IsRunspacePushed + { + get + { + return this.runspaceStack.Count > 0; + } + } + + Runspace IHostSupportsInteractiveSession.Runspace + { + get + { + return this.CurrentRunspace.Runspace; + } + } + + void IHostSupportsInteractiveSession.PushRunspace(Runspace runspace) + { + this.PushRunspace( + RunspaceDetails.Create(runspace)); + } + + void IHostSupportsInteractiveSession.PopRunspace() + { + this.PopRunspace(); + } + #endregion } } diff --git a/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs b/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs index 9f8a596e0..e25cc33b0 100644 --- a/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs +++ b/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs @@ -3,7 +3,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Utility; using System; +using System.Collections; +using System.Management.Automation.Runspaces; namespace Microsoft.PowerShell.EditorServices.Session { @@ -33,6 +36,8 @@ public enum PowerShellProcessArchitecture /// public class PowerShellVersionDetails { + #region Properties + /// /// Gets the version of the PowerShell runtime. /// @@ -54,6 +59,10 @@ public class PowerShellVersionDetails /// public PowerShellProcessArchitecture Architecture { get; private set; } + #endregion + + #region Constructors + /// /// Creates an instance of the PowerShellVersionDetails class. /// @@ -72,5 +81,83 @@ public PowerShellVersionDetails( this.Edition = editionString; this.Architecture = architecture; } + + #endregion + + #region Public Methods + + /// + /// Gets the PowerShell version details for the given runspace. + /// + /// The runspace for which version details will be gathered. + /// A new PowerShellVersionDetails instance. + public static PowerShellVersionDetails GetVersionDetails(Runspace runspace) + { + Version powerShellVersion = new Version(5, 0); + string versionString = null; + string powerShellEdition = "Desktop"; + var architecture = PowerShellProcessArchitecture.Unknown; + + try + { + var psVersionTable = PowerShellContext.ExecuteScriptAndGetItem("$PSVersionTable", runspace); + if (psVersionTable != null) + { + var edition = psVersionTable["PSEdition"] as string; + if (edition != null) + { + powerShellEdition = edition; + } + + // The PSVersion value will either be of Version or SemanticVersion. + // In the former case, take the value directly. In the latter case, + // generate a Version from its string representation. + var version = psVersionTable["PSVersion"]; + if (version is Version) + { + powerShellVersion = (Version)version; + } + else if (string.Equals(powerShellEdition, "Core", StringComparison.CurrentCultureIgnoreCase)) + { + // Expected version string format is 6.0.0-alpha so build a simpler version from that + powerShellVersion = new Version(version.ToString().Split('-')[0]); + } + + var gitCommitId = psVersionTable["GitCommitId"] as string; + if (gitCommitId != null) + { + versionString = gitCommitId; + } + else + { + versionString = powerShellVersion.ToString(); + } + + var arch = PowerShellContext.ExecuteScriptAndGetItem("$env:PROCESSOR_ARCHITECTURE", runspace); + if (string.Equals(arch, "AMD64", StringComparison.CurrentCultureIgnoreCase)) + { + architecture = PowerShellProcessArchitecture.X64; + } + else if (string.Equals(arch, "x86", StringComparison.CurrentCultureIgnoreCase)) + { + architecture = PowerShellProcessArchitecture.X86; + } + } + } + catch (Exception ex) + { + Logger.Write( + LogLevel.Warning, + "Failed to look up PowerShell version. Defaulting to version 5. " + ex.Message); + } + + return new PowerShellVersionDetails( + powerShellVersion, + versionString, + powerShellEdition, + architecture); + } + + #endregion } } diff --git a/src/PowerShellEditorServices/Session/RemoteFileManager.cs b/src/PowerShellEditorServices/Session/RemoteFileManager.cs new file mode 100644 index 000000000..a2818f1d3 --- /dev/null +++ b/src/PowerShellEditorServices/Session/RemoteFileManager.cs @@ -0,0 +1,299 @@ +// +// 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.Extensions; +using Microsoft.PowerShell.EditorServices.Utility; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Manages files that are accessed from a remote PowerShell session. + /// Also manages the registration and handling of the 'psedit' function + /// in 'LocalProcess' and 'Remote' runspaces. + /// + public class RemoteFileManager + { + #region Fields + + private string processTempPath; + private PowerShellContext powerShellContext; + private IEditorOperations editorOperations; + + private Dictionary filesPerRunspace = + new Dictionary(); + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the RemoteFileManager class. + /// + /// + /// The PowerShellContext to use for file loading operations. + /// + /// + /// The IEditorOperations instance to use for opening/closing files in the editor. + /// + public RemoteFileManager( + PowerShellContext powerShellContext, + IEditorOperations editorOperations) + { + Validate.IsNotNull(nameof(powerShellContext), powerShellContext); + Validate.IsNotNull(nameof(editorOperations), editorOperations); + + this.powerShellContext = powerShellContext; + this.powerShellContext.RunspaceChanged += PowerShellContext_RunspaceChanged; + + this.editorOperations = editorOperations; + + this.processTempPath = + Path.Combine( + Path.GetTempPath(), + "PSES-" + Process.GetCurrentProcess().Id, + "RemoteFiles"); + + // Delete existing session file cache path if it already exists + try + { + if (Directory.Exists(this.processTempPath)) + { + Directory.Delete(this.processTempPath, true); + } + } + catch (IOException e) + { + Logger.Write( + LogLevel.Error, + $"Could not delete existing remote files folder for current session: {this.processTempPath}\r\n\r\n{e.ToString()}"); + } + } + + #endregion + + #region Public Methods + + /// + /// Opens a remote file, fetching its contents if necessary. + /// + /// + /// The remote file path to be opened. + /// + /// + /// The runspace from which where the remote file will be fetched. + /// + /// + /// The local file path where the remote file's contents have been stored. + /// + public async Task FetchRemoteFile( + string remoteFilePath, + RunspaceDetails runspaceDetails) + { + string localFilePath = null; + + if (!string.IsNullOrEmpty(remoteFilePath)) + { + try + { + RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); + localFilePath = this.GetMappedPath(remoteFilePath, runspaceDetails); + + if (!pathMappings.IsRemotePathOpened(remoteFilePath)) + { + // Does the local file already exist? + if (!File.Exists(localFilePath)) + { + // Load the file contents from the remote machine and create the buffer + PSCommand command = new PSCommand(); + command.AddCommand("Microsoft.PowerShell.Management\\Get-Content"); + command.AddParameter("Path", remoteFilePath); + command.AddParameter("Raw"); + command.AddParameter("Encoding", "Byte"); + + byte[] fileContent = + (await this.powerShellContext.ExecuteCommand(command, false, false)) + .FirstOrDefault(); + + if (fileContent != null) + { + File.WriteAllBytes(localFilePath, fileContent); + } + else + { + Logger.Write( + LogLevel.Warning, + $"Could not load contents of remote file '{remoteFilePath}'"); + } + + pathMappings.AddOpenedLocalPath(localFilePath); + } + } + } + catch (IOException e) + { + Logger.Write( + LogLevel.Error, + $"Caught {e.GetType().Name} while attempting to get remote file at path '{remoteFilePath}'\r\n\r\n{e.ToString()}"); + } + } + + return localFilePath; + } + + + /// + /// For a remote or local cache path, get the corresponding local or + /// remote file path. + /// + /// + /// The remote or local file path. + /// + /// + /// The runspace from which the remote file was fetched. + /// + /// The mapped file path. + public string GetMappedPath( + string filePath, + RunspaceDetails runspaceDetails) + { + RemotePathMappings remotePathMappings = this.GetPathMappings(runspaceDetails); + return remotePathMappings.GetMappedPath(filePath); + } + + #endregion + + #region Private Methods + + private RemotePathMappings GetPathMappings(RunspaceDetails runspaceDetails) + { + RemotePathMappings remotePathMappings = null; + + if (!this.filesPerRunspace.TryGetValue(runspaceDetails, out remotePathMappings)) + { + remotePathMappings = new RemotePathMappings(runspaceDetails, this); + this.filesPerRunspace.Add(runspaceDetails, remotePathMappings); + } + + return remotePathMappings; + } + + private async void PowerShellContext_RunspaceChanged(object sender, RunspaceChangedEventArgs e) + { + if (e.ChangeAction == RunspaceChangeAction.Enter) + { + // TODO: Register psedit function and event handler + } + else + { + // Close any remote files that were opened + RemotePathMappings remotePathMappings; + if (this.filesPerRunspace.TryGetValue(e.PreviousRunspace, out remotePathMappings)) + { + foreach (string remotePath in remotePathMappings.OpenedPaths) + { + await this.editorOperations.CloseFile(remotePath); + } + } + + // TODO: Clean up psedit registration + } + } + + #endregion + + #region Nested Classes + + private class RemotePathMappings + { + private RunspaceDetails runspaceDetails; + private RemoteFileManager remoteFileManager; + private HashSet openedPaths = new HashSet(); + private Dictionary pathMappings = new Dictionary(); + + public IEnumerable OpenedPaths + { + get { return openedPaths; } + } + + public RemotePathMappings( + RunspaceDetails runspaceDetails, + RemoteFileManager remoteFileManager) + { + this.runspaceDetails = runspaceDetails; + this.remoteFileManager = remoteFileManager; + } + + public void AddPathMapping(string remotePath, string localPath) + { + // Add mappings in both directions + this.pathMappings[localPath.ToLower()] = remotePath; + this.pathMappings[remotePath.ToLower()] = localPath; + } + + public void AddOpenedLocalPath(string openedLocalPath) + { + this.openedPaths.Add(openedLocalPath); + } + + public bool IsRemotePathOpened(string remotePath) + { + return this.openedPaths.Contains(remotePath); + } + + public string GetMappedPath(string filePath) + { + string mappedPath = null; + + if (!this.pathMappings.TryGetValue(filePath.ToLower(), out mappedPath)) + { + // If the path isn't mapped yet, generate it + if (!filePath.StartsWith(this.remoteFileManager.processTempPath)) + { + mappedPath = + this.MapRemotePathToLocal( + filePath, + runspaceDetails.ConnectionString); + + this.AddPathMapping(filePath, mappedPath); + } + } + + return mappedPath; + } + + private string MapRemotePathToLocal(string remotePath, string connectionString) + { + // The path generated by this code will look something like + // %TEMP%\PSES-[PID]\RemoteFiles\1205823508\computer-name\MyFile.ps1 + // The "path hash" is just the hashed representation of the remote + // file's full path (sans directory) to try and ensure some amount of + // uniqueness across different files on the remote machine. We put + // the "connection string" after the path slug so that it can be used + // as the differentiator string in editors like VS Code when more than + // one tab has the same filename. + + var sessionDir = Directory.CreateDirectory(this.remoteFileManager.processTempPath); + var pathHashDir = + sessionDir.CreateSubdirectory( + Path.GetDirectoryName(remotePath).GetHashCode().ToString()); + + var remoteFileDir = pathHashDir.CreateSubdirectory(connectionString); + + return + Path.Combine( + remoteFileDir.FullName, + Path.GetFileName(remotePath)); + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Session/RunspaceChangedEventArgs.cs b/src/PowerShellEditorServices/Session/RunspaceChangedEventArgs.cs new file mode 100644 index 000000000..12dc76ade --- /dev/null +++ b/src/PowerShellEditorServices/Session/RunspaceChangedEventArgs.cs @@ -0,0 +1,69 @@ +// +// 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.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Defines the set of actions that will cause the runspace to be changed. + /// + public enum RunspaceChangeAction + { + /// + /// The runspace change was caused by entering a new session. + /// + Enter, + + /// + /// The runspace change was caused by exiting the current session. + /// + Exit, + + /// + /// The runspace change was caused by shutting down the service. + /// + Shutdown + } + + /// + /// Provides arguments for the PowerShellContext.RunspaceChanged event. + /// + public class RunspaceChangedEventArgs + { + /// + /// Gets the RunspaceChangeAction which caused this event. + /// + public RunspaceChangeAction ChangeAction { get; private set; } + + /// + /// Gets a RunspaceDetails object describing the previous runspace. + /// + public RunspaceDetails PreviousRunspace { get; private set; } + + /// + /// Gets a RunspaceDetails object describing the new runspace. + /// + public RunspaceDetails NewRunspace { get; private set; } + + /// + /// Creates a new instance of the RunspaceChangedEventArgs class. + /// + /// The action which caused the runspace to change. + /// The previously active runspace. + /// The newly active runspace. + public RunspaceChangedEventArgs( + RunspaceChangeAction changeAction, + RunspaceDetails previousRunspace, + RunspaceDetails newRunspace) + { + Validate.IsNotNull(nameof(previousRunspace), previousRunspace); + + this.ChangeAction = changeAction; + this.PreviousRunspace = previousRunspace; + this.NewRunspace = newRunspace; + } + } +} diff --git a/src/PowerShellEditorServices/Session/RunspaceDetails.cs b/src/PowerShellEditorServices/Session/RunspaceDetails.cs new file mode 100644 index 000000000..dffe9cfee --- /dev/null +++ b/src/PowerShellEditorServices/Session/RunspaceDetails.cs @@ -0,0 +1,203 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.CSharp.RuntimeBinder; +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Specifies the possible types of a runspace. + /// + public enum RunspaceLocation + { + /// + /// A runspace in the current process on the same machine. + /// + Local, + + /// + /// A runspace in a different process on the same machine. + /// + LocalProcess, + + /// + /// A runspace on a different machine. + /// + Remote, + + // NOTE: We don't have a RemoteProcess variable here because there's + // no reliable way to know when the user has used Enter-PSHostProcess + // to jump into a different PowerShell process on the remote machine. + // Even if we check the PID every time the prompt gets written, there's + // still a chance the user is running a script that contains the + // Enter-PSHostProcess command and it won't be caught until after the + // script finishes. + } + + /// + /// Provides details about a runspace being used in the current + /// editing session. + /// + public class RunspaceDetails + { + #region Properties + + /// + /// Gets the id of the underlying Runspace object. + /// + public Guid Id { get; private set; } + + /// + /// Gets the Runspace instance for which this class contains details. + /// + internal Runspace Runspace { get; private set; } + + /// + /// Gets the PowerShell version of the new runspace. + /// + public PowerShellVersionDetails PowerShellVersion { get; private set; } + + /// + /// Gets the runspace location, either Local or Remote. + /// + public RunspaceLocation Location { get; private set; } + + /// + /// Gets a boolean which indicates whether this runspace is the result + /// of attaching with Debug-Runspace. + /// + public bool IsAttached { get; private set; } + + /// + /// Gets the "connection string" for the runspace, generally the + /// ComputerName for a remote runspace or the ProcessId of an + /// "Attach" runspace. + /// + public string ConnectionString { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the RunspaceDetails class. + /// + /// + /// The runspace for which this instance contains details. + /// + /// + /// The PowerShellVersionDetails of the runspace. + /// + /// + /// The RunspaceLocale of the runspace. + /// + /// + /// The connection string of the runspace. + /// + public RunspaceDetails( + Runspace runspace, + PowerShellVersionDetails powerShellVersion, + RunspaceLocation runspaceLocation, + string connectionString) + { + this.Id = runspace.InstanceId; + this.Runspace = runspace; + this.PowerShellVersion = powerShellVersion; + this.Location = runspaceLocation; + this.ConnectionString = connectionString; + } + + #endregion + + #region Public Methods + + /// + /// Creates and populates a new RunspaceDetails instance for the given runspace. + /// + /// The runspace for which details will be gathered. + /// A new RunspaceDetails instance. + public static RunspaceDetails Create(Runspace runspace) + { + Validate.IsNotNull(nameof(runspace), runspace); + + var runspaceLocation = RunspaceLocation.Local; + var versionDetails = PowerShellVersionDetails.GetVersionDetails(runspace); + + string connectionString = null; + + if (runspace.ConnectionInfo != null) + { + // Use 'dynamic' to avoid missing NamedPipeRunspaceConnectionInfo + // on PS v3 and v4 + try + { + dynamic connectionInfo = runspace.ConnectionInfo; + if (connectionInfo.ProcessId != null) + { + runspaceLocation = RunspaceLocation.LocalProcess; + connectionString = connectionInfo.ProcessId.ToString(); + } + } + catch (RuntimeBinderException) + { + // ProcessId property isn't on the object, move on. + } + + if (runspace.ConnectionInfo.ComputerName != "localhost") + { + runspaceLocation = RunspaceLocation.Remote; + connectionString = + runspace.ConnectionInfo.ComputerName + + (connectionString != null ? $"-{connectionString}" : string.Empty); + } + } + + return + new RunspaceDetails( + runspace, + versionDetails, + runspaceLocation, + connectionString); + } + + /// + /// Creates a clone of the given runspace through which another + /// runspace was attached. Sets the IsAttached property of the + /// resulting RunspaceDetails object to true. + /// + /// + /// The RunspaceDetails object which the new object based. + /// + /// + /// The id of the runspace that has been attached. + /// + /// + /// A new RunspaceDetails instance for the attached runspace. + /// + public static RunspaceDetails CreateAttached( + RunspaceDetails runspaceDetails, + Guid attachedRunspaceId) + { + RunspaceDetails newRunspace = + new RunspaceDetails( + runspaceDetails.Runspace, + runspaceDetails.PowerShellVersion, + runspaceDetails.Location, + runspaceDetails.ConnectionString); + + // Since this is an attached runspace, set the IsAttached + // property and carry forward the ID of the attached runspace + newRunspace.IsAttached = true; + newRunspace.Id = attachedRunspaceId; + + return newRunspace; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Session/RunspaceHandle.cs b/src/PowerShellEditorServices/Session/RunspaceHandle.cs index 0c9ae0ab5..b7fc0e8f1 100644 --- a/src/PowerShellEditorServices/Session/RunspaceHandle.cs +++ b/src/PowerShellEditorServices/Session/RunspaceHandle.cs @@ -4,6 +4,7 @@ // using System; +using System.Management.Automation.Host; using System.Management.Automation.Runspaces; namespace Microsoft.PowerShell.EditorServices @@ -19,17 +20,21 @@ public class RunspaceHandle : IDisposable /// /// Gets the runspace that is held by this handle. /// - public Runspace Runspace { get; private set; } + public Runspace Runspace + { + get + { + return ((IHostSupportsInteractiveSession)this.powerShellContext).Runspace; + } + } /// /// Initializes a new instance of the RunspaceHandle class using the /// given runspace. /// - /// The runspace instance which is temporarily owned by this handle. /// The PowerShellContext instance which manages the runspace. - public RunspaceHandle(Runspace runspace, PowerShellContext powerShellContext) + public RunspaceHandle(PowerShellContext powerShellContext) { - this.Runspace = runspace; this.powerShellContext = powerShellContext; } @@ -42,7 +47,6 @@ public void Dispose() // Release the handle and clear the runspace so that // no further operations can be performed on it. this.powerShellContext.ReleaseRunspaceHandle(this); - this.Runspace = null; } } } diff --git a/src/PowerShellEditorServices/Session/SessionPSHost.cs b/src/PowerShellEditorServices/Session/SessionPSHost.cs index 85a650858..7f8678694 100644 --- a/src/PowerShellEditorServices/Session/SessionPSHost.cs +++ b/src/PowerShellEditorServices/Session/SessionPSHost.cs @@ -25,6 +25,7 @@ internal class ConsoleServicePSHost : PSHost, IHostSupportsInteractiveSession private IConsoleHost consoleHost; private Guid instanceId = Guid.NewGuid(); private ConsoleServicePSHostUserInterface hostUserInterface; + private IHostSupportsInteractiveSession hostSupportsInteractiveSession; #endregion @@ -51,10 +52,16 @@ internal IConsoleHost ConsoleHost /// /// Provides details about the host application. /// - public ConsoleServicePSHost(HostDetails hostDetails) + /// + /// An implementation of IHostSupportsInteractiveSession for runspace management. + /// + public ConsoleServicePSHost( + HostDetails hostDetails, + IHostSupportsInteractiveSession hostSupportsInteractiveSession) { this.hostDetails = hostDetails; this.hostUserInterface = new ConsoleServicePSHostUserInterface(); + this.hostSupportsInteractiveSession = hostSupportsInteractiveSession; } #endregion @@ -93,16 +100,6 @@ public override PSHostUserInterface UI get { return this.hostUserInterface; } } - public bool IsRunspacePushed - { - get { return false; } - } - - public Runspace Runspace - { - get; internal set; - } - public override void EnterNestedPrompt() { Logger.Write(LogLevel.Verbose, "EnterNestedPrompt() called."); @@ -129,16 +126,69 @@ public override void SetShouldExit(int exitCode) { this.consoleHost.ExitSession(exitCode); } + + if (this.IsRunspacePushed) + { + this.PopRunspace(); + } + } + + #endregion + + #region IHostSupportsInteractiveSession Implementation + + public bool IsRunspacePushed + { + get + { + if (this.hostSupportsInteractiveSession != null) + { + return this.hostSupportsInteractiveSession.IsRunspacePushed; + } + else + { + throw new NotImplementedException(); + } + } + } + + public Runspace Runspace + { + get + { + if (this.hostSupportsInteractiveSession != null) + { + return this.hostSupportsInteractiveSession.Runspace; + } + else + { + throw new NotImplementedException(); + } + } } public void PushRunspace(Runspace runspace) { - throw new NotImplementedException(); + if (this.hostSupportsInteractiveSession != null) + { + this.hostSupportsInteractiveSession.PushRunspace(runspace); + } + else + { + throw new NotImplementedException(); + } } public void PopRunspace() { - throw new NotImplementedException(); + if (this.hostSupportsInteractiveSession != null) + { + this.hostSupportsInteractiveSession.PopRunspace(); + } + else + { + throw new NotImplementedException(); + } } #endregion diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 8e66f4599..db1affd8f 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -9,6 +9,7 @@ using System.Linq; using System.IO; using System.Text; +using System.Diagnostics; namespace Microsoft.PowerShell.EditorServices { @@ -93,13 +94,23 @@ public ScriptFile GetFile(string filePath) return scriptFile; } + /// + /// Gets a new ScriptFile instance which is identified by the given file path. + /// + /// The file path for which a buffer will be retrieved. + /// A ScriptFile instance if there is a buffer for the path, null otherwise. + public ScriptFile GetFileBuffer(string filePath) + { + return this.GetFileBuffer(filePath, null); + } + /// /// Gets a new ScriptFile instance which is identified by the given file /// path and initially contains the given buffer contents. /// - /// - /// - /// + /// The file path for which a buffer will be retrieved. + /// The initial buffer contents if there is not an existing ScriptFile for this path. + /// A ScriptFile instance for the specified path. public ScriptFile GetFileBuffer(string filePath, string initialBuffer) { Validate.IsNotNullOrEmptyString("filePath", filePath); @@ -110,7 +121,7 @@ public ScriptFile GetFileBuffer(string filePath, string initialBuffer) // Make sure the file isn't already loaded into the workspace ScriptFile scriptFile = null; - if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile)) + if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile) && initialBuffer != null) { scriptFile = new ScriptFile( diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index 844006803..05009ce5d 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -705,7 +705,7 @@ await this.languageServiceClient.SendEvent( [Fact] public async Task ServiceReturnsPowerShellVersionDetails() { - PowerShellVersionResponse versionDetails = + PowerShellVersion versionDetails = await this.SendRequest( PowerShellVersionRequest.Type, new PowerShellVersionRequest()); diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 4e8aa6ec4..51a46bab3 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Debugging; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; @@ -23,8 +24,8 @@ public class DebugServiceTests : IDisposable private PowerShellContext powerShellContext; private SynchronizationContext runnerContext; - private AsyncQueue debuggerStoppedQueue = - new AsyncQueue(); + private AsyncQueue debuggerStoppedQueue = + new AsyncQueue(); private AsyncQueue sessionStateQueue = new AsyncQueue(); @@ -33,7 +34,7 @@ public DebugServiceTests() this.powerShellContext = new PowerShellContext(); this.powerShellContext.SessionStateChanged += powerShellContext_SessionStateChanged; - this.workspace = new Workspace(this.powerShellContext.PowerShellVersion); + this.workspace = new Workspace(this.powerShellContext.LocalPowerShellVersion.Version); // Load the test debug file this.debugScriptFile = @@ -69,7 +70,7 @@ void debugService_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) // TODO: Needed? } - async void debugService_DebuggerStopped(object sender, DebuggerStopEventArgs e) + async void debugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) { await this.debuggerStoppedQueue.EnqueueAsync(e); } @@ -889,13 +890,13 @@ public async Task AssertDebuggerStopped( { SynchronizationContext syncContext = SynchronizationContext.Current; - DebuggerStopEventArgs eventArgs = + DebuggerStoppedEventArgs eventArgs = await this.debuggerStoppedQueue.DequeueAsync(); - Assert.Equal(scriptPath, eventArgs.InvocationInfo.ScriptName); + Assert.Equal(scriptPath, eventArgs.ScriptPath); if (lineNumber > -1) { - Assert.Equal(lineNumber, eventArgs.InvocationInfo.ScriptLineNumber); + Assert.Equal(lineNumber, eventArgs.LineNumber); } } diff --git a/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs b/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs index 3ac6a22f3..8adfe64d1 100644 --- a/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs @@ -183,6 +183,11 @@ public Task OpenFile(string filePath) throw new NotImplementedException(); } + public Task CloseFile(string filePath) + { + throw new NotImplementedException(); + } + public Task InsertText(string filePath, string text, BufferRange insertRange) { throw new NotImplementedException(); diff --git a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs index c303e11a4..b0de1205d 100644 --- a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs @@ -33,7 +33,7 @@ public class LanguageServiceTests : IDisposable public LanguageServiceTests() { this.powerShellContext = new PowerShellContext(); - this.workspace = new Workspace(this.powerShellContext.PowerShellVersion); + this.workspace = new Workspace(this.powerShellContext.LocalPowerShellVersion.Version); this.languageService = new LanguageService(this.powerShellContext); }