diff --git a/runtime/doc/tags b/runtime/doc/tags index cfb54e16f6..4f4a24d77e 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -10952,8 +10952,12 @@ termdebug-example terminal.txt /*termdebug-example* termdebug-frames terminal.txt /*termdebug-frames* termdebug-mappings terminal.txt /*termdebug-mappings* 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-stepping terminal.txt /*termdebug-stepping* +termdebug-substitute-path terminal.txt /*termdebug-substitute-path* termdebug-timeout terminal.txt /*termdebug-timeout* termdebug-variables terminal.txt /*termdebug-variables* termdebug_contributing terminal.txt /*termdebug_contributing* diff --git a/runtime/doc/terminal.txt b/runtime/doc/terminal.txt index b667832f05..cbb997dc3a 100644 --- a/runtime/doc/terminal.txt +++ b/runtime/doc/terminal.txt @@ -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 @@ -44,6 +44,7 @@ If the result is "1" you have it. Prompt mode |termdebug-prompt| Mappings |termdebug-mappings| Communication |termdebug-communication| + Remote Debugging |termdebug-remote| Customizing |termdebug-customizing| {only available when compiled with the |+terminal| feature} @@ -1635,12 +1636,103 @@ interrupt the running program. But after using the MI command 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 ~ *g:termdebugger* To change the name of the gdb command, set "debugger" entry in g:termdebug_config or the "g:termdebugger" variable before invoking `:Termdebug`: > let g:termdebug_config['command'] = "mygdb" + If there is no g:termdebug_config you can use: > 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: > let g:termdebug_config['command'] = ['rr', 'replay', '--'] + If there is no g:termdebug_config you can use: > 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: > 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 "pty", use a function to add the necessary arguments: > 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: > let g:termdebug_config['sign'] = '>>' 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'] = '>>' If you would like to use decimal (base 10) breakpoint signs: > let g:termdebug_config['sign_decimal'] = 1 diff --git a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim index a4183c2749..cefa1a0855 100644 --- a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim +++ b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim @@ -4,7 +4,7 @@ vim9script # Author: Bram Moolenaar # Copyright: Vim license applies, see ":help license" -# Last Change: 2025 Sep 15 +# Last Change: 2025 Oct 08 # Converted to Vim9: Ubaldo Tiberi # 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. # 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 +# but a terminal window is used to run remote debugged programs. # The communication with gdb uses GDB/MI. See: # https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html @@ -435,8 +436,73 @@ def IsGdbStarted(): bool return true enddef -def CreateProgramPty(): string - ptybufnr = term_start('NONE', { +# Check if the debugger is running remotely and return a suitable command to pty remotely +def GetRemotePtyCmd(gdb_cmd: list): list + # 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\") + 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\") + endif + break + endif + + endfor # j + + return pty +enddef + +def CreateProgramPty(cmd: list = null_list): string + ptybufnr = term_start(!cmd ? 'NONE' : cmd, { term_name: ptybufname, vertical: vvertical}) if ptybufnr == 0 @@ -454,20 +520,76 @@ def CreateProgramPty(): string 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 -def CreateCommunicationPty(): string +def CreateCommunicationPty(cmd: list = null_list): string # Create a hidden terminal window to communicate with gdb - commbufnr = term_start('NONE', { - term_name: commbufname, - out_cb: CommOutput, - hidden: 1 - }) + var options: dict = { term_name: commbufname, out_cb: CommOutput, hidden: 1 } + + if !cmd + 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 return null_string 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): list + var new_args: list = [] + for arg in args + new_args += [substitute(arg, ' ', '\\ ', 'g')] + endfor + return new_args enddef def CreateGdbConsole(dict: dict, pty: string, commpty: string): string @@ -497,6 +619,12 @@ def CreateGdbConsole(dict: dict, pty: string, commpty: string): string gdb_cmd += ['-ex', 'echo startupdone\n'] 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') gdb_cmd = g:termdebug_config.command_filter(gdb_cmd) endif @@ -599,14 +727,18 @@ enddef # Open a terminal window without a job, to run the debugged program in. def StartDebug_term(dict: dict) - 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 Echoerr('Failed to open the program terminal window') CloseBuffers() return endif - var commpty = CreateCommunicationPty() + var commpty = CreateCommunicationPty(term_cmd) if commpty is null_string Echoerr('Failed to open the communication terminal window') CloseBuffers() @@ -656,16 +788,27 @@ def StartDebug_prompt(dict: dict) var gdb_args = get(dict, 'gdb_args', []) var proc_args = get(dict, 'proc_args', []) - # Add -quiet to avoid the intro message causing a hit-enter prompt. - gdb_cmd += ['-quiet'] + # directly communicate via mi2. This option must precede any -iex options for proper + # interpretation. + gdb_cmd += ['--interpreter=mi2'] # Disable pagination, it causes everything to stop at the gdb, needs to be run early gdb_cmd += ['-iex', 'set pagination off'] # Interpret commands while the target is running. This should usually only # be exec-interrupt, since many commands don't work properly while the # target is running (so execute during startup). gdb_cmd += ['-iex', 'set mi-async on'] - # directly communicate via mi2 - gdb_cmd += ['--interpreter=mi2'] + # Add -quiet to avoid the intro message causing a hit-enter prompt. + 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 gdb_cmd += gdb_args @@ -686,24 +829,61 @@ def StartDebug_prompt(dict: dict) set modified gdb_channel = job_getchannel(gdbjob) - ptybufnr = 0 - if has('win32') - # MS-Windows: run in a new console window for maximum compatibility - SendCommand('set new-console on') - elseif has('terminal') - # Unix: Run the debugged program in a terminal window. Open it below the - # gdb window. - belowright ptybufnr = term_start('NONE', { - term_name: 'debugged program', - vertical: vvertical - }) - if ptybufnr == 0 - Echoerr('Failed to open the program terminal window') + # Retrieve command if remote pty is needed + var term_cmd = GetRemotePtyCmd(gdb_cmd) + + # If we are not using socat maybe is a shell: + var interact = indexof(term_cmd, 'v:val =~? "^socat"') < 0 + + if has('terminal') && (term_cmd != null || !has('win32')) + + # Try open terminal twice because sync with gdbjob may not succeed + # the first time (docker daemon for example) + var trials: number = 2 + var pty: string = null_string + + 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) - return + elseif pty !~? "^/dev/pts" + # remove the prompt + pty = pty->matchstr('/dev/pts/\d\+') endif - ptywin = win_getid() - var pty = job_info(term_getjob(ptybufnr))['tty_out'] + SendCommand($'tty {pty}') # Since GDB runs in a prompt window, the environment has not been set to @@ -714,6 +894,9 @@ def StartDebug_prompt(dict: dict) SendCommand($'set env COLUMNS = {winwidth(ptywin)}') SendCommand($'set env COLORS = {&t_Co}') 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 # TODO: open a new terminal, get the tty name, pass on to gdb SendCommand('show inferior-tty') @@ -930,7 +1113,7 @@ enddef const NullRepl = 'XXXNULLXXX' # Extract the "name" value from a gdb message with fullname="name". -def GetFullname(msg: string): string +def GetLocalFullname(msg: string): string if msg !~ 'fullname' return '' endif @@ -944,6 +1127,50 @@ def GetFullname(msg: string): string return name 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 = 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 = 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". def GetAsmAddr(msg: string): string if msg !~ 'addr=' @@ -1159,7 +1386,7 @@ def CommOutput(chan: channel, message: string) enddef def GotoProgram() - if has('win32') + if has('win32') && !ptywin if executable('powershell') system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', pid)) endif @@ -1374,7 +1601,8 @@ def Until(at: string) ch_log('assume that program is running after this command') # 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}') else ch_log('dropping command, program is running: exec-until') @@ -1393,7 +1621,8 @@ def SetBreakpoint(at: string, tbreak=false) endif # 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 = '' if tbreak cmd = $'-break-insert -t {AT}' @@ -1407,7 +1636,8 @@ def SetBreakpoint(at: string, tbreak=false) enddef def ClearBreakpoint() - var fname = fnameescape(expand('%:p')) + var fname = Remote2LocalPath(expand('%:p')) + fname = fnameescape(fname) var lnum = line('.') var bploc = printf('%s:%d', fname, lnum) var nr = 0 @@ -1444,7 +1674,8 @@ def ClearBreakpoint() enddef def ToggleBreak() - var fname = fnameescape(expand('%:p')) + var fname = Remote2LocalPath(expand('%:p')) + fname = fnameescape(fname) var lnum = line('.') var bploc = printf('%s:%d', fname, lnum) if has_key(breakpoint_locations, bploc) @@ -1859,7 +2090,7 @@ def HandleCursor(msg: string) var fname = '' if msg =~ 'fullname=' - fname = GetFullname(msg) + fname = GetLocalFullname(msg) endif if msg =~ 'addr=' @@ -1887,12 +2118,15 @@ def HandleCursor(msg: string) SendCommand('-stack-list-variables 2') 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', '') if lnum =~ '^[0-9]*$' GotoSourcewinOrCreateIt() - if expand('%:p') != fnamemodify(fname, ':p') - echomsg $"different fname: '{expand('%:p')}' vs '{fnamemodify(fname, ':p')}'" + if expand('%:p') != fnamemodify(fremote, ':p') + echomsg $"different fname: '{expand('%:p')}' vs '{fnamemodify(fremote, ':p')}'" augroup Termdebug # Always open a file read-only instead of showing the ATTENTION # prompt, since it is unlikely we want to edit the file. @@ -1904,11 +2138,11 @@ def HandleCursor(msg: string) augroup END if &modified # TODO: find existing window - exe $'split {fnameescape(fname)}' + exe $'split {fnameescape(fremote)}' sourcewin = win_getid() InstallWinbar(false) else - exe $'edit {fnameescape(fname)}' + exe $'edit {fnameescape(fremote)}' endif augroup Termdebug au! SwapExists @@ -1917,7 +2151,7 @@ def HandleCursor(msg: string) exe $":{lnum}" normal! zv 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}) if !exists('b:save_signcolumn') b:save_signcolumn = &signcolumn @@ -1991,10 +2225,11 @@ def HandleNewBreakpoint(msg: string, modifiedFlag: bool) endif for mm in SplitMsg(msg) - var fname = GetFullname(mm) + var fname = GetLocalFullname(mm) if empty(fname) continue endif + var fremote = Local2RemotePath(fname) nr = substitute(mm, '.*number="\([0-9.]*\)\".*', '\1', '') if empty(nr) return @@ -2034,11 +2269,11 @@ def HandleNewBreakpoint(msg: string, modifiedFlag: bool) endif var posMsg = '' - if bufloaded(fname) + if bufloaded(fremote) PlaceSign(id, subid, entry) posMsg = $' at line {lnum}.' else - posMsg = $' in {fname} at line {lnum}.' + posMsg = $' in {fremote} at line {lnum}.' endif var actionTaken = '' if !modifiedFlag @@ -2055,8 +2290,9 @@ enddef def PlaceSign(id: number, subid: number, entry: dict) var nr = printf('%d.%d', id, subid) + var remote = Local2RemotePath(entry['fname']) sign_place(Breakpoint2SignNumber(id, subid), 'TermDebug', - $'debugBreakpoint{nr}', entry['fname'], + $'debugBreakpoint{nr}', remote, {lnum: entry['lnum'], priority: 110}) entry['placed'] = 1 enddef diff --git a/src/testdir/test_plugin_termdebug.vim b/src/testdir/test_plugin_termdebug.vim index 79455ba088..1f6fd36e4b 100644 --- a/src/testdir/test_plugin_termdebug.vim +++ b/src/testdir/test_plugin_termdebug.vim @@ -689,4 +689,86 @@ func Test_termdebug_toggle_break() %bw! 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