0
0
mirror of https://github.com/vim/vim.git synced 2025-10-16 07:24:23 -04:00

runtime(termdebug): Add remote debugging capabilities

closes: #18429

Co-authored-by: Christian Brabandt <cb@256bit.org>
Signed-off-by: Miguel Barro <miguel.barro@live.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Miguel Barro
2025-10-08 18:15:51 +00:00
committed by Christian Brabandt
parent 143686b3c4
commit 3c5221f8ee
4 changed files with 475 additions and 52 deletions

View File

@@ -10952,8 +10952,12 @@ termdebug-example terminal.txt /*termdebug-example*
termdebug-frames terminal.txt /*termdebug-frames* termdebug-frames terminal.txt /*termdebug-frames*
termdebug-mappings terminal.txt /*termdebug-mappings* termdebug-mappings terminal.txt /*termdebug-mappings*
termdebug-prompt terminal.txt /*termdebug-prompt* termdebug-prompt terminal.txt /*termdebug-prompt*
termdebug-remote terminal.txt /*termdebug-remote*
termdebug-remote-example terminal.txt /*termdebug-remote-example*
termdebug-remote-window terminal.txt /*termdebug-remote-window*
termdebug-starting terminal.txt /*termdebug-starting* termdebug-starting terminal.txt /*termdebug-starting*
termdebug-stepping terminal.txt /*termdebug-stepping* termdebug-stepping terminal.txt /*termdebug-stepping*
termdebug-substitute-path terminal.txt /*termdebug-substitute-path*
termdebug-timeout terminal.txt /*termdebug-timeout* termdebug-timeout terminal.txt /*termdebug-timeout*
termdebug-variables terminal.txt /*termdebug-variables* termdebug-variables terminal.txt /*termdebug-variables*
termdebug_contributing terminal.txt /*termdebug_contributing* termdebug_contributing terminal.txt /*termdebug_contributing*

View File

@@ -1,4 +1,4 @@
*terminal.txt* For Vim version 9.1. Last change: 2025 Sep 15 *terminal.txt* For Vim version 9.1. Last change: 2025 Oct 08
VIM REFERENCE MANUAL by Bram Moolenaar VIM REFERENCE MANUAL by Bram Moolenaar
@@ -44,6 +44,7 @@ If the result is "1" you have it.
Prompt mode |termdebug-prompt| Prompt mode |termdebug-prompt|
Mappings |termdebug-mappings| Mappings |termdebug-mappings|
Communication |termdebug-communication| Communication |termdebug-communication|
Remote Debugging |termdebug-remote|
Customizing |termdebug-customizing| Customizing |termdebug-customizing|
{only available when compiled with the |+terminal| feature} {only available when compiled with the |+terminal| feature}
@@ -1635,12 +1636,103 @@ interrupt the running program. But after using the MI command
communication channel. communication channel.
Remote debugging ~
*termdebug-remote*
One of the main issues of remote debugging is the access to the debuggee's
source files. The plugin can profit from system and vim's networking
capabilities to workaround this.
*termdebug-remote-example*
The |termdebug-example| can be replicated by running the `gdb` debugger to
debug Vim on a remote Linux machine accessible via `ssh`.
- Build Vim as explained in the local example.
- If "socat" is not available in the remote machine 'terminal' mode will not
work properly. Fall back to |termdebug_use_prompt|: >
:let g:termdebug_config = {}
:let g:termdebug_config['use_prompt'] = v:true
- Specify the command line to run the remote `gdb` instance: >
:let g:termdebug_config['command'] = ['ssh', 'hostname', 'gdb']
< Explaining `ssh` is beyond the scope of this example, but notice the
command line can be greatly simplified by specifying the user, keys and
other options into the `$HOME/.ssh/config` file.
- Provide a hint for translating remote paths into |netrw| paths: >
:let g:termdebug_config['substitute_path'] = { '/': 'scp://hostname//' }
- Load the termdebug plugin and start debugging Vim: >
:packadd termdebug
:Termdebug vim
You now have the same three windows of the local example and can follow the
very same steps. The only difference is that the source windows displays a
netrw buffer instead of a local one.
*termdebug-substitute-path*
Use the `g:termdebug_config['substitute_path']` entry to map remote to local
files using the same strategy that gdb's `substitute-path` command uses.
For example:
- Use |netrw| to access files remoting via ssh: >
let g:termdebug_config['command'] = ['ssh', 'hostname', 'gdb']
let g:termdebug_config['substitute_path'] = { '/': 'scp://hostname//' }
< Note: that the key specifies the remote machine root path and the value
the local one.
- Use Windows' `UNC` paths to access `WSL2` sources: >
let g:termdebug_config['command'] = ['wsl', 'gdb']
let g:termdebug_config['substitute_path'] = {
\ '/': '\\wsl.localhost\Ubuntu-22.04\',
\ '/mnt/c/': 'C:/' }
< Note: that several mappings are required: one for each drive unit
and one for the linux filesystem (queried via `wslpath`).
In this mode any `ssh` or `wsl` command would be detected and a similar
command would be used to launch `socat` in a remote `tty` terminal session
and connect it to `gdb`.
If `socat` is not available a plain remote terminal would be used as
fallback.
The next session shows how to override this default behaviour.
*termdebug-remote-window*
In order to use another remote terminal client, set "remote_window" entry
in `g:termdebug_config` variable before invoking `:Termdebug`. For example:
- Debugging inside a docker container using "prompt" mode: >
let g:termdebug_config['use_prompt'] = v:true
let g:termdebug_config['command'] = ['docker', 'run', '-i',
\ '--rm', '--name', 'container-name', 'image-name', 'gdb']
let g:termdebug_config['remote_window'] =
\ ['docker', 'exec', '-ti', 'container-name'
\ ,'socat', '-dd', '-', 'PTY,raw,echo=0']
- Debugging inside a docker container using a "terminal buffer".
The container should be already running because unlike the previous
case for `terminal mode` "program" and "communication" ptys are created
before the gdb one: >
$ docker run -ti --rm --name container-name immage-name
< Then, launch the debugger: >
let g:termdebug_config['use_prompt'] = v:false " default
let g:termdebug_config['command'] =
\ ['docker', 'exec', '-ti', 'container-name', 'gdb']
let g:termdebug_config['remote_window'] =
\ ['docker', 'exec', '-ti', 'container-name'
\ ,'socat', '-dd', '-', 'PTY,raw,echo=0']
Note: "command" cannot use `-t` on |termdebug-prompt| mode because prompt
buffers cannot handle `tty` connections.
The "remote_window" command must use `-t` because otherwise it will lack
a `pty slave device` for gdb to connect.
Note: "socat" must be available in the remote machine on "terminal" mode.
Note: docker container sources can be accessible combining `volumes`
with mappings (see |termdebug-substitute-path|).
GDB command ~ GDB command ~
*g:termdebugger* *g:termdebugger*
To change the name of the gdb command, set "debugger" entry in To change the name of the gdb command, set "debugger" entry in
g:termdebug_config or the "g:termdebugger" variable before invoking g:termdebug_config or the "g:termdebugger" variable before invoking
`:Termdebug`: > `:Termdebug`: >
let g:termdebug_config['command'] = "mygdb" let g:termdebug_config['command'] = "mygdb"
If there is no g:termdebug_config you can use: > If there is no g:termdebug_config you can use: >
let g:termdebugger = "mygdb" let g:termdebugger = "mygdb"
@@ -1648,6 +1740,7 @@ However, the latter form will be deprecated in future releases.
If the command needs an argument use a List: > If the command needs an argument use a List: >
let g:termdebug_config['command'] = ['rr', 'replay', '--'] let g:termdebug_config['command'] = ['rr', 'replay', '--']
If there is no g:termdebug_config you can use: > If there is no g:termdebug_config you can use: >
let g:termdebugger = ['rr', 'replay', '--'] let g:termdebugger = ['rr', 'replay', '--']
@@ -1655,6 +1748,13 @@ Several arguments will be added to make gdb work well for the debugger.
If you want to modify them, add a function to filter the argument list: > If you want to modify them, add a function to filter the argument list: >
let g:termdebug_config['command_filter'] = MyDebugFilter let g:termdebug_config['command_filter'] = MyDebugFilter
A "command_filter" scenario is solving escaping issues on remote debugging
over "ssh". For convenience a default filter is provided for escaping
whitespaces inside the arguments. It is automatically configured for "ssh",
but can be employed in other use cases like this: >
let g:termdebug_config['command_filter'] =
/ function('g:Termdebug_escape_whitespace')
If you do not want the arguments to be added, but you do need to set the If you do not want the arguments to be added, but you do need to set the
"pty", use a function to add the necessary arguments: > "pty", use a function to add the necessary arguments: >
let g:termdebug_config['command_add_args'] = MyAddArguments let g:termdebug_config['command_add_args'] = MyAddArguments
@@ -1717,7 +1817,8 @@ than 99 will be displayed as "9+".
If you want to customize the breakpoint signs to show `>>` in the signcolumn: > If you want to customize the breakpoint signs to show `>>` in the signcolumn: >
let g:termdebug_config['sign'] = '>>' let g:termdebug_config['sign'] = '>>'
You can also specify individual signs for the first several breakpoints: > You can also specify individual signs for the first several breakpoints: >
let g:termdebug_config['signs'] = ['>1', '>2', '>3', '>4', '>5', '>6', '>7', '>8', '>9'] let g:termdebug_config['signs'] = ['>1', '>2', '>3', '>4', '>5',
\ '>6', '>7', '>8', '>9']
let g:termdebug_config['sign'] = '>>' let g:termdebug_config['sign'] = '>>'
If you would like to use decimal (base 10) breakpoint signs: > If you would like to use decimal (base 10) breakpoint signs: >
let g:termdebug_config['sign_decimal'] = 1 let g:termdebug_config['sign_decimal'] = 1

View File

@@ -4,7 +4,7 @@ vim9script
# Author: Bram Moolenaar # Author: Bram Moolenaar
# Copyright: Vim license applies, see ":help license" # Copyright: Vim license applies, see ":help license"
# Last Change: 2025 Sep 15 # Last Change: 2025 Oct 08
# Converted to Vim9: Ubaldo Tiberi <ubaldo.tiberi@gmail.com> # Converted to Vim9: Ubaldo Tiberi <ubaldo.tiberi@gmail.com>
# WORK IN PROGRESS - The basics works stable, more to come # WORK IN PROGRESS - The basics works stable, more to come
@@ -34,6 +34,7 @@ vim9script
# Gdb is run as a job with callbacks for I/O. # Gdb is run as a job with callbacks for I/O.
# On Unix another terminal window is opened to run the debugged program # On Unix another terminal window is opened to run the debugged program
# On MS-Windows a separate console is opened to run the debugged program # On MS-Windows a separate console is opened to run the debugged program
# but a terminal window is used to run remote debugged programs.
# The communication with gdb uses GDB/MI. See: # The communication with gdb uses GDB/MI. See:
# https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html # https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html
@@ -435,8 +436,73 @@ def IsGdbStarted(): bool
return true return true
enddef enddef
def CreateProgramPty(): string # Check if the debugger is running remotely and return a suitable command to pty remotely
ptybufnr = term_start('NONE', { def GetRemotePtyCmd(gdb_cmd: list<string>): list<string>
# Check if the user provided a command to launch the program window
var term_cmd = null_list
if exists('g:termdebug_config') && has_key(g:termdebug_config, 'remote_window')
term_cmd = g:termdebug_config['remote_window']
term_cmd = type(term_cmd) == v:t_list ? copy(term_cmd) : [term_cmd]
else
# Check if it is a remote gdb, the program terminal should be started
# on the remote machine.
const remote_pattern = '^\(ssh\|wsl\)'
if gdb_cmd[0] =~? remote_pattern
var gdb_pos = indexof(gdb_cmd, $'v:val =~? "^{GetCommand()[-1]}"')
if gdb_pos > 0
# strip debugger call
term_cmd = gdb_cmd[0 : gdb_pos - 1]
# roundtrip to check if socat is available on the remote side
silent call system(join(term_cmd, ' ') .. ' socat -h')
if v:shell_error
Echowarn('Install socat on the remote machine for a program window better experience')
else
# create a devoted tty slave device and link to stdin/stdout
term_cmd += ['socat', '-dd', '-', 'PTY,raw,echo=0']
ch_log($'launching remote ttys using "{join(term_cmd)}"')
endif
endif
endif
endif
return term_cmd
enddef
# Retrieve the remote pty device from a remote terminal
# If interact is true, use remote tty command to get the pty device
def GetRemotePtyDev(bufnr: number, interact: bool): string
var pty: string = null_string
var line = null_string
for j in range(5)
if interact
term_sendkeys(bufnr, "tty\<CR>")
endif
for i in range(0, term_getsize(bufnr)[0])
line = term_getline(bufnr, i)
if line =~? "/dev/pts"
pty = line
break
endif
term_wait(bufnr, 100)
endfor # i
if pty != null_string
# Clear the terminal window
if interact
term_sendkeys(bufnr, "clear\<CR>")
endif
break
endif
endfor # j
return pty
enddef
def CreateProgramPty(cmd: list<string> = null_list): string
ptybufnr = term_start(!cmd ? 'NONE' : cmd, {
term_name: ptybufname, term_name: ptybufname,
vertical: vvertical}) vertical: vvertical})
if ptybufnr == 0 if ptybufnr == 0
@@ -454,20 +520,76 @@ def CreateProgramPty(): string
endif endif
endif endif
return job_info(term_getjob(ptybufnr))['tty_out'] if !cmd
return job_info(term_getjob(ptybufnr))['tty_out']
else
var interact = indexof(cmd, 'v:val =~? "^socat"') < 0
var pty = GetRemotePtyDev(ptybufnr, interact)
if pty !~? "/dev/pts"
Echoerr('Failed to get the program window tty')
exe $'bwipe! {ptybufnr}'
pty = null_string
elseif pty !~? "^/dev/pts"
# remove the prompt
pty = pty->matchstr('/dev/pts/\d\+')
endif
return pty
endif
enddef enddef
def CreateCommunicationPty(): string def CreateCommunicationPty(cmd: list<string> = null_list): string
# Create a hidden terminal window to communicate with gdb # Create a hidden terminal window to communicate with gdb
commbufnr = term_start('NONE', { var options: dict<any> = { term_name: commbufname, out_cb: CommOutput, hidden: 1 }
term_name: commbufname,
out_cb: CommOutput, if !cmd
hidden: 1 commbufnr = term_start('NONE', options)
}) else
# avoid message wrapping that prevents proper parsing
options['term_cols'] = 500
commbufnr = term_start(cmd, options)
endif
if commbufnr == 0 if commbufnr == 0
return null_string return null_string
endif endif
return job_info(term_getjob(commbufnr))['tty_out']
if !cmd
return job_info(term_getjob(commbufnr))['tty_out']
else
# CommunicationPty only will be reliable with socat
if indexof(cmd, 'v:val =~? "^socat"') < 0
Echoerr('Communication window should be started with socat')
exe $'bwipe! {commbufnr}'
return null_string
endif
var pty = GetRemotePtyDev(commbufnr, false)
if pty !~? "/dev/pts"
Echoerr('Failed to get the communication window tty')
exe $'bwipe! {commbufnr}'
pty = null_string
elseif pty !~? "^/dev/pts"
# remove the prompt
pty = pty->matchstr('/dev/pts/\d\+')
endif
return pty
endif
enddef
# Convenient filter to workaround remote escaping issues.
# For example, ssh doesn't escape spaces for the gdb arguments.
# Workaround doing:
# let g:termdebug_config['command_filter'] = function('g:Termdebug_escape_whitespace')
def g:Termdebug_escape_whitespace(args: list<string>): list<string>
var new_args: list<string> = []
for arg in args
new_args += [substitute(arg, ' ', '\\ ', 'g')]
endfor
return new_args
enddef enddef
def CreateGdbConsole(dict: dict<any>, pty: string, commpty: string): string def CreateGdbConsole(dict: dict<any>, pty: string, commpty: string): string
@@ -497,6 +619,12 @@ def CreateGdbConsole(dict: dict<any>, pty: string, commpty: string): string
gdb_cmd += ['-ex', 'echo startupdone\n'] gdb_cmd += ['-ex', 'echo startupdone\n']
endif endif
# Escape whitespaces in the gdb arguments for ssh remoting
if exists('g:termdebug_config') && !has_key(g:termdebug_config, 'command_filter') &&
gdb_cmd[0] =~? '^ssh'
g:termdebug_config['command_filter'] = function('g:Termdebug_escape_whitespace')
endif
if exists('g:termdebug_config') && has_key(g:termdebug_config, 'command_filter') if exists('g:termdebug_config') && has_key(g:termdebug_config, 'command_filter')
gdb_cmd = g:termdebug_config.command_filter(gdb_cmd) gdb_cmd = g:termdebug_config.command_filter(gdb_cmd)
endif endif
@@ -599,14 +727,18 @@ enddef
# Open a terminal window without a job, to run the debugged program in. # Open a terminal window without a job, to run the debugged program in.
def StartDebug_term(dict: dict<any>) def StartDebug_term(dict: dict<any>)
var programpty = CreateProgramPty() # Retrieve command if remote pty is needed
var gdb_cmd = GetCommand()
var term_cmd = GetRemotePtyCmd(gdb_cmd)
var programpty = CreateProgramPty(term_cmd)
if programpty is null_string if programpty is null_string
Echoerr('Failed to open the program terminal window') Echoerr('Failed to open the program terminal window')
CloseBuffers() CloseBuffers()
return return
endif endif
var commpty = CreateCommunicationPty() var commpty = CreateCommunicationPty(term_cmd)
if commpty is null_string if commpty is null_string
Echoerr('Failed to open the communication terminal window') Echoerr('Failed to open the communication terminal window')
CloseBuffers() CloseBuffers()
@@ -656,16 +788,27 @@ def StartDebug_prompt(dict: dict<any>)
var gdb_args = get(dict, 'gdb_args', []) var gdb_args = get(dict, 'gdb_args', [])
var proc_args = get(dict, 'proc_args', []) var proc_args = get(dict, 'proc_args', [])
# Add -quiet to avoid the intro message causing a hit-enter prompt. # directly communicate via mi2. This option must precede any -iex options for proper
gdb_cmd += ['-quiet'] # interpretation.
gdb_cmd += ['--interpreter=mi2']
# Disable pagination, it causes everything to stop at the gdb, needs to be run early # Disable pagination, it causes everything to stop at the gdb, needs to be run early
gdb_cmd += ['-iex', 'set pagination off'] gdb_cmd += ['-iex', 'set pagination off']
# Interpret commands while the target is running. This should usually only # Interpret commands while the target is running. This should usually only
# be exec-interrupt, since many commands don't work properly while the # be exec-interrupt, since many commands don't work properly while the
# target is running (so execute during startup). # target is running (so execute during startup).
gdb_cmd += ['-iex', 'set mi-async on'] gdb_cmd += ['-iex', 'set mi-async on']
# directly communicate via mi2 # Add -quiet to avoid the intro message causing a hit-enter prompt.
gdb_cmd += ['--interpreter=mi2'] gdb_cmd += ['-quiet']
# Escape whitespaces in the gdb arguments for ssh remoting
if exists('g:termdebug_config') && !has_key(g:termdebug_config, 'command_filter') &&
gdb_cmd[0] =~? '^ssh'
g:termdebug_config['command_filter'] = function('g:Termdebug_escape_whitespace')
endif
if exists('g:termdebug_config') && has_key(g:termdebug_config, 'command_filter')
gdb_cmd = g:termdebug_config.command_filter(gdb_cmd)
endif
# Adding arguments requested by the user # Adding arguments requested by the user
gdb_cmd += gdb_args gdb_cmd += gdb_args
@@ -686,24 +829,61 @@ def StartDebug_prompt(dict: dict<any>)
set modified set modified
gdb_channel = job_getchannel(gdbjob) gdb_channel = job_getchannel(gdbjob)
ptybufnr = 0 # Retrieve command if remote pty is needed
if has('win32') var term_cmd = GetRemotePtyCmd(gdb_cmd)
# MS-Windows: run in a new console window for maximum compatibility
SendCommand('set new-console on') # If we are not using socat maybe is a shell:
elseif has('terminal') var interact = indexof(term_cmd, 'v:val =~? "^socat"') < 0
# Unix: Run the debugged program in a terminal window. Open it below the
# gdb window. if has('terminal') && (term_cmd != null || !has('win32'))
belowright ptybufnr = term_start('NONE', {
term_name: 'debugged program', # Try open terminal twice because sync with gdbjob may not succeed
vertical: vvertical # the first time (docker daemon for example)
}) var trials: number = 2
if ptybufnr == 0 var pty: string = null_string
Echoerr('Failed to open the program terminal window')
while trials > 0
# Run the debugged program in a window. Open it below the
# gdb window.
belowright ptybufnr = term_start(
term_cmd != null ? term_cmd : 'NONE', {
term_name: 'debugged program',
vertical: vvertical
})
if ptybufnr == 0
Echoerr('Failed to open the program terminal window')
job_stop(gdbjob)
return
endif
ptywin = win_getid()
if term_cmd is null
pty = job_info(term_getjob(ptybufnr))['tty_out']
else
# Retrieve remote pty value
pty = GetRemotePtyDev(ptybufnr, interact)
endif
if pty !~? "/dev/pts"
exe $'bwipe! {ptybufnr}'
--trials
pty = null_string
else
break
endif
endwhile
if pty !~? "/dev/pts"
Echoerr('Failed to get the program windows tty')
job_stop(gdbjob) job_stop(gdbjob)
return elseif pty !~? "^/dev/pts"
# remove the prompt
pty = pty->matchstr('/dev/pts/\d\+')
endif endif
ptywin = win_getid()
var pty = job_info(term_getjob(ptybufnr))['tty_out']
SendCommand($'tty {pty}') SendCommand($'tty {pty}')
# Since GDB runs in a prompt window, the environment has not been set to # Since GDB runs in a prompt window, the environment has not been set to
@@ -714,6 +894,9 @@ def StartDebug_prompt(dict: dict<any>)
SendCommand($'set env COLUMNS = {winwidth(ptywin)}') SendCommand($'set env COLUMNS = {winwidth(ptywin)}')
SendCommand($'set env COLORS = {&t_Co}') SendCommand($'set env COLORS = {&t_Co}')
SendCommand($'set env VIM_TERMINAL = {v:version}') SendCommand($'set env VIM_TERMINAL = {v:version}')
elseif has('win32')
# MS-Windows: run in a new console window for maximum compatibility
SendCommand('set new-console on')
else else
# TODO: open a new terminal, get the tty name, pass on to gdb # TODO: open a new terminal, get the tty name, pass on to gdb
SendCommand('show inferior-tty') SendCommand('show inferior-tty')
@@ -930,7 +1113,7 @@ enddef
const NullRepl = 'XXXNULLXXX' const NullRepl = 'XXXNULLXXX'
# Extract the "name" value from a gdb message with fullname="name". # Extract the "name" value from a gdb message with fullname="name".
def GetFullname(msg: string): string def GetLocalFullname(msg: string): string
if msg !~ 'fullname' if msg !~ 'fullname'
return '' return ''
endif endif
@@ -944,6 +1127,50 @@ def GetFullname(msg: string): string
return name return name
enddef enddef
# Turn a remote machine local path into a remote one.
def Local2RemotePath(path: string): string
# If no mappings are provided keep the path.
if !exists('g:termdebug_config') || !has_key(g:termdebug_config, 'substitute_path')
return path
endif
var mappings: list<any> = items(g:termdebug_config['substitute_path'])
# Try to match the longest local path first.
sort(mappings, (a, b) => len(b[0]) - len(a[0]))
for [local, remote] in mappings
const pattern = '^' .. escape(local, '\.*~()')
if path =~ pattern
return substitute(path, pattern, escape(remote, '\.*~()'), '')
endif
endfor
return path
enddef
# Turn a remote path into a local one to the remote machine.
def Remote2LocalPath(path: string): string
# If no mappings are provided keep the path.
if !exists('g:termdebug_config') || !has_key(g:termdebug_config, 'substitute_path')
return path
endif
var mappings: list<any> = items(g:termdebug_config['substitute_path'])
# Try to match the longest remote path first.
sort(mappings, (a, b) => len(b[1]) - len(a[1]))
for [local, remote] in mappings
const pattern = '^' .. escape(substitute(remote, '[\/]', '[\\/]', 'g'), '.*~()')
if path =~ pattern
return substitute(path, pattern, local, '')
endif
endfor
return path
enddef
# Extract the "addr" value from a gdb message with addr="0x0001234". # Extract the "addr" value from a gdb message with addr="0x0001234".
def GetAsmAddr(msg: string): string def GetAsmAddr(msg: string): string
if msg !~ 'addr=' if msg !~ 'addr='
@@ -1159,7 +1386,7 @@ def CommOutput(chan: channel, message: string)
enddef enddef
def GotoProgram() def GotoProgram()
if has('win32') if has('win32') && !ptywin
if executable('powershell') if executable('powershell')
system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', pid)) system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', pid))
endif endif
@@ -1374,7 +1601,8 @@ def Until(at: string)
ch_log('assume that program is running after this command') ch_log('assume that program is running after this command')
# Use the fname:lnum format # Use the fname:lnum format
var AT = empty(at) ? QuoteArg($"{expand('%:p')}:{line('.')}") : at var fname = Remote2LocalPath(expand('%:p'))
var AT = empty(at) ? QuoteArg($"{fname}:{line('.')}") : at
SendCommand($'-exec-until {AT}') SendCommand($'-exec-until {AT}')
else else
ch_log('dropping command, program is running: exec-until') ch_log('dropping command, program is running: exec-until')
@@ -1393,7 +1621,8 @@ def SetBreakpoint(at: string, tbreak=false)
endif endif
# Use the fname:lnum format, older gdb can't handle --source. # Use the fname:lnum format, older gdb can't handle --source.
var AT = empty(at) ? QuoteArg($"{expand('%:p')}:{line('.')}") : at var fname = Remote2LocalPath(expand('%:p'))
var AT = empty(at) ? QuoteArg($"{fname}:{line('.')}") : at
var cmd = '' var cmd = ''
if tbreak if tbreak
cmd = $'-break-insert -t {AT}' cmd = $'-break-insert -t {AT}'
@@ -1407,7 +1636,8 @@ def SetBreakpoint(at: string, tbreak=false)
enddef enddef
def ClearBreakpoint() def ClearBreakpoint()
var fname = fnameescape(expand('%:p')) var fname = Remote2LocalPath(expand('%:p'))
fname = fnameescape(fname)
var lnum = line('.') var lnum = line('.')
var bploc = printf('%s:%d', fname, lnum) var bploc = printf('%s:%d', fname, lnum)
var nr = 0 var nr = 0
@@ -1444,7 +1674,8 @@ def ClearBreakpoint()
enddef enddef
def ToggleBreak() def ToggleBreak()
var fname = fnameescape(expand('%:p')) var fname = Remote2LocalPath(expand('%:p'))
fname = fnameescape(fname)
var lnum = line('.') var lnum = line('.')
var bploc = printf('%s:%d', fname, lnum) var bploc = printf('%s:%d', fname, lnum)
if has_key(breakpoint_locations, bploc) if has_key(breakpoint_locations, bploc)
@@ -1859,7 +2090,7 @@ def HandleCursor(msg: string)
var fname = '' var fname = ''
if msg =~ 'fullname=' if msg =~ 'fullname='
fname = GetFullname(msg) fname = GetLocalFullname(msg)
endif endif
if msg =~ 'addr=' if msg =~ 'addr='
@@ -1887,12 +2118,15 @@ def HandleCursor(msg: string)
SendCommand('-stack-list-variables 2') SendCommand('-stack-list-variables 2')
endif endif
if msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) # Translate to remote file name if needed.
const fremote = Local2RemotePath(fname)
if msg =~ '^\(\*stopped\|=thread-selected\)' && (fremote != fname || filereadable(fname))
var lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '') var lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
if lnum =~ '^[0-9]*$' if lnum =~ '^[0-9]*$'
GotoSourcewinOrCreateIt() GotoSourcewinOrCreateIt()
if expand('%:p') != fnamemodify(fname, ':p') if expand('%:p') != fnamemodify(fremote, ':p')
echomsg $"different fname: '{expand('%:p')}' vs '{fnamemodify(fname, ':p')}'" echomsg $"different fname: '{expand('%:p')}' vs '{fnamemodify(fremote, ':p')}'"
augroup Termdebug augroup Termdebug
# Always open a file read-only instead of showing the ATTENTION # Always open a file read-only instead of showing the ATTENTION
# prompt, since it is unlikely we want to edit the file. # prompt, since it is unlikely we want to edit the file.
@@ -1904,11 +2138,11 @@ def HandleCursor(msg: string)
augroup END augroup END
if &modified if &modified
# TODO: find existing window # TODO: find existing window
exe $'split {fnameescape(fname)}' exe $'split {fnameescape(fremote)}'
sourcewin = win_getid() sourcewin = win_getid()
InstallWinbar(false) InstallWinbar(false)
else else
exe $'edit {fnameescape(fname)}' exe $'edit {fnameescape(fremote)}'
endif endif
augroup Termdebug augroup Termdebug
au! SwapExists au! SwapExists
@@ -1917,7 +2151,7 @@ def HandleCursor(msg: string)
exe $":{lnum}" exe $":{lnum}"
normal! zv normal! zv
sign_unplace('TermDebug', {id: pc_id}) sign_unplace('TermDebug', {id: pc_id})
sign_place(pc_id, 'TermDebug', 'debugPC', fname, sign_place(pc_id, 'TermDebug', 'debugPC', fremote,
{lnum: str2nr(lnum), priority: 110}) {lnum: str2nr(lnum), priority: 110})
if !exists('b:save_signcolumn') if !exists('b:save_signcolumn')
b:save_signcolumn = &signcolumn b:save_signcolumn = &signcolumn
@@ -1991,10 +2225,11 @@ def HandleNewBreakpoint(msg: string, modifiedFlag: bool)
endif endif
for mm in SplitMsg(msg) for mm in SplitMsg(msg)
var fname = GetFullname(mm) var fname = GetLocalFullname(mm)
if empty(fname) if empty(fname)
continue continue
endif endif
var fremote = Local2RemotePath(fname)
nr = substitute(mm, '.*number="\([0-9.]*\)\".*', '\1', '') nr = substitute(mm, '.*number="\([0-9.]*\)\".*', '\1', '')
if empty(nr) if empty(nr)
return return
@@ -2034,11 +2269,11 @@ def HandleNewBreakpoint(msg: string, modifiedFlag: bool)
endif endif
var posMsg = '' var posMsg = ''
if bufloaded(fname) if bufloaded(fremote)
PlaceSign(id, subid, entry) PlaceSign(id, subid, entry)
posMsg = $' at line {lnum}.' posMsg = $' at line {lnum}.'
else else
posMsg = $' in {fname} at line {lnum}.' posMsg = $' in {fremote} at line {lnum}.'
endif endif
var actionTaken = '' var actionTaken = ''
if !modifiedFlag if !modifiedFlag
@@ -2055,8 +2290,9 @@ enddef
def PlaceSign(id: number, subid: number, entry: dict<any>) def PlaceSign(id: number, subid: number, entry: dict<any>)
var nr = printf('%d.%d', id, subid) var nr = printf('%d.%d', id, subid)
var remote = Local2RemotePath(entry['fname'])
sign_place(Breakpoint2SignNumber(id, subid), 'TermDebug', sign_place(Breakpoint2SignNumber(id, subid), 'TermDebug',
$'debugBreakpoint{nr}', entry['fname'], $'debugBreakpoint{nr}', remote,
{lnum: entry['lnum'], priority: 110}) {lnum: entry['lnum'], priority: 110})
entry['placed'] = 1 entry['placed'] = 1
enddef enddef

View File

@@ -689,4 +689,86 @@ func Test_termdebug_toggle_break()
%bw! %bw!
endfunc endfunc
" Check substitution capabilities and simulate remote debugging
func Test_termdebug_remote_basic()
let bin_name = 'XTD_basicremote'
let src_name = bin_name .. '.c'
call s:generate_files(bin_name)
defer s:cleanup_files(bin_name)
" Duplicate sources to test the mapping
const pwd = getcwd()
const src_shadow_dir = "shadow"
call mkdir(src_shadow_dir)
const src_shadow_file = $"{src_shadow_dir}/{src_name}"
call filecopy(src_name, src_shadow_file)
defer delete(src_shadow_dir, 'rf')
let modes = [v:true]
" termdebug only wokrs fine if socat is available on the remote machine
" otherwise the communication pty will be unstable
if executable('socat')
let modes += [v:false]
endif
for use_prompt in modes
" Set up mock remote and mapping
let g:termdebug_config = {}
let g:termdebug_config['use_prompt'] = use_prompt
" favor socat if available
if executable('socat')
let g:termdebug_config['remote_window'] =
\ ['socat', '-d', '-d', '-', 'PTY,raw,echo=0']
else
let g:termdebug_config['remote_window'] = ['sh']
endif
let g:termdebug_config['substitute_path'] = {}
let g:termdebug_config['substitute_path'][pwd] = pwd . '/' . src_shadow_dir
defer execute("unlet g:termdebug_config")
" Launch the debugger and set breakpoints in the shadow file instead
exe $"edit {src_shadow_file}"
exe $"Termdebug ./{bin_name}"
call WaitForAssert({-> assert_true(get(g:, "termdebug_is_running", v:false))})
call WaitForAssert({-> assert_equal(3, winnr('$'))})
let gdb_buf = winbufnr(1)
wincmd b
Break 9
sleep 100m
redraw!
call assert_equal([
\ {'lnum': 9, 'id': 1014, 'name': 'debugBreakpoint1.0',
\ 'priority': 110, 'group': 'TermDebug'}],
\ sign_getplaced('', #{group: 'TermDebug'})[0].signs)
Run
call term_wait(gdb_buf, 400)
redraw!
call WaitForAssert({-> assert_equal([
\ {'lnum': 9, 'id': 12, 'name': 'debugPC', 'priority': 110,
\ 'group': 'TermDebug'},
\ {'lnum': 9, 'id': 1014, 'name': 'debugBreakpoint1.0',
\ 'priority': 110, 'group': 'TermDebug'}],
\ sign_getplaced('', #{group: 'TermDebug'})[0].signs)})
Finish
call term_wait(gdb_buf)
redraw!
call WaitForAssert({-> assert_equal([
\ {'lnum': 9, 'id': 1014, 'name': 'debugBreakpoint1.0',
\ 'priority': 110, 'group': 'TermDebug'},
\ {'lnum': 20, 'id': 12, 'name': 'debugPC',
\ 'priority': 110, 'group': 'TermDebug'}],
\ sign_getplaced('', #{group: 'TermDebug'})[0].signs)})
" Cleanup, make sure the gdb job is terminated before return
" otherwise may interfere with next test
Gdb
bw!
call WaitForAssert({-> assert_equal(1, winnr('$'))})
endfor
%bw!
endfunc
" vim: shiftwidth=2 sts=2 expandtab " vim: shiftwidth=2 sts=2 expandtab