diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index 007b75d49..578cf1f70 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -18,6 +18,154 @@ namespace Microsoft.PowerShell.EditorServices.Services { internal class BreakpointService { + private const string _getPSBreakpointLegacy = @" + [CmdletBinding()] + param ( + [Parameter()] + [string] + $Script, + + [Parameter()] + [int] + $RunspaceId = [Runspace]::DefaultRunspace.Id + ) + + $runspace = if ($PSBoundParameters.ContainsKey('RunspaceId')) { + Get-Runspace -Id $RunspaceId + $null = $PSBoundParameters.Remove('RunspaceId') + } + else { + [Runspace]::DefaultRunspace + } + + $debugger = $runspace.Debugger + $getBreakpointsMeth = $debugger.GetType().GetMethod( + 'GetBreakpoints', + [System.Reflection.BindingFlags]'NonPublic, Public, Instance', + $null, + [type[]]@(), + $null) + + $runspaceIdProp = [System.Management.Automation.PSNoteProperty]::new( + 'RunspaceId', + $runspaceId) + + @( + if (-not $getBreakpointsMeth) { + if ($RunspaceId -ne [Runspace]::DefaultRunspace.Id) { + throw 'Failed to find GetBreakpoints method on Debugger.' + } + + Microsoft.PowerShell.Utility\Get-PSBreakpoint @PSBoundParameters + } + else { + $getBreakpointsMeth.Invoke($debugger, @()) | Where-Object { + if ($Script) { + $_.Script -eq $Script + } + else { + $true + } + } + } + ) | ForEach-Object { + $_.PSObject.Properties.Add($runspaceIdProp) + $_ + } + "; + + private const string _removePSBreakpointLegacy = @" + [CmdletBinding(DefaultParameterSetName = 'Breakpoint')] + param ( + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Breakpoint')] + [System.Management.Automation.Breakpoint[]] + $Breakpoint, + + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Id')] + [int[]] + $Id, + + [Parameter(ParameterSetName = 'Id')] + [int] + $RunspaceId = [Runspace]::DefaultRunspace.Id + ) + + begin { + $removeBreakpointMeth = [Runspace]::DefaultRunspace.Debugger.GetType().GetMethod( + 'RemoveBreakpoint', + [System.Reflection.BindingFlags]'NonPublic, Public, Instance', + $null, + [type[]]@([System.Management.Automation.Breakpoint]), + $null) + $getBreakpointMeth = [Runspace]::DefaultRunspace.Debugger.GetType().GetMethod( + 'GetBreakpoint', + [System.Reflection.BindingFlags]'NonPublic, Public, Instance', + $null, + [type[]]@([int]), + $null) + + $breakpointCollection = [System.Collections.Generic.List[System.Management.Automation.Breakpoint]]::new() + } + + process { + if ($PSCmdlet.ParameterSetName -eq 'Id') { + $runspace = Get-Runspace -Id $RunspaceId + $runspaceProp = [System.Management.Automation.PSNoteProperty]::new( + 'Runspace', + $Runspace) + + $breakpoints = if ($getBreakpointMeth) { + foreach ($breakpointId in $Id) { + $getBreakpointMeth.Invoke($runspace.Debugger, @($breakpointId)) + } + } + elseif ($runspace -eq [Runspace]::DefaultRunspace) { + Microsoft.PowerShell.Utility\Get-PSBreakpoint -Id $Id + } + else { + throw 'Failed to find GetBreakpoint method on Debugger.' + } + + $breakpoints | ForEach-Object { + $_.PSObject.Properties.Add($runspaceProp) + $breakpointCollection.Add($_) + } + } + else { + foreach ($b in $Breakpoint) { + # RunspaceId may be set by _getPSBreakpointLegacy when + # targeting a breakpoint in a specific runspace. + $runspace = if ($b.PSObject.Properties.Match('RunspaceId')) { + Get-Runspace -Id $b.RunspaceId + } + else { + [Runspace]::DefaultRunspace + } + + $b.PSObject.Properties.Add( + [System.Management.Automation.PSNoteProperty]::new('Runspace', $runspace)) + $breakpointCollection.Add($b) + } + } + } + + end { + foreach ($b in $breakpointCollection) { + if ($removeBreakpointMeth) { + $removeBreakpointMeth.Invoke($b.Runspace.Debugger, @($b)) + } + elseif ($b.Runspace -eq [Runspace]::DefaultRunspace) { + # If we don't have the method, we can only remove breakpoints + # from the default runspace using Remove-PSBreakpoint. + $b | Microsoft.PowerShell.Utility\Remove-PSBreakpoint + } + else { + throw 'Failed to find RemoveBreakpoint method on Debugger.' + } + } + } + "; + /// /// Code used on WinPS 5.1 to set breakpoints without Script path validation. /// It uses reflection because the APIs were not public until 7.0 but just in @@ -45,7 +193,11 @@ internal class BreakpointService [Parameter(ParameterSetName = 'Command', Mandatory = $true)] [string] - $Command + $Command, + + [Parameter()] + [int] + $RunspaceId ) if ($Script) { @@ -65,6 +217,9 @@ internal class BreakpointService $null) if (-not $cmdCtor) { + if ($PSBoundParameters.ContainsKey('RunspaceId')) { + throw 'Failed to find constructor for CommandBreakpoint.' + } Microsoft.PowerShell.Utility\Set-PSBreakpoint @PSBoundParameters return } @@ -82,6 +237,9 @@ internal class BreakpointService $null) if (-not $lineCtor) { + if ($PSBoundParameters.ContainsKey('RunspaceId')) { + throw 'Failed to find constructor for LineBreakpoint.' + } Microsoft.PowerShell.Utility\Set-PSBreakpoint @PSBoundParameters return } @@ -89,8 +247,14 @@ internal class BreakpointService $b = $lineCtor.Invoke(@($Script, $Line, $Column, $Action)) } - [Runspace]::DefaultRunspace.Debugger.SetBreakpoints( - [System.Management.Automation.Breakpoint[]]@($b)) + $runspace = if ($PSBoundParameters.ContainsKey('RunspaceId')) { + Get-Runspace -Id $RunspaceId + } + else { + [Runspace]::DefaultRunspace + } + + $runspace.Debugger.SetBreakpoints([System.Management.Automation.Breakpoint[]]@($b)) $b "; @@ -128,10 +292,14 @@ public async Task> GetBreakpointsAsync() } // Legacy behavior - PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); + PSCommand psCommand = new PSCommand().AddScript(_getPSBreakpointLegacy, useLocalScope: true); + if (_debugStateService.RunspaceId is not null) + { + psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value); + } return await _executionService - .ExecutePSCommandAsync(psCommand, CancellationToken.None) - .ConfigureAwait(false); + .ExecutePSCommandAsync(psCommand, CancellationToken.None) + .ConfigureAwait(false); } public async Task> SetBreakpointsAsync(IReadOnlyList breakpoints) @@ -195,7 +363,7 @@ public async Task> SetBreakpointsAsync(IReadOnl // path which may or may not exist. psCommand .AddScript(_setPSBreakpointLegacy, useLocalScope: true) - .AddParameter("Script", breakpoint.Source) + .AddParameter("Script", breakpoint.MappedSource ?? breakpoint.Source) .AddParameter("Line", breakpoint.LineNumber); // Check if the user has specified the column number for the breakpoint. @@ -211,6 +379,11 @@ public async Task> SetBreakpointsAsync(IReadOnl { psCommand.AddParameter("Action", actionScriptBlock); } + + if (_debugStateService.RunspaceId is not null) + { + psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value); + } } // If no PSCommand was created then there are no breakpoints to set. @@ -219,7 +392,16 @@ public async Task> SetBreakpointsAsync(IReadOnl IEnumerable setBreakpoints = await _executionService .ExecutePSCommandAsync(psCommand, CancellationToken.None) .ConfigureAwait(false); - configuredBreakpoints.AddRange(setBreakpoints.Select((breakpoint) => BreakpointDetails.Create(breakpoint))); + + int bpIdx = 0; + foreach (Breakpoint setBp in setBreakpoints) + { + BreakpointDetails setBreakpoint = BreakpointDetails.Create( + setBp, + sourceBreakpoint: breakpoints[bpIdx]); + configuredBreakpoints.Add(setBreakpoint); + bpIdx++; + } } return configuredBreakpoints; } @@ -326,14 +508,17 @@ public async Task RemoveAllBreakpointsAsync(string scriptPath = null) } // Legacy behavior - PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); - + PSCommand psCommand = new PSCommand().AddScript(_getPSBreakpointLegacy, useLocalScope: true); + if (_debugStateService.RunspaceId is not null) + { + psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value); + } if (!string.IsNullOrEmpty(scriptPath)) { psCommand.AddParameter("Script", scriptPath); } - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); + psCommand.AddScript(_removePSBreakpointLegacy, useLocalScope: true); await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); } catch (Exception e) @@ -369,8 +554,12 @@ public async Task RemoveBreakpointsAsync(IEnumerable breakpoints) if (breakpointIds.Any()) { PSCommand psCommand = new PSCommand() - .AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint") + .AddScript(_removePSBreakpointLegacy, useLocalScope: true) .AddParameter("Id", breakpoints.Select(b => b.Id).ToArray()); + if (_debugStateService.RunspaceId is not null) + { + psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value); + } await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs index e3237a3b0..c887b02ac 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs @@ -94,17 +94,6 @@ private void OnRunspaceChanged(object sender, RunspaceChangedEventArgs e) { switch (e.ChangeAction) { - case RunspaceChangeAction.Enter: - if (_debugStateService.WaitingForAttach - && e.NewRunspace.RunspaceOrigin == RunspaceOrigin.DebuggedRunspace) - { - // Sends the InitializedEvent so that the debugger will continue - // sending configuration requests - _debugStateService.WaitingForAttach = false; - _debugStateService.ServerStarted.TrySetResult(true); - } - return; - case RunspaceChangeAction.Exit: if (_debugContext.IsStopped) { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index fd7090df4..645e8a858 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -15,7 +15,6 @@ using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Services @@ -49,6 +48,7 @@ internal class DebugService private VariableContainerDetails scriptScopeVariables; private VariableContainerDetails localScopeVariables; private StackFrameDetails[] stackFrameDetails; + private PathMapping[] _pathMappings; private readonly SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore(); #endregion @@ -123,22 +123,22 @@ public DebugService( /// /// Sets the list of line breakpoints for the current debugging session. /// - /// The ScriptFile in which breakpoints will be set. + /// The path in which breakpoints will be set. /// BreakpointDetails for each breakpoint that will be set. /// If true, causes all existing breakpoints to be cleared before setting new ones. + /// If true, skips the remote file manager mapping of the script path. /// An awaitable Task that will provide details about the breakpoints that were set. public async Task> SetLineBreakpointsAsync( - ScriptFile scriptFile, + string scriptPath, IReadOnlyList breakpoints, - bool clearExisting = true) + bool clearExisting = true, + bool skipRemoteMapping = false) { DscBreakpointCapability dscBreakpoints = await _debugContext.GetDscBreakpointCapabilityAsync().ConfigureAwait(false); - string scriptPath = scriptFile.FilePath; - _psesHost.Runspace.ThrowCancelledIfUnusable(); // Make sure we're using the remote script path - if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null) + if (!skipRemoteMapping && _psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null) { if (!_remoteFileManager.IsUnderRemoteTempPath(scriptPath)) { @@ -162,7 +162,7 @@ public async Task> SetLineBreakpointsAsync( { if (clearExisting) { - await _breakpointService.RemoveAllBreakpointsAsync(scriptFile.FilePath).ConfigureAwait(false); + await _breakpointService.RemoveAllBreakpointsAsync(scriptPath).ConfigureAwait(false); } return await _breakpointService.SetBreakpointsAsync(breakpoints).ConfigureAwait(false); @@ -603,6 +603,59 @@ public VariableScope[] GetVariableScopes(int stackFrameId) }; } + internal void SetPathMappings(PathMapping[] pathMappings) => _pathMappings = pathMappings; + + internal void UnsetPathMappings() => _pathMappings = null; + + internal bool TryGetMappedLocalPath(string remotePath, out string localPath) + { + if (_pathMappings is not null) + { + foreach (PathMapping mapping in _pathMappings) + { + if (string.IsNullOrWhiteSpace(mapping.LocalRoot) || string.IsNullOrWhiteSpace(mapping.RemoteRoot)) + { + // If either path mapping is null, we can't map the path. + continue; + } + + if (remotePath.StartsWith(mapping.RemoteRoot, StringComparison.OrdinalIgnoreCase)) + { + localPath = mapping.LocalRoot + remotePath.Substring(mapping.RemoteRoot.Length); + return true; + } + } + } + + localPath = null; + return false; + } + + internal bool TryGetMappedRemotePath(string localPath, out string remotePath) + { + if (_pathMappings is not null) + { + foreach (PathMapping mapping in _pathMappings) + { + if (string.IsNullOrWhiteSpace(mapping.LocalRoot) || string.IsNullOrWhiteSpace(mapping.RemoteRoot)) + { + // If either path mapping is null, we can't map the path. + continue; + } + + if (localPath.StartsWith(mapping.LocalRoot, StringComparison.OrdinalIgnoreCase)) + { + // If the local path starts with the local path mapping, we can replace it with the remote path. + remotePath = mapping.RemoteRoot + localPath.Substring(mapping.LocalRoot.Length); + return true; + } + } + } + + remotePath = null; + return false; + } + #endregion #region Private Methods @@ -873,14 +926,19 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) StackFrameDetails stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, commandVariables); string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath; - if (scriptNameOverride is not null - && string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) + bool isNoScriptPath = string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath); + if (scriptNameOverride is not null && isNoScriptPath) { stackFrameDetailsEntry.ScriptPath = scriptNameOverride; } + else if (TryGetMappedLocalPath(stackFrameScriptPath, out string localMappedPath) + && !isNoScriptPath) + { + stackFrameDetailsEntry.ScriptPath = localMappedPath; + } else if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null - && !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) + && !isNoScriptPath) { stackFrameDetailsEntry.ScriptPath = _remoteFileManager.GetMappedPath(stackFrameScriptPath, _psesHost.CurrentRunspace); @@ -981,9 +1039,13 @@ await _executionService.ExecutePSCommandAsync( // Begin call stack and variables fetch. We don't need to block here. StackFramesAndVariablesFetched = FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null); + if (!noScriptName && TryGetMappedLocalPath(e.InvocationInfo.ScriptName, out string mappedLocalPath)) + { + localScriptPath = mappedLocalPath; + } // If this is a remote connection and the debugger stopped at a line // in a script file, get the file contents - if (_psesHost.CurrentRunspace.IsOnRemoteMachine + else if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null && !noScriptName) { @@ -1034,8 +1096,12 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) { // TODO: This could be either a path or a script block! string scriptPath = lineBreakpoint.Script; - if (_psesHost.CurrentRunspace.IsOnRemoteMachine - && _remoteFileManager is not null) + if (TryGetMappedLocalPath(scriptPath, out string mappedLocalPath)) + { + scriptPath = mappedLocalPath; + } + else if (_psesHost.CurrentRunspace.IsOnRemoteMachine + && _remoteFileManager is not null) { string mappedPath = _remoteFileManager.GetMappedPath(scriptPath, _psesHost.CurrentRunspace); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs index 9736b3e85..164ea44ca 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs @@ -4,27 +4,24 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.JsonRpc; namespace Microsoft.PowerShell.EditorServices.Services { internal class DebugStateService { private readonly SemaphoreSlim _setBreakpointInProgressHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + private readonly SemaphoreSlim _inLaunchOrAttachHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + private TaskCompletionSource _waitForConfigDone; internal bool NoDebug { get; set; } - internal string[] Arguments { get; set; } - internal bool IsRemoteAttach { get; set; } internal int? RunspaceId { get; set; } internal bool IsAttachSession { get; set; } - internal bool WaitingForAttach { get; set; } - - internal string ScriptToLaunch { get; set; } - internal bool ExecutionCompleted { get; set; } internal bool IsInteractiveDebugSession { get; set; } @@ -34,13 +31,79 @@ internal class DebugStateService internal bool IsUsingTempIntegratedConsole { get; set; } - internal string ExecuteMode { get; set; } - // This gets set at the end of the Launch/Attach handler which set debug state. internal TaskCompletionSource ServerStarted { get; set; } internal int ReleaseSetBreakpointHandle() => _setBreakpointInProgressHandle.Release(); internal Task WaitForSetBreakpointHandleAsync() => _setBreakpointInProgressHandle.WaitAsync(); + + /// + /// Sends the InitializedEvent and waits for the configuration done + /// event to be sent by the client. + /// + /// The action being performed, either "attach" or "launch". + /// A cancellation token to cancel the operation. + /// A launch or attach request is already in progress + internal async Task WaitForConfigurationDoneAsync( + string action, + CancellationToken cancellationToken) + { + Task waitForConfigDone; + + // First check we are not already running a launch or attach request. + await _inLaunchOrAttachHandle.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_waitForConfigDone is not null) + { + // If we are already waiting for a configuration done, then we cannot start another + // launch or attach request. + throw new RpcErrorException(0, null, $"Cannot start a new {action} request when one is already in progress."); + } + + _waitForConfigDone = new TaskCompletionSource(); + waitForConfigDone = _waitForConfigDone.Task; + } + finally + { + _inLaunchOrAttachHandle.Release(); + } + + using CancellationTokenRegistration _ = cancellationToken.Register(_waitForConfigDone.SetCanceled); + + // Sends the InitializedEvent so that the debugger will continue + // sending configuration requests before the final configuration + // done. + ServerStarted.TrySetResult(true); + await waitForConfigDone.ConfigureAwait(false); + } + + /// + /// Sets the configuration done task to complete, indicating that the + /// client has sent all the initial configuration information and the + /// debugger is ready to start. + /// + /// A cancellation token to cancel the operation. + internal async Task SetConfigurationDoneAsync( + CancellationToken cancellationToken) + { + await _inLaunchOrAttachHandle.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_waitForConfigDone is null) + { + // If we are not waiting for a configuration done, then we cannot set it. + throw new RpcErrorException(0, null, "Unexpected configuration done request when server is not expecting it."); + } + + _waitForConfigDone.TrySetResult(true); + _waitForConfigDone = null; + } + finally + { + _inLaunchOrAttachHandle.Release(); + } + } } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs index 7884fbda5..ebb0646d2 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs @@ -136,7 +136,7 @@ public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase { BreakpointDetails lineBreakpoint => SetLineBreakpointDelegate( debugger, - lineBreakpoint.Source, + lineBreakpoint.MappedSource ?? lineBreakpoint.Source, lineBreakpoint.LineNumber, lineBreakpoint.ColumnNumber ?? 0, actionScriptBlock, diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs index 0a1c268b9..4177b3816 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs @@ -24,6 +24,11 @@ internal sealed class BreakpointDetails : BreakpointDetailsBase /// public string Source { get; private set; } + /// + /// Gets the source where the breakpoint is mapped to, will be null if no mapping exists. Used only for debug purposes. + /// + public string MappedSource { get; private set; } + /// /// Gets the line number at which the breakpoint is set. /// @@ -50,6 +55,7 @@ private BreakpointDetails() /// /// /// + /// /// internal static BreakpointDetails Create( string source, @@ -57,7 +63,8 @@ internal static BreakpointDetails Create( int? column = null, string condition = null, string hitCondition = null, - string logMessage = null) + string logMessage = null, + string mappedSource = null) { Validate.IsNotNullOrEmptyString(nameof(source), source); @@ -69,7 +76,8 @@ internal static BreakpointDetails Create( ColumnNumber = column, Condition = condition, HitCondition = hitCondition, - LogMessage = logMessage + LogMessage = logMessage, + MappedSource = mappedSource }; } @@ -79,10 +87,12 @@ internal static BreakpointDetails Create( /// /// The Breakpoint instance from which details will be taken. /// The BreakpointUpdateType to determine if the breakpoint is verified. + /// /// The breakpoint source from the debug client, if any. /// A new instance of the BreakpointDetails class. internal static BreakpointDetails Create( Breakpoint breakpoint, - BreakpointUpdateType updateType = BreakpointUpdateType.Set) + BreakpointUpdateType updateType = BreakpointUpdateType.Set, + BreakpointDetails sourceBreakpoint = null) { Validate.IsNotNull(nameof(breakpoint), breakpoint); @@ -96,10 +106,11 @@ internal static BreakpointDetails Create( { Id = breakpoint.Id, Verified = updateType != BreakpointUpdateType.Disabled, - Source = lineBreakpoint.Script, + Source = sourceBreakpoint?.MappedSource is not null ? sourceBreakpoint.Source : lineBreakpoint.Script, LineNumber = lineBreakpoint.Line, ColumnNumber = lineBreakpoint.Column, - Condition = lineBreakpoint.Action?.ToString() + Condition = lineBreakpoint.Action?.ToString(), + MappedSource = sourceBreakpoint?.MappedSource, }; if (lineBreakpoint.Column > 0) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs index 68d23c966..1c26c48de 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs @@ -79,6 +79,11 @@ public async Task Handle(SetBreakpointsArguments request } // At this point, the source file has been verified as a PowerShell script. + string mappedSource = null; + if (_debugService.TryGetMappedRemotePath(scriptFile.FilePath, out string remoteMappedPath)) + { + mappedSource = remoteMappedPath; + } IReadOnlyList breakpointDetails = request.Breakpoints .Select((srcBreakpoint) => BreakpointDetails.Create( scriptFile.FilePath, @@ -86,7 +91,8 @@ public async Task Handle(SetBreakpointsArguments request srcBreakpoint.Column, srcBreakpoint.Condition, srcBreakpoint.HitCondition, - srcBreakpoint.LogMessage)).ToList(); + srcBreakpoint.LogMessage, + mappedSource: mappedSource)).ToList(); // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. IReadOnlyList updatedBreakpointDetails = breakpointDetails; @@ -98,8 +104,9 @@ public async Task Handle(SetBreakpointsArguments request { updatedBreakpointDetails = await _debugService.SetLineBreakpointsAsync( - scriptFile, - breakpointDetails).ConfigureAwait(false); + mappedSource ?? scriptFile.FilePath, + breakpointDetails, + skipRemoteMapping: mappedSource is not null).ConfigureAwait(false); } catch (Exception e) { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index 146bbeae0..747b2d2c0 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -1,161 +1,35 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Management.Automation; -using System.Management.Automation.Language; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShell; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Utility; -using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -using OmniSharp.Extensions.DebugAdapter.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.Handlers { internal class ConfigurationDoneHandler : IConfigurationDoneHandler { - // TODO: We currently set `WriteInputToHost` as true, which writes our debugged commands' - // `GetInvocationText` and that reveals some obscure implementation details we should - // instead hide from the user with pretty strings (or perhaps not write out at all). - // - // This API is mostly used for F5 execution so it requires the foreground. - private static readonly PowerShellExecutionOptions s_debuggerExecutionOptions = new() - { - RequiresForeground = true, - WriteInputToHost = true, - WriteOutputToHost = true, - ThrowOnError = false, - AddToHistory = true, - }; - - private readonly ILogger _logger; - private readonly IDebugAdapterServerFacade _debugAdapterServer; private readonly DebugService _debugService; private readonly DebugStateService _debugStateService; - private readonly DebugEventHandlerService _debugEventHandlerService; - private readonly IInternalPowerShellExecutionService _executionService; - private readonly WorkspaceService _workspaceService; - private readonly IPowerShellDebugContext _debugContext; - // TODO: Decrease these arguments since they're a bunch of interfaces that can be simplified - // (i.e., `IRunspaceContext` should just be available on `IPowerShellExecutionService`). public ConfigurationDoneHandler( - ILoggerFactory loggerFactory, - IDebugAdapterServerFacade debugAdapterServer, DebugService debugService, - DebugStateService debugStateService, - DebugEventHandlerService debugEventHandlerService, - IInternalPowerShellExecutionService executionService, - WorkspaceService workspaceService, - IPowerShellDebugContext debugContext) + DebugStateService debugStateService) { - _logger = loggerFactory.CreateLogger(); - _debugAdapterServer = debugAdapterServer; _debugService = debugService; _debugStateService = debugStateService; - _debugEventHandlerService = debugEventHandlerService; - _executionService = executionService; - _workspaceService = workspaceService; - _debugContext = debugContext; } - public Task Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken) + public async Task Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken) { _debugService.IsClientAttached = true; - if (!string.IsNullOrEmpty(_debugStateService.ScriptToLaunch)) - { - // NOTE: This is an unawaited task because responding to "configuration done" means - // setting up the debugger, and in our case that means starting the script but not - // waiting for it to finish. - Task _ = LaunchScriptAsync(_debugStateService.ScriptToLaunch).HandleErrorsAsync(_logger); - } - - if (_debugStateService.IsInteractiveDebugSession && _debugService.IsDebuggerStopped) - { - if (_debugService.CurrentDebuggerStoppedEventArgs is not null) - { - // If this is an interactive session and there's a pending breakpoint, send that - // information along to the debugger client. - _debugEventHandlerService.TriggerDebuggerStopped(_debugService.CurrentDebuggerStoppedEventArgs); - } - else - { - // If this is an interactive session and there's a pending breakpoint that has - // not been propagated through the debug service, fire the debug service's - // OnDebuggerStop event. - _debugService.OnDebuggerStopAsync(null, _debugContext.LastStopEventArgs); - } - } - - return Task.FromResult(new ConfigurationDoneResponse()); - } - - // NOTE: We test this function in `DebugServiceTests` so it both needs to be internal, and - // use conditional-access on `_debugStateService` and `_debugAdapterServer` as its not set - // by tests. - internal async Task LaunchScriptAsync(string scriptToLaunch) - { - PSCommand command; - if (System.IO.File.Exists(scriptToLaunch)) - { - // For a saved file we just execute its path (after escaping it), with the configured operator - // (which can't be called that because it's a reserved keyword in C#). - string executeMode = _debugStateService?.ExecuteMode == "Call" ? "&" : "."; - command = PSCommandHelpers.BuildDotSourceCommandWithArguments( - PSCommandHelpers.EscapeScriptFilePath(scriptToLaunch), _debugStateService?.Arguments, executeMode); - } - else // It's a URI to an untitled script, or a raw script. - { - bool isScriptFile = _workspaceService.TryGetFile(scriptToLaunch, out ScriptFile untitledScript); - if (isScriptFile) - { - // Parse untitled files with their `Untitled:` URI as the filename which will - // cache the URI and contents within the PowerShell parser. By doing this, we - // light up the ability to debug untitled files with line breakpoints. - ScriptBlockAst ast = Parser.ParseInput( - untitledScript.Contents, - untitledScript.DocumentUri.ToString(), - out Token[] _, - out ParseError[] _); - - // In order to use utilize the parser's cache (and therefore hit line - // breakpoints) we need to use the AST's `ScriptBlock` object. Due to - // limitations in PowerShell's public API, this means we must use the - // `PSCommand.AddArgument(object)` method, hence this hack where we dot-source - // `$args[0]. Fortunately the dot-source operator maintains a stack of arguments - // on each invocation, so passing the user's arguments directly in the initial - // `AddScript` surprisingly works. - command = PSCommandHelpers - .BuildDotSourceCommandWithArguments("$args[0]", _debugStateService?.Arguments) - .AddArgument(ast.GetScriptBlock()); - } - else - { - // Without the new APIs we can only execute the untitled script's contents. - // Command breakpoints and `Wait-Debugger` will work. We must wrap the script - // with newlines so that any included comments don't break the command. - command = PSCommandHelpers.BuildDotSourceCommandWithArguments( - string.Concat( - "{" + System.Environment.NewLine, - isScriptFile ? untitledScript.Contents : scriptToLaunch, - System.Environment.NewLine + "}"), - _debugStateService?.Arguments); - } - } - - await _executionService.ExecutePSCommandAsync( - command, - CancellationToken.None, - s_debuggerExecutionOptions).ConfigureAwait(false); + // Tells the attach/launch request handler that the config is done + // and it can continue starting the script. + await _debugStateService.SetConfigurationDoneAsync(cancellationToken).ConfigureAwait(false); - _debugAdapterServer?.SendNotification(EventNames.Terminated); + return new ConfigurationDoneResponse(); } } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs index 798ccc621..7ca10ffce 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs @@ -50,6 +50,7 @@ public async Task Handle(DisconnectArguments request, Cancel // We should instead ensure that the debugger is in some valid state, lock it and then tear things down _debugEventHandlerService.UnregisterEventHandlers(); + _debugService.UnsetPathMappings(); if (!_debugStateService.ExecutionCompleted) { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index 4201cc5e6..5d7947b3f 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Management.Automation; +using System.Management.Automation.Language; using System.Management.Automation.Remoting; using System.Threading; using System.Threading.Tasks; @@ -17,6 +18,7 @@ using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.DebugAdapter.Protocol.Server; @@ -75,6 +77,12 @@ internal record PsesLaunchRequestArguments : LaunchRequestArguments /// properties of the 'environmentVariables' are used as key/value pairs. /// public Dictionary Env { get; set; } + + /// + /// Gets or sets the path mappings for the debugging session. This is + /// only used when the current runspace is remote. + /// + public PathMapping[] PathMappings { get; set; } = []; } internal record PsesAttachRequestArguments : AttachRequestArguments @@ -88,10 +96,52 @@ internal record PsesAttachRequestArguments : AttachRequestArguments public string RunspaceName { get; set; } public string CustomPipeName { get; set; } + + /// + /// Gets or sets the path mappings for the remote debugging session. + /// + public PathMapping[] PathMappings { get; set; } = []; + + /// Gets or sets a boolean value that determines whether to write the + /// PSES.Attached event to the target runspace after attaching. + /// + public bool NotifyOnAttach { get; set; } } internal class LaunchAndAttachHandler : ILaunchHandler, IAttachHandler, IOnDebugAdapterServerStarted { + private const string _newAttachEventScript = @" + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [int] + $RunspaceId + ) + + $ErrorActionPreference = 'Stop' + + $runspace = Get-Runspace -Id $RunspaceId + $runspace.Events.GenerateEvent( + 'PSES.Attached', + 'PSES', + @(), + $null) + "; + + // TODO: We currently set `WriteInputToHost` as true, which writes our debugged commands' + // `GetInvocationText` and that reveals some obscure implementation details we should + // instead hide from the user with pretty strings (or perhaps not write out at all). + // + // This API is mostly used for F5 execution so it requires the foreground. + private static readonly PowerShellExecutionOptions s_debuggerExecutionOptions = new() + { + RequiresForeground = true, + WriteInputToHost = true, + WriteOutputToHost = true, + ThrowOnError = false, + AddToHistory = true, + }; + private static readonly int s_currentPID = System.Diagnostics.Process.GetCurrentProcess().Id; private static readonly Version s_minVersionForCustomPipeName = new(6, 2); private readonly ILogger _logger; @@ -99,6 +149,7 @@ internal class LaunchAndAttachHandler : ILaunchHandler(); + // DebugServiceTests will call this with a null DebugStateService. + if (_debugStateService is not null) + { + _debugStateService.ServerStarted = new TaskCompletionSource(); + } _remoteFileManagerService = remoteFileManagerService; } public async Task Handle(PsesLaunchRequestArguments request, CancellationToken cancellationToken) + { + _debugService.SetPathMappings(request.PathMappings); + try + { + return await HandleImpl(request, cancellationToken).ConfigureAwait(false); + } + catch + { + _debugService.UnsetPathMappings(); + throw; + } + } + + public async Task HandleImpl(PsesLaunchRequestArguments request, CancellationToken cancellationToken) { // The debugger has officially started. We use this to later check if we should stop it. ((PsesInternalHost)_executionService).DebugContext.IsActive = true; @@ -205,10 +276,7 @@ await _executionService.ExecutePSCommandAsync( // Store the launch parameters so that they can be used later _debugStateService.NoDebug = request.NoDebug; - _debugStateService.ScriptToLaunch = request.Script; - _debugStateService.Arguments = request.Args; _debugStateService.IsUsingTempIntegratedConsole = request.CreateTemporaryIntegratedConsole; - _debugStateService.ExecuteMode = request.ExecuteMode; if (request.CreateTemporaryIntegratedConsole && !string.IsNullOrEmpty(request.Script) @@ -219,22 +287,39 @@ await _executionService.ExecutePSCommandAsync( // If the current session is remote, map the script path to the remote // machine if necessary - if (_debugStateService.ScriptToLaunch != null + string requestScript = request.Script; + if (requestScript != null && _runspaceContext.CurrentRunspace.IsOnRemoteMachine) { - _debugStateService.ScriptToLaunch = - _remoteFileManagerService.GetMappedPath( - _debugStateService.ScriptToLaunch, - _runspaceContext.CurrentRunspace); + if (_debugService.TryGetMappedRemotePath(requestScript, out string remoteMappedPath)) + { + requestScript = remoteMappedPath; + } + else + { + // If the script is not mapped, we will map it to the remote path + // using the RemoteFileManagerService. + requestScript = + _remoteFileManagerService.GetMappedPath( + requestScript, + _runspaceContext.CurrentRunspace); + } } // If no script is being launched, mark this as an interactive // debugging session - _debugStateService.IsInteractiveDebugSession = string.IsNullOrEmpty(_debugStateService.ScriptToLaunch); + _debugStateService.IsInteractiveDebugSession = string.IsNullOrEmpty(requestScript); // Sends the InitializedEvent so that the debugger will continue // sending configuration requests - _debugStateService.ServerStarted.TrySetResult(true); + await _debugStateService.WaitForConfigurationDoneAsync("launch", cancellationToken).ConfigureAwait(false); + + if (!_debugStateService.IsInteractiveDebugSession) + { + // NOTE: This is an unawaited task because we are starting the script but not + // waiting for it to finish. + Task _ = LaunchScriptAsync(requestScript, request.Args, request.ExecuteMode).HandleErrorsAsync(_logger); + } return new LaunchResponse(); } @@ -250,11 +335,13 @@ public async Task Handle(PsesAttachRequestArguments request, Can _debugService.IsDebuggingRemoteRunspace = true; try { + _debugService.SetPathMappings(request.PathMappings); return await HandleImpl(request, cancellationToken).ConfigureAwait(false); } catch { _debugService.IsDebuggingRemoteRunspace = false; + _debugService.UnsetPathMappings(); throw; } } @@ -353,16 +440,90 @@ void RunspaceChangedHandler(object s, RunspaceChangedEventArgs _) // Clear any existing breakpoints before proceeding await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false); - _debugStateService.WaitingForAttach = true; + // The debugger is now ready to receive breakpoint requests. We do + // this before running Debug-Runspace so the runspace is not busy + // and can set breakpoints before the final configuration done. + await _debugStateService.WaitForConfigurationDoneAsync("attach", cancellationToken).ConfigureAwait(false); + + if (request.NotifyOnAttach) + { + // This isn't perfect as there is still a race condition + // this and Debug-Runspace setting up the debugger below but it + // is the best we can do without changes to PowerShell. + await _executionService.ExecutePSCommandAsync( + new PSCommand().AddScript(_newAttachEventScript, useLocalScope: true) + .AddParameter("RunspaceId", _debugStateService.RunspaceId), + cancellationToken).ConfigureAwait(false); + } + Task nonAwaitedTask = _executionService .ExecutePSCommandAsync(debugRunspaceCmd, CancellationToken.None, PowerShellExecutionOptions.ImmediateInteractive) .ContinueWith(OnExecutionCompletedAsync, TaskScheduler.Default); - _debugStateService.ServerStarted.TrySetResult(true); - return new AttachResponse(); } + // NOTE: We test this function in `DebugServiceTests` so it both needs to be internal, and + // use conditional-access on `_debugStateService` and `_debugAdapterServer` as its not set + // by tests. + internal async Task LaunchScriptAsync(string scriptToLaunch, string[] arguments, string requestExecuteMode) + { + PSCommand command; + if (File.Exists(scriptToLaunch)) + { + // For a saved file we just execute its path (after escaping it), with the configured operator + // (which can't be called that because it's a reserved keyword in C#). + string executeMode = requestExecuteMode == "Call" ? "&" : "."; + command = PSCommandHelpers.BuildDotSourceCommandWithArguments( + PSCommandHelpers.EscapeScriptFilePath(scriptToLaunch), arguments, executeMode); + } + else // It's a URI to an untitled script, or a raw script. + { + bool isScriptFile = _workspaceService.TryGetFile(scriptToLaunch, out ScriptFile untitledScript); + if (isScriptFile) + { + // Parse untitled files with their `Untitled:` URI as the filename which will + // cache the URI and contents within the PowerShell parser. By doing this, we + // light up the ability to debug untitled files with line breakpoints. + ScriptBlockAst ast = Parser.ParseInput( + untitledScript.Contents, + untitledScript.DocumentUri.ToString(), + out Token[] _, + out ParseError[] _); + + // In order to use utilize the parser's cache (and therefore hit line + // breakpoints) we need to use the AST's `ScriptBlock` object. Due to + // limitations in PowerShell's public API, this means we must use the + // `PSCommand.AddArgument(object)` method, hence this hack where we dot-source + // `$args[0]. Fortunately the dot-source operator maintains a stack of arguments + // on each invocation, so passing the user's arguments directly in the initial + // `AddScript` surprisingly works. + command = PSCommandHelpers + .BuildDotSourceCommandWithArguments("$args[0]", arguments) + .AddArgument(ast.GetScriptBlock()); + } + else + { + // Without the new APIs we can only execute the untitled script's contents. + // Command breakpoints and `Wait-Debugger` will work. We must wrap the script + // with newlines so that any included comments don't break the command. + command = PSCommandHelpers.BuildDotSourceCommandWithArguments( + string.Concat( + "{" + Environment.NewLine, + isScriptFile ? untitledScript.Contents : scriptToLaunch, + Environment.NewLine + "}"), + arguments); + } + } + + await _executionService.ExecutePSCommandAsync( + command, + CancellationToken.None, + s_debuggerExecutionOptions).ConfigureAwait(false); + + _debugAdapterServer?.SendNotification(EventNames.Terminated); + } + private async Task AttachToComputer(string computerName, CancellationToken cancellationToken) { _debugStateService.IsRemoteAttach = true; @@ -486,6 +647,7 @@ private async Task OnExecutionCompletedAsync(Task executeTask) _debugEventHandlerService.UnregisterEventHandlers(); _debugService.IsDebuggingRemoteRunspace = false; + _debugService.UnsetPathMappings(); if (!isRunspaceClosed && _debugStateService.IsAttachSession) { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs index 858f6a815..735d672d1 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs @@ -38,7 +38,12 @@ public async Task Handle(StackTraceArguments request, Cancel InvocationInfo invocationInfo = debugService.CurrentDebuggerStoppedEventArgs?.OriginalEvent?.InvocationInfo ?? throw new RpcErrorException(0, null!, "InvocationInfo was not available on CurrentDebuggerStoppedEvent args. This is a bug and you should report it."); - StackFrame breakpointLabel = CreateBreakpointLabel(invocationInfo); + string? scriptNameOverride = null; + if (debugService.TryGetMappedLocalPath(invocationInfo.ScriptName, out string mappedLocalPath)) + { + scriptNameOverride = mappedLocalPath; + } + StackFrame breakpointLabel = CreateBreakpointLabel(invocationInfo, scriptNameOverride: scriptNameOverride); if (skip == 0 && take == 1) // This indicates the client is doing an initial fetch, so we want to return quickly to unblock the UI and wait on the remaining stack frames for the subsequent requests. { @@ -116,13 +121,13 @@ public static StackFrame CreateStackFrame(StackFrameDetails stackFrame, long id) }; } - public static StackFrame CreateBreakpointLabel(InvocationInfo invocationInfo, int id = 0) => new() + public static StackFrame CreateBreakpointLabel(InvocationInfo invocationInfo, int id = 0, string? scriptNameOverride = null) => new() { Name = "", Id = id, Source = new() { - Path = invocationInfo.ScriptName + Path = scriptNameOverride ?? invocationInfo.ScriptName }, Line = invocationInfo.ScriptLineNumber, Column = invocationInfo.OffsetInLine, diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/PathMapping.cs b/src/PowerShellEditorServices/Services/DebugAdapter/PathMapping.cs new file mode 100644 index 000000000..6bf6a6ad9 --- /dev/null +++ b/src/PowerShellEditorServices/Services/DebugAdapter/PathMapping.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +namespace Microsoft.PowerShell.EditorServices.Services; + +/// +/// Used for attach requests to map a local and remote path together. +/// +internal record PathMapping +{ + /// + /// Gets or sets the local root of this mapping entry. + /// + public string? LocalRoot { get; set; } + + /// + /// Gets or sets the remote root of this mapping entry. + /// + public string? RemoteRoot { get; set; } +} + +#nullable disable diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs index 763094be4..344b798f7 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs @@ -91,10 +91,12 @@ public static async Task GetDscCapabilityAsync( .AddCommand(@"Microsoft.PowerShell.Core\Import-Module") .AddParameter("Name", "PSDesiredStateConfiguration") .AddParameter("PassThru") - .AddParameter("ErrorAction", ActionPreference.Ignore); + .AddParameter("ErrorAction", ActionPreference.Ignore) + .AddCommand(@"Microsoft.PowerShell.Utility\Select-Object") + .AddParameter("ExpandProperty", "Name"); - IReadOnlyList dscModule = - await executionService.ExecutePSCommandAsync( + IReadOnlyList dscModule = + await executionService.ExecutePSCommandAsync( psCommand, CancellationToken.None, new PowerShellExecutionOptions { ThrowOnError = false }) diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs index b51f3d8dd..ceed8a333 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -2,12 +2,15 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Xml.Linq; using Microsoft.PowerShell.EditorServices.Handlers; using Nerdbank.Streams; using OmniSharp.Extensions.DebugAdapter.Client; @@ -65,6 +68,12 @@ public class DebugAdapterProtocolMessageTests(ITestOutputHelper output) : IAsync /// private readonly TaskCompletionSource terminatedTcs = new(); + private readonly TaskCompletionSource serverInitializedTcs = new(); + /// + /// This task is useful for waiting until the server has sent the Initialized event. + /// + private Task serverInitialized => serverInitializedTcs.Task; + public async Task InitializeAsync() { // Cleanup testScriptLogPath if it exists due to an interrupted previous run @@ -105,12 +114,18 @@ send until a launch is sent. .WithInput(psesStream) .WithOutput(psesStream) // The "early" return mentioned above - .OnInitialized(async (dapClient, _, _, _) => initializedLanguageClientTcs.SetResult(dapClient)) + .OnInitialized((dapClient, _, _, _) => + { + initializedLanguageClientTcs.SetResult(dapClient); + return Task.CompletedTask; + }) // This TCS is useful to wait for a breakpoint to be hit - .OnStopped(async (StoppedEvent e) => + .OnStopped((StoppedEvent e) => { - nextStoppedTcs.SetResult(e); + TaskCompletionSource currentStoppedTcs = nextStoppedTcs; nextStoppedTcs = new(); + + currentStoppedTcs.SetResult(e); }) .OnRequest("startDebugging", (StartDebuggingAttachRequestArguments request) => { @@ -122,6 +137,10 @@ send until a launch is sent. terminatedTcs.SetResult(e); return Task.CompletedTask; }) + .OnDebugAdapterInitialized((initEvent) => + { + serverInitializedTcs.SetResult(initEvent); + }) ; }); @@ -255,9 +274,11 @@ public void CanInitializeWithCorrectServerSettings() public async Task UsesDotSourceOperatorAndQuotesAsync() { string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)")); - await client.LaunchScript(filePath); + Task launchTask = client.LaunchScript(filePath); + await serverInitialized; ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments()); Assert.NotNull(configDoneResponse); + await launchTask; string actual = await ReadScriptLogLineAsync(); Assert.StartsWith(". '", actual); @@ -267,9 +288,11 @@ public async Task UsesDotSourceOperatorAndQuotesAsync() public async Task UsesCallOperatorWithSettingAsync() { string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)")); - await client.LaunchScript(filePath, executeMode: "Call"); + Task launchTask = client.LaunchScript(filePath, executeMode: "Call"); + await serverInitialized; ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments()); Assert.NotNull(configDoneResponse); + await launchTask; string actual = await ReadScriptLogLineAsync(); Assert.StartsWith("& '", actual); @@ -280,10 +303,12 @@ public async Task CanLaunchScriptWithNoBreakpointsAsync() { string filePath = NewTestFile(GenerateLoggingScript("works")); - await client.LaunchScript(filePath); + Task launchTask = client.LaunchScript(filePath); + await serverInitialized; ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments()); Assert.NotNull(configDoneResponse); + await launchTask; string actual = await ReadScriptLogLineAsync(); Assert.Equal("works", actual); @@ -301,7 +326,8 @@ public async Task CanSetBreakpointsAsync() "after breakpoint" )); - await client.LaunchScript(filePath); + Task launchTask = client.LaunchScript(filePath); + await serverInitialized; // {"command":"setBreakpoints","arguments":{"source":{"name":"dfsdfg.ps1","path":"/Users/tyleonha/Code/PowerShell/Misc/foo/dfsdfg.ps1"},"lines":[2],"breakpoints":[{"line":2}],"sourceModified":false},"type":"request","seq":3} SetBreakpointsResponse setBreakpointsResponse = await client.SetBreakpoints(new SetBreakpointsArguments @@ -319,6 +345,8 @@ public async Task CanSetBreakpointsAsync() ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments()); Assert.NotNull(configDoneResponse); + await launchTask; + // Wait until we hit the breakpoint StoppedEvent stoppedEvent = await nextStopped; Assert.Equal("breakpoint", stoppedEvent.Reason); @@ -362,8 +390,10 @@ await client.SetBreakpoints( ); // Signal to start the script + Task launchTask = client.LaunchScript(filePath); + await serverInitialized; await client.RequestConfigurationDone(new ConfigurationDoneArguments()); - await client.LaunchScript(filePath); + await launchTask; // Try to get the stacktrace, which should throw as we are not currently at a breakpoint. await Assert.ThrowsAsync(() => client.RequestStackTrace( @@ -382,7 +412,8 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons() )); // Request a launch. Note that per DAP spec, launch doesn't actually begin until ConfigDone finishes. - await client.LaunchScript(filePath); + Task launchTask = client.LaunchScript(filePath); + await serverInitialized; SetBreakpointsResponse setBreakpointsResponse = await client.SetBreakpoints(new SetBreakpointsArguments { @@ -398,6 +429,8 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons() _ = client.RequestConfigurationDone(new ConfigurationDoneArguments()); + await launchTask; + // Wait for the breakpoint to be hit StoppedEvent stoppedEvent = await nextStopped; Assert.Equal("breakpoint", stoppedEvent.Reason); @@ -444,7 +477,8 @@ public async Task CanStepPastSystemWindowsForms() "Write-Host $form" })); - await client.LaunchScript(filePath); + Task launchTask = client.LaunchScript(filePath); + await serverInitialized; SetFunctionBreakpointsResponse setBreakpointsResponse = await client.SetFunctionBreakpoints( new SetFunctionBreakpointsArguments @@ -458,6 +492,8 @@ public async Task CanStepPastSystemWindowsForms() ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments()); Assert.NotNull(configDoneResponse); + await launchTask; + await Task.Delay(5000); VariablesResponse variablesResponse = await client.RequestVariables( @@ -482,9 +518,11 @@ public async Task CanLaunchScriptWithCommentedLastLineAsync() // PsesLaunchRequestArguments.Script, which is then assigned to // DebugStateService.ScriptToLaunch in that handler, and finally used by the // ConfigurationDoneHandler in LaunchScriptAsync. - await client.LaunchScript(script); + Task launchTask = client.LaunchScript(script); + await serverInitialized; _ = await client.RequestConfigurationDone(new ConfigurationDoneArguments()); + await launchTask; // We can check that the script was invoked as expected, which is to dot-source a script // block with the contents surrounded by newlines. While we can't check that the last @@ -531,8 +569,10 @@ public async Task CanRunPesterTestFile() } }", isPester: true); - await client.LaunchScript($"Invoke-Pester -Script '{pesterTest}'"); + Task launchTask = client.LaunchScript($"Invoke-Pester -Script '{pesterTest}'"); + await serverInitialized; await client.RequestConfigurationDone(new ConfigurationDoneArguments()); + await launchTask; Assert.Equal("pester", await ReadScriptLogLineAsync()); } @@ -565,8 +605,10 @@ public async Task CanLaunchScriptWithNewChildAttachSession( terminatedTcs.TrySetCanceled(); }); - await client.LaunchScript(script); + Task launchTask = client.LaunchScript(script); + await serverInitialized; await client.RequestConfigurationDone(new ConfigurationDoneArguments()); + await launchTask; StartDebuggingAttachRequestArguments attachRequest = await startDebuggingAttachRequestTcs.Task; Assert.Equal("attach", attachRequest.Request); @@ -599,8 +641,10 @@ public async Task CanLaunchScriptWithNewChildAttachSessionAsJob() terminatedTcs.TrySetCanceled(); }); - await client.LaunchScript(script); + Task launchTask = client.LaunchScript(script); + await serverInitialized; await client.RequestConfigurationDone(new ConfigurationDoneArguments()); + await launchTask; StartDebuggingAttachRequestArguments attachRequest = await startDebuggingAttachRequestTcs.Task; Assert.Equal("attach", attachRequest.Request); @@ -613,8 +657,369 @@ public async Task CanLaunchScriptWithNewChildAttachSessionAsJob() await terminatedTcs.Task; } - private record StartDebuggingAttachRequestArguments(PsesAttachRequestArguments Configuration, string Request); + [SkippableFact] + public async Task CanAttachScriptWithEventWait() + { + Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, + "Breakpoints can't be set in Constrained Language Mode."); + + string[] logStatements = ["before breakpoint", "after breakpoint"]; + + await RunWithAttachableProcess(logStatements, async (filePath, processId, runspaceId) => + { + Task nextStoppedTask = nextStopped; + + Task attachTask = client.Attach( + new PsesAttachRequestArguments + { + ProcessId = processId, + RunspaceId = runspaceId, + NotifyOnAttach = true, + }); + + await serverInitialized; + + SetBreakpointsResponse setBreakpointsResponse = await client.SetBreakpoints(new SetBreakpointsArguments + { + Source = new Source { Name = Path.GetFileName(filePath), Path = filePath }, + Breakpoints = new SourceBreakpoint[] { new SourceBreakpoint { Line = 2 } }, + SourceModified = false, + }); + + Breakpoint breakpoint = setBreakpointsResponse.Breakpoints.First(); + Assert.True(breakpoint.Verified); + Assert.NotNull(breakpoint.Source); + Assert.Equal(filePath, breakpoint.Source.Path, ignoreCase: s_isWindows); + Assert.Equal(2, breakpoint.Line); + + ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments()); + Assert.NotNull(configDoneResponse); + + AttachResponse attachResponse = await attachTask; + Assert.NotNull(attachResponse); + + StoppedEvent stoppedEvent; + if (PsesStdioLanguageServerProcessHost.IsWindowsPowerShell) + { + // WinPS will break on first statement when Debug-Runspace + // is called. This does not happen in pwsh 7. + stoppedEvent = await nextStoppedTask; + Assert.Equal("step", stoppedEvent.Reason); + Assert.NotNull(stoppedEvent.ThreadId); + + nextStoppedTask = nextStopped; + + await client.RequestStackTrace(new StackTraceArguments { ThreadId = (int)stoppedEvent.ThreadId }); + await client.RequestContinue(new ContinueArguments { ThreadId = (int)stoppedEvent.ThreadId }); + } + + // Wait until we hit the breakpoint + stoppedEvent = await nextStopped; + Assert.Equal("breakpoint", stoppedEvent.Reason); + + // The code before the breakpoint should have already run + Assert.Equal("before breakpoint", await ReadScriptLogLineAsync()); + + // Assert that the stopped breakpoint is the one we set + StackTraceResponse stackTraceResponse = await client.RequestStackTrace(new StackTraceArguments { ThreadId = 1 }); + DapStackFrame? stoppedTopFrame = stackTraceResponse.StackFrames?.First(); + Assert.NotNull(stoppedTopFrame); + Assert.Equal(2, stoppedTopFrame.Line); + + _ = await client.RequestContinue(new ContinueArguments { ThreadId = 1 }); + + string afterBreakpointActual = await ReadScriptLogLineAsync(); + Assert.Equal("after breakpoint", afterBreakpointActual); + }, waitForNotifyEvent: true); + } + + [SkippableFact] + public async Task CanAttachScriptWithPathMappings() + { + Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, + "Breakpoints can't be set in Constrained Language Mode."); + + string[] logStatements = ["$PSCommandPath", "after breakpoint"]; + + await RunWithAttachableProcess(logStatements, async (filePath, processId, runspaceId) => + { + string localParent = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + string localScriptPath = Path.Combine(localParent, Path.GetFileName(filePath)); + Directory.CreateDirectory(localParent); + File.Copy(filePath, localScriptPath); + + Task nextStoppedTask = nextStopped; + + Task attachTask = client.Attach( + new PsesAttachRequestArguments + { + ProcessId = processId, + RunspaceId = runspaceId, + PathMappings = [ + new() + { + LocalRoot = localParent + Path.DirectorySeparatorChar, + RemoteRoot = Path.GetDirectoryName(filePath) + Path.DirectorySeparatorChar + } + ] + }); + + await serverInitialized; + + SetBreakpointsResponse setBreakpointsResponse = await client.SetBreakpoints(new SetBreakpointsArguments + { + Source = new Source { Name = Path.GetFileName(localScriptPath), Path = localScriptPath }, + Breakpoints = new SourceBreakpoint[] { new SourceBreakpoint { Line = 2 } }, + SourceModified = false, + }); + + Breakpoint breakpoint = setBreakpointsResponse.Breakpoints.First(); + Assert.True(breakpoint.Verified); + Assert.NotNull(breakpoint.Source); + Assert.Equal(localScriptPath, breakpoint.Source.Path, ignoreCase: s_isWindows); + Assert.Equal(2, breakpoint.Line); + + ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments()); + Assert.NotNull(configDoneResponse); + + AttachResponse attachResponse = await attachTask; + Assert.NotNull(attachResponse); + + // Wait-Debugger stop + StoppedEvent stoppedEvent = await nextStoppedTask; + Assert.Equal("step", stoppedEvent.Reason); + Assert.NotNull(stoppedEvent.ThreadId); + + nextStoppedTask = nextStopped; + + // It is important we wait for the stack trace before continue. + // The stopped event starts to get the stack trace info in the + // background and requesting the stack trace is the only way to + // ensure it is done and won't conflict with the continue request. + await client.RequestStackTrace(new StackTraceArguments { ThreadId = (int)stoppedEvent.ThreadId }); + await client.RequestContinue(new ContinueArguments { ThreadId = (int)stoppedEvent.ThreadId }); + + // Wait until we hit the breakpoint + stoppedEvent = await nextStoppedTask; + Assert.Equal("breakpoint", stoppedEvent.Reason); + Assert.NotNull(stoppedEvent.ThreadId); -#nullable disable + // The code before the breakpoint should have already run + // It will contain the actual script being run + string beforeBreakpointActual = await ReadScriptLogLineAsync(); + Assert.Equal(filePath, beforeBreakpointActual); + + // Assert that the stopped breakpoint is the one we set + StackTraceResponse stackTraceResponse = await client.RequestStackTrace(new StackTraceArguments { ThreadId = (int)stoppedEvent.ThreadId }); + DapStackFrame? stoppedTopFrame = stackTraceResponse.StackFrames?.First(); + + // The top frame should have a source path of our local script. + Assert.NotNull(stoppedTopFrame); + Assert.Equal(2, stoppedTopFrame.Line); + Assert.NotNull(stoppedTopFrame.Source); + Assert.Equal(localScriptPath, stoppedTopFrame.Source.Path, ignoreCase: s_isWindows); + + await client.RequestContinue(new ContinueArguments { ThreadId = 1 }); + + string afterBreakpointActual = await ReadScriptLogLineAsync(); + Assert.Equal("after breakpoint", afterBreakpointActual); + }); + } + + private async Task RunWithAttachableProcess(string[] logStatements, Func action, bool waitForNotifyEvent = false) + { + /* + There is no public API in pwsh to wait for an attach event. We + use reflection to wait until the AvailabilityChanged event is + subscribed to by Debug-Runspace as a marker that it is ready to + continue. + + We also run the test script in another runspace as WinPS' + Debug-Runspace will break on the first statement after the + attach and we want that to be the Wait-Debugger call. + + We can use https://github.com/PowerShell/PowerShell/pull/25788 + once that is merged and we are running against that version but + WinPS will always need this. + */ + string scriptEntrypoint = @" + param([string]$TestScript, [string]$WaitScript) + + $debugRunspaceCmd = Get-Command Debug-Runspace -Module Microsoft.PowerShell.Utility + $runspaceBase = [PSObject].Assembly.GetType( + 'System.Management.Automation.Runspaces.RunspaceBase') + $availabilityChangedField = $runspaceBase.GetField( + 'AvailabilityChanged', + [System.Reflection.BindingFlags]'NonPublic, Instance') + if (-not $availabilityChangedField) { + throw 'Failed to get AvailabilityChanged event field' + } + + $ps = [PowerShell]::Create() + $runspace = $ps.Runspace + + if ($WaitScript) { + $null = $ps.AddScript($WaitScript, $true).AddStatement() + } + else { + $null = $ps.AddCommand('Wait-Debugger').AddStatement() + } + $null = $ps.AddCommand($TestScript) + + # Let the runner know what Runspace to attach to and that it + # is ready to run. + 'RID: {0}' -f $runspace.Id + + $start = Get-Date + while ($true) { + $subscribed = $availabilityChangedField.GetValue($runspace) | + Where-Object Target -is $debugRunspaceCmd.ImplementingType + if ($subscribed) { + break + } + + if (((Get-Date) - $start).TotalSeconds -gt 10) { + throw 'Timeout waiting for Debug-Runspace to be subscribed.' + } + } + + $ps.Invoke() + foreach ($e in $ps.Streams.Error) { + Write-Error -ErrorRecord $e + } + + # Keep running until the runner has deleted the test script to + # ensure the process doesn't finish before the test does in + # normal circumstances. + while (Test-Path -LiteralPath $TestScript) { + Start-Sleep -Seconds 1 + } + "; + + string filePath = NewTestFile(GenerateLoggingScript(logStatements)); + + List args = [filePath]; + if (waitForNotifyEvent) + { + args.Add(@" + $e = Wait-Event -SourceIdentifier PSES.Attached -Timeout 10 + if ($e) { + $e | Remove-Event -Force + } + else { + throw 'Timed out waiting for PSES.Attached event.' + } + "); + } + string encArgs = CreatePwshEncodedArgs(args.ToArray()); + string encCommand = Convert.ToBase64String(Encoding.Unicode.GetBytes(scriptEntrypoint)); + + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = PsesStdioLanguageServerProcessHost.PwshExe, + Arguments = $"-NoLogo -NoProfile -EncodedCommand {encCommand} -EncodedArguments {encArgs}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + psi.EnvironmentVariables["TERM"] = "dumb"; // Avoids color/VT sequences in test output. + + TaskCompletionSource ridOutput = new(); + + // Task shouldn't take longer than 30 seconds to complete. + using CancellationTokenSource debugTaskCts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + using CancellationTokenRegistration _ = debugTaskCts.Token.Register(ridOutput.SetCanceled); + using Process? psProc = Process.Start(psi); + try + { + Assert.NotNull(psProc); + psProc.OutputDataReceived += (sender, args) => + { + if (!string.IsNullOrEmpty(args.Data)) + { + if (args.Data.StartsWith("RID: ")) + { + int rid = int.Parse(args.Data.Substring(5)); + ridOutput.SetResult(rid); + } + + output.WriteLine("STDOUT: {0}", args.Data); + } + }; + psProc.ErrorDataReceived += (sender, args) => + { + if (!string.IsNullOrEmpty(args.Data)) + { + output.WriteLine("STDERR: {0}", args.Data); + } + }; + psProc.EnableRaisingEvents = true; + psProc.BeginOutputReadLine(); + psProc.BeginErrorReadLine(); + + Task procExited = psProc.WaitForExitAsync(debugTaskCts.Token); + Task waitRid = ridOutput.Task; + + // Wait for the process to fail or the script to start. + Task finishedTask = await Task.WhenAny(waitRid, procExited); + if (finishedTask == procExited) + { + await procExited; + Assert.Fail("The attached process exited before the PowerShell entrypoint could start."); + } + int rid = await waitRid; + + Task debugTask = action(filePath, psProc.Id, rid); + finishedTask = await Task.WhenAny(procExited, debugTask); + if (finishedTask == procExited) + { + await procExited; + Assert.Fail("Attached process exited before the script could start."); + } + + await debugTask; + + File.Delete(filePath); + psProc.Kill(); + await procExited; + } + catch + { + if (psProc is not null && !psProc.HasExited) + { + psProc.Kill(); + } + + throw; + } + } + + private static string CreatePwshEncodedArgs(params string[] args) + { + // Only way to pass args to -EncodedCommand is to use CLIXML with + // -EncodedArguments. Not pretty but the structure isn't too + // complex and saves us trying to embed/escape strings in a script. + string clixmlNamespace = "http://schemas.microsoft.com/powershell/2004/04"; + string clixml = new XDocument( + new XDeclaration("1.0", "utf-16", "yes"), + new XElement(XName.Get("Objs", clixmlNamespace), + new XAttribute("Version", "1.1.0.1"), + new XElement(XName.Get("Obj", clixmlNamespace), + new XAttribute("RefId", "0"), + new XElement(XName.Get("TN", clixmlNamespace), + new XAttribute("RefId", "0"), + new XElement(XName.Get("T", clixmlNamespace), "System.Collections.ArrayList"), + new XElement(XName.Get("T", clixmlNamespace), "System.Object") + ), + new XElement(XName.Get("LST", clixmlNamespace), + args.Select(s => new XElement(XName.Get("S", clixmlNamespace), s)) + ) + ))).ToString(SaveOptions.DisableFormatting); + + return Convert.ToBase64String(Encoding.Unicode.GetBytes(clixml)); + } + + private record StartDebuggingAttachRequestArguments(PsesAttachRequestArguments Configuration, string Request); } } diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 03690ec21..ab4f4ee49 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -201,7 +201,7 @@ await debugService.SetCommandBreakpointsAsync( public async Task DebuggerAcceptsScriptArgs(string[] args) { IReadOnlyList breakpoints = await debugService.SetLineBreakpointsAsync( - oddPathScriptFile, + oddPathScriptFile.FilePath, new[] { BreakpointDetails.Create(oddPathScriptFile.FilePath, 3) }); Assert.Single(breakpoints); @@ -310,7 +310,7 @@ public async Task DebuggerSetsAndClearsLineBreakpoints() { IReadOnlyList breakpoints = await debugService.SetLineBreakpointsAsync( - debugScriptFile, + debugScriptFile.FilePath, new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 5), BreakpointDetails.Create(debugScriptFile.FilePath, 10) @@ -323,7 +323,7 @@ await debugService.SetLineBreakpointsAsync( Assert.Equal(10, breakpoints[1].LineNumber); breakpoints = await debugService.SetLineBreakpointsAsync( - debugScriptFile, + debugScriptFile.FilePath, new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 2) }); confirmedBreakpoints = await GetConfirmedBreakpoints(debugScriptFile); @@ -331,7 +331,7 @@ await debugService.SetLineBreakpointsAsync( Assert.Equal(2, breakpoints[0].LineNumber); await debugService.SetLineBreakpointsAsync( - debugScriptFile, + debugScriptFile.FilePath, Array.Empty()); IReadOnlyList remainingBreakpoints = await GetConfirmedBreakpoints(debugScriptFile); @@ -342,7 +342,7 @@ await debugService.SetLineBreakpointsAsync( public async Task DebuggerStopsOnLineBreakpoints() { await debugService.SetLineBreakpointsAsync( - debugScriptFile, + debugScriptFile.FilePath, new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 5), BreakpointDetails.Create(debugScriptFile.FilePath, 7) @@ -361,7 +361,7 @@ public async Task DebuggerStopsOnConditionalBreakpoints() const int breakpointValue2 = 20; await debugService.SetLineBreakpointsAsync( - debugScriptFile, + debugScriptFile.FilePath, new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 7, null, $"$i -eq {breakpointValue1} -or $i -eq {breakpointValue2}"), }); @@ -397,7 +397,7 @@ public async Task DebuggerStopsOnHitConditionBreakpoint() const int hitCount = 5; await debugService.SetLineBreakpointsAsync( - debugScriptFile, + debugScriptFile.FilePath, new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 6, null, null, $"{hitCount}"), }); @@ -420,7 +420,7 @@ public async Task DebuggerStopsOnConditionalAndHitConditionBreakpoint() const int hitCount = 5; await debugService.SetLineBreakpointsAsync( - debugScriptFile, + debugScriptFile.FilePath, new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 6, null, "$i % 2 -eq 0", $"{hitCount}") }); Task _ = ExecuteDebugFileAsync(); @@ -441,7 +441,7 @@ public async Task DebuggerProvidesMessageForInvalidConditionalBreakpoint() { IReadOnlyList breakpoints = await debugService.SetLineBreakpointsAsync( - debugScriptFile, + debugScriptFile.FilePath, new[] { // TODO: Add this breakpoint back when it stops moving around?! The ordering // of these two breakpoints seems to do with which framework executes the @@ -469,7 +469,7 @@ public async Task DebuggerFindsParsableButInvalidSimpleBreakpointConditions() { IReadOnlyList breakpoints = await debugService.SetLineBreakpointsAsync( - debugScriptFile, + debugScriptFile.FilePath, new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 5, column: null, condition: "$i == 100"), BreakpointDetails.Create(debugScriptFile.FilePath, 7, column: null, condition: "$i > 100") @@ -548,14 +548,14 @@ await debugService.SetCommandBreakpointsAsync( else { await debugService.SetLineBreakpointsAsync( - scriptFile, + scriptFile.FilePath, new[] { BreakpointDetails.Create(scriptPath, 1) }); } - ConfigurationDoneHandler configurationDoneHandler = new( - NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null); + LaunchAndAttachHandler launchAndAttachHandler = new( + NullLoggerFactory.Instance, null, null, null, debugService, psesHost, psesHost, workspace, null, null); - Task _ = configurationDoneHandler.LaunchScriptAsync(scriptPath); + Task _ = launchAndAttachHandler.LaunchScriptAsync(scriptPath, [], "DotSource"); await AssertDebuggerStopped(scriptPath, 1); VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.CommandVariablesName); @@ -574,9 +574,9 @@ await debugService.SetLineBreakpointsAsync( [Fact] public async Task RecordsF5CommandInPowerShellHistory() { - ConfigurationDoneHandler configurationDoneHandler = new( - NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null); - await configurationDoneHandler.LaunchScriptAsync(debugScriptFile.FilePath); + LaunchAndAttachHandler launchAndAttachHandler = new( + NullLoggerFactory.Instance, null, null, null, debugService, psesHost, psesHost, workspace, null, null); + await launchAndAttachHandler.LaunchScriptAsync(debugScriptFile.FilePath, [], "DotSource"); IReadOnlyList historyResult = await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("(Get-History).CommandLine"), @@ -614,9 +614,9 @@ public async Task RecordsF8CommandInHistory() [Fact] public async Task OddFilePathsLaunchCorrectly() { - ConfigurationDoneHandler configurationDoneHandler = new( - NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null); - await configurationDoneHandler.LaunchScriptAsync(oddPathScriptFile.FilePath); + LaunchAndAttachHandler launchAndAttachHandler = new( + NullLoggerFactory.Instance, null, null, null, debugService, psesHost, psesHost, workspace, null, null); + await launchAndAttachHandler.LaunchScriptAsync(oddPathScriptFile.FilePath, [], "DotSource"); IReadOnlyList historyResult = await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("(Get-History).CommandLine"), @@ -630,7 +630,7 @@ public async Task OddFilePathsLaunchCorrectly() public async Task DebuggerVariableStringDisplaysCorrectly() { await debugService.SetLineBreakpointsAsync( - variableScriptFile, + variableScriptFile.FilePath, new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 8) }); Task _ = ExecuteVariableScriptFileAsync(); @@ -648,7 +648,7 @@ await debugService.SetLineBreakpointsAsync( public async Task DebuggerGetsVariables() { await debugService.SetLineBreakpointsAsync( - variableScriptFile, + variableScriptFile.FilePath, new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 21) }); Task _ = ExecuteVariableScriptFileAsync(); @@ -698,7 +698,7 @@ await debugService.SetLineBreakpointsAsync( public async Task DebuggerSetsVariablesNoConversion() { await debugService.SetLineBreakpointsAsync( - variableScriptFile, + variableScriptFile.FilePath, new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 14) }); Task _ = ExecuteVariableScriptFileAsync(); @@ -751,7 +751,7 @@ await debugService.SetLineBreakpointsAsync( public async Task DebuggerSetsVariablesWithConversion() { await debugService.SetLineBreakpointsAsync( - variableScriptFile, + variableScriptFile.FilePath, new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 14) }); // Execute the script and wait for the breakpoint to be hit @@ -807,7 +807,7 @@ await debugService.SetLineBreakpointsAsync( public async Task DebuggerVariableEnumDisplaysCorrectly() { await debugService.SetLineBreakpointsAsync( - variableScriptFile, + variableScriptFile.FilePath, new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 15) }); // Execute the script and wait for the breakpoint to be hit @@ -827,7 +827,7 @@ await debugService.SetLineBreakpointsAsync( public async Task DebuggerVariableHashtableDisplaysCorrectly() { await debugService.SetLineBreakpointsAsync( - variableScriptFile, + variableScriptFile.FilePath, new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 11) }); // Execute the script and wait for the breakpoint to be hit @@ -860,7 +860,7 @@ await debugService.SetLineBreakpointsAsync( public async Task DebuggerVariableNullStringDisplaysCorrectly() { await debugService.SetLineBreakpointsAsync( - variableScriptFile, + variableScriptFile.FilePath, new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 16) }); // Execute the script and wait for the breakpoint to be hit @@ -880,7 +880,7 @@ await debugService.SetLineBreakpointsAsync( public async Task DebuggerVariablePSObjectDisplaysCorrectly() { await debugService.SetLineBreakpointsAsync( - variableScriptFile, + variableScriptFile.FilePath, new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 17) }); // Execute the script and wait for the breakpoint to be hit @@ -1076,7 +1076,7 @@ await GetVariables(VariableContainerDetails.ScriptScopeName), public async Task DebuggerVariablePSCustomObjectDisplaysCorrectly() { await debugService.SetLineBreakpointsAsync( - variableScriptFile, + variableScriptFile.FilePath, new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 18) }); // Execute the script and wait for the breakpoint to be hit @@ -1105,7 +1105,7 @@ await debugService.SetLineBreakpointsAsync( public async Task DebuggerVariableProcessObjectDisplaysCorrectly() { await debugService.SetLineBreakpointsAsync( - variableScriptFile, + variableScriptFile.FilePath, new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 19) }); // Execute the script and wait for the breakpoint to be hit