-
Notifications
You must be signed in to change notification settings - Fork 0
/
debuglib.lua
647 lines (571 loc) · 19.7 KB
/
debuglib.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
-- debuglib.lua -*- indent-tabs-mode: nil -*-
-- Commands implementing the debugger
-- Loaded automatically at debugger start
-- _____ _
-- / ____| | |
-- | | ___ _ __ ___ _ __ ___ __ _ _ __ __| |___
-- | | / _ \| '_ ` _ \| '_ ` _ \ / _` | '_ \ / _` / __|
-- | |___| (_) | | | | | | | | | | | (_| | | | | (_| \__ \
-- \_____\___/|_| |_| |_|_| |_| |_|\__,_|_| |_|\__,_|___/
require("java_bridge/java_bridge")
local Event = require("debuglib/event")
local Frame = require("debuglib/frame")
-- options
options = {}
-- current stack depth
depth = 1
-- breakpoint list
breakpoints = {}
-- thread condition variable -- initialized in C code
thread_resume_monitor = nil
-- next line tracking
next_line_location = nil
next_line_method_id = nil
-- i/o
dbgio = require("console_io")
-- lock to prevent multiple threads from entering the debugger simultaneously
-- TODO should be moved up into C code to protect lua_State
-- TODO doesn't prevent re-entry on same thread, will require at least g() to be called twice
-- TODO make sure this doesn't deadlock on re-entry
debug_lock = jmonitor.new("debug_lock")
-- current thread being debugged, should be set when debug_lock is acquired
-- and cleared when debug_lock is released
debug_thread = nil
-- used to wait for a debug event
debug_event = jmonitor.new("debug_event")
ThreadName = {}
ThreadName.CMD_THREAD = "this is init'd in start_cmd"
threads = {}
function current_thread()
-- "java.lang.Thread.currentThread()" here will cause recursion with env class lookup
local thread = jclass.find("java/lang/Thread").currentThread()
-- return thread object if already created
-- TODO track and remove with thread destroy event
if threads[thread.name] then
return threads[thread.name]
end
threads[thread.name] = thread
dbgio:print("New Java thread: ", thread)
return thread
end
-- ============================================================
-- Starts command interpreter
-- ============================================================
function start_cmd()
ThreadName.CMD_THREAD = current_thread().name
local x = function (err)
dbgio:write("Error: ")
dbgio:print(debug.traceback(err, 2))
end
if options.runfile then
local code, err = loadfile(options.runfile)
if not code then
dbgio:print("Cannot load " ..err)
else
local success, m2 = xpcall(code, x)
if success and not m2 then
return true -- prevert restarting of start_cmd()
end
end
end
-- command loop
while true do
dbgio:write("yt> ")
local cmd = dbgio:read_line() or ""
if cmd:sub(1, 1) == "=" then
cmd = "return " .. cmd:sub(2)
end
local chunk = load(cmd)
if debug_thread == nil then
local success, m2 = xpcall(chunk, x)
if success then
dbgio:print(m2)
end
else
local event = Event.new(threads[ThreadName.CMD_THREAD], Event.TYPE_COMMAND, {chunk=chunk})
debug_thread.event_queue:push(event)
end
end
end
-- ============================================================
-- Continue execution
-- ============================================================
function g()
if debug_thread ~= nil then
local event = Event.new(debug_thread, Event.TYPE_RESUME)
debug_thread.event_queue:push(event)
else
thread_resume_monitor:notify_without_lock()
end
end
-- ============================================================
-- Help
-- ============================================================
function help()
dbgio:print("Help is on the way...")
end
-- ============================================================
-- Print stack trace
-- ============================================================
function stack()
if #current_thread().frames == 0 then
return("No code running")
end
return current_thread().frames
end
-- ============================================================
-- Print local variables in current stack frame
-- ============================================================
function locals()
local frame = current_thread().frames[depth]
local var_table = frame.method_id.local_variable_table
if var_table == nil then
dbgio:print("No local variable table")
return
end
for k, v in pairs(var_table) do
dbgio:print(string.format("%10s = %s", k, frame[k]))
end
end
-- ============================================================
-- Move to the next executing line of the program
-- Possible scenarios:
-- 1. the next line of code is encountered after one or
-- more single step events
-- a. This may involve a method call and a potentially
-- very large number of single step events
-- 2. we are at the end of the method and continue to the
-- method of the preceding stack frame
-- ============================================================
-- temporarily renamed from next() to next_line(),
-- next() conflicts with lua interator function
function next_line(num)
num = num or 1
-- find location of next line
local f = current_thread().frames[depth]
local line_nums = f.method_id.line_number_table
for idx, ln in ipairs(line_nums) do
if f.location < ln.location then
next_line_location = ln.location
next_line_method_id = f.method_id
lj_set_jvmti_callback("single_step", cb_single_step)
lj_set_jvmti_callback("method_exit", cb_method_exit)
g()
return
end
end
-- fallback to step out of method (from last line of method)
lj_set_jvmti_callback("method_exit", cb_method_exit)
g()
end
-- ============================================================
-- ============================================================
function step(num)
num = num or 1
-- TODO see step_next_line() in yt.c
end
-- ============================================================
-- Move one frame up the stack
-- ============================================================
function up(num)
num = num or 1
return frame(depth + num)
end
-- ============================================================
-- Move one frame down the stack
-- ============================================================
function down(num)
num = num or 1
return frame(depth - num)
end
-- ============================================================
-- Set the stack depth
-- ============================================================
function frame(num)
num = num or 1
local f = current_thread().frames[num]
if not f then
dbgio:print("Invalid frame")
return nil
end
depth = num
return f
end
-- ============================================================
-- Add a new breakpoint
-- takes a method declaration, line number (can be 0)
-- ============================================================
function bp(method, line_num)
local b = {}
b.line_num = line_num or 0
if type(method) == "string" then
b.method_id = jmethod_id.find(method)
if not b.method_id then
error("Cannot find method to set breakpoint")
end
elseif type(method) == "table" and method.classname == "jmethod_id" then
b.method_id = method
elseif type(method) == "table" and method.classname == "jcallable_method" then
for i = 1, #method.possible_methods do
bp(method.possible_methods[i])
end
return
else
error("Invalid method, must be method declaration of form \"pkg/Class.name()V\" or a jmethod_id object")
end
--b.location = line_num -- TEMP to use raw offset --method_location_for_line_num(b.method_id, b.line_num)
b.location = method_location_for_line_num(b.method_id, b.line_num)
if b.location < 0 then
b.location = 0
end
-- make sure bp doesn't already exist
for idx, bp in ipairs(breakpoints) do
if bp.method_id == b.method_id and bp.location == b.location then
dbgio:print("Breakpoint already exists")
return
end
end
-- add tostring()
setmetatable(b, b)
b.__tostring = function(bp)
local disp = string.format("%s.%s%s",
bp.method_id.class.name,
bp.method_id.name,
bp.method_id.sig)
if (bp.line_num) then
disp = disp .. " (line " .. bp.line_num .. ")"
end
return disp
end
lj_set_breakpoint(b.method_id.method_id_raw, b.location)
table.insert(breakpoints, b)
dbgio:print("ok")
return b
end
-- ============================================================
-- List breakpoint(s)
-- ============================================================
function bl(num)
-- print only one
if num then
local bp = breakpoints[num]
if not bp then
dbgio:print("Invalid breakpoint")
return
end
dbgio:print(string.format("%4d: %s", num, bp))
return bp
end
-- print all
if #breakpoints == 0 then
dbgio:print("No breakpoints")
return
end
for idx, bp in ipairs(breakpoints) do
bl(idx)
end
return breakpoints
end
-- ============================================================
-- Clear breakpoint(s)
-- ============================================================
function bc(num)
-- clear all
if not num then
if #breakpoints == 0 then
dbgio:print("No breakpoints")
end
for i = 1, #breakpoints do
bc(1)
end
return
end
local b = breakpoints[num]
if not b then
dbgio:print("unknown breakpoint")
return
end
local desc = string.format("%s", b)
lj_clear_breakpoint(b.method_id.method_id_raw, b.location)
table.remove(breakpoints, num)
dbgio:print("cleared ", desc)
end
-- ___ ____ __ _______ _____ _____ _ _ _ _
-- | \ \ / / \/ |__ __|_ _| / ____| | | | | | |
-- | |\ \ / /| \ / | | | | | | | __ _| | | |__ __ _ ___| | _____
-- _ | | \ \/ / | |\/| | | | | | | | / _` | | | '_ \ / _` |/ __| |/ / __|
-- | |__| | \ / | | | | | | _| |_ | |___| (_| | | | |_) | (_| | (__| <\__ \
-- \____/ \/ |_| |_| |_| |_____| \_____\__,_|_|_|_.__/ \__,_|\___|_|\_\___/
-- ============================================================
-- Handle the callback when a breakpoint is hit
-- ============================================================
function cb_breakpoint(thread_raw, method_id_raw, location)
debug_lock:lock()
debug_thread = current_thread()
-- TODO should be done in C code?
local method_id = jmethod_id.from_raw_method_id(method_id_raw)
local bp
for idx, v in pairs(breakpoints) do
-- TODO and test location
if v.method_id == method_id then
bp = v
end
end
assert(bp)
depth = 1
dbgio:print()
dbgio:print(current_thread().frames[1])
local need_to_handle_events = true
-- run handler if present and resume thread if requested
if bp.handler then
local x = function (err)
dbgio:print(debug.traceback("Error during bp.handler: " .. err, 2))
end
local success, m2 = xpcall(bp.handler, x, bp, debug_thread)
-- return false/nil (no return) means we resume the thread
if success and not m2 then
need_to_handle_events = false
end
end
if need_to_handle_events then
debug_event:broadcast_without_lock()
debug_thread:handle_events()
end
debug_thread = nil
debug_lock:unlock()
end
function cb_method_entry(thread_raw, method_id_raw)
end
function cb_method_exit(thread_raw, method_id_raw, was_popped_by_exception, return_value)
debug_lock:lock()
debug_thread = current_thread()
dbgio:print(current_thread().frames[depth])
debug_event:broadcast_without_lock()
debug_thread:handle_events()
debug_thread = nil
debug_lock:unlock()
end
function cb_single_step(thread_raw, method_id_raw, location)
debug_lock:lock()
debug_thread = current_thread()
local method_id = jmethod_id.from_raw_method_id(method_id_raw)
if next_line_method_id ~= method_id then
-- single stepped into a different method, disable single steps until
-- it exits
lj_clear_jvmti_callback("single_step")
local previous_frame_count = #current_thread().frames
local check_nested_method_return = function(thread_raw, method_id_raw, was_popped_by_exception, return_value)
local thread = jthread.create(thread_raw)
if #thread.frames == previous_frame_count then
lj_set_jvmti_callback("method_exit", cb_method_exit)
lj_set_jvmti_callback("single_step", cb_single_step)
end
end
lj_set_jvmti_callback("method_exit", check_nested_method_return)
elseif next_line_location and location >= next_line_location and next_line_method_id == method_id then
local data = {method_id=method_id, location=location}
lj_clear_jvmti_callback("method_exit")
lj_clear_jvmti_callback("single_step")
next_line_location = nil
next_line_method_id = nil
dbgio:print(current_thread().frames[depth])
debug_event:broadcast_without_lock()
debug_thread:handle_events()
end
debug_thread = nil
debug_lock:unlock()
end
function init_jvmti_callbacks()
lj_set_jvmti_callback("breakpoint", cb_breakpoint)
end
-- _ _ _ _ _
-- | | | | | (_) |
-- | | | | |_ _| |___
-- | | | | __| | / __|
-- | |__| | |_| | \__ \
-- \____/ \__|_|_|___/
-- ============================================================
-- parse debugger options
-- formatted like opt1=val1,opt2=val2
function setopts(optstring)
if #optstring == 0 then
return
end
-- separator location or nil if single param
local seploc = string.find(optstring, ",")
local opt
if seploc then
opt = string.sub(optstring, 1, seploc - 1)
setopts(string.sub(optstring, seploc + 1))
else
opt = optstring
end
-- parse option key and value
seploc = string.find(opt, "=")
if not seploc then
dbgio:print("Invalid option: ", opt)
return
end
options[string.sub(opt, 1, seploc - 1)] = string.sub(opt, seploc + 1, -1)
end
-- ============================================================
-- "temporary" table to facilitate addressing Java classes
-- in the form pkg.x.Class directly in Lua
function fq_class_search(pkg, previous_t)
local t = {}
t.pkg = pkg
if previous_t then
t.pkg = previous_t.pkg .. "." .. t.pkg
end
local mt = getmetatable(t) or (setmetatable(t, {}) and getmetatable(t))
mt.__index = function(t, k)
local class = jclass.find(string.gsub(t.pkg .. "." .. k, "[.]", "/"))
if class then
-- we found a class
return class
else
-- search another level of packages
return fq_class_search(k, t)
end
end
mt.__tostring = function(t)
return "fully qualified class search, pkg=" .. t.pkg
end
return t
end
-- ============================================================
-- Find a local variable
-- Return "value, k" if found, "nil, nil" otherwise
function get_local_variable(k)
local frame = current_thread().frames[depth]
if not frame or
not frame.method_id.local_variable_table or
not frame.method_id.local_variable_table[k] then
return nil, nil
end
return frame[k], k
end
-- ============================================================
-- make locals, Java classes, etc available throughout
function init_locals_environment()
local java_pkg_tlds = {"java", "javax", "com", "org", "net", "testsuite"}
local mt = getmetatable(_ENV) or (setmetatable(_ENV, {}) and getmetatable(_ENV))
mt.__index = function(t, k)
if not k then return nil end
if rawget(t, k) then
return rawget(t, k)
end
if k == "this" then
return current_thread().frames[depth]:local_slot(0, "Ljava/lang/Object;")
end
-- find a local variable
local lv, name = get_local_variable(k)
if name then return lv end
-- TODO: try members of `this'
-- find a fully-qualified class
local class = jclass.find(k)
if class then return class end
-- TODO: try class name without package
-- last possibility:
-- start com.*.Class search
for idx, tld in ipairs(java_pkg_tlds) do
if k == tld then
return fq_class_search(k, nil)
end
end
end
mt.__newindex = function(t, k, v)
rawset(t, k, v)
end
end
-- ============================================================
-- Find the location (offset) in a method for a given line number
function method_location_for_line_num(jmethod_id, line_num)
if not line_num then return -1 end
local lnt = jmethod_id.line_number_table
if not lnt then return -1 end
for idx, ln in ipairs(lnt) do
if line_num == ln.line_num then
return ln.location
elseif line_num < ln.line_num and idx > 1 then
return lnt[idx-1].location
elseif line_num < ln.line_num then
return 0
end
end
return lnt[#lnt].location
end
-- ============================================================
-- http://snippets.luacode.org/?p=snippets/Simple_Table_Dump_7
-- fixed to print recursive tables
function dump(o)
dump_params = {}
dump_depth = 0
return dump_internal(o)
end
function dump_internal(o)
if type(o) == "table" and dump_depth > 1 then
return "<table>"
end
dump_depth = dump_depth + 1
if type(o) == "table" and dump_params[o] ~= nil then
dump_depth = dump_depth - 1
return "<recursion>"
end
dump_params[o] = 1
local prefix = ""
for i = 1, dump_depth do
prefix = prefix .. " "
end
local classname = ""
if type(o) == "table" and o.classname then
classname = o.classname
end
if classname == "jclass" then
return o.dump(prefix)
elseif classname == "jfield_id" or classname == "jmethod_id" then
return string.format("%s%s", prefix, o)
elseif type(o) == 'table' then
local s = '{ ' .. "\n"
for k,v in pairs(o) do
if type(k) == 'table' then
k = dump_internal(k)
elseif type(k) ~= 'number' then
k = '"'..k..'"'
end
if type(v) == "DISABLEtable" then
s = s .. '['..k..'] = ' .. "<table>" .. ','
else
s = s .. prefix .. '['..k..'] = ' .. dump_internal(v) .. ','
end
s = s .. "\n"
end
dump_depth = dump_depth - 1
return s .. '} '
else
dump_depth = dump_depth - 1
return tostring(o)
end
end
-- Import a class by inserting it into the global env with the key given by the class' simple name
function import(class)
if type(class) == "table" and class.classname == "jclass" then
_ENV[class.getSimpleName().toString()] = class
return class
elseif type(class) == "string" then
cl = jclass.find(class:gsub("%.", "/"))
return import(cl)
else
error("Not a valid class")
end
end
init_locals_environment()
init_jvmti_callbacks()
Class = java.lang.Class
Thread = java.lang.Thread
System = java.lang.System
String = java.lang.String
File = java.io.File
-- HACK: special value to use in place of 'nil' to indicate Java NULL. Lua fucks up with nil at the end of a list (varargs :(
JavaNull = {0.841470985}
print("debuglib.lua - loaded with " .. _VERSION)