Skip to content

Fix up debugger attach handlers #2130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public override Task<PauseResponse> Handle(PauseArguments request, CancellationT
}
catch (NotSupportedException e)
{
throw new RpcErrorException(0, e.Message);
throw new RpcErrorException(0, null, e.Message);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ internal record PsesAttachRequestArguments : AttachRequestArguments
{
public string ComputerName { get; set; }

public string ProcessId { get; set; }
public int ProcessId { get; set; }

public string RunspaceId { get; set; }
public int RunspaceId { get; set; }

public string RunspaceName { get; set; }

Expand All @@ -87,6 +87,7 @@ internal record PsesAttachRequestArguments : AttachRequestArguments

internal class LaunchAndAttachHandler : ILaunchHandler<PsesLaunchRequestArguments>, IAttachHandler<PsesAttachRequestArguments>, IOnDebugAdapterServerStarted
{
private static readonly int currentProcessId = System.Diagnostics.Process.GetCurrentProcess().Id;
private static readonly Version s_minVersionForCustomPipeName = new(6, 2);
private readonly ILogger<LaunchAndAttachHandler> _logger;
private readonly BreakpointService _breakpointService;
Expand Down Expand Up @@ -190,7 +191,7 @@ public async Task<LaunchResponse> Handle(PsesLaunchRequestArguments request, Can
&& !string.IsNullOrEmpty(request.Script)
&& ScriptFile.IsUntitledPath(request.Script))
{
throw new RpcErrorException(0, "Running an Untitled file in a temporary Extension Terminal is currently not supported.");
throw new RpcErrorException(0, null, "Running an Untitled file in a temporary Extension Terminal is currently not supported!");
}

// If the current session is remote, map the script path to the remote
Expand Down Expand Up @@ -239,59 +240,26 @@ private async Task<AttachResponse> HandleImpl(PsesAttachRequestArguments request
{
// The debugger has officially started. We use this to later check if we should stop it.
((PsesInternalHost)_executionService).DebugContext.IsActive = true;

_debugStateService.IsAttachSession = true;

_debugEventHandlerService.RegisterEventHandlers();

bool processIdIsSet = !string.IsNullOrEmpty(request.ProcessId) && request.ProcessId != "undefined";
bool processIdIsSet = request.ProcessId != 0;
bool customPipeNameIsSet = !string.IsNullOrEmpty(request.CustomPipeName) && request.CustomPipeName != "undefined";

PowerShellVersionDetails runspaceVersion = _runspaceContext.CurrentRunspace.PowerShellVersionDetails;

// If there are no host processes to attach to or the user cancels selection, we get a null for the process id.
// This is not an error, just a request to stop the original "attach to" request.
// Testing against "undefined" is a HACK because I don't know how to make "Cancel" on quick pick loading
// to cancel on the VSCode side without sending an attachRequest with processId set to "undefined".
if (!processIdIsSet && !customPipeNameIsSet)
{
_logger.LogInformation(
$"Attach request aborted, received {request.ProcessId} for processId.");

throw new RpcErrorException(0, "User aborted attach to PowerShell host process.");
string msg = $"User aborted attach to PowerShell host process: {request.ProcessId}";
_logger.LogTrace(msg);
throw new RpcErrorException(0, null, msg);
}

if (request.ComputerName != null)
if (!string.IsNullOrEmpty(request.ComputerName))
{
if (runspaceVersion.Version.Major < 4)
{
throw new RpcErrorException(0, $"Remote sessions are only available with PowerShell 4 and higher (current session is {runspaceVersion.Version}).");
}
else if (_runspaceContext.CurrentRunspace.RunspaceOrigin != RunspaceOrigin.Local)
{
throw new RpcErrorException(0, "Cannot attach to a process in a remote session when already in a remote session.");
}

PSCommand enterPSSessionCommand = new PSCommand()
.AddCommand("Enter-PSSession")
.AddParameter("ComputerName", request.ComputerName);

try
{
await _executionService.ExecutePSCommandAsync(
enterPSSessionCommand,
cancellationToken,
PowerShellExecutionOptions.ImmediateInteractive)
.ConfigureAwait(false);
}
catch (Exception e)
{
string msg = $"Could not establish remote session to computer '{request.ComputerName}'";
_logger.LogError(e, msg);
throw new RpcErrorException(0, msg);
}

_debugStateService.IsRemoteAttach = true;
await AttachToComputer(request.ComputerName, cancellationToken).ConfigureAwait(false);
}

// Set up a temporary runspace changed event handler so we can ensure
Expand All @@ -305,131 +273,62 @@ void RunspaceChangedHandler(object s, RunspaceChangedEventArgs _)
runspaceChanged.TrySetResult(true);
}

_executionService.RunspaceChanged += RunspaceChangedHandler;

if (processIdIsSet && int.TryParse(request.ProcessId, out int processId) && (processId > 0))
if (processIdIsSet)
{
if (runspaceVersion.Version.Major < 5)
if (request.ProcessId == currentProcessId)
{
throw new RpcErrorException(0, $"Attaching to a process is only available with PowerShell 5 and higher (current session is {runspaceVersion.Version}).");
throw new RpcErrorException(0, null, $"Attaching to the Extension Terminal is not supported!");
}

PSCommand enterPSHostProcessCommand = new PSCommand()
.AddCommand("Enter-PSHostProcess")
.AddParameter("Id", processId);

try
{
await _executionService.ExecutePSCommandAsync(
enterPSHostProcessCommand,
cancellationToken,
PowerShellExecutionOptions.ImmediateInteractive)
.ConfigureAwait(false);
}
catch (Exception e)
{
string msg = $"Could not attach to process with Id: '{request.ProcessId}'";
_logger.LogError(e, msg);
throw new RpcErrorException(0, msg);
}
_executionService.RunspaceChanged += RunspaceChangedHandler;
await AttachToProcess(request.ProcessId, cancellationToken).ConfigureAwait(false);
await runspaceChanged.Task.ConfigureAwait(false);
}
else if (customPipeNameIsSet)
{
if (runspaceVersion.Version < s_minVersionForCustomPipeName)
{
throw new RpcErrorException(0, $"Attaching to a process with CustomPipeName is only available with PowerShell 6.2 and higher (current session is {runspaceVersion.Version}).");
}

PSCommand enterPSHostProcessCommand = new PSCommand()
.AddCommand("Enter-PSHostProcess")
.AddParameter("CustomPipeName", request.CustomPipeName);

try
{
await _executionService.ExecutePSCommandAsync(
enterPSHostProcessCommand,
cancellationToken,
PowerShellExecutionOptions.ImmediateInteractive)
.ConfigureAwait(false);
}
catch (Exception e)
{
string msg = $"Could not attach to process with CustomPipeName: '{request.CustomPipeName}'";
_logger.LogError(e, msg);
throw new RpcErrorException(0, msg);
}
_executionService.RunspaceChanged += RunspaceChangedHandler;
await AttachToPipe(request.CustomPipeName, cancellationToken).ConfigureAwait(false);
await runspaceChanged.Task.ConfigureAwait(false);
}
else if (request.ProcessId != "current")
else
{
_logger.LogError(
$"Attach request failed, '{request.ProcessId}' is an invalid value for the processId.");

throw new RpcErrorException(0, "A positive integer must be specified for the processId field.");
throw new RpcErrorException(0, null, "Invalid configuration with no process ID nor custom pipe name!");
}

await runspaceChanged.Task.ConfigureAwait(false);

// Execute the Debug-Runspace command but don't await it because it
// will block the debug adapter initialization process. The
// will block the debug adapter initialization process. The
// InitializedEvent will be sent as soon as the RunspaceChanged
// event gets fired with the attached runspace.

PSCommand debugRunspaceCmd = new PSCommand().AddCommand("Debug-Runspace");
if (request.RunspaceName != null)
if (!string.IsNullOrEmpty(request.RunspaceName))
{
PSCommand getRunspaceIdCommand = new PSCommand()
PSCommand psCommand = new PSCommand()
.AddCommand(@"Microsoft.PowerShell.Utility\Get-Runspace")
.AddParameter("Name", request.RunspaceName)
.AddCommand(@"Microsoft.PowerShell.Utility\Select-Object")
.AddParameter("ExpandProperty", "Id");

try
{
IEnumerable<int?> ids = await _executionService.ExecutePSCommandAsync<int?>(
getRunspaceIdCommand,
cancellationToken)
.ConfigureAwait(false);

foreach (int? id in ids)
{
_debugStateService.RunspaceId = id;
break;
IReadOnlyList<int> results = await _executionService.ExecutePSCommandAsync<int>(psCommand, cancellationToken).ConfigureAwait(false);

// TODO: If we don't end up setting this, we should throw
}
}
catch (Exception getRunspaceException)
if (results.Count == 0)
{
_logger.LogError(
getRunspaceException,
"Unable to determine runspace to attach to. Message: {message}",
getRunspaceException.Message);
throw new RpcErrorException(0, null, $"Could not find ID of runspace: {request.RunspaceName}");
}

// TODO: We have the ID, why not just use that?
debugRunspaceCmd.AddParameter("Name", request.RunspaceName);
// Translate the runspace name to the runspace ID.
request.RunspaceId = results[0];
}
else if (request.RunspaceId != null)
{
if (!int.TryParse(request.RunspaceId, out int runspaceId) || runspaceId <= 0)
{
_logger.LogError(
$"Attach request failed, '{request.RunspaceId}' is an invalid value for the processId.");

throw new RpcErrorException(0, "A positive integer must be specified for the RunspaceId field.");
}

_debugStateService.RunspaceId = runspaceId;

debugRunspaceCmd.AddParameter("Id", runspaceId);
}
else
if (request.RunspaceId < 1)
{
_debugStateService.RunspaceId = 1;

debugRunspaceCmd.AddParameter("Id", 1);
throw new RpcErrorException(0, null, "A positive integer must be specified for the RunspaceId!");
}

_debugStateService.RunspaceId = request.RunspaceId;
debugRunspaceCmd.AddParameter("Id", request.RunspaceId);

// Clear any existing breakpoints before proceeding
await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false);

Expand All @@ -438,11 +337,89 @@ await _executionService.ExecutePSCommandAsync(
.ExecutePSCommandAsync(debugRunspaceCmd, CancellationToken.None, PowerShellExecutionOptions.ImmediateInteractive)
.ContinueWith(OnExecutionCompletedAsync, TaskScheduler.Default);

if (runspaceVersion.Version.Major >= 7)
_debugStateService.ServerStarted.TrySetResult(true);

return new AttachResponse();
}

private async Task AttachToComputer(string computerName, CancellationToken cancellationToken)
{
_debugStateService.IsRemoteAttach = true;

if (_runspaceContext.CurrentRunspace.RunspaceOrigin != RunspaceOrigin.Local)
{
_debugStateService.ServerStarted.TrySetResult(true);
throw new RpcErrorException(0, null, "Cannot attach to a process in a remote session when already in a remote session!");
}

PSCommand psCommand = new PSCommand()
.AddCommand("Enter-PSSession")
.AddParameter("ComputerName", computerName);

try
{
await _executionService.ExecutePSCommandAsync(
psCommand,
cancellationToken,
PowerShellExecutionOptions.ImmediateInteractive)
.ConfigureAwait(false);
}
catch (Exception e)
{
string msg = $"Could not establish remote session to computer: {computerName}";
_logger.LogError(e, msg);
throw new RpcErrorException(0, null, msg);
}
}

private async Task AttachToProcess(int processId, CancellationToken cancellationToken)
{
PSCommand enterPSHostProcessCommand = new PSCommand()
.AddCommand(@"Microsoft.PowerShell.Core\Enter-PSHostProcess")
.AddParameter("Id", processId);

try
{
await _executionService.ExecutePSCommandAsync(
enterPSHostProcessCommand,
cancellationToken,
PowerShellExecutionOptions.ImmediateInteractive)
.ConfigureAwait(false);
}
catch (Exception e)
{
string msg = $"Could not attach to process with ID: {processId}";
_logger.LogError(e, msg);
throw new RpcErrorException(0, null, msg);
}
}

private async Task AttachToPipe(string pipeName, CancellationToken cancellationToken)
{
PowerShellVersionDetails runspaceVersion = _runspaceContext.CurrentRunspace.PowerShellVersionDetails;

if (runspaceVersion.Version < s_minVersionForCustomPipeName)
{
throw new RpcErrorException(0, null, $"Attaching to a process with CustomPipeName is only available with PowerShell 6.2 and higher. Current session is: {runspaceVersion.Version}");
}

PSCommand enterPSHostProcessCommand = new PSCommand()
.AddCommand(@"Microsoft.PowerShell.Core\Enter-PSHostProcess")
.AddParameter("CustomPipeName", pipeName);

try
{
await _executionService.ExecutePSCommandAsync(
enterPSHostProcessCommand,
cancellationToken,
PowerShellExecutionOptions.ImmediateInteractive)
.ConfigureAwait(false);
}
catch (Exception e)
{
string msg = $"Could not attach to process with CustomPipeName: {pipeName}";
_logger.LogError(e, msg);
throw new RpcErrorException(0, null, msg);
}
return new AttachResponse();
}

// PSES follows the following flow:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,20 @@ await _debugService.SetVariableAsync(

return new SetVariableResponse { Value = updatedValue };
}
catch (Exception ex) when (ex is ArgumentTransformationMetadataException or
catch (Exception e) when (e is ArgumentTransformationMetadataException or
InvalidPowerShellExpressionException or
SessionStateUnauthorizedAccessException)
{
// Catch common, innocuous errors caused by the user supplying a value that can't be converted or the variable is not settable.
_logger.LogTrace($"Failed to set variable: {ex.Message}");
throw new RpcErrorException(0, ex.Message);
string msg = $"Failed to set variable: {e.Message}";
_logger.LogTrace(msg);
throw new RpcErrorException(0, null, msg);
}
catch (Exception ex)
catch (Exception e)
{
_logger.LogError($"Unexpected error setting variable: {ex.Message}");
string msg =
$"Unexpected error: {ex.GetType().Name} - {ex.Message} Please report this error to the PowerShellEditorServices project on GitHub.";
throw new RpcErrorException(0, msg);
string msg = $"Unexpected error setting variable: {e.Message}";
_logger.LogError(msg);
throw new RpcErrorException(0, null, msg);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal interface IGetRunspaceHandler : IJsonRpcRequestHandler<GetRunspaceParam

internal class GetRunspaceParams : IRequest<RunspaceResponse[]>
{
public string ProcessId { get; set; }
public int ProcessId { get; set; }
}

internal class RunspaceResponse
Expand Down
Loading