| |
| # |
| # This file defines the layer that talks to lldb |
| # |
| |
| import os |
| import re |
| import sys |
| import lldb |
| import vim |
| from vim_ui import UI |
| |
| # ================================================= |
| # Convert some enum value to its string counterpart |
| # ================================================= |
| |
| # Shamelessly copy/pasted from lldbutil.py in the test suite |
| |
| |
| def state_type_to_str(enum): |
| """Returns the stateType string given an enum.""" |
| if enum == lldb.eStateInvalid: |
| return "invalid" |
| elif enum == lldb.eStateUnloaded: |
| return "unloaded" |
| elif enum == lldb.eStateConnected: |
| return "connected" |
| elif enum == lldb.eStateAttaching: |
| return "attaching" |
| elif enum == lldb.eStateLaunching: |
| return "launching" |
| elif enum == lldb.eStateStopped: |
| return "stopped" |
| elif enum == lldb.eStateRunning: |
| return "running" |
| elif enum == lldb.eStateStepping: |
| return "stepping" |
| elif enum == lldb.eStateCrashed: |
| return "crashed" |
| elif enum == lldb.eStateDetached: |
| return "detached" |
| elif enum == lldb.eStateExited: |
| return "exited" |
| elif enum == lldb.eStateSuspended: |
| return "suspended" |
| else: |
| raise Exception("Unknown StateType enum") |
| |
| |
| class StepType: |
| INSTRUCTION = 1 |
| INSTRUCTION_OVER = 2 |
| INTO = 3 |
| OVER = 4 |
| OUT = 5 |
| |
| |
| class LLDBController(object): |
| """ Handles Vim and LLDB events such as commands and lldb events. """ |
| |
| # Timeouts (sec) for waiting on new events. Because vim is not multi-threaded, we are restricted to |
| # servicing LLDB events from the main UI thread. Usually, we only process events that are already |
| # sitting on the queue. But in some situations (when we are expecting an event as a result of some |
| # user interaction) we want to wait for it. The constants below set these wait period in which the |
| # Vim UI is "blocked". Lower numbers will make Vim more responsive, but LLDB will be delayed and higher |
| # numbers will mean that LLDB events are processed faster, but the Vim UI may appear less responsive at |
| # times. |
| eventDelayStep = 2 |
| eventDelayLaunch = 1 |
| eventDelayContinue = 1 |
| |
| def __init__(self): |
| """ Creates the LLDB SBDebugger object and initializes the UI class. """ |
| self.target = None |
| self.process = None |
| self.load_dependent_modules = True |
| |
| self.dbg = lldb.SBDebugger.Create() |
| self.commandInterpreter = self.dbg.GetCommandInterpreter() |
| |
| self.ui = UI() |
| |
| def completeCommand(self, a, l, p): |
| """ Returns a list of viable completions for command a with length l and cursor at p """ |
| |
| assert l[0] == 'L' |
| # Remove first 'L' character that all commands start with |
| l = l[1:] |
| |
| # Adjust length as string has 1 less character |
| p = int(p) - 1 |
| |
| result = lldb.SBStringList() |
| num = self.commandInterpreter.HandleCompletion(l, p, 1, -1, result) |
| |
| if num == -1: |
| # FIXME: insert completion character... what's a completion |
| # character? |
| pass |
| elif num == -2: |
| # FIXME: replace line with result.GetStringAtIndex(0) |
| pass |
| |
| if result.GetSize() > 0: |
| results = filter(None, [result.GetStringAtIndex(x) |
| for x in range(result.GetSize())]) |
| return results |
| else: |
| return [] |
| |
| def doStep(self, stepType): |
| """ Perform a step command and block the UI for eventDelayStep seconds in order to process |
| events on lldb's event queue. |
| FIXME: if the step does not complete in eventDelayStep seconds, we relinquish control to |
| the main thread to avoid the appearance of a "hang". If this happens, the UI will |
| update whenever; usually when the user moves the cursor. This is somewhat annoying. |
| """ |
| if not self.process: |
| sys.stderr.write("No process to step") |
| return |
| |
| t = self.process.GetSelectedThread() |
| if stepType == StepType.INSTRUCTION: |
| t.StepInstruction(False) |
| if stepType == StepType.INSTRUCTION_OVER: |
| t.StepInstruction(True) |
| elif stepType == StepType.INTO: |
| t.StepInto() |
| elif stepType == StepType.OVER: |
| t.StepOver() |
| elif stepType == StepType.OUT: |
| t.StepOut() |
| |
| self.processPendingEvents(self.eventDelayStep, True) |
| |
| def doSelect(self, command, args): |
| """ Like doCommand, but suppress output when "select" is the first argument.""" |
| a = args.split(' ') |
| return self.doCommand(command, args, "select" != a[0], True) |
| |
| def doProcess(self, args): |
| """ Handle 'process' command. If 'launch' is requested, use doLaunch() instead |
| of the command interpreter to start the inferior process. |
| """ |
| a = args.split(' ') |
| if len(args) == 0 or (len(a) > 0 and a[0] != 'launch'): |
| self.doCommand("process", args) |
| #self.ui.update(self.target, "", self) |
| else: |
| self.doLaunch('-s' not in args, "") |
| |
| def doAttach(self, process_name): |
| """ Handle process attach. """ |
| error = lldb.SBError() |
| |
| self.processListener = lldb.SBListener("process_event_listener") |
| self.target = self.dbg.CreateTarget('') |
| self.process = self.target.AttachToProcessWithName( |
| self.processListener, process_name, False, error) |
| if not error.Success(): |
| sys.stderr.write("Error during attach: " + str(error)) |
| return |
| |
| self.ui.activate() |
| self.pid = self.process.GetProcessID() |
| |
| print "Attached to %s (pid=%d)" % (process_name, self.pid) |
| |
| def doDetach(self): |
| if self.process is not None and self.process.IsValid(): |
| pid = self.process.GetProcessID() |
| state = state_type_to_str(self.process.GetState()) |
| self.process.Detach() |
| self.processPendingEvents(self.eventDelayLaunch) |
| |
| def doLaunch(self, stop_at_entry, args): |
| """ Handle process launch. """ |
| error = lldb.SBError() |
| |
| fs = self.target.GetExecutable() |
| exe = os.path.join(fs.GetDirectory(), fs.GetFilename()) |
| if self.process is not None and self.process.IsValid(): |
| pid = self.process.GetProcessID() |
| state = state_type_to_str(self.process.GetState()) |
| self.process.Destroy() |
| |
| launchInfo = lldb.SBLaunchInfo(args.split(' ')) |
| self.process = self.target.Launch(launchInfo, error) |
| if not error.Success(): |
| sys.stderr.write("Error during launch: " + str(error)) |
| return |
| |
| # launch succeeded, store pid and add some event listeners |
| self.pid = self.process.GetProcessID() |
| self.processListener = lldb.SBListener("process_event_listener") |
| self.process.GetBroadcaster().AddListener( |
| self.processListener, lldb.SBProcess.eBroadcastBitStateChanged) |
| |
| print "Launched %s %s (pid=%d)" % (exe, args, self.pid) |
| |
| if not stop_at_entry: |
| self.doContinue() |
| else: |
| self.processPendingEvents(self.eventDelayLaunch) |
| |
| def doTarget(self, args): |
| """ Pass target command to interpreter, except if argument is not one of the valid options, or |
| is create, in which case try to create a target with the argument as the executable. For example: |
| target list ==> handled by interpreter |
| target create blah ==> custom creation of target 'blah' |
| target blah ==> also creates target blah |
| """ |
| target_args = [ # "create", |
| "delete", |
| "list", |
| "modules", |
| "select", |
| "stop-hook", |
| "symbols", |
| "variable"] |
| |
| a = args.split(' ') |
| if len(args) == 0 or (len(a) > 0 and a[0] in target_args): |
| self.doCommand("target", args) |
| return |
| elif len(a) > 1 and a[0] == "create": |
| exe = a[1] |
| elif len(a) == 1 and a[0] not in target_args: |
| exe = a[0] |
| |
| err = lldb.SBError() |
| self.target = self.dbg.CreateTarget( |
| exe, None, None, self.load_dependent_modules, err) |
| if not self.target: |
| sys.stderr.write( |
| "Error creating target %s. %s" % |
| (str(exe), str(err))) |
| return |
| |
| self.ui.activate() |
| self.ui.update(self.target, "created target %s" % str(exe), self) |
| |
| def doContinue(self): |
| """ Handle 'contiue' command. |
| FIXME: switch to doCommand("continue", ...) to handle -i ignore-count param. |
| """ |
| if not self.process or not self.process.IsValid(): |
| sys.stderr.write("No process to continue") |
| return |
| |
| self.process.Continue() |
| self.processPendingEvents(self.eventDelayContinue) |
| |
| def doBreakpoint(self, args): |
| """ Handle breakpoint command with command interpreter, except if the user calls |
| "breakpoint" with no other args, in which case add a breakpoint at the line |
| under the cursor. |
| """ |
| a = args.split(' ') |
| if len(args) == 0: |
| show_output = False |
| |
| # User called us with no args, so toggle the bp under cursor |
| cw = vim.current.window |
| cb = vim.current.buffer |
| name = cb.name |
| line = cw.cursor[0] |
| |
| # Since the UI is responsbile for placing signs at bp locations, we have to |
| # ask it if there already is one or more breakpoints at (file, |
| # line)... |
| if self.ui.haveBreakpoint(name, line): |
| bps = self.ui.getBreakpoints(name, line) |
| args = "delete %s" % " ".join([str(b.GetID()) for b in bps]) |
| self.ui.deleteBreakpoints(name, line) |
| else: |
| args = "set -f %s -l %d" % (name, line) |
| else: |
| show_output = True |
| |
| self.doCommand("breakpoint", args, show_output) |
| return |
| |
| def doRefresh(self): |
| """ process pending events and update UI on request """ |
| status = self.processPendingEvents() |
| |
| def doShow(self, name): |
| """ handle :Lshow <name> """ |
| if not name: |
| self.ui.activate() |
| return |
| |
| if self.ui.showWindow(name): |
| self.ui.update(self.target, "", self) |
| |
| def doHide(self, name): |
| """ handle :Lhide <name> """ |
| if self.ui.hideWindow(name): |
| self.ui.update(self.target, "", self) |
| |
| def doExit(self): |
| self.dbg.Terminate() |
| self.dbg = None |
| |
| def getCommandResult(self, command, command_args): |
| """ Run cmd in the command interpreter and returns (success, output) """ |
| result = lldb.SBCommandReturnObject() |
| cmd = "%s %s" % (command, command_args) |
| |
| self.commandInterpreter.HandleCommand(cmd, result) |
| return (result.Succeeded(), result.GetOutput() |
| if result.Succeeded() else result.GetError()) |
| |
| def doCommand( |
| self, |
| command, |
| command_args, |
| print_on_success=True, |
| goto_file=False): |
| """ Run cmd in interpreter and print result (success or failure) on the vim status line. """ |
| (success, output) = self.getCommandResult(command, command_args) |
| if success: |
| self.ui.update(self.target, "", self, goto_file) |
| if len(output) > 0 and print_on_success: |
| print output |
| else: |
| sys.stderr.write(output) |
| |
| def getCommandOutput(self, command, command_args=""): |
| """ runs cmd in the command interpreter andreturns (status, result) """ |
| result = lldb.SBCommandReturnObject() |
| cmd = "%s %s" % (command, command_args) |
| self.commandInterpreter.HandleCommand(cmd, result) |
| return (result.Succeeded(), result.GetOutput() |
| if result.Succeeded() else result.GetError()) |
| |
| def processPendingEvents(self, wait_seconds=0, goto_file=True): |
| """ Handle any events that are queued from the inferior. |
| Blocks for at most wait_seconds, or if wait_seconds == 0, |
| process only events that are already queued. |
| """ |
| |
| status = None |
| num_events_handled = 0 |
| |
| if self.process is not None: |
| event = lldb.SBEvent() |
| old_state = self.process.GetState() |
| new_state = None |
| done = False |
| if old_state == lldb.eStateInvalid or old_state == lldb.eStateExited: |
| # Early-exit if we are in 'boring' states |
| pass |
| else: |
| while not done and self.processListener is not None: |
| if not self.processListener.PeekAtNextEvent(event): |
| if wait_seconds > 0: |
| # No events on the queue, but we are allowed to wait for wait_seconds |
| # for any events to show up. |
| self.processListener.WaitForEvent( |
| wait_seconds, event) |
| new_state = lldb.SBProcess.GetStateFromEvent(event) |
| |
| num_events_handled += 1 |
| |
| done = not self.processListener.PeekAtNextEvent(event) |
| else: |
| # An event is on the queue, process it here. |
| self.processListener.GetNextEvent(event) |
| new_state = lldb.SBProcess.GetStateFromEvent(event) |
| |
| # continue if stopped after attaching |
| if old_state == lldb.eStateAttaching and new_state == lldb.eStateStopped: |
| self.process.Continue() |
| |
| # If needed, perform any event-specific behaviour here |
| num_events_handled += 1 |
| |
| if num_events_handled == 0: |
| pass |
| else: |
| if old_state == new_state: |
| status = "" |
| self.ui.update(self.target, status, self, goto_file) |
| |
| |
| def returnCompleteCommand(a, l, p): |
| """ Returns a "\n"-separated string with possible completion results |
| for command a with length l and cursor at p. |
| """ |
| separator = "\n" |
| results = ctrl.completeCommand(a, l, p) |
| vim.command('return "%s%s"' % (separator.join(results), separator)) |
| |
| |
| def returnCompleteWindow(a, l, p): |
| """ Returns a "\n"-separated string with possible completion results |
| for commands that expect a window name parameter (like hide/show). |
| FIXME: connect to ctrl.ui instead of hardcoding the list here |
| """ |
| separator = "\n" |
| results = [ |
| 'breakpoints', |
| 'backtrace', |
| 'disassembly', |
| 'locals', |
| 'threads', |
| 'registers'] |
| vim.command('return "%s%s"' % (separator.join(results), separator)) |
| |
| global ctrl |
| ctrl = LLDBController() |