Skip to content

Commit

Permalink
Fix #53 Merge branch 'eval'
Browse files Browse the repository at this point in the history
  • Loading branch information
sakhnik committed Feb 3, 2019
2 parents e5a5d32 + 3149e23 commit 54aa577
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 115 deletions.
5 changes: 5 additions & 0 deletions autoload/nvimgdb.vim
@@ -1,6 +1,11 @@
lua gdb = require("gdb.app")


augroup NvimGdbInternal
au!
au User NvimGdbQuery ""
augroup END

function! s:GdbKill()
" Prevent "ghost" [noname] buffers when leaving debug when 'hidden' is on
if &hidden
Expand Down
54 changes: 42 additions & 12 deletions doc/nvimgdb.txt
Expand Up @@ -9,11 +9,13 @@ CONTENTS *NvimgdbContents*
2. Commands ............. |NvigdbCommands|
3. Mappings ............. |NvimgdbMappings|
4. Variables ............ |NvimgdbVariables|
5. Backends ............. |NvimgdbBackends|
5.1 GDB .............. |NvimgdbGDB|
6. Limitations .......... |NvimgdbLimitations|
7. Development .......... |NvimgdbDevelopment|
8. Trivia ............... |NvimgdbTrivia|
5. Events ............... |NvimgdbEvents|
6. Functions ............ |NvimgdbFunctions|
7. Backends ............. |NvimgdbBackends|
7.1 GDB .............. |NvimgdbGDB|
8. Limitations .......... |NvimgdbLimitations|
9. Development .......... |NvimgdbDevelopment|
10. Trivia ............... |NvimgdbTrivia|

==============================================================================
Section 1: Usage *NvimgdbUsage*
Expand Down Expand Up @@ -195,10 +197,39 @@ have even higher priority and will disable conflicting keymaps from the
previous ones. Please examine `:messages` to make sure nothing is rejected.

==============================================================================
Section 5: Backends *NvimgdbBackends*
Section 5: Events *NvimgdbEvents*

The plugins fires `User` events allowing users to customize their workflows.
For instance, it's possible to execute commands automatically with `autocmd`.

*NvimGdbQuery*
NvimGdbQuery Fired whenever the plugin queried for breakpoints.
This can be used to execute additional queries
with `NvimGdb-customCommand()`

==============================================================================
Section 6: Functions *NvimgdbFunctions*

The plugin is implemented in Lua/Moonscript. So some supported functions
should be called via `:lua` and `luaeval()`.

*NvimGdb-customCommand()*
gdb.customCommand("expr") Execute debugger command and return the output
of the command. This can be combined with
`NvimgdbEvents` to implement watch
expressions. For instance, the following could
echo local variables on every GDB stop: >
:autocmd User NvimGdbQuery lua print(gdb.customCommand('info locals'))
<
or if LLDB is used: >
:autocmd User NvimGdbQuery lua print(gdb.customCommand('frame var'))
<

==============================================================================
Section 7: Backends *NvimgdbBackends*

------------------------------------------------------------------------------
Section 5.1 GDB *NvimgdbGDB*
Section 7.1 GDB *NvimgdbGDB*

- GDB is run via a proxy pty application, which allows to execute concealed
service commands, like "info breakpoints" on each stop. Thus, the plugin
Expand All @@ -216,7 +247,7 @@ Section 5.1 GDB *NvimgdbGDB*
<

==============================================================================
Section 6: Limitations *NvimgdbLimitations*
Section 8: Limitations *NvimgdbLimitations*

- The plugin is sensitive to the debugger settings. If prompt or frame format
is changed, random errors may occur.
Expand All @@ -227,7 +258,7 @@ Section 6: Limitations *NvimgdbLimitations*
`:GdbFrameUp` followed by `:GdbFrameDown`.

==============================================================================
Section 7: Development *NvimgdbDevelopment*
Section 9: Development *NvimgdbDevelopment*

- The keymaps are defined buffer-local for every buffer when it's entered,
and undefined when a buffer is left. This was done to ensure that users's
Expand All @@ -240,16 +271,15 @@ Section 7: Development *NvimgdbDevelopment*
commands from it in a background thread.

- PDB is like GDB run via a proxy application. Although, PDB doesn't have
stock distinctive prefix to bypass the history. So an alias is created
for that nvim-gdb-info-breakpoints.
stock distinctive prefix to bypass the history.

- Breakpoints are queried from GDB, LLDB and PDB on every pause using the
established side channels: the pty proxy for GDB and PDB, and Python script
running inside the LLDB. The communication is done via unix domain sockets
(see lua/gdb/breakpoint.moon).

==============================================================================
Section 8: Trivia *NvimgdbTrivia*
Section 10: Trivia *NvimgdbTrivia*

License inherits from neovim's.

Expand Down
101 changes: 71 additions & 30 deletions lib/BaseProxy.py
Expand Up @@ -23,26 +23,49 @@
class BaseProxy(object):
"""This class does the actual work of the pseudo terminal."""

def __init__(self, features, server_address, argv):
def __init__(self, app_name):
"""Create a spawned process."""

self.features = features
parser = argparse.ArgumentParser(
description="Run %s through a filtering proxy."
% app_name)
parser.add_argument('cmd', metavar='ARGS', nargs='+',
help='%s command with arguments'
% app_name)
parser.add_argument('-a', '--address', metavar='ADDR',
help='Local socket to receive commands.')
args = parser.parse_args()

self.server_address = args.address
self.argv = args.cmd
#self.logfile = open("/tmp/log.txt", "w")
self.logfile = None

if server_address:
if self.server_address:
# Create a UDS socket
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.sock.bind(server_address)
self.sock.bind(self.server_address)
self.sock.settimeout(0.5)
else:
self.sock = None

# Create the filter
self.filter = StreamFilter.StreamFilter(self.features.command_begin,
self.features.command_end)
self.filter = [(StreamFilter.Filter(), None)]

def log(self, msg):
try:
if not self.logfile is None:
self.logfile.write(msg)
self.logfile.write("\n")
self.logfile.flush()
except Exception as e:
print(e)
raise

def run(self):
pid, self.master_fd = pty.fork()
if pid == pty.CHILD:
os.execlp(argv[0], *argv)
os.execlp(self.argv[0], *self.argv)

old_handler = signal.signal(signal.SIGWINCH,
lambda signum, frame: self._set_pty_size())
Expand All @@ -63,13 +86,27 @@ def __init__(self, features, server_address, argv):
self.master_fd = None
signal.signal(signal.SIGWINCH, old_handler)

if server_address:
if self.server_address:
# Make sure the socket does not already exist
try:
os.unlink(server_address)
os.unlink(self.server_address)
except OSError:
pass

def set_filter(self, filter, handler):
self.log("set_filter %s %s" % (str(filter), str(handler)))
if len(self.filter) == 1:
self.log("filter accepted")
# Only one command at a time. Should be an assertion here,
# but we wouldn't want to terminate the program.
if self.filter:
self._timeout()
self.filter.append((filter, handler))
return True
else:
self.log("filter rejected")
return False

def _set_pty_size(self):
"""Set the window size of the child pty."""
assert self.master_fd is not None
Expand Down Expand Up @@ -104,8 +141,18 @@ def _process(self):
self.stdin_read(data)
if self.sock in rfds:
data, self.last_addr = self.sock.recvfrom(65536)
command = self.features.FilterCommand(data)
self.write_master(command)
if data[-1] == b'\n':
self.log("WARNING: the command ending with <nl>. The StreamProxy filter known to fail.")
try:
self.log("Got command '%s'" % data.decode('utf-8'))
command = self.FilterCommand(data)
self.log("Translated command '%s'" % command.decode('utf-8'))
except Exception as e:
self.log("Exception %s" % str(e))
raise
if command:
self.write_master(command)
self.write_master(b'\n')

def _write(self, fd, data):
"""Write the data to the file."""
Expand All @@ -114,17 +161,25 @@ def _write(self, fd, data):
data = data[n:]

def _timeout(self):
data = self.filter.Timeout()
filter, _ = self.filter[-1]
data = filter.Timeout()
self._write(pty.STDOUT_FILENO, data)

def write_stdout(self, data):
"""Write to stdout for the child process."""
data, filtered = self.filter.Filter(data)
filter, handler = self.filter[-1]
data, filtered = filter.Filter(data)
self._write(pty.STDOUT_FILENO, data)
if filtered:
res = self.features.ProcessResponse(filtered)
if res:
self.sock.sendto(res, 0, self.last_addr)
self.log("Filter matched %d bytes" % len(filtered))
try:
self.filter.pop()
res = handler(filtered)
if res:
self.sock.sendto(res, 0, self.last_addr)
except Exception as e:
self.log("Exception: %s" % str(e))
raise

def write_master(self, data):
"""Write to the child process from its controlling terminal."""
Expand All @@ -137,17 +192,3 @@ def master_read(self, data):
def stdin_read(self, data):
"""Handle data from the controlling terminal."""
self.write_master(data)

@staticmethod
def Create(features):
parser = argparse.ArgumentParser(
description="Run %s through a filtering proxy."
% features.app_name)
parser.add_argument('cmd', metavar='ARGS', nargs='+',
help='%s command with arguments'
% features.app_name)
parser.add_argument('-a', '--address', metavar='ADDR',
help='Local socket to receive commands.')
args = parser.parse_args()

return BaseProxy(features, args.address, args.cmd)
13 changes: 12 additions & 1 deletion lib/StreamFilter.py
Expand Up @@ -23,7 +23,18 @@ def reset(self):
self.idx = 0


class StreamFilter:
class Filter:
"""Pass-through filter."""
def Filter(self, input):
"""Process input, filter between tokens, return the output."""
return input, None

def Timeout(self):
"""Process timeout, return whatever was kept in the buffer."""
return b''


class StreamFilter(Filter):
"""Stream filter class."""

def __init__(self, start, finish):
Expand Down
34 changes: 23 additions & 11 deletions lib/gdbproxy.py
Expand Up @@ -12,18 +12,19 @@
import os

from BaseProxy import BaseProxy
from StreamFilter import StreamFilter

class GdbProxy(BaseProxy):
PROMPT = b"\n(gdb) "

class _GdbFeatures:
def __init__(self):
self.app_name = "GDB"
self.command_begin = b"server nvim-gdb-"
self.command_end = b"\n(gdb) "
self.last_src = None
super().__init__("GDB")

def ProcessResponse(self, response):
def ProcessInfoBreakpoints(self, last_src, response):
# Gdb invokes a custom gdb command implemented in Python.
# It itself is responsible for sending the processed result
# to the correct address.
self.log("Process info breakpoints %d bytes" % len(response))

# Select lines in the current file with enabled breakpoints.
pattern = re.compile("([^:]+):(\d+)")
Expand All @@ -33,7 +34,7 @@ def ProcessResponse(self, response):
fields = re.split("\s+", line)
if fields[3] == 'y': # Is enabled?
m = pattern.fullmatch(fields[-1]) # file.cpp:line
if (m and (self.last_src.endswith(m.group(1)) or self.last_src.endswith(os.path.realpath(m.group(1))))):
if (m and (last_src.endswith(m.group(1)) or last_src.endswith(os.path.realpath(m.group(1))))):
line = m.group(2)
brId = int(fields[0])
try:
Expand All @@ -43,16 +44,27 @@ def ProcessResponse(self, response):
except Exception as e:
pass

self.last_src = None
return json.dumps(breaks).encode('utf-8')

def ProcessHandleCommand(self, cmd, response):
self.log("Process handle command %d bytes" % len(response))
return response[(len(cmd) + 1):-len(GdbProxy.PROMPT)].strip()

def FilterCommand(self, command):
tokens = re.split(r'\s+', command.decode('utf-8'))
if tokens[0] == 'info-breakpoints':
self.last_src = tokens[1]
return b'server nvim-gdb-info-breakpoints\n'
last_src = tokens[1]
cmd = b'server info breakpoints'
res = self.set_filter(StreamFilter(cmd, GdbProxy.PROMPT),
lambda d: self.ProcessInfoBreakpoints(last_src, d))
return cmd if res else b''
elif tokens[0] == 'handle-command':
cmd = b'server ' + command[len('handle-command '):]
res = self.set_filter(StreamFilter(cmd, GdbProxy.PROMPT),
lambda d: self.ProcessHandleCommand(cmd, d))
return cmd if res else b''
return command


if __name__ == '__main__':
BaseProxy.Create(_GdbFeatures())
GdbProxy().run()
1 change: 0 additions & 1 deletion lib/gdbwrap.sh
Expand Up @@ -25,7 +25,6 @@ gdb_init=`mktemp /tmp/gdb_init.XXXXXX`
cat >$gdb_init <<EOF
set confirm off
set pagination off
alias -a nvim-gdb-info-breakpoints = info breakpoints
EOF

cleanup()
Expand Down
14 changes: 14 additions & 0 deletions lib/lldb_commands.py
Expand Up @@ -53,6 +53,20 @@ def server(server_address):
# response_addr = command[3]
breaks = _GetBreaks(fname)
sock.sendto(breaks.encode('utf-8'), 0, addr)
elif command[0] == "handle-command":
try:
command_to_handle = " ".join(command[1:]).encode('ascii')
return_object = lldb.SBCommandReturnObject()
lldb.debugger.GetCommandInterpreter().HandleCommand(command_to_handle, return_object)
result = ''
if return_object.GetError():
result += return_object.GetError()
if return_object.GetOutput():
result += return_object.GetOutput()
result = b'' if result is None else result.encode('utf-8')
sock.sendto(result.strip(), 0, addr)
except Exception as e:
self.log("Exception " + str(e))
finally:
try:
os.unlink(server_address)
Expand Down

0 comments on commit 54aa577

Please sign in to comment.