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 | |
Daniel Malea | a7c86e1 | 2013-05-22 16:04:28 +0000 | [diff] [blame] | 144 | def doAttach(self, process_name): |
| 145 | """ Handle process attach. """ |
| 146 | error = lldb.SBError() |
| 147 | |
| 148 | self.processListener = lldb.SBListener("process_event_listener") |
| 149 | self.target = self.dbg.CreateTarget('') |
| 150 | self.process = self.target.AttachToProcessWithName(self.processListener, process_name, False, error) |
| 151 | if not error.Success(): |
| 152 | sys.stderr.write("Error during attach: " + str(error)) |
| 153 | return |
| 154 | |
| 155 | self.ui.activate() |
Daniel Malea | a7c86e1 | 2013-05-22 16:04:28 +0000 | [diff] [blame] | 156 | self.pid = self.process.GetProcessID() |
Daniel Malea | a7c86e1 | 2013-05-22 16:04:28 +0000 | [diff] [blame] | 157 | |
| 158 | print "Attached to %s (pid=%d)" % (process_name, self.pid) |
| 159 | |
| 160 | def doDetach(self): |
| 161 | if self.process is not None and self.process.IsValid(): |
| 162 | pid = self.process.GetProcessID() |
| 163 | state = state_type_to_str(self.process.GetState()) |
| 164 | self.process.Detach() |
| 165 | self.processPendingEvents(self.eventDelayLaunch) |
| 166 | |
Daniel Malea | ac4ce0c | 2013-02-11 17:18:14 +0000 | [diff] [blame] | 167 | def doLaunch(self, stop_at_entry, args): |
| 168 | """ Handle process launch. """ |
| 169 | error = lldb.SBError() |
| 170 | |
| 171 | fs = self.target.GetExecutable() |
| 172 | exe = os.path.join(fs.GetDirectory(), fs.GetFilename()) |
| 173 | if self.process is not None and self.process.IsValid(): |
| 174 | pid = self.process.GetProcessID() |
| 175 | state = state_type_to_str(self.process.GetState()) |
| 176 | self.process.Destroy() |
| 177 | |
| 178 | launchInfo = lldb.SBLaunchInfo(args.split(' ')) |
| 179 | self.process = self.target.Launch(launchInfo, error) |
| 180 | if not error.Success(): |
| 181 | sys.stderr.write("Error during launch: " + str(error)) |
| 182 | return |
| 183 | |
| 184 | # launch succeeded, store pid and add some event listeners |
| 185 | self.pid = self.process.GetProcessID() |
| 186 | self.processListener = lldb.SBListener("process_event_listener") |
| 187 | self.process.GetBroadcaster().AddListener(self.processListener, lldb.SBProcess.eBroadcastBitStateChanged) |
| 188 | |
| 189 | print "Launched %s %s (pid=%d)" % (exe, args, self.pid) |
| 190 | |
| 191 | if not stop_at_entry: |
| 192 | self.doContinue() |
| 193 | else: |
| 194 | self.processPendingEvents(self.eventDelayLaunch) |
| 195 | |
| 196 | def doTarget(self, args): |
| 197 | """ Pass target command to interpreter, except if argument is not one of the valid options, or |
| 198 | is create, in which case try to create a target with the argument as the executable. For example: |
| 199 | target list ==> handled by interpreter |
| 200 | target create blah ==> custom creation of target 'blah' |
| 201 | target blah ==> also creates target blah |
| 202 | """ |
| 203 | target_args = [#"create", |
| 204 | "delete", |
| 205 | "list", |
| 206 | "modules", |
| 207 | "select", |
| 208 | "stop-hook", |
| 209 | "symbols", |
| 210 | "variable"] |
| 211 | |
| 212 | a = args.split(' ') |
| 213 | if len(args) == 0 or (len(a) > 0 and a[0] in target_args): |
| 214 | self.doCommand("target", args) |
| 215 | return |
| 216 | elif len(a) > 1 and a[0] == "create": |
| 217 | exe = a[1] |
| 218 | elif len(a) == 1 and a[0] not in target_args: |
| 219 | exe = a[0] |
| 220 | |
| 221 | err = lldb.SBError() |
| 222 | self.target = self.dbg.CreateTarget(exe, None, None, self.load_dependent_modules, err) |
| 223 | if not self.target: |
| 224 | sys.stderr.write("Error creating target %s. %s" % (str(exe), str(err))) |
| 225 | return |
| 226 | |
| 227 | self.ui.activate() |
| 228 | self.ui.update(self.target, "created target %s" % str(exe), self) |
| 229 | |
| 230 | def doContinue(self): |
| 231 | """ Handle 'contiue' command. |
| 232 | FIXME: switch to doCommand("continue", ...) to handle -i ignore-count param. |
| 233 | """ |
| 234 | if not self.process or not self.process.IsValid(): |
| 235 | sys.stderr.write("No process to continue") |
| 236 | return |
| 237 | |
| 238 | self.process.Continue() |
| 239 | self.processPendingEvents(self.eventDelayContinue) |
| 240 | |
| 241 | def doBreakpoint(self, args): |
| 242 | """ Handle breakpoint command with command interpreter, except if the user calls |
| 243 | "breakpoint" with no other args, in which case add a breakpoint at the line |
| 244 | under the cursor. |
| 245 | """ |
| 246 | a = args.split(' ') |
| 247 | if len(args) == 0: |
| 248 | show_output = False |
| 249 | |
| 250 | # User called us with no args, so toggle the bp under cursor |
| 251 | cw = vim.current.window |
| 252 | cb = vim.current.buffer |
| 253 | name = cb.name |
| 254 | line = cw.cursor[0] |
| 255 | |
| 256 | # Since the UI is responsbile for placing signs at bp locations, we have to |
| 257 | # ask it if there already is one or more breakpoints at (file, line)... |
| 258 | if self.ui.haveBreakpoint(name, line): |
| 259 | bps = self.ui.getBreakpoints(name, line) |
| 260 | args = "delete %s" % " ".join([str(b.GetID()) for b in bps]) |
| 261 | self.ui.deleteBreakpoints(name, line) |
| 262 | else: |
| 263 | args = "set -f %s -l %d" % (name, line) |
| 264 | else: |
| 265 | show_output = True |
| 266 | |
| 267 | self.doCommand("breakpoint", args, show_output) |
| 268 | return |
| 269 | |
| 270 | def doRefresh(self): |
| 271 | """ process pending events and update UI on request """ |
| 272 | status = self.processPendingEvents() |
| 273 | |
| 274 | def doShow(self, name): |
| 275 | """ handle :Lshow <name> """ |
Daniel Malea | 2e88c6b6 | 2013-05-22 16:05:55 +0000 | [diff] [blame] | 276 | if not name: |
| 277 | self.ui.activate() |
| 278 | return |
| 279 | |
Daniel Malea | ac4ce0c | 2013-02-11 17:18:14 +0000 | [diff] [blame] | 280 | if self.ui.showWindow(name): |
| 281 | self.ui.update(self.target, "", self) |
| 282 | |
| 283 | def doHide(self, name): |
| 284 | """ handle :Lhide <name> """ |
| 285 | if self.ui.hideWindow(name): |
| 286 | self.ui.update(self.target, "", self) |
| 287 | |
| 288 | def doExit(self): |
| 289 | self.dbg.Terminate() |
| 290 | self.dbg = None |
| 291 | |
| 292 | def getCommandResult(self, command, command_args): |
| 293 | """ Run cmd in the command interpreter and returns (success, output) """ |
| 294 | result = lldb.SBCommandReturnObject() |
| 295 | cmd = "%s %s" % (command, command_args) |
| 296 | |
| 297 | self.commandInterpreter.HandleCommand(cmd, result) |
| 298 | return (result.Succeeded(), result.GetOutput() if result.Succeeded() else result.GetError()) |
| 299 | |
| 300 | def doCommand(self, command, command_args, print_on_success = True, goto_file=False): |
| 301 | """ Run cmd in interpreter and print result (success or failure) on the vim status line. """ |
| 302 | (success, output) = self.getCommandResult(command, command_args) |
| 303 | if success: |
| 304 | self.ui.update(self.target, "", self, goto_file) |
| 305 | if len(output) > 0 and print_on_success: |
| 306 | print output |
| 307 | else: |
| 308 | sys.stderr.write(output) |
| 309 | |
| 310 | def getCommandOutput(self, command, command_args=""): |
| 311 | """ runs cmd in the command interpreter andreturns (status, result) """ |
| 312 | result = lldb.SBCommandReturnObject() |
| 313 | cmd = "%s %s" % (command, command_args) |
| 314 | self.commandInterpreter.HandleCommand(cmd, result) |
| 315 | return (result.Succeeded(), result.GetOutput() if result.Succeeded() else result.GetError()) |
| 316 | |
| 317 | def processPendingEvents(self, wait_seconds=0, goto_file=True): |
| 318 | """ Handle any events that are queued from the inferior. |
| 319 | Blocks for at most wait_seconds, or if wait_seconds == 0, |
| 320 | process only events that are already queued. |
| 321 | """ |
| 322 | |
| 323 | status = None |
| 324 | num_events_handled = 0 |
| 325 | |
| 326 | if self.process is not None: |
| 327 | event = lldb.SBEvent() |
| 328 | old_state = self.process.GetState() |
| 329 | new_state = None |
| 330 | done = False |
| 331 | if old_state == lldb.eStateInvalid or old_state == lldb.eStateExited: |
| 332 | # Early-exit if we are in 'boring' states |
| 333 | pass |
| 334 | else: |
| 335 | while not done and self.processListener is not None: |
| 336 | if not self.processListener.PeekAtNextEvent(event): |
| 337 | if wait_seconds > 0: |
| 338 | # No events on the queue, but we are allowed to wait for wait_seconds |
| 339 | # for any events to show up. |
| 340 | self.processListener.WaitForEvent(wait_seconds, event) |
| 341 | new_state = lldb.SBProcess.GetStateFromEvent(event) |
| 342 | |
| 343 | num_events_handled += 1 |
| 344 | |
| 345 | done = not self.processListener.PeekAtNextEvent(event) |
| 346 | else: |
| 347 | # An event is on the queue, process it here. |
| 348 | self.processListener.GetNextEvent(event) |
| 349 | new_state = lldb.SBProcess.GetStateFromEvent(event) |
| 350 | |
Daniel Malea | bb43721 | 2013-05-28 21:27:03 +0000 | [diff] [blame] | 351 | # continue if stopped after attaching |
| 352 | if old_state == lldb.eStateAttaching and new_state == lldb.eStateStopped: |
| 353 | self.process.Continue() |
| 354 | |
Daniel Malea | ac4ce0c | 2013-02-11 17:18:14 +0000 | [diff] [blame] | 355 | # If needed, perform any event-specific behaviour here |
| 356 | num_events_handled += 1 |
| 357 | |
| 358 | if num_events_handled == 0: |
| 359 | pass |
| 360 | else: |
| 361 | if old_state == new_state: |
| 362 | status = "" |
| 363 | self.ui.update(self.target, status, self, goto_file) |
| 364 | |
| 365 | |
| 366 | def returnCompleteCommand(a, l, p): |
| 367 | """ Returns a "\n"-separated string with possible completion results |
| 368 | for command a with length l and cursor at p. |
| 369 | """ |
| 370 | separator = "\n" |
| 371 | results = ctrl.completeCommand(a, l, p) |
| 372 | vim.command('return "%s%s"' % (separator.join(results), separator)) |
| 373 | |
| 374 | def returnCompleteWindow(a, l, p): |
| 375 | """ Returns a "\n"-separated string with possible completion results |
| 376 | for commands that expect a window name parameter (like hide/show). |
| 377 | FIXME: connect to ctrl.ui instead of hardcoding the list here |
| 378 | """ |
| 379 | separator = "\n" |
| 380 | results = ['breakpoints', 'backtrace', 'disassembly', 'locals', 'threads', 'registers'] |
| 381 | vim.command('return "%s%s"' % (separator.join(results), separator)) |
| 382 | |
| 383 | global ctrl |
| 384 | ctrl = LLDBController() |