Skip to content

META - Attach Enhancements #2246

Description

Prerequisites

  • I have written a descriptive issue title.
  • I have searched all issues to ensure it has not already been requested.

Summary

This is a meta issue I'm opening to hopefully explain some of the new feature requests I've opened and why.

I'm currently working on trying to add support for debugging Ansible modules written in PowerShell through a client like VSCode. My current approach is the following:

  • I have this launch configuration
{
"version": "0.2.0",
"configurations": [
{
"name": "Ansible PowerShell Debugger",
"type": "PowerShell",
"request": "launch",
"script": "/home/jborean/dev/ansible/bin/ansible-pwsh.ps1",
"args": [],
"cwd": "/home/jborean/dev/ansible",
"serverReadyAction": {
"action": "startDebugging",
"pattern": "Listening for debug request on port \\d+",
"name": "Ansible Launch Debugger Environment"
}
},
{
"name": "Ansible Launch Debugger Environment",
"type": "debugpy",
"request": "launch",
"program": "/home/jborean/dev/ansible/bin/ansible-debug.py",
"args": [
"win-hostname"
],
"console": "integratedTerminal",
"cwd": "/home/jborean/dev/ansible-tester",
},
],
}

This launch configuration starts a PowerShell script ansible-pwsh.ps1 which sets up a local socket and exposes it through a named pipe locally. It will listen for requests on the socket and setup an attach configuration to start the debug session.

ansible-pwsh.ps1
#!/usr/bin/env pwshusingnamespaceSystem.IOusingnamespaceSystem.IO.PipesusingnamespaceSystem.Management.Automation.LanguageusingnamespaceSystem.Net.SocketsusingnamespaceSystem.TextusingnamespaceSystem.ThreadingusingnamespaceSystem.Threading.Tasks#Requires -Version7.4
[CmdletBinding()]
param ()
$ErrorActionPreference='Stop'$configPath='~/.ansible/test/debugging'if (-not (Test-Path-LiteralPath $configPath)) {
New-Item-ItemType Directory -Path $configPath|Out-Null
}
$configFile=Join-Path-Path $configPath-ChildPath 'pwsh_debugger.json'$sock=$nulltry {
# Start listening to a socket locally that we will forward to the remote# host via SSH.$sock= [TcpListener]::new(
[IPAddress]::IPv6Loopback,0)
$sock.Server.DualMode=$true$sock.Start()
$configInfo=@{
pid=$PIDport=$sock.LocalEndpoint.Port
} |ConvertTo-Json-Compress
Set-Content-LiteralPath $configFile-Value $configInfoWrite-Host"Listening for debug request on port $($sock.LocalEndpoint.Port)"while ($true) {
$task=$sock.AcceptTcpClientAsync()
while (-not$task.AsyncWaitHandle.WaitOne(300)) { }
$client=$task.GetAwaiter().GetResult()
Write-Host"Client connected from $($client.Client.RemoteEndPoint)"$pipe=$stream=$nulltry {
$stream=$client.GetStream()
$reader= [StreamReader]::new($stream, [Encoding]::UTF8,$true,$client.ReceiveBufferSize,$true)
$task=$reader.ReadLineAsync()
while (-not$task.AsyncWaitHandle.WaitOne(300)) { }
$debugInfoRaw=$task.GetAwaiter().GetResult()
$reader.Dispose()
Write-Host"Received debug request`n$debugInfoRaw"$debugInfo=ConvertFrom-Json-InputObject $debugInfoRaw$pipeName="MyPipe-$(New-Guid)"$pipe= [NamedPipeServerStream]::new(
$pipeName,
[PipeDirection]::InOut,1,
[PipeTransmissionMode]::Byte,
[PipeOptions]::Asynchronous)
$task=$pipe.WaitForConnectionAsync()
Write-Host"Starting VSCode attach to Pipe $pipeName"$attachConfig=@{
name=$debugInfo.ModuleNametype="PowerShell"request="attach"customPipeName=$pipeNamerunspaceId=$debugInfo.RunspaceIdpathMappings=@($debugInfo.PathMappings)
}
$attachTask=Start-NewDebugSession-Request Attach -Configuration $attachConfigWrite-Host"Waiting for VSCode to attach to Pipe"while (-not$task.AsyncWaitHandle.WaitOne(300)) {}
$null=$task.GetAwaiter().GetResult()
Write-Host"VSCode attached to pipe. Sending back confirmation byte to debug session."$task=$stream.WriteAsync([byte[]]@(0),0,1)
while (-not$task.AsyncWaitHandle.WaitOne(300)) {}
$null=$task.GetAwaiter().GetResult()
# FIXME: SSH socket forwarding won't break the copy so need to find out how to detect that.Write-Host"Starting socket <-> pipe streaming"$writeTask=$pipe.CopyToAsync($stream)
$readTask=$stream.CopyToAsync($pipe)
Write-Host"Waiting for startDebugging attach response to arrive"while (-not$attachTask.AsyncWaitHandle.WaitOne(300)) { }
$null=$attachTask.GetAwaiter().GetResult()
$task= [Task]::WhenAny($writeTask,$readTask)
while (-not$task.AsyncWaitHandle.WaitOne(300)) {}
$finishedTask=$task.GetAwaiter().GetResult()
if ($finishedTask-eq$writeTask) {
Write-Host"VSCode disconnected from Pipe $pipeName and RunspaceId $runspaceId"
}
elseif ($finishedTask-eq$readTask) {
Write-Host"Socket disconnected from Pipe $pipeName and RunspaceId $runspaceId"
}
else {
Write-Host"Unknown task finished for Pipe $pipeName and RunspaceId $runspaceId"
}
}
finally {
${pipe}?.Dispose()
${stream}?.Dispose()
$client.Dispose()
}
}
}
finally {
if (Test-Path-LiteralPath $configFile) {
Remove-Item-LiteralPath $configFile-Force
}
${sock}?.Dispose()
}

The ansible-debug.py script is a simple script that will forward that local socket to the Windows host through SSH and setup some env var that Ansible uses to read the PowerShell debug information.

ansible-debug.py
#!/usr/bin/env python# PYTHON_ARGCOMPLETE_OKfrom __future__ importannotationsimportargparseimportcontextlibimportjsonimportosimportpathlibimportreimportsubprocessimportsysimporttypingastHAS_ARGCOMPLETE=Falsetry:
importargcompleteHAS_ARGCOMPLETE=TrueexceptImportError:
pass_PORT_ALLOC_PATTERN=re.compile(r'Allocated port (\d+) for remote forward to .*')
def_parse_args(args: list[str]) ->argparse.Namespace:
parser=argparse.ArgumentParser(description="Ansible Module Debugging CLI")
parser.add_argument(
'hostname',
nargs=1,
type=str,
help="The SSH hostname to forward the debug ports to. This should include username and/or port if necessary.",
)
ifHAS_ARGCOMPLETE:
argcomplete.autocomplete(parser)
returnparser.parse_args(args)
@contextlib.contextmanagerdefforward_ports(
hostname: str,
local_port: int,
) ->t.Generator[int]:
ssh_cmd= [
'ssh',
'-v', # Verbose output to capture port allocation'-NT', # Don't execute remote command, disable PTY allocation'-o', 'ExitOnForwardFailure=yes',
'-R', f'localhost:0:localhost:{local_port}',
hostname
]
# Start SSH process with stderr captured for debug outputprocess=subprocess.Popen(
ssh_cmd,
stderr=subprocess.STDOUT,
stdout=subprocess.PIPE,
text=True,
)
try:
assertprocess.stdoutisnotNoneproc_out= []
port=0forlineinprocess.stdout:
ifmatch:=_PORT_ALLOC_PATTERN.match(line):
port=int(match.group(1))
breakproc_out.append(line)
ifport==0:
raiseException(f"SSH failed to forward ports. Output:\n{'\n'.join(proc_out)}")
yieldportfinally:
process.terminate()
defmain() ->None:
args=_parse_args(sys.argv[1:])
config_path=pathlib.Path.home() /'.ansible'/'test'/'debugging'/'pwsh_debugger.json'ifnotconfig_path.exists():
raiseException(f"PowerShell debugger configuration file not found. Please ensure it exists at {config_path.absolute()}")
debug_info=json.loads(config_path.read_text())
local_port=debug_info.get('port', 0)
ifnotlocal_port:
raiseException("No local port specified in the PowerShell debugger configuration file.")
withforward_ports(args.hostname[0], local_port=local_port) asremote_port:
debug_options= {
'wait': False,
'host': 'localhost',
'port': remote_port,
}
new_env=os.environ.copy() | {
'_ANSIBLE_ANSIBALLZ_PWSH_DEBUGGER_CONFIG': json.dumps(debug_options),
}
res=subprocess.run('/bin/bash', env=new_env, check=False)
sys.exit(res.returncode)
if__name__=='__main__':
main()

From there when Ansible starts the PowerShell process on the Windows host, it'll connect to the socket, send the runspace/debug information over that socket, redirect any socket data to the local named pipe for that process and wait for VSCode to "attach".

From a workflow perspective this works fine but it has required a few changes to PSES to get working locally. These changes are mentioned in the following issues:

Proposed Design

See the linked issues.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-EnhancementA feature request (enhancement).Needs: TriageMaintainer attention needed!

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions