Skip to content

Commit 899d73a

Browse files
authored
Merge pull request #85 from equivalence1/ruby-attach-to-process
refactoring: separated debuggers and util methods
2 parents f56827b + 705c66f commit 899d73a

File tree

6 files changed

+357
-337
lines changed

6 files changed

+357
-337
lines changed

bin/gdb_wrapper

Lines changed: 3 additions & 337 deletions
Original file line numberDiff line numberDiff line change
@@ -74,343 +74,9 @@ end
7474
require 'ruby-debug-ide/greeter'
7575
Debugger::print_greeting_msg(nil, nil)
7676

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'
41480

41581
debugger = choose_debugger(options.ruby_path, options.pid, options.gems_to_include, debugger_loader_path, argv)
41682
debugger.attach_to_process

0 commit comments

Comments
 (0)