diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1
index 36ade043e..0ac9ff998 100644
--- a/PowerShellEditorServices.build.ps1
+++ b/PowerShellEditorServices.build.ps1
@@ -64,6 +64,8 @@ $script:RequiredBuildAssets = @{
'publish/OmniSharp.Extensions.JsonRpc.dll',
'publish/OmniSharp.Extensions.LanguageProtocol.dll',
'publish/OmniSharp.Extensions.LanguageServer.dll',
+ 'publish/OmniSharp.Extensions.DebugAdapter.dll',
+ 'publish/OmniSharp.Extensions.DebugAdapter.Server.dll',
'publish/runtimes/linux-64/native/libdisablekeyecho.so',
'publish/runtimes/osx-64/native/libdisablekeyecho.dylib',
'publish/Serilog.dll',
diff --git a/docs/api/index.md b/docs/api/index.md
index a845c1d66..94c6a5d4a 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -20,9 +20,9 @@ the PowerShell debugger.
Use the @Microsoft.PowerShell.EditorServices.Console.ConsoleService to provide interactive
console support in the user's editor.
-Use the @Microsoft.PowerShell.EditorServices.Extensions.ExtensionService to allow
+Use the @Microsoft.PowerShell.EditorServices.Engine.Services.ExtensionService to allow
the user to extend the host editor with new capabilities using PowerShell code.
The core of all the services is the @Microsoft.PowerShell.EditorServices.PowerShellContext
class. This class manages a session's runspace and handles script and command
-execution no matter what state the runspace is in.
\ No newline at end of file
+execution no matter what state the runspace is in.
diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md
index 6e964416c..66953aa68 100644
--- a/docs/guide/extensions.md
+++ b/docs/guide/extensions.md
@@ -9,7 +9,7 @@ uses PowerShell Editor Services.
### Introducing `$psEditor`
The entry point for the PowerShell Editor Services extensibility model is the `$psEditor`
-object of the type @Microsoft.PowerShell.EditorServices.Extensions.EditorObject. For
+object of the type @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject. For
those familiar with the PowerShell ISE's `$psISE` object, the `$psEditor` object is very
similar. The primary difference is that this model has been generalized to work against
any editor which leverages PowerShell Editor Services for its PowerShell editing experience.
@@ -19,7 +19,7 @@ any editor which leverages PowerShell Editor Services for its PowerShell editing
> please file an issue on our GitHub page.
This object gives access to all of the high-level services in the current
-editing session. For example, the @Microsoft.PowerShell.EditorServices.Extensions.EditorObject.Workspace
+editing session. For example, the @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject.Workspace
property gives access to the editor's workspace, allowing you to create or open files
in the editor.
@@ -79,17 +79,17 @@ Register-EditorCommand `
-ScriptBlock { Write-Output "My command's script block was invoked!" }
```
-### The @Microsoft.PowerShell.EditorServices.Extensions.EditorContext parameter
+### The @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext parameter
Your function, cmdlet, or ScriptBlock can optionally accept a single parameter
-of type @Microsoft.PowerShell.EditorServices.Extensions.EditorContext which provides
+of type @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext which provides
information about the state of the host editor at the time your command was
invoked. With this object you can easily perform operations like manipulatin the
state of the user's active editor buffer or changing the current selection.
The usual convention is that a `$context` parameter is added to your editor
command's function. For now it is recommended that you fully specify the
-type of the @Microsoft.PowerShell.EditorServices.Extensions.EditorContext object
+type of the @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext object
so that you get full IntelliSense on your context parameter.
Here is an example of using the `$context` parameter:
@@ -99,7 +99,7 @@ Register-EditorCommand `
-Name "MyModule.MyEditorCommandWithContext" `
-DisplayName "My command with context usage" `
-ScriptBlock {
- param([Microsoft.PowerShell.EditorServices.Extensions.EditorContext]$context)
+ param([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]$context)
Write-Output "The user's cursor is on line $($context.CursorPosition.Line)!"
}
```
@@ -165,4 +165,4 @@ in that editor starts up.
> NOTE: In the future we plan to provide an easy way for the user to opt-in
> to the automatic loading of any editor command modules that they've installed
> from the PowerShell Gallery. If this interests you, please let us know on
-> [this GitHub issue](https://github.com/PowerShell/PowerShellEditorServices/issues/215).
\ No newline at end of file
+> [this GitHub issue](https://github.com/PowerShell/PowerShellEditorServices/issues/215).
diff --git a/global.json b/global.json
index 80bff6046..9847f02c9 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,5 @@
{
"sdk": {
- "version": "2.1.602"
+ "version": "2.1.801"
}
}
diff --git a/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 b/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1
index e7b34e076..12b205c98 100644
--- a/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1
+++ b/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1
@@ -5,7 +5,7 @@
Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.VSCode.dll"
-if ($psEditor -is [Microsoft.PowerShell.EditorServices.Extensions.EditorObject]) {
+if ($psEditor -is [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject]) {
[Microsoft.PowerShell.EditorServices.VSCode.ComponentRegistration]::Register($psEditor.Components)
}
else {
diff --git a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1 b/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1
index 0e9088bd3..d76c69fd4 100644
--- a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1
+++ b/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1
@@ -41,7 +41,7 @@ function New-VSCodeHtmlContentView {
)
process {
- if ($psEditor -is [Microsoft.PowerShell.EditorServices.Extensions.EditorObject]) {
+ if ($psEditor -is [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject]) {
$viewFeature = $psEditor.Components.Get([Microsoft.PowerShell.EditorServices.VSCode.CustomViews.IHtmlContentViews])
$view = $viewFeature.CreateHtmlContentViewAsync($Title).Result
diff --git a/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 b/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1
index 7c73e8358..e808cc16a 100644
--- a/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1
+++ b/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1
@@ -3,7 +3,7 @@ Register-EditorCommand `
-DisplayName 'Open Editor Profile' `
-SuppressOutput `
-ScriptBlock {
- param([Microsoft.PowerShell.EditorServices.Extensions.EditorContext]$context)
+ param([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]$context)
If (!(Test-Path -Path $Profile)) { New-Item -Path $Profile -ItemType File }
$psEditor.Workspace.OpenFile($Profile)
}
@@ -13,18 +13,18 @@ Register-EditorCommand `
-DisplayName 'Open Profile from List (Current User)' `
-SuppressOutput `
-ScriptBlock {
- param([Microsoft.PowerShell.EditorServices.Extensions.EditorContext]$context)
-
- $Current = Split-Path -Path $profile -Leaf
+ param([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]$context)
+
+ $Current = Split-Path -Path $profile -Leaf
$List = @($Current,'Microsoft.VSCode_profile.ps1','Microsoft.PowerShell_profile.ps1','Microsoft.PowerShellISE_profile.ps1','Profile.ps1') | Select-Object -Unique
$Choices = [System.Management.Automation.Host.ChoiceDescription[]] @($List)
$Selection = $host.ui.PromptForChoice('Please Select a Profile', '(Current User)', $choices,'0')
$Name = $List[$Selection]
-
+
$ProfileDir = Split-Path $Profile -Parent
$ProfileName = Join-Path -Path $ProfileDir -ChildPath $Name
-
+
If (!(Test-Path -Path $ProfileName)) { New-Item -Path $ProfileName -ItemType File }
-
+
$psEditor.Workspace.OpenFile($ProfileName)
- }
\ No newline at end of file
+ }
diff --git a/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 b/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1
index 47623296a..efd5b481e 100644
--- a/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1
+++ b/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1
@@ -47,7 +47,7 @@ function Register-EditorCommand {
$commandArgs += $Function
}
- $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Extensions.EditorCommand -ArgumentList $commandArgs
+ $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand -ArgumentList $commandArgs
if ($psEditor.RegisterCommand($editorCommand))
{
Write-Verbose "Registered new command '$Name'"
diff --git a/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 b/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1
index 9ddec5021..466d9368e 100644
--- a/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1
+++ b/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1
@@ -7,7 +7,7 @@ function Import-EditorCommand {
<#
.EXTERNALHELP ..\PowerShellEditorServices.Commands-help.xml
#>
- [OutputType([Microsoft.PowerShell.EditorServices.Extensions.EditorCommand])]
+ [OutputType([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand])]
[CmdletBinding(DefaultParameterSetName='ByCommand')]
param(
[Parameter(Position=0,
@@ -75,7 +75,7 @@ function Import-EditorCommand {
$commands = $Command | Get-Command -ErrorAction SilentlyContinue
}
}
- $attributeType = [Microsoft.PowerShell.EditorServices.Extensions.EditorCommandAttribute]
+ $attributeType = [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommandAttribute]
foreach ($aCommand in $commands) {
# Get the attribute from our command to get name info.
$details = $aCommand.ScriptBlock.Attributes | Where-Object TypeId -eq $attributeType
@@ -99,7 +99,7 @@ function Import-EditorCommand {
}
# Check for a context parameter.
$contextParameter = $aCommand.Parameters.Values |
- Where-Object ParameterType -eq ([Microsoft.PowerShell.EditorServices.Extensions.EditorContext])
+ Where-Object ParameterType -eq ([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext])
# If one is found then add a named argument. Otherwise call the command directly.
if ($contextParameter) {
@@ -109,7 +109,7 @@ function Import-EditorCommand {
$scriptBlock = [scriptblock]::Create($aCommand.Name)
}
- $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Extensions.EditorCommand @(
+ $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand @(
<# commandName: #> $details.Name,
<# displayName: #> $details.DisplayName,
<# suppressOutput: #> $details.SuppressOutput,
diff --git a/module/docs/Import-EditorCommand.md b/module/docs/Import-EditorCommand.md
index d84c66d53..b04487595 100644
--- a/module/docs/Import-EditorCommand.md
+++ b/module/docs/Import-EditorCommand.md
@@ -30,7 +30,7 @@ The Import-EditorCommand function will search the specified module for functions
Alternatively, you can specify command info objects (like those from the Get-Command cmdlet) to be processed directly.
-To tag a command as an editor command, attach the attribute 'Microsoft.PowerShell.EditorServices.Extensions.EditorCommandAttribute' to the function like you would with 'CmdletBindingAttribute'. The attribute accepts the named parameters 'Name', 'DisplayName', and 'SuppressOutput'.
+To tag a command as an editor command, attach the attribute 'Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommandAttribute' to the function like you would with 'CmdletBindingAttribute'. The attribute accepts the named parameters 'Name', 'DisplayName', and 'SuppressOutput'.
## EXAMPLES
@@ -55,7 +55,7 @@ Registers all editor commands that contain "Editor" in the name and return all s
```powershell
function Invoke-MyEditorCommand {
[CmdletBinding()]
- [Microsoft.PowerShell.EditorServices.Extensions.EditorCommand(DisplayName='My Command', SuppressOutput)]
+ [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand(DisplayName='My Command', SuppressOutput)]
param()
end {
ConvertTo-ScriptExtent -Offset 0 | Set-ScriptExtent -Text 'My Command!'
@@ -145,7 +145,7 @@ You can pass commands to register as editor commands.
## OUTPUTS
-### Microsoft.PowerShell.EditorServices.Extensions.EditorCommand
+### Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand
If the "PassThru" parameter is specified editor commands that were successfully registered
will be returned. This function does not output to the pipeline otherwise.
diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs
index 83f61a729..b92d9d487 100644
--- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs
+++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs
@@ -6,15 +6,20 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.IO.Pipes;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Reflection;
using System.Runtime.InteropServices;
+using System.Security.AccessControl;
+using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.PowerShell.EditorServices.Engine.Server;
+using Microsoft.PowerShell.EditorServices.Engine.Services;
using Microsoft.PowerShell.EditorServices.Utility;
using Serilog;
@@ -58,6 +63,15 @@ public class EditorServicesHost
{
#region Private Fields
+ // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard.
+ private const int CurrentUserOnly = 0x20000000;
+
+ // In .NET Framework, NamedPipeServerStream has a constructor that takes in a PipeSecurity object. We will use reflection to call the constructor,
+ // since .NET Framework doesn't have the `CurrentUserOnly` PipeOption.
+ // doc: https://docs.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.-ctor?view=netframework-4.7.2#System_IO_Pipes_NamedPipeServerStream__ctor_System_String_System_IO_Pipes_PipeDirection_System_Int32_System_IO_Pipes_PipeTransmissionMode_System_IO_Pipes_PipeOptions_System_Int32_System_Int32_System_IO_Pipes_PipeSecurity_
+ private static readonly ConstructorInfo s_netFrameworkPipeServerConstructor =
+ typeof(NamedPipeServerStream).GetConstructor(new[] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) });
+
private readonly HostDetails _hostDetails;
private readonly PSHost _internalHost;
@@ -69,6 +83,7 @@ public class EditorServicesHost
private readonly string[] _additionalModules;
private PsesLanguageServer _languageServer;
+ private PsesDebugServer _debugServer;
private Microsoft.Extensions.Logging.ILogger _logger;
@@ -221,16 +236,15 @@ public void StartLanguageService(
EditorServiceTransportConfig config,
ProfilePaths profilePaths)
{
- while (System.Diagnostics.Debugger.IsAttached)
- {
- System.Console.WriteLine($"{Process.GetCurrentProcess().Id}");
- Thread.Sleep(2000);
- }
+ // Uncomment to debug language service
+ // while (!System.Diagnostics.Debugger.IsAttached)
+ // {
+ // System.Console.WriteLine($"{Process.GetCurrentProcess().Id}");
+ // Thread.Sleep(2000);
+ // }
_logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}");
-
-
switch (config.TransportType)
{
case EditorServiceTransportType.NamedPipe:
@@ -269,6 +283,8 @@ public void StartLanguageService(
config.TransportType, config.Endpoint));
}
+
+ private bool alreadySubscribedDebug;
///
/// Starts the debug service with the specified config.
///
@@ -280,17 +296,85 @@ public void StartDebugService(
ProfilePaths profilePaths,
bool useExistingSession)
{
- /*
- this.debugServiceListener = CreateServiceListener(MessageProtocolType.DebugAdapter, config);
- this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect;
- this.debugServiceListener.Start();
+ //while (System.Diagnostics.Debugger.IsAttached)
+ //{
+ // System.Console.WriteLine($"{Process.GetCurrentProcess().Id}");
+ // Thread.Sleep(2000);
+ //}
- this.logger.Write(
- LogLevel.Normal,
- string.Format(
- "Debug service started, type = {0}, endpoint = {1}",
- config.TransportType, config.Endpoint));
- */
+ _logger.LogInformation($"Debug NamedPipe: {config.InOutPipeName}\nDebug OutPipe: {config.OutPipeName}");
+
+ switch (config.TransportType)
+ {
+ case EditorServiceTransportType.NamedPipe:
+ NamedPipeServerStream inNamedPipe = CreateNamedPipe(
+ config.InOutPipeName ?? config.InPipeName,
+ config.OutPipeName,
+ out NamedPipeServerStream outNamedPipe);
+
+ _debugServer = new PsesDebugServer(
+ _factory,
+ inNamedPipe,
+ outNamedPipe ?? inNamedPipe);
+
+ Task[] tasks = outNamedPipe != null
+ ? new[] { inNamedPipe.WaitForConnectionAsync(), outNamedPipe.WaitForConnectionAsync() }
+ : new[] { inNamedPipe.WaitForConnectionAsync() };
+ Task.WhenAll(tasks)
+ .ContinueWith(async task =>
+ {
+ _logger.LogInformation("Starting debug server");
+ await _debugServer.StartAsync(_languageServer.LanguageServer.Services);
+ _logger.LogInformation(
+ $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}");
+ });
+
+ break;
+
+ case EditorServiceTransportType.Stdio:
+ _debugServer = new PsesDebugServer(
+ _factory,
+ Console.OpenStandardInput(),
+ Console.OpenStandardOutput());
+
+ Task.Run(async () =>
+ {
+ _logger.LogInformation("Starting debug server");
+
+ IServiceProvider serviceProvider = useExistingSession
+ ? _languageServer.LanguageServer.Services
+ : new ServiceCollection().AddSingleton(
+ (provider) => PowerShellContextService.Create(
+ _factory,
+ provider.GetService(),
+ profilePaths,
+ _featureFlags,
+ _enableConsoleRepl,
+ _internalHost,
+ _hostDetails,
+ _additionalModules))
+ .BuildServiceProvider();
+
+ await _debugServer.StartAsync(serviceProvider);
+ _logger.LogInformation(
+ $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}");
+ });
+ break;
+
+ default:
+ throw new NotSupportedException($"The transport {config.TransportType} is not supported");
+ }
+
+ if(!alreadySubscribedDebug)
+ {
+ alreadySubscribedDebug = true;
+ _debugServer.SessionEnded += (sender, eventArgs) =>
+ {
+ _debugServer.Dispose();
+ alreadySubscribedDebug = false;
+ StartDebugService(config, profilePaths, useExistingSession);
+ };
+ }
}
///
@@ -350,6 +434,81 @@ private void CurrentDomain_UnhandledException(
_logger.LogError($"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}");
}
+ private static NamedPipeServerStream CreateNamedPipe(
+ string inOutPipeName,
+ string outPipeName,
+ out NamedPipeServerStream outPipe)
+ {
+ // .NET Core implementation is simplest so try that first
+ if (VersionUtils.IsNetCore)
+ {
+ outPipe = outPipeName == null
+ ? null
+ : new NamedPipeServerStream(
+ pipeName: outPipeName,
+ direction: PipeDirection.Out,
+ maxNumberOfServerInstances: 1,
+ transmissionMode: PipeTransmissionMode.Byte,
+ options: (PipeOptions)CurrentUserOnly);
+
+ return new NamedPipeServerStream(
+ pipeName: inOutPipeName,
+ direction: PipeDirection.InOut,
+ maxNumberOfServerInstances: 1,
+ transmissionMode: PipeTransmissionMode.Byte,
+ options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly);
+ }
+
+ // Now deal with Windows PowerShell
+ // We need to use reflection to get a nice constructor
+
+ var pipeSecurity = new PipeSecurity();
+
+ WindowsIdentity identity = WindowsIdentity.GetCurrent();
+ WindowsPrincipal principal = new WindowsPrincipal(identity);
+
+ if (principal.IsInRole(WindowsBuiltInRole.Administrator))
+ {
+ // Allow the Administrators group full access to the pipe.
+ pipeSecurity.AddAccessRule(new PipeAccessRule(
+ new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)),
+ PipeAccessRights.FullControl, AccessControlType.Allow));
+ }
+ else
+ {
+ // Allow the current user read/write access to the pipe.
+ pipeSecurity.AddAccessRule(new PipeAccessRule(
+ WindowsIdentity.GetCurrent().User,
+ PipeAccessRights.ReadWrite, AccessControlType.Allow));
+ }
+
+ outPipe = outPipeName == null
+ ? null
+ : (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke(
+ new object[] {
+ outPipeName,
+ PipeDirection.InOut,
+ 1, // maxNumberOfServerInstances
+ PipeTransmissionMode.Byte,
+ PipeOptions.Asynchronous,
+ 1024, // inBufferSize
+ 1024, // outBufferSize
+ pipeSecurity
+ });
+
+ return (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke(
+ new object[] {
+ inOutPipeName,
+ PipeDirection.InOut,
+ 1, // maxNumberOfServerInstances
+ PipeTransmissionMode.Byte,
+ PipeOptions.Asynchronous,
+ 1024, // inBufferSize
+ 1024, // outBufferSize
+ pipeSecurity
+ });
+ }
+
#endregion
}
}
diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj
index 3c219d986..3a51e5b13 100644
--- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj
+++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj
@@ -21,7 +21,7 @@
-
+
@@ -29,6 +29,6 @@
+
-
diff --git a/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs
index 08086d5d8..4742bbe09 100644
--- a/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs
+++ b/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs
@@ -61,7 +61,7 @@ protected override (Stream input, Stream output) GetInputOutputStreams()
_outNamedPipeName,
out NamedPipeServerStream outNamedPipe);
- var logger = _loggerFactory.CreateLogger("NamedPipeConnection");
+ var logger = LoggerFactory.CreateLogger("NamedPipeConnection");
logger.LogInformation("Waiting for connection");
namedPipe.WaitForConnection();
diff --git a/src/PowerShellEditorServices.Engine/Server/PsesDebugServer.cs b/src/PowerShellEditorServices.Engine/Server/PsesDebugServer.cs
new file mode 100644
index 000000000..1088cd145
--- /dev/null
+++ b/src/PowerShellEditorServices.Engine/Server/PsesDebugServer.cs
@@ -0,0 +1,102 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.PowerShell.EditorServices.Engine.Handlers;
+using Microsoft.PowerShell.EditorServices.Engine.Services;
+using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization;
+using OmniSharp.Extensions.JsonRpc;
+using OmniSharp.Extensions.LanguageServer.Server;
+
+namespace Microsoft.PowerShell.EditorServices.Engine.Server
+{
+ public class PsesDebugServer : IDisposable
+ {
+ protected readonly ILoggerFactory _loggerFactory;
+ private readonly Stream _inputStream;
+ private readonly Stream _outputStream;
+
+ private IJsonRpcServer _jsonRpcServer;
+
+ public PsesDebugServer(
+ ILoggerFactory factory,
+ Stream inputStream,
+ Stream outputStream)
+ {
+ _loggerFactory = factory;
+ _inputStream = inputStream;
+ _outputStream = outputStream;
+ }
+
+ public async Task StartAsync(IServiceProvider languageServerServiceProvider)
+ {
+ _jsonRpcServer = await JsonRpcServer.From(options =>
+ {
+ options.Serializer = new DapProtocolSerializer();
+ options.Reciever = new DapReciever();
+ options.LoggerFactory = _loggerFactory;
+ ILogger logger = options.LoggerFactory.CreateLogger("DebugOptionsStartup");
+ options.Services = new ServiceCollection()
+ .AddSingleton(languageServerServiceProvider.GetService())
+ .AddSingleton(languageServerServiceProvider.GetService())
+ .AddSingleton(languageServerServiceProvider.GetService())
+ .AddSingleton(this)
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton();
+
+ options
+ .WithInput(_inputStream)
+ .WithOutput(_outputStream);
+
+ logger.LogInformation("Adding handlers");
+
+ options
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler();
+
+ logger.LogInformation("Handlers added");
+ });
+ }
+
+ public void Dispose()
+ {
+ _jsonRpcServer.Dispose();
+ }
+
+ #region Events
+
+ public event EventHandler SessionEnded;
+
+ internal void OnSessionEnded()
+ {
+ SessionEnded?.Invoke(this, null);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs
index f0ce7f920..7da6fc1e3 100644
--- a/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs
+++ b/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs
@@ -22,7 +22,9 @@ namespace Microsoft.PowerShell.EditorServices.Engine.Server
{
internal abstract class PsesLanguageServer
{
- protected readonly ILoggerFactory _loggerFactory;
+ internal ILoggerFactory LoggerFactory { get; private set; }
+ internal ILanguageServer LanguageServer { get; private set; }
+
private readonly LogLevel _minimumLogLevel;
private readonly bool _enableConsoleRepl;
private readonly HashSet _featureFlags;
@@ -32,8 +34,6 @@ internal abstract class PsesLanguageServer
private readonly ProfilePaths _profilePaths;
private readonly TaskCompletionSource _serverStart;
- private ILanguageServer _languageServer;
-
internal PsesLanguageServer(
ILoggerFactory factory,
LogLevel minimumLogLevel,
@@ -44,7 +44,7 @@ internal PsesLanguageServer(
PSHost internalHost,
ProfilePaths profilePaths)
{
- _loggerFactory = factory;
+ LoggerFactory = factory;
_minimumLogLevel = minimumLogLevel;
_enableConsoleRepl = enableConsoleRepl;
_featureFlags = featureFlags;
@@ -57,10 +57,10 @@ internal PsesLanguageServer(
public async Task StartAsync()
{
- _languageServer = await LanguageServer.From(options =>
+ LanguageServer = await OmniSharp.Extensions.LanguageServer.Server.LanguageServer.From(options =>
{
options.AddDefaultLoggingProvider();
- options.LoggerFactory = _loggerFactory;
+ options.LoggerFactory = LoggerFactory;
ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup");
options.Services = new ServiceCollection()
.AddSingleton()
@@ -68,11 +68,18 @@ public async Task StartAsync()
.AddSingleton()
.AddSingleton(
(provider) =>
- GetFullyInitializedPowerShellContext(
+ PowerShellContextService.Create(
+ LoggerFactory,
provider.GetService(),
- _profilePaths))
+ _profilePaths,
+ _featureFlags,
+ _enableConsoleRepl,
+ _internalHost,
+ _hostDetails,
+ _additionalModules))
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddSingleton(
(provider) =>
{
@@ -155,58 +162,7 @@ await serviceProvider.GetService().SetWorkingDirectory
public async Task WaitForShutdown()
{
await _serverStart.Task;
- await _languageServer.WaitForExit;
- }
-
- private PowerShellContextService GetFullyInitializedPowerShellContext(
- OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer,
- ProfilePaths profilePaths)
- {
- var logger = _loggerFactory.CreateLogger();
-
- // PSReadLine can only be used when -EnableConsoleRepl is specified otherwise
- // issues arise when redirecting stdio.
- var powerShellContext = new PowerShellContextService(
- logger,
- languageServer,
- _featureFlags.Contains("PSReadLine") && _enableConsoleRepl);
-
- EditorServicesPSHostUserInterface hostUserInterface =
- _enableConsoleRepl
- ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost)
- : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger);
-
- EditorServicesPSHost psHost =
- new EditorServicesPSHost(
- powerShellContext,
- _hostDetails,
- hostUserInterface,
- logger);
-
- Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost);
- powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface);
-
- powerShellContext.ImportCommandsModuleAsync(
- Path.Combine(
- Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location),
- @"..\Commands"));
-
- // TODO: This can be moved to the point after the $psEditor object
- // gets initialized when that is done earlier than LanguageServer.Initialize
- foreach (string module in this._additionalModules)
- {
- var command =
- new PSCommand()
- .AddCommand("Microsoft.PowerShell.Core\\Import-Module")
- .AddParameter("Name", module);
-
- powerShellContext.ExecuteCommandAsync(
- command,
- sendOutputToHost: false,
- sendErrorToHost: true);
- }
-
- return powerShellContext;
+ await LanguageServer.WaitForExit;
}
protected abstract (Stream input, Stream output) GetInputOutputStreams();
diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugEventHandlerService.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugEventHandlerService.cs
new file mode 100644
index 000000000..e4410f439
--- /dev/null
+++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugEventHandlerService.cs
@@ -0,0 +1,177 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System.Management.Automation;
+using Microsoft.Extensions.Logging;
+using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter;
+using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext;
+using Microsoft.PowerShell.EditorServices.Utility;
+using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
+using OmniSharp.Extensions.JsonRpc;
+
+namespace Microsoft.PowerShell.EditorServices.Engine.Services
+{
+ internal class DebugEventHandlerService
+ {
+ private readonly ILogger _logger;
+ private readonly PowerShellContextService _powerShellContextService;
+ private readonly DebugService _debugService;
+ private readonly DebugStateService _debugStateService;
+ private readonly IJsonRpcServer _jsonRpcServer;
+
+ public DebugEventHandlerService(
+ ILoggerFactory factory,
+ PowerShellContextService powerShellContextService,
+ DebugService debugService,
+ DebugStateService debugStateService,
+ IJsonRpcServer jsonRpcServer)
+ {
+ _logger = factory.CreateLogger();
+ _powerShellContextService = powerShellContextService;
+ _debugService = debugService;
+ _debugStateService = debugStateService;
+ _jsonRpcServer = jsonRpcServer;
+ }
+
+ internal void RegisterEventHandlers()
+ {
+ _powerShellContextService.RunspaceChanged += PowerShellContext_RunspaceChanged;
+ _debugService.BreakpointUpdated += DebugService_BreakpointUpdated;
+ _debugService.DebuggerStopped += DebugService_DebuggerStopped;
+ _powerShellContextService.DebuggerResumed += PowerShellContext_DebuggerResumed;
+ }
+
+ internal void UnregisterEventHandlers()
+ {
+ _powerShellContextService.RunspaceChanged -= PowerShellContext_RunspaceChanged;
+ _debugService.BreakpointUpdated -= DebugService_BreakpointUpdated;
+ _debugService.DebuggerStopped -= DebugService_DebuggerStopped;
+ _powerShellContextService.DebuggerResumed -= PowerShellContext_DebuggerResumed;
+ }
+
+ #region Public methods
+
+ internal void TriggerDebuggerStopped(DebuggerStoppedEventArgs e)
+ {
+ DebugService_DebuggerStopped(null, e);
+ }
+
+ #endregion
+
+ #region Event Handlers
+
+ private void DebugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e)
+ {
+ // Provide the reason for why the debugger has stopped script execution.
+ // See https://github.com/Microsoft/vscode/issues/3648
+ // The reason is displayed in the breakpoints viewlet. Some recommended reasons are:
+ // "step", "breakpoint", "function breakpoint", "exception" and "pause".
+ // We don't support exception breakpoints and for "pause", we can't distinguish
+ // between stepping and the user pressing the pause/break button in the debug toolbar.
+ string debuggerStoppedReason = "step";
+ if (e.OriginalEvent.Breakpoints.Count > 0)
+ {
+ debuggerStoppedReason =
+ e.OriginalEvent.Breakpoints[0] is CommandBreakpoint
+ ? "function breakpoint"
+ : "breakpoint";
+ }
+
+ _jsonRpcServer.SendNotification(EventNames.Stopped,
+ new StoppedEvent
+ {
+ ThreadId = 1,
+ Reason = debuggerStoppedReason
+ });
+ }
+
+ private void PowerShellContext_RunspaceChanged(object sender, RunspaceChangedEventArgs e)
+ {
+ if (_debugStateService.WaitingForAttach &&
+ e.ChangeAction == RunspaceChangeAction.Enter &&
+ e.NewRunspace.Context == RunspaceContext.DebuggedRunspace)
+ {
+ // Send the InitializedEvent so that the debugger will continue
+ // sending configuration requests
+ _debugStateService.WaitingForAttach = false;
+ _jsonRpcServer.SendNotification(EventNames.Initialized);
+ }
+ else if (
+ e.ChangeAction == RunspaceChangeAction.Exit &&
+ _powerShellContextService.IsDebuggerStopped)
+ {
+ // Exited the session while the debugger is stopped,
+ // send a ContinuedEvent so that the client changes the
+ // UI to appear to be running again
+ _jsonRpcServer.SendNotification(EventNames.Continued,
+ new ContinuedEvent
+ {
+ ThreadId = 1,
+ AllThreadsContinued = true
+ });
+ }
+ }
+
+ private void PowerShellContext_DebuggerResumed(object sender, DebuggerResumeAction e)
+ {
+ _jsonRpcServer.SendNotification(EventNames.Continued,
+ new ContinuedEvent
+ {
+ AllThreadsContinued = true,
+ ThreadId = 1
+ });
+ }
+
+ private void DebugService_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e)
+ {
+ string reason = "changed";
+
+ if (_debugStateService.SetBreakpointInProgress)
+ {
+ // Don't send breakpoint update notifications when setting
+ // breakpoints on behalf of the client.
+ return;
+ }
+
+ switch (e.UpdateType)
+ {
+ case BreakpointUpdateType.Set:
+ reason = "new";
+ break;
+
+ case BreakpointUpdateType.Removed:
+ reason = "removed";
+ break;
+ }
+
+ OmniSharp.Extensions.DebugAdapter.Protocol.Models.Breakpoint breakpoint;
+ if (e.Breakpoint is LineBreakpoint)
+ {
+ breakpoint = LspDebugUtils.CreateBreakpoint(BreakpointDetails.Create(e.Breakpoint));
+ }
+ else if (e.Breakpoint is CommandBreakpoint)
+ {
+ _logger.LogTrace("Function breakpoint updated event is not supported yet");
+ return;
+ }
+ else
+ {
+ _logger.LogError($"Unrecognized breakpoint type {e.Breakpoint.GetType().FullName}");
+ return;
+ }
+
+ breakpoint.Verified = e.UpdateType != BreakpointUpdateType.Disabled;
+
+ _jsonRpcServer.SendNotification(EventNames.Breakpoint,
+ new BreakpointEvent
+ {
+ Reason = reason,
+ Breakpoint = breakpoint
+ });
+ }
+
+ #endregion
+ }
+}
diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugService.cs
new file mode 100644
index 000000000..64889ae5c
--- /dev/null
+++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugService.cs
@@ -0,0 +1,1352 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Management.Automation;
+using System.Management.Automation.Language;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.PowerShell.EditorServices.Utility;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Microsoft.PowerShell.EditorServices.Engine.Logging;
+using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument;
+using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext;
+using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter;
+
+namespace Microsoft.PowerShell.EditorServices.Engine.Services
+{
+ ///
+ /// Provides a high-level service for interacting with the
+ /// PowerShell debugger in the runspace managed by a PowerShellContext.
+ ///
+ internal class DebugService
+ {
+ #region Fields
+
+ private const string PsesGlobalVariableNamePrefix = "__psEditorServices_";
+ private const string TemporaryScriptFileName = "Script Listing.ps1";
+
+ private readonly ILogger logger;
+ private readonly PowerShellContextService powerShellContext;
+ private RemoteFileManagerService remoteFileManager;
+
+ // TODO: This needs to be managed per nested session
+ private readonly Dictionary> breakpointsPerFile =
+ new Dictionary>();
+
+ private int nextVariableId;
+ private string temporaryScriptListingPath;
+ private List variables;
+ private VariableContainerDetails globalScopeVariables;
+ private VariableContainerDetails scriptScopeVariables;
+ private StackFrameDetails[] stackFrameDetails;
+ private readonly PropertyInfo invocationTypeScriptPositionProperty;
+
+ private static int breakpointHitCounter;
+
+ private readonly SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore();
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets or sets a boolean that indicates whether a debugger client is
+ /// currently attached to the debugger.
+ ///
+ public bool IsClientAttached { get; set; }
+
+ ///
+ /// Gets a boolean that indicates whether the debugger is currently
+ /// stopped at a breakpoint.
+ ///
+ public bool IsDebuggerStopped => this.powerShellContext.IsDebuggerStopped;
+
+ ///
+ /// Gets the current DebuggerStoppedEventArgs when the debugger
+ /// is stopped.
+ ///
+ public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; }
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the DebugService class and uses
+ /// the given PowerShellContext for all future operations.
+ ///
+ ///
+ /// The PowerShellContext to use for all debugging operations.
+ ///
+ /// An ILogger implementation used for writing log messages.
+ //public DebugService(PowerShellContextService powerShellContext, ILogger logger)
+ // : this(powerShellContext, null, logger)
+ //{
+ //}
+
+ ///
+ /// Initializes a new instance of the DebugService class and uses
+ /// the given PowerShellContext for all future operations.
+ ///
+ ///
+ /// The PowerShellContext to use for all debugging operations.
+ ///
+ ////
+ //// A RemoteFileManagerService instance to use for accessing files in remote sessions.
+ ////
+ /// An ILogger implementation used for writing log messages.
+ public DebugService(
+ PowerShellContextService powerShellContext,
+ RemoteFileManagerService remoteFileManager,
+ ILoggerFactory factory)
+ {
+ Validate.IsNotNull(nameof(powerShellContext), powerShellContext);
+
+ this.logger = factory.CreateLogger();
+ this.powerShellContext = powerShellContext;
+ this.powerShellContext.DebuggerStop += this.OnDebuggerStopAsync;
+ this.powerShellContext.DebuggerResumed += this.OnDebuggerResumed;
+
+ this.powerShellContext.BreakpointUpdated += this.OnBreakpointUpdated;
+
+ this.remoteFileManager = remoteFileManager;
+
+ this.invocationTypeScriptPositionProperty =
+ typeof(InvocationInfo)
+ .GetProperty(
+ "ScriptPosition",
+ BindingFlags.NonPublic | BindingFlags.Instance);
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Sets the list of line breakpoints for the current debugging session.
+ ///
+ /// The ScriptFile 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.
+ /// An awaitable Task that will provide details about the breakpoints that were set.
+ public async Task SetLineBreakpointsAsync(
+ ScriptFile scriptFile,
+ BreakpointDetails[] breakpoints,
+ bool clearExisting = true)
+ {
+ var resultBreakpointDetails = new List();
+
+ var dscBreakpoints =
+ this.powerShellContext
+ .CurrentRunspace
+ .GetCapability();
+
+ string scriptPath = scriptFile.FilePath;
+ // Make sure we're using the remote script path
+ if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote &&
+ this.remoteFileManager != null)
+ {
+ if (!this.remoteFileManager.IsUnderRemoteTempPath(scriptPath))
+ {
+ this.logger.LogTrace(
+ $"Could not set breakpoints for local path '{scriptPath}' in a remote session.");
+
+ return resultBreakpointDetails.ToArray();
+ }
+
+ string mappedPath =
+ this.remoteFileManager.GetMappedPath(
+ scriptPath,
+ this.powerShellContext.CurrentRunspace);
+
+ scriptPath = mappedPath;
+ }
+ else if (
+ this.temporaryScriptListingPath != null &&
+ this.temporaryScriptListingPath.Equals(scriptPath, StringComparison.CurrentCultureIgnoreCase))
+ {
+ this.logger.LogTrace(
+ $"Could not set breakpoint on temporary script listing path '{scriptPath}'.");
+
+ return resultBreakpointDetails.ToArray();
+ }
+
+ // Fix for issue #123 - file paths that contain wildcard chars [ and ] need to
+ // quoted and have those wildcard chars escaped.
+ string escapedScriptPath =
+ PowerShellContextService.WildcardEscapePath(scriptPath);
+
+ if (dscBreakpoints == null || !dscBreakpoints.IsDscResourcePath(escapedScriptPath))
+ {
+ if (clearExisting)
+ {
+ await this.ClearBreakpointsInFileAsync(scriptFile);
+ }
+
+ foreach (BreakpointDetails breakpoint in breakpoints)
+ {
+ PSCommand psCommand = new PSCommand();
+ psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint");
+ psCommand.AddParameter("Script", escapedScriptPath);
+ psCommand.AddParameter("Line", breakpoint.LineNumber);
+
+ // Check if the user has specified the column number for the breakpoint.
+ if (breakpoint.ColumnNumber.HasValue && breakpoint.ColumnNumber.Value > 0)
+ {
+ // It bums me out that PowerShell will silently ignore a breakpoint
+ // where either the line or the column is invalid. I'd rather have an
+ // error or warning message I could relay back to the client.
+ psCommand.AddParameter("Column", breakpoint.ColumnNumber.Value);
+ }
+
+ // Check if this is a "conditional" line breakpoint.
+ if (!String.IsNullOrWhiteSpace(breakpoint.Condition) ||
+ !String.IsNullOrWhiteSpace(breakpoint.HitCondition))
+ {
+ ScriptBlock actionScriptBlock =
+ GetBreakpointActionScriptBlock(breakpoint);
+
+ // If there was a problem with the condition string,
+ // move onto the next breakpoint.
+ if (actionScriptBlock == null)
+ {
+ resultBreakpointDetails.Add(breakpoint);
+ continue;
+ }
+
+ psCommand.AddParameter("Action", actionScriptBlock);
+ }
+
+ IEnumerable configuredBreakpoints =
+ await this.powerShellContext.ExecuteCommandAsync(psCommand);
+
+ // The order in which the breakpoints are returned is significant to the
+ // VSCode client and should match the order in which they are passed in.
+ resultBreakpointDetails.AddRange(
+ configuredBreakpoints.Select(BreakpointDetails.Create));
+ }
+ }
+ else
+ {
+ resultBreakpointDetails =
+ await dscBreakpoints.SetLineBreakpointsAsync(
+ this.powerShellContext,
+ escapedScriptPath,
+ breakpoints);
+ }
+
+ return resultBreakpointDetails.ToArray();
+ }
+
+ ///
+ /// Sets the list of command breakpoints for the current debugging session.
+ ///
+ /// CommandBreakpointDetails for each command breakpoint that will be set.
+ /// If true, causes all existing function breakpoints to be cleared before setting new ones.
+ /// An awaitable Task that will provide details about the breakpoints that were set.
+ public async Task SetCommandBreakpointsAsync(
+ CommandBreakpointDetails[] breakpoints,
+ bool clearExisting = true)
+ {
+ var resultBreakpointDetails = new List();
+
+ if (clearExisting)
+ {
+ await this.ClearCommandBreakpointsAsync();
+ }
+
+ if (breakpoints.Length > 0)
+ {
+ foreach (CommandBreakpointDetails breakpoint in breakpoints)
+ {
+ PSCommand psCommand = new PSCommand();
+ psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint");
+ psCommand.AddParameter("Command", breakpoint.Name);
+
+ // Check if this is a "conditional" command breakpoint.
+ if (!String.IsNullOrWhiteSpace(breakpoint.Condition) ||
+ !String.IsNullOrWhiteSpace(breakpoint.HitCondition))
+ {
+ ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint);
+
+ // If there was a problem with the condition string,
+ // move onto the next breakpoint.
+ if (actionScriptBlock == null)
+ {
+ resultBreakpointDetails.Add(breakpoint);
+ continue;
+ }
+
+ psCommand.AddParameter("Action", actionScriptBlock);
+ }
+
+ IEnumerable configuredBreakpoints =
+ await this.powerShellContext.ExecuteCommandAsync(psCommand);
+
+ // The order in which the breakpoints are returned is significant to the
+ // VSCode client and should match the order in which they are passed in.
+ resultBreakpointDetails.AddRange(
+ configuredBreakpoints.Select(CommandBreakpointDetails.Create));
+ }
+ }
+
+ return resultBreakpointDetails.ToArray();
+ }
+
+ ///
+ /// Sends a "continue" action to the debugger when stopped.
+ ///
+ public void Continue()
+ {
+ this.powerShellContext.ResumeDebugger(
+ DebuggerResumeAction.Continue);
+ }
+
+ ///
+ /// Sends a "step over" action to the debugger when stopped.
+ ///
+ public void StepOver()
+ {
+ this.powerShellContext.ResumeDebugger(
+ DebuggerResumeAction.StepOver);
+ }
+
+ ///
+ /// Sends a "step in" action to the debugger when stopped.
+ ///
+ public void StepIn()
+ {
+ this.powerShellContext.ResumeDebugger(
+ DebuggerResumeAction.StepInto);
+ }
+
+ ///
+ /// Sends a "step out" action to the debugger when stopped.
+ ///
+ public void StepOut()
+ {
+ this.powerShellContext.ResumeDebugger(
+ DebuggerResumeAction.StepOut);
+ }
+
+ ///
+ /// Causes the debugger to break execution wherever it currently
+ /// is at the time. This is equivalent to clicking "Pause" in a
+ /// debugger UI.
+ ///
+ public void Break()
+ {
+ // Break execution in the debugger
+ this.powerShellContext.BreakExecution();
+ }
+
+ ///
+ /// Aborts execution of the debugger while it is running, even while
+ /// it is stopped. Equivalent to calling PowerShellContext.AbortExecution.
+ ///
+ public void Abort()
+ {
+ this.powerShellContext.AbortExecution(shouldAbortDebugSession: true);
+ }
+
+ ///
+ /// Gets the list of variables that are children of the scope or variable
+ /// that is identified by the given referenced ID.
+ ///
+ ///
+ /// An array of VariableDetails instances which describe the requested variables.
+ public VariableDetailsBase[] GetVariables(int variableReferenceId)
+ {
+ VariableDetailsBase[] childVariables;
+ this.debugInfoHandle.Wait();
+ try
+ {
+ if ((variableReferenceId < 0) || (variableReferenceId >= this.variables.Count))
+ {
+ logger.LogWarning($"Received request for variableReferenceId {variableReferenceId} that is out of range of valid indices.");
+ return new VariableDetailsBase[0];
+ }
+
+ VariableDetailsBase parentVariable = this.variables[variableReferenceId];
+ if (parentVariable.IsExpandable)
+ {
+ childVariables = parentVariable.GetChildren(this.logger);
+ foreach (var child in childVariables)
+ {
+ // Only add child if it hasn't already been added.
+ if (child.Id < 0)
+ {
+ child.Id = this.nextVariableId++;
+ this.variables.Add(child);
+ }
+ }
+ }
+ else
+ {
+ childVariables = new VariableDetailsBase[0];
+ }
+
+ return childVariables;
+ }
+ finally
+ {
+ this.debugInfoHandle.Release();
+ }
+ }
+
+ ///
+ /// Evaluates a variable expression in the context of the stopped
+ /// debugger. This method decomposes the variable expression to
+ /// walk the cached variable data for the specified stack frame.
+ ///
+ /// The variable expression string to evaluate.
+ /// The ID of the stack frame in which the expression should be evaluated.
+ /// A VariableDetailsBase object containing the result.
+ public VariableDetailsBase GetVariableFromExpression(string variableExpression, int stackFrameId)
+ {
+ // NOTE: From a watch we will get passed expressions that are not naked variables references.
+ // Probably the right way to do this woudld be to examine the AST of the expr before calling
+ // this method to make sure it is a VariableReference. But for the most part, non-naked variable
+ // references are very unlikely to find a matching variable e.g. "$i+5.2" will find no var matching "$i+5".
+
+ // Break up the variable path
+ string[] variablePathParts = variableExpression.Split('.');
+
+ VariableDetailsBase resolvedVariable = null;
+ IEnumerable variableList;
+
+ // Ensure debug info isn't currently being built.
+ this.debugInfoHandle.Wait();
+ try
+ {
+ variableList = this.variables;
+ }
+ finally
+ {
+ this.debugInfoHandle.Release();
+ }
+
+ foreach (var variableName in variablePathParts)
+ {
+ if (variableList == null)
+ {
+ // If there are no children left to search, break out early
+ return null;
+ }
+
+ resolvedVariable =
+ variableList.FirstOrDefault(
+ v =>
+ string.Equals(
+ v.Name,
+ variableName,
+ StringComparison.CurrentCultureIgnoreCase));
+
+ if (resolvedVariable != null &&
+ resolvedVariable.IsExpandable)
+ {
+ // Continue by searching in this variable's children
+ variableList = this.GetVariables(resolvedVariable.Id);
+ }
+ }
+
+ return resolvedVariable;
+ }
+
+ ///
+ /// Sets the specified variable by container variableReferenceId and variable name to the
+ /// specified new value. If the variable cannot be set or converted to that value this
+ /// method will throw InvalidPowerShellExpressionException, ArgumentTransformationMetadataException, or
+ /// SessionStateUnauthorizedAccessException.
+ ///
+ /// The container (Autos, Local, Script, Global) that holds the variable.
+ /// The name of the variable prefixed with $.
+ /// The new string value. This value must not be null. If you want to set the variable to $null
+ /// pass in the string "$null".
+ /// The string representation of the value the variable was set to.
+ public async Task SetVariableAsync(int variableContainerReferenceId, string name, string value)
+ {
+ Validate.IsNotNull(nameof(name), name);
+ Validate.IsNotNull(nameof(value), value);
+
+ this.logger.LogTrace($"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'");
+
+ // An empty or whitespace only value is not a valid expression for SetVariable.
+ if (value.Trim().Length == 0)
+ {
+ throw new InvalidPowerShellExpressionException("Expected an expression.");
+ }
+
+ // Evaluate the expression to get back a PowerShell object from the expression string.
+ PSCommand psCommand = new PSCommand();
+ psCommand.AddScript(value);
+ var errorMessages = new StringBuilder();
+ var results =
+ await this.powerShellContext.ExecuteCommandAsync