|
74 | 74 | require 'ruby-debug-ide/greeter'
|
75 | 75 | Debugger::print_greeting_msg(nil, nil)
|
76 | 76 |
|
77 |
| -class NativeDebugger |
78 |
| - |
79 |
| - attr_reader :pid, :main_thread, :process_threads, :pipe |
80 |
| - |
81 |
| - # @param executable -- path to ruby interpreter |
82 |
| - # @param pid -- pid of process you want to debug |
83 |
| - # @param flags -- flags you want to specify to your debugger as a string (e.g. "-nx -nh" for gdb to disable .gdbinit) |
84 |
| - def initialize(executable, pid, flags, gems_to_include, debugger_loader_path, argv) |
85 |
| - @pid = pid |
86 |
| - @delimiter = '__OUTPUT_FINISHED__' # for getting response |
87 |
| - @tbreak = '__func_to_set_breakpoint_at' |
88 |
| - @main_thread = nil |
89 |
| - @process_threads = nil |
90 |
| - debase_path = gems_to_include.select {|gem_path| gem_path =~ /debase/} |
91 |
| - if debase_path.size == 0 |
92 |
| - raise 'No debase gem found.' |
93 |
| - end |
94 |
| - @path_to_attach = find_attach_lib(debase_path[0]) |
95 |
| - |
96 |
| - @gems_to_include = '["' + gems_to_include * '", "' + '"]' |
97 |
| - @debugger_loader_path = debugger_loader_path |
98 |
| - @argv = argv |
99 |
| - |
100 |
| - @eval_string = "rb_eval_string_protect(\"require '#{@debugger_loader_path}'; load_debugger(#{@gems_to_include.gsub("\"", "'")}, #{@argv.gsub("\"", "'")})\", (int *)0)" |
101 |
| - |
102 |
| - launch_string = "#{self} #{executable} #{flags}" |
103 |
| - @pipe = IO.popen(launch_string, 'r+') |
104 |
| - $stdout.puts "executed '#{launch_string}'" |
105 |
| - end |
106 |
| - |
107 |
| - def find_attach_lib(debase_path) |
108 |
| - attach_lib = debase_path + '/attach' |
109 |
| - known_extensions = %w(.so .bundle .dll) |
110 |
| - known_extensions.each do |ext| |
111 |
| - if File.file?(attach_lib + ext) |
112 |
| - return attach_lib + ext |
113 |
| - end |
114 |
| - end |
115 |
| - |
116 |
| - raise 'Could not find attach library' |
117 |
| - end |
118 |
| - |
119 |
| - def attach_to_process |
120 |
| - execute "attach #{@pid}" |
121 |
| - end |
122 |
| - |
123 |
| - def execute(command) |
124 |
| - @pipe.puts command |
125 |
| - $stdout.puts "executed `#{command}` command inside #{self}." |
126 |
| - if command == 'q' |
127 |
| - return '' |
128 |
| - end |
129 |
| - get_response |
130 |
| - end |
131 |
| - |
132 |
| - def get_response |
133 |
| - # we need this hack to understand that debugger gave us all output from last executed command |
134 |
| - print_delimiter |
135 |
| - |
136 |
| - content = '' |
137 |
| - loop do |
138 |
| - line = @pipe.readline |
139 |
| - break if check_delimiter(line) |
140 |
| - DebugPrinter.print_debug('respond line: ' + line) |
141 |
| - next if line =~ /\(lldb\)/ # lldb repeats your input to its output |
142 |
| - content += line |
143 |
| - end |
144 |
| - |
145 |
| - content |
146 |
| - end |
147 |
| - |
148 |
| - def update_threads |
149 |
| - |
150 |
| - end |
151 |
| - |
152 |
| - def check_already_under_debug |
153 |
| - |
154 |
| - end |
155 |
| - |
156 |
| - def print_delimiter |
157 |
| - |
158 |
| - end |
159 |
| - |
160 |
| - def check_delimiter(line) |
161 |
| - |
162 |
| - end |
163 |
| - |
164 |
| - def switch_to_thread |
165 |
| - |
166 |
| - end |
167 |
| - |
168 |
| - def set_tbreak(str) |
169 |
| - execute "tbreak #{str}" |
170 |
| - end |
171 |
| - |
172 |
| - def continue |
173 |
| - $stdout.puts 'continuing' |
174 |
| - @pipe.puts 'c' |
175 |
| - loop do |
176 |
| - line = @pipe.readline |
177 |
| - break if line =~ /#{Regexp.escape(@tbreak)}/ |
178 |
| - end |
179 |
| - get_response |
180 |
| - end |
181 |
| - |
182 |
| - def call_start_attach |
183 |
| - raise 'No main thread found. Did you forget to call `update_threads`?' if @main_thread == nil |
184 |
| - @main_thread.switch |
185 |
| - end |
186 |
| - |
187 |
| - def wait_line_event |
188 |
| - call_start_attach |
189 |
| - continue |
190 |
| - end |
191 |
| - |
192 |
| - def load_debugger |
193 |
| - |
194 |
| - end |
195 |
| - |
196 |
| - def exit |
197 |
| - execute 'q' |
198 |
| - @pipe.close |
199 |
| - end |
200 |
| - |
201 |
| - def to_s |
202 |
| - 'native_debugger' |
203 |
| - end |
204 |
| - |
205 |
| -end |
206 |
| - |
207 |
| -class LLDB < NativeDebugger |
208 |
| - |
209 |
| - def initialize(executable, pid, flags, gems_to_include, debugger_loader_path, argv) |
210 |
| - super(executable, pid, flags, gems_to_include, debugger_loader_path, argv) |
211 |
| - end |
212 |
| - |
213 |
| - def set_flags |
214 |
| - |
215 |
| - end |
216 |
| - |
217 |
| - def update_threads |
218 |
| - @process_threads = [] |
219 |
| - info_threads = (execute 'thread list').split("\n") |
220 |
| - info_threads.each do |thread_info| |
221 |
| - next unless thread_info =~ /[\s*]*thread\s#\d+.*/ |
222 |
| - is_main = thread_info[0] == '*' |
223 |
| - thread_num = thread_info.sub(/[\s*]*thread\s#/, '').sub(/:\s.*$/, '').to_i |
224 |
| - thread = ProcessThread.new(thread_num, is_main, thread_info, self) |
225 |
| - if thread.is_main |
226 |
| - @main_thread = thread |
227 |
| - end |
228 |
| - @process_threads << thread |
229 |
| - end |
230 |
| - @process_threads |
231 |
| - end |
232 |
| - |
233 |
| - def check_already_under_debug |
234 |
| - threads = execute 'thread list' |
235 |
| - threads =~ /ruby-debug-ide/ |
236 |
| - end |
237 |
| - |
238 |
| - def switch_to_thread(thread_num) |
239 |
| - execute "thread select #{thread_num}" |
240 |
| - end |
241 |
| - |
242 |
| - def call_start_attach |
243 |
| - super() |
244 |
| - execute "expr (void *) dlopen(\"#{@path_to_attach}\", 2)" |
245 |
| - execute 'expr (int) start_attach()' |
246 |
| - set_tbreak(@tbreak) |
247 |
| - end |
248 |
| - |
249 |
| - def print_delimiter |
250 |
| - @pipe.puts "script print \"#{@delimiter}\"" |
251 |
| - end |
252 |
| - |
253 |
| - def check_delimiter(line) |
254 |
| - line =~ /#{@delimiter}$/ |
255 |
| - end |
256 |
| - |
257 |
| - def load_debugger |
258 |
| - execute "expr (VALUE) #{@eval_string}" |
259 |
| - end |
260 |
| - |
261 |
| - def to_s |
262 |
| - 'lldb' |
263 |
| - end |
264 |
| - |
265 |
| -end |
266 |
| - |
267 |
| -class GDB < NativeDebugger |
268 |
| - |
269 |
| - def initialize(executable, pid, flags, gems_to_include, debugger_loader_path, argv) |
270 |
| - super(executable, pid, flags, gems_to_include, debugger_loader_path, argv) |
271 |
| - end |
272 |
| - |
273 |
| - def set_flags |
274 |
| - execute 'set scheduler-locking off' # we will deadlock with it |
275 |
| - execute 'set unwindonsignal on' # in case of some signal we will exit gdb |
276 |
| - end |
277 |
| - |
278 |
| - def update_threads |
279 |
| - @process_threads = [] |
280 |
| - info_threads = (execute 'info threads').split("\n") |
281 |
| - info_threads.each do |thread_info| |
282 |
| - next unless thread_info =~ /[\s*]*\d+\s+Thread.*/ |
283 |
| - $stdout.puts "thread_info: #{thread_info}" |
284 |
| - is_main = thread_info[0] == '*' |
285 |
| - thread_num = thread_info.sub(/[\s*]*/, '').sub(/\s.*$/, '').to_i |
286 |
| - thread = ProcessThread.new(thread_num, is_main, thread_info, self) |
287 |
| - if thread.is_main |
288 |
| - @main_thread = thread |
289 |
| - end |
290 |
| - @process_threads << thread |
291 |
| - end |
292 |
| - @process_threads |
293 |
| - end |
294 |
| - |
295 |
| - def check_already_under_debug |
296 |
| - threads = execute 'info threads' |
297 |
| - threads =~ /ruby-debug-ide/ |
298 |
| - end |
299 |
| - |
300 |
| - def switch_to_thread(thread_num) |
301 |
| - execute "thread #{thread_num}" |
302 |
| - end |
303 |
| - |
304 |
| - def call_start_attach |
305 |
| - super() |
306 |
| - execute "call dlopen(\"#{@path_to_attach}\", 2)" |
307 |
| - execute 'call start_attach()' |
308 |
| - set_tbreak(@tbreak) |
309 |
| - end |
310 |
| - |
311 |
| - def print_delimiter |
312 |
| - @pipe.puts "print \"#{@delimiter}\"" |
313 |
| - end |
314 |
| - |
315 |
| - def check_delimiter(line) |
316 |
| - line =~ /\$\d+\s=\s"#{@delimiter}"/ |
317 |
| - end |
318 |
| - |
319 |
| - def load_debugger |
320 |
| - execute "call #{@eval_string}" |
321 |
| - end |
322 |
| - |
323 |
| - def to_s |
324 |
| - 'gdb' |
325 |
| - end |
326 |
| - |
327 |
| -end |
328 |
| - |
329 |
| -class ProcessThread |
330 |
| - |
331 |
| - attr_reader :thread_num, :is_main, :thread_info, :last_bt |
332 |
| - |
333 |
| - def initialize(thread_num, is_main, thread_info, native_debugger) |
334 |
| - @thread_num = thread_num |
335 |
| - @is_main = is_main |
336 |
| - @native_debugger = native_debugger |
337 |
| - @thread_info = thread_info |
338 |
| - @last_bt = nil |
339 |
| - end |
340 |
| - |
341 |
| - def switch |
342 |
| - @native_debugger.switch_to_thread(thread_num) |
343 |
| - end |
344 |
| - |
345 |
| - def finish |
346 |
| - @native_debugger.execute 'finish' |
347 |
| - end |
348 |
| - |
349 |
| - def get_bt |
350 |
| - @last_bt = @native_debugger.execute 'bt' |
351 |
| - end |
352 |
| - |
353 |
| - def any_caller_match(bt, pattern) |
354 |
| - bt =~ /#{pattern}/ |
355 |
| - end |
356 |
| - |
357 |
| - def is_inside_malloc(bt = get_bt) |
358 |
| - if any_caller_match(bt, '(malloc\.c)') |
359 |
| - $stderr.puts "process #{@native_debugger.pid} is currently inside malloc." |
360 |
| - true |
361 |
| - else |
362 |
| - false |
363 |
| - end |
364 |
| - end |
365 |
| - |
366 |
| - def is_inside_gc(bt = get_bt) |
367 |
| - if any_caller_match(bt, '(gc\.c)') |
368 |
| - $stderr.puts "process #{@native_debugger.pid} is currently in garbage collection phase." |
369 |
| - true |
370 |
| - else |
371 |
| - false |
372 |
| - end |
373 |
| - end |
374 |
| - |
375 |
| - def need_finish_frame |
376 |
| - bt = get_bt |
377 |
| - is_inside_malloc(bt) || is_inside_gc(bt) |
378 |
| - end |
379 |
| - |
380 |
| -end |
381 |
| - |
382 |
| -def command_exists(command) |
383 |
| - checking_command = "checking command #{command} for existence\n" |
384 |
| - `command -v #{command} >/dev/null 2>&1 || { exit 1; }` |
385 |
| - if $?.exitstatus != 0 |
386 |
| - DebugPrinter.print_debug("#{checking_command}command does not exist.") |
387 |
| - else |
388 |
| - DebugPrinter.print_debug("#{checking_command}command does exist.") |
389 |
| - end |
390 |
| - $?.exitstatus == 0 |
391 |
| -end |
392 |
| - |
393 |
| -def choose_debugger(ruby_path, pid, gems_to_include, debugger_loader_path, argv) |
394 |
| - if command_exists('lldb') |
395 |
| - debugger = LLDB.new(ruby_path, pid, '--no-lldbinit', gems_to_include, debugger_loader_path, argv) |
396 |
| - elsif command_exists('gdb') |
397 |
| - debugger = GDB.new(ruby_path, pid, '-nh -nx', gems_to_include, debugger_loader_path, argv) |
398 |
| - else |
399 |
| - raise 'Neither gdb nor lldb was found. Aborting.' |
400 |
| - end |
401 |
| - |
402 |
| - trap('INT') do |
403 |
| - unless debugger.pipe.closed? |
404 |
| - $stderr.puts "backtraces for threads:\n\n" |
405 |
| - debugger.process_threads.each do |thread| |
406 |
| - $stderr.puts "#{thread.thread_info}\n#{thread.last_bt}\n\n" |
407 |
| - end |
408 |
| - end |
409 |
| - exit! |
410 |
| - end |
411 |
| - |
412 |
| - debugger |
413 |
| -end |
| 77 | +require 'ruby-debug-ide/attach/util' |
| 78 | +require 'ruby-debug-ide/attach/native_debugger' |
| 79 | +require 'ruby-debug-ide/attach/process_thread' |
414 | 80 |
|
415 | 81 | debugger = choose_debugger(options.ruby_path, options.pid, options.gems_to_include, debugger_loader_path, argv)
|
416 | 82 | debugger.attach_to_process
|
|
0 commit comments