Daniel Malea | ac4ce0c | 2013-02-11 17:18:14 +0000 | [diff] [blame^] | 1 | |
| 2 | # |
| 3 | # This file defines the layer that talks to lldb |
| 4 | # |
| 5 | |
| 6 | import os, re, sys |
| 7 | import lldb |
| 8 | import vim |
| 9 | from vim_ui import UI |
| 10 | |
| 11 | # ================================================= |
| 12 | # Convert some enum value to its string counterpart |
| 13 | # ================================================= |
| 14 | |
| 15 | # Shamelessly copy/pasted from lldbutil.py in the test suite |
| 16 | def state_type_to_str(enum): |
| 17 | """Returns the stateType string given an enum.""" |
| 18 | if enum == lldb.eStateInvalid: |
| 19 | return "invalid" |
| 20 | elif enum == lldb.eStateUnloaded: |
| 21 | return "unloaded" |
| 22 | elif enum == lldb.eStateConnected: |
| 23 | return "connected" |
| 24 | elif enum == lldb.eStateAttaching: |
| 25 | return "attaching" |
| 26 | elif enum == lldb.eStateLaunching: |
| 27 | return "launching" |
| 28 | elif enum == lldb.eStateStopped: |
| 29 | return "stopped" |
| 30 | elif enum == lldb.eStateRunning: |
| 31 | return "running" |
| 32 | elif enum == lldb.eStateStepping: |
| 33 | return "stepping" |
| 34 | elif enum == lldb.eStateCrashed: |
| 35 | return "crashed" |
| 36 | elif enum == lldb.eStateDetached: |
| 37 | return "detached" |
| 38 | elif enum == lldb.eStateExited: |
| 39 | return "exited" |
| 40 | elif enum == lldb.eStateSuspended: |
| 41 | return "suspended" |
| 42 | else: |
| 43 | raise Exception("Unknown StateType enum") |
| 44 | |
| 45 | class StepType: |
| 46 | INSTRUCTION = 1 |
| 47 | INSTRUCTION_OVER = 2 |
| 48 | INTO = 3 |
| 49 | OVER = 4 |
| 50 | OUT = 5 |
| 51 | |
| 52 | class LLDBController(object): |
| 53 | """ Handles Vim and LLDB events such as commands and lldb events. """ |
| 54 | |
| 55 | # Timeouts (sec) for waiting on new events. Because vim is not multi-threaded, we are restricted to |
| 56 | # servicing LLDB events from the main UI thread. Usually, we only process events that are already |
| 57 | # sitting on the queue. But in some situations (when we are expecting an event as a result of some |
| 58 | # user interaction) we want to wait for it. The constants below set these wait period in which the |
| 59 | # Vim UI is "blocked". Lower numbers will make Vim more responsive, but LLDB will be delayed and higher |
| 60 | # numbers will mean that LLDB events are processed faster, but the Vim UI may appear less responsive at |
| 61 | # times. |
| 62 | eventDelayStep = 2 |
| 63 | eventDelayLaunch = 1 |
| 64 | eventDelayContinue = 1 |
| 65 | |
| 66 | def __init__(self): |
| 67 | """ Creates the LLDB SBDebugger object and initializes the UI class. """ |
| 68 | self.target = None |
| 69 | self.process = None |
| 70 | self.load_dependent_modules = True |
| 71 | |
| 72 | self.dbg = lldb.SBDebugger.Create() |
| 73 | self.commandInterpreter = self.dbg.GetCommandInterpreter() |
| 74 | |
| 75 | self.ui = UI() |
| 76 | |
| 77 | def completeCommand(self, a, l, p): |
| 78 | """ Returns a list of viable completions for command a with length l and cursor at p """ |
| 79 | |
| 80 | assert l[0] == 'L' |
| 81 | # Remove first 'L' character that all commands start with |
| 82 | l = l[1:] |
| 83 | |
| 84 | # Adjust length as string has 1 less character |
| 85 | p = int(p) - 1 |
| 86 | |
| 87 | result = lldb.SBStringList() |
| 88 | num = self.commandInterpreter.HandleCompletion(l, p, 1, -1, result) |
| 89 | |
| 90 | if num == -1: |
| 91 | # FIXME: insert completion character... what's a completion character? |
| 92 | pass |
| 93 | elif num == -2: |
| 94 | # FIXME: replace line with result.GetStringAtIndex(0) |
| 95 | pass |
| 96 | |
| 97 | if result.GetSize() > 0: |
| 98 | results = filter(None, [result.GetStringAtIndex(x) for x in range(result.GetSize())]) |
| 99 | return results |
| 100 | else: |
| 101 | return [] |
| 102 | |
| 103 | def doStep(self, stepType): |
| 104 | """ Perform a step command and block the UI for eventDelayStep seconds in order to process |
| 105 | events on lldb's event queue. |
| 106 | FIXME: if the step does not complete in eventDelayStep seconds, we relinquish control to |
| 107 | the main thread to avoid the appearance of a "hang". If this happens, the UI will |
| 108 | update whenever; usually when the user moves the cursor. This is somewhat annoying. |
| 109 | """ |
| 110 | if not self.process: |
| 111 | sys.stderr.write("No process to step") |
| 112 | return |
| 113 | |
| 114 | t = self.process.GetSelectedThread() |
| 115 | if stepType == StepType.INSTRUCTION: |
| 116 | t.StepInstruction(False) |
| 117 | if stepType == StepType.INSTRUCTION_OVER: |
| 118 | t.StepInstruction(True) |
| 119 | elif stepType == StepType.INTO: |
| 120 | t.StepInto() |
| 121 | elif stepType == StepType.OVER: |
| 122 | t.StepOver() |
| 123 | elif stepType == StepType.OUT: |
| 124 | t.StepOut() |
| 125 | |
| 126 | self.processPendingEvents(self.eventDelayStep, True) |
| 127 | |
| 128 | def doSelect(self, command, args): |
| 129 | """ Like doCommand, but suppress output when "select" is the first argument.""" |
| 130 | a = args.split(' ') |
| 131 | return self.doCommand(command, args, "select" != a[0], True) |
| 132 | |
| 133 | def doProcess(self, args): |
| 134 | """ Handle 'process' command. If 'launch' is requested, use doLaunch() instead |
| 135 | of the command interpreter to start the inferior process. |
| 136 | """ |
| 137 | a = args.split(' ') |
| 138 | if len(args) == 0 or (len(a) > 0 and a[0] != 'launch'): |
| 139 | self.doCommand("process", args) |
| 140 | #self.ui.update(self.target, "", self) |
| 141 | else: |
| 142 | self.doLaunch('-s' not in args, "") |
| 143 | |
| 144 | def doLaunch(self, stop_at_entry, args): |
| 145 | """ Handle process launch. """ |
| 146 | error = lldb.SBError() |
| 147 | |
| 148 | fs = self.target.GetExecutable() |
| 149 | exe = os.path.join(fs.GetDirectory(), fs.GetFilename()) |
| 150 | if self.process is not None and self.process.IsValid(): |
| 151 | pid = self.process.GetProcessID() |
| 152 | state = state_type_to_str(self.process.GetState()) |
| 153 | self.process.Destroy() |
| 154 | |
| 155 | launchInfo = lldb.SBLaunchInfo(args.split(' ')) |
| 156 | self.process = self.target.Launch(launchInfo, error) |
| 157 | if not error.Success(): |
| 158 | sys.stderr.write("Error during launch: " + str(error)) |
| 159 | return |
| 160 | |
| 161 | # launch succeeded, store pid and add some event listeners |
| 162 | self.pid = self.process.GetProcessID() |
| 163 | self.processListener = lldb.SBListener("process_event_listener") |
| 164 | self.process.GetBroadcaster().AddListener(self.processListener, lldb.SBProcess.eBroadcastBitStateChanged) |
| 165 | |
| 166 | print "Launched %s %s (pid=%d)" % (exe, args, self.pid) |
| 167 | |
| 168 | if not stop_at_entry: |
| 169 | self.doContinue() |
| 170 | else: |
| 171 | self.processPendingEvents(self.eventDelayLaunch) |
| 172 | |
| 173 | def doTarget(self, args): |
| 174 | """ Pass target command to interpreter, except if argument is not one of the valid options, or |
| 175 | is create, in which case try to create a target with the argument as the executable. For example: |
| 176 | target list ==> handled by interpreter |
| 177 | target create blah ==> custom creation of target 'blah' |
| 178 | target blah ==> also creates target blah |
| 179 | """ |
| 180 | target_args = [#"create", |
| 181 | "delete", |
| 182 | "list", |
| 183 | "modules", |
| 184 | "select", |
| 185 | "stop-hook", |
| 186 | "symbols", |
| 187 | "variable"] |
| 188 | |
| 189 | a = args.split(' ') |
| 190 | if len(args) == 0 or (len(a) > 0 and a[0] in target_args): |
| 191 | self.doCommand("target", args) |
| 192 | return |
| 193 | elif len(a) > 1 and a[0] == "create": |
| 194 | exe = a[1] |
| 195 | elif len(a) == 1 and a[0] not in target_args: |
| 196 | exe = a[0] |
| 197 | |
| 198 | err = lldb.SBError() |
| 199 | self.target = self.dbg.CreateTarget(exe, None, None, self.load_dependent_modules, err) |
| 200 | if not self.target: |
| 201 | sys.stderr.write("Error creating target %s. %s" % (str(exe), str(err))) |
| 202 | return |
| 203 | |
| 204 | self.ui.activate() |
| 205 | self.ui.update(self.target, "created target %s" % str(exe), self) |
| 206 | |
| 207 | def doContinue(self): |
| 208 | """ Handle 'contiue' command. |
| 209 | FIXME: switch to doCommand("continue", ...) to handle -i ignore-count param. |
| 210 | """ |
| 211 | if not self.process or not self.process.IsValid(): |
| 212 | sys.stderr.write("No process to continue") |
| 213 | return |
| 214 | |
| 215 | self.process.Continue() |
| 216 | self.processPendingEvents(self.eventDelayContinue) |
| 217 | |
| 218 | def doBreakpoint(self, args): |
| 219 | """ Handle breakpoint command with command interpreter, except if the user calls |
| 220 | "breakpoint" with no other args, in which case add a breakpoint at the line |
| 221 | under the cursor. |
| 222 | """ |
| 223 | a = args.split(' ') |
| 224 | if len(args) == 0: |
| 225 | show_output = False |
| 226 | |
| 227 | # User called us with no args, so toggle the bp under cursor |
| 228 | cw = vim.current.window |
| 229 | cb = vim.current.buffer |
| 230 | name = cb.name |
| 231 | line = cw.cursor[0] |
| 232 | |
| 233 | # Since the UI is responsbile for placing signs at bp locations, we have to |
| 234 | # ask it if there already is one or more breakpoints at (file, line)... |
| 235 | if self.ui.haveBreakpoint(name, line): |
| 236 | bps = self.ui.getBreakpoints(name, line) |
| 237 | args = "delete %s" % " ".join([str(b.GetID()) for b in bps]) |
| 238 | self.ui.deleteBreakpoints(name, line) |
| 239 | else: |
| 240 | args = "set -f %s -l %d" % (name, line) |
| 241 | else: |
| 242 | show_output = True |
| 243 | |
| 244 | self.doCommand("breakpoint", args, show_output) |
| 245 | return |
| 246 | |
| 247 | def doRefresh(self): |
| 248 | """ process pending events and update UI on request """ |
| 249 | status = self.processPendingEvents() |
| 250 | |
| 251 | def doShow(self, name): |
| 252 | """ handle :Lshow <name> """ |
| 253 | if self.ui.showWindow(name): |
| 254 | self.ui.update(self.target, "", self) |
| 255 | |
| 256 | def doHide(self, name): |
| 257 | """ handle :Lhide <name> """ |
| 258 | if self.ui.hideWindow(name): |
| 259 | self.ui.update(self.target, "", self) |
| 260 | |
| 261 | def doExit(self): |
| 262 | self.dbg.Terminate() |
| 263 | self.dbg = None |
| 264 | |
| 265 | def getCommandResult(self, command, command_args): |
| 266 | """ Run cmd in the command interpreter and returns (success, output) """ |
| 267 | result = lldb.SBCommandReturnObject() |
| 268 | cmd = "%s %s" % (command, command_args) |
| 269 | |
| 270 | self.commandInterpreter.HandleCommand(cmd, result) |
| 271 | return (result.Succeeded(), result.GetOutput() if result.Succeeded() else result.GetError()) |
| 272 | |
| 273 | def doCommand(self, command, command_args, print_on_success = True, goto_file=False): |
| 274 | """ Run cmd in interpreter and print result (success or failure) on the vim status line. """ |
| 275 | (success, output) = self.getCommandResult(command, command_args) |
| 276 | if success: |
| 277 | self.ui.update(self.target, "", self, goto_file) |
| 278 | if len(output) > 0 and print_on_success: |
| 279 | print output |
| 280 | else: |
| 281 | sys.stderr.write(output) |
| 282 | |
| 283 | def getCommandOutput(self, command, command_args=""): |
| 284 | """ runs cmd in the command interpreter andreturns (status, result) """ |
| 285 | result = lldb.SBCommandReturnObject() |
| 286 | cmd = "%s %s" % (command, command_args) |
| 287 | self.commandInterpreter.HandleCommand(cmd, result) |
| 288 | return (result.Succeeded(), result.GetOutput() if result.Succeeded() else result.GetError()) |
| 289 | |
| 290 | def processPendingEvents(self, wait_seconds=0, goto_file=True): |
| 291 | """ Handle any events that are queued from the inferior. |
| 292 | Blocks for at most wait_seconds, or if wait_seconds == 0, |
| 293 | process only events that are already queued. |
| 294 | """ |
| 295 | |
| 296 | status = None |
| 297 | num_events_handled = 0 |
| 298 | |
| 299 | if self.process is not None: |
| 300 | event = lldb.SBEvent() |
| 301 | old_state = self.process.GetState() |
| 302 | new_state = None |
| 303 | done = False |
| 304 | if old_state == lldb.eStateInvalid or old_state == lldb.eStateExited: |
| 305 | # Early-exit if we are in 'boring' states |
| 306 | pass |
| 307 | else: |
| 308 | while not done and self.processListener is not None: |
| 309 | if not self.processListener.PeekAtNextEvent(event): |
| 310 | if wait_seconds > 0: |
| 311 | # No events on the queue, but we are allowed to wait for wait_seconds |
| 312 | # for any events to show up. |
| 313 | self.processListener.WaitForEvent(wait_seconds, event) |
| 314 | new_state = lldb.SBProcess.GetStateFromEvent(event) |
| 315 | |
| 316 | num_events_handled += 1 |
| 317 | |
| 318 | done = not self.processListener.PeekAtNextEvent(event) |
| 319 | else: |
| 320 | # An event is on the queue, process it here. |
| 321 | self.processListener.GetNextEvent(event) |
| 322 | new_state = lldb.SBProcess.GetStateFromEvent(event) |
| 323 | |
| 324 | # If needed, perform any event-specific behaviour here |
| 325 | num_events_handled += 1 |
| 326 | |
| 327 | if num_events_handled == 0: |
| 328 | pass |
| 329 | else: |
| 330 | if old_state == new_state: |
| 331 | status = "" |
| 332 | self.ui.update(self.target, status, self, goto_file) |
| 333 | |
| 334 | |
| 335 | def returnCompleteCommand(a, l, p): |
| 336 | """ Returns a "\n"-separated string with possible completion results |
| 337 | for command a with length l and cursor at p. |
| 338 | """ |
| 339 | separator = "\n" |
| 340 | results = ctrl.completeCommand(a, l, p) |
| 341 | vim.command('return "%s%s"' % (separator.join(results), separator)) |
| 342 | |
| 343 | def returnCompleteWindow(a, l, p): |
| 344 | """ Returns a "\n"-separated string with possible completion results |
| 345 | for commands that expect a window name parameter (like hide/show). |
| 346 | FIXME: connect to ctrl.ui instead of hardcoding the list here |
| 347 | """ |
| 348 | separator = "\n" |
| 349 | results = ['breakpoints', 'backtrace', 'disassembly', 'locals', 'threads', 'registers'] |
| 350 | vim.command('return "%s%s"' % (separator.join(results), separator)) |
| 351 | |
| 352 | global ctrl |
| 353 | ctrl = LLDBController() |