Skip to content

Commit

Permalink
Fix #20: Merge branch 'proxy'
Browse files Browse the repository at this point in the history
  • Loading branch information
sakhnik committed Jul 29, 2018
2 parents 2d8430b + 4b7c4ab commit 6923d05
Show file tree
Hide file tree
Showing 11 changed files with 449 additions and 51 deletions.
101 changes: 84 additions & 17 deletions autoload/nvimgdb.vim
Expand Up @@ -15,7 +15,7 @@ let s:backend_gdb = {
\ 'paused': [
\ ['Continuing.', 'continue'],
\ ['\v[\o32]{2}([^:]+):(\d+):\d+', 'jump'],
\ ['\vBreakpoint (\d+) at ([^:]+): file ([^,]+), line (\d+).', 'breakpoint'],
\ ['(gdb)', 'info_breakpoints'],
\ ],
\ 'running': [
\ ['\v^Breakpoint \d+', 'pause'],
Expand Down Expand Up @@ -86,31 +86,74 @@ function s:GdbPaused_jump(file, line, ...) dict
exe "e " . a:file
let target_buf = bufnr(a:file)
endif

if bufnr('%') != target_buf
" Switch to the new buffer
exe 'buffer ' target_buf
let self._current_buf = target_buf
call s:RefreshBreakpointSigns(self._current_buf)
endif

exe ':' a:line
let self._current_line = a:line
exe window 'wincmd w'
call self.update_current_line_sign(1)
endfunction

" Transition "paused" -> "paused": refresh breakpoints in the current file
function s:GdbPaused_info_breakpoints(...) dict
if t:gdb != self
" Don't do anything if we are not in the current debugger tab
return
endif

" Check whether the backend supports querying breakpoints on each step.
if !has_key(t:gdb._impl, "InfoBreakpoints")
return
endif

" Get the source code buffer number
if bufnr('%') == self._client_buf
" The debugger terminal window is currently focused, so perform a couple
" of jumps.
let window = winnr()
exe self._jump_window 'wincmd w'
let bufnum = bufnr('%')
exe window 'wincmd w'
else
let bufnum = bufnr('%')
endif
" Get the source code file name
let fname = s:GetFullBufferPath(bufnum)

" If no file name or a weird name with spaces, ignore it (to avoid
" misinterpretation)
if fname == '' || stridx(fname, ' ') != -1
return
endif

" Query the breakpoints for the shown file
let breaks = t:gdb._impl.InfoBreakpoints(fname)
let self._breakpoints[fname] = breaks
call s:RefreshBreakpointSigns(bufnum)
call self.update_current_line_sign(1)
endfunction

" Transition "paused" -> "paused": jump to the frame location
function s:GdbPaused_breakpoint(num, skip, file, line, ...) dict
" If the backend supports querying breakpoints randomly, no need to watch
" for set breakpoints.
if has_key(t:gdb._impl, "InfoBreakpoints")
return
endif

if exists("self._pending_breakpoint_file")
let file_name = self._pending_breakpoint_file
let linenr = self._pending_breakpoint_linenr
unlet self._pending_breakpoint_file
unlet self._pending_breakpoint_linenr
else
let linenr = a:line
let file_name = t:gdb._impl.FindSource(a:file)
if empty(file_name)
return
endif
return
endif

" Remember the breakpoint number
Expand All @@ -137,6 +180,9 @@ function s:GdbRunning_pause(...) dict
endfor
let self._initialized = 1
endif

" TODO: find a better way
call t:gdb._state_paused.info_breakpoints()
endfunction


Expand Down Expand Up @@ -317,6 +363,7 @@ function! s:InitMachine(backend, struct)
let data._state_paused = vimexpect#State(data.backend["paused"])
let data._state_paused.continue = function("s:GdbPaused_continue", data)
let data._state_paused.jump = function("s:GdbPaused_jump", data)
let data._state_paused.info_breakpoints = function("s:GdbPaused_info_breakpoints", data)
let data._state_paused.breakpoint = function("s:GdbPaused_breakpoint", data)

let data._state_running = vimexpect#State(data.backend["running"])
Expand Down Expand Up @@ -347,7 +394,12 @@ function! s:OnTabEnter()
if t:gdb._parser.state() == t:gdb._state_paused
call t:gdb.update_current_line_sign(1)
endif
call s:RefreshBreakpointSigns(t:gdb._current_buf)
if has_key(t:gdb._impl, "InfoBreakpoints")
" Ensure breakpoints are shown if are queried dynamically
call t:gdb._state_paused.info_breakpoints()
else
call s:RefreshBreakpointSigns(t:gdb._current_buf)
endif
endfunction

function! s:OnTabLeave()
Expand All @@ -363,6 +415,8 @@ function! s:OnBufEnter()
if !exists('t:gdb') | return | endif
if &buftype ==# 'terminal' | return | endif
call s:SetKeymaps()
" Ensure breakpoints are shown if are queried dynamically
call t:gdb._state_paused.info_breakpoints()
endfunction

function! s:OnBufLeave()
Expand All @@ -372,7 +426,7 @@ function! s:OnBufLeave()
endfunction


function! nvimgdb#Spawn(backend, client_cmd)
function! nvimgdb#Spawn(backend, proxy_cmd, client_cmd)
let gdb = s:InitMachine(a:backend, s:Gdb)
exe 'let gdb._impl = nvimgdb#' . a:backend . '#GetImpl()'
let gdb._initialized = 0
Expand All @@ -388,7 +442,15 @@ function! nvimgdb#Spawn(backend, client_cmd)
sp
" go to the bottom window and spawn gdb client
wincmd j
enew | let gdb._client_id = termopen(a:client_cmd, gdb)

" Prepare the debugger command to run
let l:command = ''
if a:proxy_cmd != ''
let l:command = s:plugin_dir . '/lib/' . a:proxy_cmd . ' -- '
endif
let l:command .= a:client_cmd

enew | let gdb._client_id = termopen(l:command, gdb)
let gdb._client_buf = bufnr('%')
let t:gdb = gdb

Expand Down Expand Up @@ -442,17 +504,22 @@ function! nvimgdb#ToggleBreak()
if has_key(file_breakpoints, linenr)
" There already is a breakpoint on this line: remove
call t:gdb.send(t:gdb.backend['delete_breakpoints'] . ' ' . file_breakpoints[linenr])
call remove(file_breakpoints, linenr)
" Finally, remember and update the breakpoint signs
let t:gdb._breakpoints[file_name] = file_breakpoints
call s:RefreshBreakpointSigns(buf)

if !has_key(t:gdb._impl, "InfoBreakpoints")
call remove(file_breakpoints, linenr)
" Finally, remember and update the breakpoint signs
let t:gdb._breakpoints[file_name] = file_breakpoints
call s:RefreshBreakpointSigns(buf)
endif
else
" Add a new breakpoint
let file_breakpoints[linenr] = 1
let t:gdb._pending_breakpoint_file = file_name
let t:gdb._pending_breakpoint_linenr = linenr
if !has_key(t:gdb._impl, "InfoBreakpoints")
let file_breakpoints[linenr] = 1
let t:gdb._pending_breakpoint_file = file_name
let t:gdb._pending_breakpoint_linenr = linenr
" Adding will be finished in the callback stopped::breakpoint
endif
call t:gdb.send(t:gdb.backend['breakpoint'] . ' ' . file_name . ':' . linenr)
" Adding will be finished in the callback stopped::breakpoint
endif
endfunction

Expand Down
22 changes: 4 additions & 18 deletions autoload/nvimgdb/gdb.vim
@@ -1,28 +1,14 @@
let s:root_dir = expand('<sfile>:p:h:h:h')
let s:impl = {}

function s:DoFindSource(file)

function s:impl.InfoBreakpoints(file)
exe 'py3 import sys'
exe 'py3 sys.argv = ["' . a:file . '"]'
exe 'py3file ' . s:root_dir . '/lib/gdb_find_source.py'
return return_value
exe 'py3file ' . s:root_dir . '/lib/gdb_info_breakpoints.py'
return json_decode(return_value)
endfunction

function s:impl.FindSource(file)
if filereadable(a:file)
return fnamemodify(a:file, ':p')
endif

let ret = s:DoFindSource(a:file)
if !len(ret)
return ""
elseif len(ret) == 1
return ret[0]
else
" TODO: inputlist()
return ""
endif
endfunction

function! nvimgdb#gdb#GetImpl()
return s:impl
Expand Down
4 changes: 0 additions & 4 deletions autoload/nvimgdb/lldb.vim
@@ -1,9 +1,5 @@
let s:impl = {}

function s:impl.FindSource(file)
return ""
endfunction

function! nvimgdb#lldb#GetImpl()
return s:impl
endfunction
13 changes: 6 additions & 7 deletions doc/nvimgdb.txt
Expand Up @@ -161,13 +161,12 @@ Section 6: Development *NvimgdbDevelopment*
to ensure the original keymaps are preserved and restored after debugging
session: https://vi.stackexchange.com/questions/7734/how-to-save-and-restore-a-mapping

- Breakpoints set can be intercepted by the plugin and marked in the source
code. This is currently implemented only in GDB, because LLDB doesn't make
difference between temporary and persistent ones. There is no need to place
a breakpoint sign for a temporary breakpoint. Under the hood, when an
unexpected breakpoint notification is recognized, the python plugin will
open and bind a socket, issue a custom command to the GDB, and wait until
the command sends a message with response via the socket.
- GDB is run via a proxy pty application, which allows to execute concealed
service commands, like "info breakpoints" on each stop. Thus, the plugin
is able to very carefully display current set of breakpoints with the
temporal ones disappearing after hit. Technically, the proxy app is a python
program that launches gdb in a pseudo terminal, listens a unix socket for
commands, and processes the output of GDB to filter out service commands.

==============================================================================
Section 7: Trivia *NvimgdbTrivia*
Expand Down
111 changes: 111 additions & 0 deletions lib/StreamFilter.py
@@ -0,0 +1,111 @@
"""Filter the stream from within given pair of tokens."""


class _StringMatcher:
def __init__(self, s, hold, succeed, fail):
self.s = s
self.hold = hold
self.succeed = succeed
self.fail = fail
self.idx = 0

def match(self, ch):
if self.s[self.idx] == ch:
self.idx += 1
if self.idx == len(self.s):
self.idx = 0
return self.succeed
return self.hold
self.idx = 0
return self.fail

def reset(self):
self.idx = 0


class StreamFilter:
"""Stream filter class."""

def __init__(self, start, finish):
"""Initialize the filter with start and finish tokens."""
self.passing = _StringMatcher(start,
self._StartHold,
self._StartMatch,
self._StartFail)
self.rejecting = _StringMatcher(finish,
self._Nop,
self._FinishMatch,
self._Nop)
self.state = self.passing
self.buffer = bytearray()

def _Nop(self, ch):
self.buffer.append(ch)
return False

def _StartHold(self, ch):
self.buffer.append(ch)
return False

def _StartFail(self, ch):
self.buffer.append(ch)
# Send the buffer out
return True

def _StartMatch(self, ch):
self.buffer.append(ch)
self.state = self.rejecting
return False

def _FinishMatch(self, ch):
self.buffer = bytearray()
self.state = self.passing
return False

def Filter(self, input):
"""Process input, filter between tokens, return the output."""
output = bytearray()
for ch in input:
action = self.state.match(ch)
if action(ch):
output.extend(self.buffer)
self.buffer = bytearray()
return bytes(output)

def Timeout(self):
"""Process timeout, return whatever was kept in the buffer."""
self.state.reset()
self.state = self.passing
output = self.buffer
self.buffer = bytearray()
return bytes(output)


if __name__ == "__main__":
import unittest

class TestFilter(unittest.TestCase):
"""Test class."""

def test_10_first(self):
"""Test a generic scenario."""
f = StreamFilter(b" server nvim-gdb-", b"\n(gdb) ")
self.assertEqual(b"hello", f.Filter(b"hello"))
self.assertEqual(b" world", f.Filter(b" world"))
self.assertEqual(b"", f.Filter(b" "))
self.assertEqual(b" again", f.Filter(b"again"))
self.assertEqual(b"", f.Filter(b" server nvim-gdb-breakpoint"))
self.assertEqual(b"", f.Filter(b"foo-bar"))
self.assertEqual(b"", f.Filter(b"\n(gdb) "))
self.assertEqual(b"asdf", f.Filter(b"asdf"))

def test_20_timeout(self):
"""Test timeout."""
f = StreamFilter(b"asdf", b"qwer")
self.assertEqual(b"zxcv", f.Filter(b"zxcv"))
self.assertEqual(b"", f.Filter(b"asdf"))
self.assertEqual(b"", f.Filter(b"xyz"))
self.assertEqual(b"asdfxyz", f.Timeout())
self.assertEqual(b"qwer", f.Filter(b"qwer"))

unittest.main()

0 comments on commit 6923d05

Please sign in to comment.