33require 'optparse'
44require 'ostruct'
55
6+ $stdout. sync = true
7+ $stderr. sync = true
8+
69options = OpenStruct . new (
7- 'pid' => nil ,
8- 'sdk_path' => nil ,
9- 'uid' => nil ,
10- 'gems_to_include' => [ ]
10+ 'pid' => nil ,
11+ 'sdk_path' => nil ,
12+ 'uid' => nil ,
13+ 'gems_to_include' => [ ]
1114)
1215
1316opts = OptionParser . new do |opts |
@@ -16,82 +19,244 @@ opts = OptionParser.new do |opts|
1619Some useful banner.
1720EOB
1821
19- opts . on ( " --pid PID" , " pid of process you want to attach to for debugging" ) do |pid |
22+ opts . on ( ' --pid PID' , ' pid of process you want to attach to for debugging' ) do |pid |
2023 options . pid = pid
2124 end
2225
23- opts . on ( " --ruby-path SDK_PATH" , " path to ruby interpreter" ) do |ruby_path |
26+ opts . on ( ' --ruby-path RUBY_PATH' , ' path to ruby interpreter' ) do |ruby_path |
2427 options . ruby_path = ruby_path
2528 end
2629
27- opts . on ( " --uid UID" , " uid which this process should set after executing gdb attach" ) do |uid |
30+ opts . on ( ' --uid UID' , ' uid which this process should set after executing gdb attach' ) do |uid |
2831 options . uid = uid
2932 end
3033
31- opts . on ( " --include-gem GEM_LIB_PATH" , " lib of gem to include" ) do |gem_lib_path |
34+ opts . on ( ' --include-gem GEM_LIB_PATH' , ' lib of gem to include' ) do |gem_lib_path |
3235 options . gems_to_include << gem_lib_path
3336 end
3437end
3538
3639opts . parse! ARGV
3740
3841unless options . pid
39- $stderr. puts " You must specify PID of process you want to attach to"
42+ $stderr. puts ' You should specify PID of process you want to attach to'
4043 exit 1
4144end
4245
4346unless options . ruby_path
44- $stderr. puts " You must specify RUBY_PATH of ruby interpreter"
47+ $stderr. puts ' You should specify path to the ruby interpreter'
4548 exit 1
4649end
4750
48- # TODO Denis told not to implement this hack
49- # So this is only for me while debugging as
50- # I don't want to get any warnings.
51- sigints_caught = 0
52- trap ( 'INT' ) do
53- sigints_caught += 1
54- if sigints_caught == 2
55- exit 0
56- end
57- end
58-
5951argv = '["' + ARGV * '", "' + '"]'
6052gems_to_include = '["' + options . gems_to_include * '", "' + '"]'
6153
62- commands_list = [ ]
54+ path_to_debugger_loader = File . expand_path ( File . dirname ( __FILE__ ) ) + '/../lib/ruby-debug-ide/attach/debugger_loader'
6355
64- def commands_list . << ( command )
65- self . push "-ex \" #{ command } \" "
56+ options . gems_to_include . each do | gem_path |
57+ $LOAD_PATH . unshift ( gem_path ) unless $LOAD_PATH . include? ( gem_path )
6658end
6759
68- path_to_debugger_loader = File . expand_path ( File . dirname ( __FILE__ ) ) + '/../lib/ruby-debug-ide/attach/debugger_loader'
60+ require 'ruby-debug-ide/greeter'
61+ Debugger ::print_greeting_msg ( nil , nil )
6962
70- # rb_finish: wait while execution comes to the next line.
71- # This is essential because we could interrupt process in a middle
72- # of some evaluations (e.g., system call)
73- commands_list << "call rb_eval_string_protect(\\ \" set_trace_func lambda{|event, file, line, id, binding, classname| if /line/ =~ event; sleep 0; set_trace_func(nil); end}\\ \" , (int *)0)"
74- commands_list << "tbreak rb_f_sleep"
75- commands_list << "cont"
63+ $pid = options . pid
64+ $last_bt = ''
65+ $gdb_tmp_file = '/tmp/gdb_out.txt'
7666
77- # evalr: loading debugger into the process
78- evalr = "call rb_eval_string_protect(%s, (int *)0)"
79- commands_list << ( "#{ evalr } " % [ "(\\ \" require '#{ path_to_debugger_loader } '; load_debugger(#{ gems_to_include . gsub ( "\" " , "'" ) } , #{ argv . gsub ( "\" " , "'" ) } )\\ \" )" ] )
67+ begin
68+ file = File . open ( $gdb_tmp_file, 'w' )
69+ file . truncate ( 0 )
70+ file . close
71+ rescue Exception => e
72+ $stderr. puts e
73+ $stderr. puts "Could not create file #{ $gdb_tmp_file} for gdb logging. Aborting."
74+ exit!
75+ end
8076
81- # q: exit gdb and continue process execution with debugger
82- commands_list << "q"
77+ gdb_executed_all_commands = false
8378
84- cmd = "gdb #{ options . ruby_path } #{ options . pid } -nh -nx -batch #{ commands_list . join ( " " ) } "
79+ IO . popen ( "gdb #{ options . ruby_path } #{ options . pid } -nh -nx" , 'r+' ) do | gdb |
8580
86- options . gems_to_include . each do |gem_path |
87- $LOAD_PATH. unshift ( gem_path ) unless $LOAD_PATH. include? ( gem_path )
88- end
81+ $gdb = gdb
82+ $main_thread = nil
8983
90- require 'ruby-debug-ide/greeter'
91- Debugger ::print_greeting_msg ( nil , nil )
92- $stderr. puts "Running command #{ cmd } "
84+ class ProcessThread
85+
86+ attr_reader :thread_num , :is_main
87+
88+ def initialize ( thread_num , is_main )
89+ @thread_num = thread_num
90+ @is_main = is_main
91+ end
92+
93+ def switch
94+ $gdb. execute "thread #{ thread_num } "
95+ end
96+
97+ def finish
98+ $gdb. finish
99+ end
100+
101+ def get_bt
102+ return $gdb. execute 'bt'
103+ end
104+
105+ def top_caller_match ( bt , pattern )
106+ return bt . split ( '#' ) [ 1 ] =~ /#{ pattern } /
107+ end
108+
109+ def any_caller_match ( bt , pattern )
110+ return bt =~ /#{ pattern } /
111+ end
112+
113+ def is_inside_malloc ( bt = get_bt )
114+ if any_caller_match ( bt , '(malloc\.c)' )
115+ $stderr. puts "process #{ $pid} is currently inside malloc."
116+ return true
117+ else
118+ return false
119+ end
120+ end
121+
122+ def is_inside_gc ( bt = get_bt )
123+ if any_caller_match ( bt , '(gc\.c)' )
124+ $stderr. puts "process #{ $pid} is currently in garbage collection phase."
125+ return true
126+ else
127+ return false
128+ end
129+ end
130+
131+ def need_finish_frame
132+ bt = get_bt
133+ return is_inside_malloc ( bt ) || is_inside_gc ( bt )
134+ end
135+
136+ end
137+
138+ def gdb . update_threads
139+ process_threads = [ ]
140+ info_threads = ( self . execute 'info threads' ) . split ( "\n " )
141+ # first line of gdb's response is ` Id Target Id Frame` info line
142+ # last line of gdb's response is `(gdb) `
143+ info_threads . shift
144+ info_threads . pop
145+ # each thread info looks like this:
146+ # 3 Thread 0x7ff535405700 (LWP 8291) "ruby-timer-thr" 0x00007ff534a15fdd in poll () at ../sysdeps/unix/syscall-template.S:81
147+ info_threads . each do |thread_info |
148+ next unless thread_info =~ /[\s *]*\d +\s +Thread.*/
149+ $stderr. puts "thread_info: #{ thread_info } "
150+ is_main = thread_info [ 0 ] == '*'
151+ thread_info . sub! ( /[\s *]*/ , '' )
152+ thread_info . sub! ( /\s .*$/ , '' )
153+ thread = ProcessThread . new ( thread_info . to_i , is_main )
154+ if thread . is_main
155+ $main_thread = thread
156+ end
157+ process_threads << thread
158+ end
159+ process_threads
160+ end
161+
162+ def gdb . get_response
163+ content = ''
164+ loop do
165+ sleep 0.01 # give time to gdb to finish command execution and print it to file
166+ file = File . open ( $gdb_tmp_file, 'r' )
167+ content = file . read
168+ file . close
169+ break if content =~ /\( gdb\) \s \z /
170+ end
171+ content
172+ end
173+
174+ def gdb . enable_logging
175+ self . puts 'set logging on'
176+ end
177+
178+ def gdb . disable_logging
179+ self . puts 'set logging off'
180+ end
181+
182+ def gdb . overwrite_file
183+ disable_logging
184+ enable_logging
185+ end
186+
187+ def gdb . execute ( command )
188+ self . overwrite_file
189+ self . puts command
190+ $stdout. puts "executed command '#{ command } ' inside gdb."
191+ if command == 'q'
192+ return ''
193+ end
194+ response = self . get_response
195+ if command == 'bt'
196+ $last_bt = response
197+ end
198+ return response
199+ end
200+
201+ def gdb . finish
202+ $stdout. puts 'trying to finish current frame.'
203+ self . execute 'finish'
204+ end
205+
206+ def gdb . set_logging
207+ self . puts "set logging file #{ $gdb_tmp_file} "
208+ self . puts 'set logging overwrite on'
209+ self . puts 'set logging redirect on'
210+ self . enable_logging
211+
212+ $stdout. puts "all gdb output redirected to #{ $gdb_tmp_file} ."
213+ end
214+
215+ def gdb . check_already_under_debug
216+ threads = self . execute 'info threads'
217+ return threads =~ /ruby-debug-ide/
218+ end
93219
94- `#{ cmd } ` or raise "GDB failed. Aborting."
220+ gdb . set_logging
221+
222+ if gdb . check_already_under_debug
223+ $stderr. puts "Process #{ $pid} is already under debug"
224+ gdb . execute 'q'
225+ end
226+
227+ gdb . execute 'set scheduler-locking off'
228+ gdb . execute 'set unwindonsignal on'
229+
230+ should_check_threads_state = true
231+
232+ while should_check_threads_state
233+ should_check_threads_state = false
234+ gdb . update_threads . each do |thread |
235+ thread . switch
236+ while thread . need_finish_frame
237+ should_check_threads_state = true
238+ thread . finish
239+ end
240+ end
241+ end
242+
243+ $main_thread. switch
244+
245+ gdb . execute "call dlopen(\" /home/equi/Job/JetBrains/Internship_2016/ruby-debug-ide/ext/libAttach.so\" , 2)"
246+ gdb . execute "call start_attach(\" require '#{ path_to_debugger_loader } '; load_debugger(#{ gems_to_include . gsub ( "\" " , "'" ) } , #{ argv . gsub ( "\" " , "'" ) } )\" )"
247+
248+ gdb_executed_all_commands = true
249+ gdb . execute 'q'
250+
251+ end
252+
253+ trap ( 'INT' ) do
254+ unless gdb_executed_all_commands
255+ $stderr. puts "Seems like could not attach to process. Its backtrace:\n #{ $last_bt} "
256+ $stderr. flush
257+ end
258+ exit 1
259+ end
95260
96261if options . uid
97262 Process ::Sys . setuid ( options . uid . to_i )
0 commit comments