diff --git a/.travis.yml b/.travis.yml index 8209d54c8..b8ee9869c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ language: cpp git: depth: 1000 + +env: + # Avoid expensive initialization of dotnet cli, see: https://donovanbrown.com/post/Stop-wasting-time-during-NET-Core-builds + DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 os: - linux diff --git a/module/PowerShellEditorServices/Commands/Public/Out-CurrentFile.ps1 b/module/PowerShellEditorServices/Commands/Public/Out-CurrentFile.ps1 index 5d0b5cf5c..75c4a192d 100644 --- a/module/PowerShellEditorServices/Commands/Public/Out-CurrentFile.ps1 +++ b/module/PowerShellEditorServices/Commands/Public/Out-CurrentFile.ps1 @@ -9,14 +9,32 @@ function Out-CurrentFile { #> [CmdletBinding()] param( - [Parameter(ValueFromPipeline, Mandatory=$true)] + [Switch]$AsNewFile, + + [Parameter(ValueFromPipeline, Mandatory = $true)] $InputObject ) Begin { $objectsToWrite = @() } Process { $objectsToWrite += $InputObject } End { + + # If requested, create a new file + if ($AsNewFile) { + $psEditor.Workspace.NewFile() + } + $outputString = "@`"`r`n{0}`r`n`"@" -f ($objectsToWrite|out-string).Trim() + + try { + # If there is no file open + $psEditor.GetEditorContext() + } + catch { + # create a new one + $psEditor.Workspace.NewFile() + } + $psEditor.GetEditorContext().CurrentFile.InsertText($outputString) } } diff --git a/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs index 619ca0a49..e814918ab 100644 --- a/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs +++ b/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs @@ -41,9 +41,7 @@ public PesterCodeLensProvider(EditorSession editorSession) /// The Pester symbol to get CodeLenses for. /// The script file the Pester symbol comes from. /// All CodeLenses for the given Pester symbol. - private CodeLens[] GetPesterLens( - PesterSymbolReference pesterSymbol, - ScriptFile scriptFile) + private CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, ScriptFile scriptFile) { var codeLensResults = new CodeLens[] { @@ -54,7 +52,11 @@ private CodeLens[] GetPesterLens( new ClientCommand( "PowerShell.RunPesterTests", "Run tests", - new object[] { scriptFile.ClientFilePath, false /* No debug */, pesterSymbol.TestName })), + new object[] { + scriptFile.ClientFilePath, + false /* No debug */, + pesterSymbol.TestName, + pesterSymbol.ScriptRegion?.StartLineNumber })), new CodeLens( this, @@ -63,7 +65,11 @@ private CodeLens[] GetPesterLens( new ClientCommand( "PowerShell.RunPesterTests", "Debug tests", - new object[] { scriptFile.ClientFilePath, true /* Run in debugger */, pesterSymbol.TestName })), + new object[] { + scriptFile.ClientFilePath, + true /* Run in the debugger */, + pesterSymbol.TestName, + pesterSymbol.ScriptRegion?.StartLineNumber })), }; return codeLensResults; @@ -99,9 +105,7 @@ public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) /// The code lens to resolve. /// /// The given CodeLens, wrapped in a task. - public Task ResolveCodeLensAsync( - CodeLens codeLens, - CancellationToken cancellationToken) + public Task ResolveCodeLensAsync(CodeLens codeLens, CancellationToken cancellationToken) { // This provider has no specific behavior for // resolving CodeLenses. diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs index e9734e002..185a1aac0 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs @@ -20,6 +20,8 @@ public class AttachRequestArguments public string ProcessId { get; set; } - public int RunspaceId { get; set; } + public string RunspaceId { get; set; } + + public string CustomPipeName { get; set; } } } diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/GetRunspaceRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/GetRunspaceRequest.cs new file mode 100644 index 000000000..e151aa7f0 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/GetRunspaceRequest.cs @@ -0,0 +1,25 @@ +// +// 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.LanguageServer +{ + public class GetRunspaceRequest + { + public static readonly + RequestType Type = + RequestType.Create("powerShell/getRunspace"); + } + + public class GetRunspaceResponse + { + public int Id { get; set; } + + public string Name { get; set; } + + public string Availability { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index a43f5a7d3..c3e203829 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -23,6 +23,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.Server { public class DebugAdapter { + private static readonly Version _minVersionForCustomPipeName = new Version(6, 2); + private EditorSession _editorSession; private bool _noDebug; @@ -344,11 +346,17 @@ protected async Task HandleAttachRequest( RegisterEventHandlers(); + bool processIdIsSet = !string.IsNullOrEmpty(attachParams.ProcessId) && attachParams.ProcessId != "undefined"; + bool customPipeNameIsSet = !string.IsNullOrEmpty(attachParams.CustomPipeName) && attachParams.CustomPipeName != "undefined"; + + PowerShellVersionDetails runspaceVersion = + _editorSession.PowerShellContext.CurrentRunspace.PowerShellVersion; + // If there are no host processes to attach to or the user cancels selection, we get a null for the process id. // This is not an error, just a request to stop the original "attach to" request. // Testing against "undefined" is a HACK because I don't know how to make "Cancel" on quick pick loading // to cancel on the VSCode side without sending an attachRequest with processId set to "undefined". - if (string.IsNullOrEmpty(attachParams.ProcessId) || (attachParams.ProcessId == "undefined")) + if (!processIdIsSet && !customPipeNameIsSet) { Logger.Write( LogLevel.Normal, @@ -364,9 +372,6 @@ await requestContext.SendError( if (attachParams.ComputerName != null) { - PowerShellVersionDetails runspaceVersion = - _editorSession.PowerShellContext.CurrentRunspace.PowerShellVersion; - if (runspaceVersion.Version.Major < 4) { await requestContext.SendError( @@ -397,16 +402,12 @@ await requestContext.SendError( _isRemoteAttach = true; } - if (int.TryParse(attachParams.ProcessId, out int processId) && (processId > 0)) + if (processIdIsSet && int.TryParse(attachParams.ProcessId, out int processId) && (processId > 0)) { - PowerShellVersionDetails runspaceVersion = - _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; } @@ -421,22 +422,29 @@ await requestContext.SendError( return; } + } + else if (customPipeNameIsSet) + { + if (runspaceVersion.Version < _minVersionForCustomPipeName) + { + await requestContext.SendError( + $"Attaching to a process with CustomPipeName is only available with PowerShell 6.2 and higher (current session is {runspaceVersion.Version})."); + return; + } - // Clear any existing breakpoints before proceeding - await ClearSessionBreakpoints(); - - // 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; - _waitingForAttach = true; - Task nonAwaitedTask = - _editorSession.PowerShellContext - .ExecuteScriptString($"\nDebug-Runspace -Id {runspaceId}") - .ContinueWith(OnExecutionCompleted); + await _editorSession.PowerShellContext.ExecuteScriptString( + $"Enter-PSHostProcess -CustomPipeName {attachParams.CustomPipeName}", + errorMessages); + + if (errorMessages.Length > 0) + { + await requestContext.SendError( + $"Could not attach to process with CustomPipeName: '{attachParams.CustomPipeName}'"); + + return; + } } - else + else if (attachParams.ProcessId != "current") { Logger.Write( LogLevel.Error, @@ -448,6 +456,32 @@ await requestContext.SendError( return; } + // Clear any existing breakpoints before proceeding + await ClearSessionBreakpoints().ConfigureAwait(continueOnCapturedContext: false); + + // 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. + + var runspaceId = 1; + if (!int.TryParse(attachParams.RunspaceId, out runspaceId) || runspaceId <= 0) + { + Logger.Write( + LogLevel.Error, + $"Attach request failed, '{attachParams.RunspaceId}' is an invalid value for the processId."); + + await requestContext.SendError( + "A positive integer must be specified for the RunspaceId field."); + + return; + } + + _waitingForAttach = true; + Task nonAwaitedTask = _editorSession.PowerShellContext + .ExecuteScriptString($"\nDebug-Runspace -Id {runspaceId}") + .ContinueWith(OnExecutionCompleted); + await requestContext.SendResult(null); } diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 0432fa2ee..de4fc99eb 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -18,6 +18,7 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; +using System.Management.Automation.Runspaces; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -162,6 +163,8 @@ public void Start() this.messageHandlers.SetRequestHandler(GetPSHostProcessesRequest.Type, this.HandleGetPSHostProcessesRequest); this.messageHandlers.SetRequestHandler(CommentHelpRequest.Type, this.HandleCommentHelpRequest); + this.messageHandlers.SetRequestHandler(GetRunspaceRequest.Type, this.HandleGetRunspaceRequestAsync); + // Initialize the extension service // TODO: This should be made awaited once Initialize is async! this.editorSession.ExtensionService.Initialize( @@ -1231,6 +1234,54 @@ protected async Task HandleCommentHelpRequest( await requestContext.SendResult(result); } + protected async Task HandleGetRunspaceRequestAsync( + string processId, + RequestContext requestContext) + { + var runspaceResponses = new List(); + + if (this.editorSession.PowerShellContext.LocalPowerShellVersion.Version.Major >= 5) + { + if (processId == null) { + processId = "current"; + } + + var isNotCurrentProcess = processId != null && processId != "current"; + + var psCommand = new PSCommand(); + + if (isNotCurrentProcess) { + psCommand.AddCommand("Enter-PSHostProcess").AddParameter("Id", processId).AddStatement(); + } + + psCommand.AddCommand("Get-Runspace"); + + StringBuilder sb = new StringBuilder(); + IEnumerable runspaces = await editorSession.PowerShellContext.ExecuteCommand(psCommand, sb); + if (runspaces != null) + { + foreach (var p in runspaces) + { + runspaceResponses.Add( + new GetRunspaceResponse + { + Id = p.Id, + Name = p.Name, + Availability = p.RunspaceAvailability.ToString() + }); + } + } + + if (isNotCurrentProcess) { + var exitCommand = new PSCommand(); + exitCommand.AddCommand("Exit-PSHostProcess"); + await editorSession.PowerShellContext.ExecuteCommand(exitCommand); + } + } + + await requestContext.SendResult(runspaceResponses.ToArray()); + } + private bool IsQueryMatch(string query, string symbolName) { return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; diff --git a/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs b/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs index 4f68f02ad..e217457fc 100644 --- a/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs +++ b/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs @@ -107,12 +107,14 @@ static internal StackFrameDetails Create( string scriptPath = (callStackFrameObject.Properties["ScriptName"].Value as string) ?? NoFileScriptPath; int startLineNumber = (int)(callStackFrameObject.Properties["ScriptLineNumber"].Value ?? 0); - if (workspaceRootPath != null && - invocationInfo != null && - !scriptPath.StartsWith(workspaceRootPath, StringComparison.OrdinalIgnoreCase)) - { - isExternal = true; - } + // TODO: RKH 2019-03-07 Temporarily disable "external" code until I have a chance to add + // settings to control this feature. + //if (workspaceRootPath != null && + // invocationInfo != null && + // !scriptPath.StartsWith(workspaceRootPath, StringComparison.OrdinalIgnoreCase)) + //{ + // isExternal = true; + //} return new StackFrameDetails { diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 6f9cb8beb..d81f9f5e7 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -148,11 +148,12 @@ public bool TryGetFile(string filePath, out ScriptFile scriptFile) return true; } catch (Exception e) when ( - e is IOException || - e is SecurityException || + e is NotSupportedException || e is FileNotFoundException || e is DirectoryNotFoundException || e is PathTooLongException || + e is IOException || + e is SecurityException || e is UnauthorizedAccessException) { this.logger.WriteHandledException($"Failed to get file for {nameof(filePath)}: '{filePath}'", e);