|
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