Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions PowerShellEditorServices.build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ task TestE2E Build, SetupHelpForTests, {

# Run E2E tests in ConstrainedLanguage mode.
if (!$script:IsNix) {
if (-not [Security.Principal.WindowsIdentity]::GetCurrent().Owner.IsWellKnown("BuiltInAdministratorsSid")) {
Write-Warning 'Skipping E2E CLM tests as they must be ran in an elevated process.'
return
}

try {
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", "0x80000007", [System.EnvironmentVariableTarget]::Machine);
exec { & dotnet $script:dotnetTestArgs $script:NetRuntime.PS7 }
Expand Down
2 changes: 1 addition & 1 deletion src/PowerShellEditorServices/Server/PsesDebugServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public void Dispose()
// It represents the debugger on the PowerShell process we're in,
// while a new debug server is spun up for every debugging session
_psesHost.DebugContext.IsDebugServerActive = false;
_debugAdapterServer.Dispose();
_debugAdapterServer?.Dispose();
_inputStream.Dispose();
_outputStream.Dispose();
_serverStopped.SetResult(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;

namespace Microsoft.PowerShell.EditorServices.Services
{
Expand Down Expand Up @@ -43,6 +44,7 @@ public async Task<List<Breakpoint>> GetBreakpointsAsync()
{
if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace))
{
_editorServicesHost.Runspace.ThrowCancelledIfUnusable();
return BreakpointApiUtils.GetBreakpoints(
_editorServicesHost.Runspace.Debugger,
_debugStateService.RunspaceId);
Expand Down
172 changes: 100 additions & 72 deletions src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
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;

Expand Down Expand Up @@ -74,6 +75,15 @@ internal class DebugService
/// </summary>
public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; }

/// <summary>
/// Tracks whether we are running <c>Debug-Runspace</c> in an out-of-process runspace.
/// </summary>
public bool IsDebuggingRemoteRunspace
{
get => _debugContext.IsDebuggingRemoteRunspace;
set => _debugContext.IsDebuggingRemoteRunspace = value;
}

#endregion

#region Constructors
Expand Down Expand Up @@ -128,6 +138,8 @@ public async Task<BreakpointDetails[]> SetLineBreakpointsAsync(
DscBreakpointCapability dscBreakpoints = await _debugContext.GetDscBreakpointCapabilityAsync(CancellationToken.None).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)
{
Expand Down Expand Up @@ -774,34 +786,35 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
const string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack";
const string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.Add(@($PSItem, $PSItem.GetFrameVariables())) }}";

_psesHost.Runspace.ThrowCancelledIfUnusable();
// If we're attached to a remote runspace, we need to serialize the list prior to
// transport because the default depth is too shallow. From testing, we determined the
// correct depth is 3. The script always calls `Get-PSCallStack`. On a local machine, we
// just return its results. On a remote machine we serialize it first and then later
// correct depth is 3. The script always calls `Get-PSCallStack`. In a local runspace, we
// just return its results. In a remote runspace we serialize it first and then later
// deserialize it.
bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine;
string returnSerializedIfOnRemoteMachine = isOnRemoteMachine
bool isRemoteRunspace = _psesHost.CurrentRunspace.Runspace.RunspaceIsRemote;
string returnSerializedIfInRemoteRunspace = isRemoteRunspace
? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, 3)"
: callStackVarName;

// PSObject is used here instead of the specific type because we get deserialized
// objects from remote sessions and want a common interface.
PSCommand psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfOnRemoteMachine}");
PSCommand psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfInRemoteRunspace}");
IReadOnlyList<PSObject> results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None).ConfigureAwait(false);

IEnumerable callStack = isOnRemoteMachine
? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject).BaseObject as IList
IEnumerable callStack = isRemoteRunspace
? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject)?.BaseObject as IList
: results;

List<StackFrameDetails> stackFrameDetailList = new();
bool isTopStackFrame = true;
foreach (object callStackFrameItem in callStack)
{
// We have to use reflection to get the variable dictionary.
IList callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList;
IList callStackFrameComponents = (callStackFrameItem as PSObject)?.BaseObject as IList;
PSObject callStackFrame = callStackFrameComponents[0] as PSObject;
IDictionary callStackVariables = isOnRemoteMachine
? (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary
IDictionary callStackVariables = isRemoteRunspace
? (callStackFrameComponents[1] as PSObject)?.BaseObject as IDictionary
: callStackFrameComponents[1] as IDictionary;

VariableContainerDetails autoVariables = new(
Expand Down Expand Up @@ -864,7 +877,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
{
stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
}
else if (isOnRemoteMachine
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
&& _remoteFileManager is not null
&& !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
{
Expand Down Expand Up @@ -908,83 +921,98 @@ private static string TrimScriptListingLine(PSObject scriptLineObj, ref int pref

internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e)
{
bool noScriptName = false;
string localScriptPath = e.InvocationInfo.ScriptName;

// If there's no ScriptName, get the "list" of the current source
if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath))
try
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there anything other than the wrap in a try/catch/finally that changed here? I didn't see anything but GitHub made it hard to compare.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah just that

{
// Get the current script listing and create the buffer
PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}");
bool noScriptName = false;
string localScriptPath = e.InvocationInfo.ScriptName;

IReadOnlyList<PSObject> scriptListingLines =
await _executionService.ExecutePSCommandAsync<PSObject>(
command, CancellationToken.None).ConfigureAwait(false);

if (scriptListingLines is not null)
// If there's no ScriptName, get the "list" of the current source
if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath))
{
int linePrefixLength = 0;
// Get the current script listing and create the buffer
PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}");

string scriptListing =
string.Join(
Environment.NewLine,
scriptListingLines
.Select(o => TrimScriptListingLine(o, ref linePrefixLength))
.Where(s => s is not null));
IReadOnlyList<PSObject> scriptListingLines =
await _executionService.ExecutePSCommandAsync<PSObject>(
command, CancellationToken.None).ConfigureAwait(false);

temporaryScriptListingPath =
_remoteFileManager.CreateTemporaryFile(
$"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}",
scriptListing,
_psesHost.CurrentRunspace);
if (scriptListingLines is not null)
{
int linePrefixLength = 0;

string scriptListing =
string.Join(
Environment.NewLine,
scriptListingLines
.Select(o => TrimScriptListingLine(o, ref linePrefixLength))
.Where(s => s is not null));

temporaryScriptListingPath =
_remoteFileManager.CreateTemporaryFile(
$"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}",
scriptListing,
_psesHost.CurrentRunspace);

localScriptPath =
temporaryScriptListingPath
?? StackFrameDetails.NoFileScriptPath;

noScriptName = localScriptPath is not null;
}
else
{
_logger.LogWarning("Could not load script context");
}
}

localScriptPath =
temporaryScriptListingPath
?? StackFrameDetails.NoFileScriptPath;
// Get call stack and variables.
await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);

noScriptName = localScriptPath is not null;
// 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
&& _remoteFileManager is not null
&& !noScriptName)
{
localScriptPath =
await _remoteFileManager.FetchRemoteFileAsync(
e.InvocationInfo.ScriptName,
_psesHost.CurrentRunspace).ConfigureAwait(false);
}
else

if (stackFrameDetails.Length > 0)
{
_logger.LogWarning("Could not load script context");
// Augment the top stack frame with details from the stop event
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
{
stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
}
}
}

// Get call stack and variables.
await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
CurrentDebuggerStoppedEventArgs =
new DebuggerStoppedEventArgs(
e,
_psesHost.CurrentRunspace,
localScriptPath);

// 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
&& _remoteFileManager is not null
&& !noScriptName)
// Notify the host that the debugger is stopped.
DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs);
}
catch (OperationCanceledException)
{
localScriptPath =
await _remoteFileManager.FetchRemoteFileAsync(
e.InvocationInfo.ScriptName,
_psesHost.CurrentRunspace).ConfigureAwait(false);
// Ignore, likely means that a remote runspace has closed.
}

if (stackFrameDetails.Length > 0)
catch (Exception exception)
{
// Augment the top stack frame with details from the stop event
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
{
stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
}
// Log in a catch all so we don't crash the process.
_logger.LogError(
exception,
"Error occurred while obtaining debug info. Message: {message}",
exception.Message);
}

CurrentDebuggerStoppedEventArgs =
new DebuggerStoppedEventArgs(
e,
_psesHost.CurrentRunspace,
localScriptPath);

// Notify the host that the debugger is stopped.
DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs);
}

private void OnDebuggerResuming(object sender, DebuggerResumingEventArgs debuggerResumingEventArgs) => CurrentDebuggerStoppedEventArgs = null;
Expand Down
Loading