# # This file defines the layer that talks to lldb # import os, re, 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()