rbtrace 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,445 @@
1
+ require 'trollop'
2
+ require 'rbtrace/rbtracer'
3
+
4
+ class RBTraceCLI
5
+ # Suggest increasing the maximum number of bytes allowed on
6
+ # a message queue to 1MB.
7
+ #
8
+ # This defaults to 16k on Linux, and is hardcoded to 2k in OSX kernel.
9
+ #
10
+ # Returns nothing.
11
+ def self.check_msgmnb
12
+ if File.exists?(msgmnb = "/proc/sys/kernel/msgmnb")
13
+ curr = File.read(msgmnb).to_i
14
+ max = 1024*1024
15
+ cmd = "sysctl kernel.msgmnb=#{max}"
16
+
17
+ if curr < max
18
+ if Process.uid == 0
19
+ STDERR.puts "*** running `#{cmd}` for you to prevent losing events (currently: #{curr} bytes)"
20
+ system(cmd)
21
+ else
22
+ STDERR.puts "*** run `sudo #{cmd}` to prevent losing events (currently: #{curr} bytes)"
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # Look for any message queues pairs (pid/-pid) that no longer have an
29
+ # associated process alive, and remove them.
30
+ #
31
+ # Returns nothing.
32
+ def self.cleanup_queues
33
+ if (pids = `ps ax -o pid`.split("\n").map{ |p| p.strip.to_i }).any?
34
+ ipcs = `ipcs -q`.split("\n").grep(/^(q|0x)/).map{ |line| line[/(0x[a-f0-9]+)/,1] }
35
+ ipcs.each do |ipci|
36
+ next if ipci.match(/^0xf/)
37
+
38
+ qi = ipci.to_i(16)
39
+ qo = 0xffffffff - qi + 1
40
+ ipco = "0x#{qo.to_s(16)}"
41
+
42
+ if ipcs.include?(ipco) and !pids.include?(qi)
43
+ STDERR.puts "*** removing stale message queue pair: #{ipci}/#{ipco}"
44
+ system("ipcrm -Q #{ipci} -Q #{ipco}")
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def self.run
51
+ check_msgmnb
52
+ cleanup_queues
53
+
54
+ parser = Trollop::Parser.new do
55
+ version <<-EOS
56
+ rbtrace: like strace, but for ruby code
57
+ version 0.4.3
58
+ (c) 2013 Aman Gupta (tmm1)
59
+ http://github.com/tmm1/rbtrace
60
+ EOS
61
+
62
+ banner <<-EOS
63
+ rbtrace shows you method calls happening inside another ruby process in real time.
64
+
65
+ to use rbtrace, simply `require "rbtrace"` in your ruby app.
66
+
67
+ for examples and more information, see http://github.com/tmm1/rbtrace
68
+
69
+ Usage:
70
+
71
+ rbtrace --exec <CMD> # run and trace <CMD>
72
+ rbtrace --pid <PID+> # trace the given process(es)
73
+ rbtrace --ps <CMD> # look for running <CMD> processes to trace
74
+
75
+ rbtrace -o <FILE> # write output to file
76
+ rbtrace -t # show method call start time
77
+ rbtrace -n # hide duration of each method call
78
+ rbtrace -r 3 # use 3 spaces to nest method calls
79
+
80
+ Tracers:
81
+
82
+ rbtrace --firehose # trace all method calls
83
+ rbtrace --slow=250 # trace method calls slower than 250ms
84
+ rbtrace --methods a b c # trace calls to given methods
85
+ rbtrace --gc # trace garbage collections
86
+
87
+ rbtrace -c io # trace common input/output functions
88
+ rbtrace -c eventmachine # trace common eventmachine functions
89
+ rbtrace -c my.tracer # trace all methods listed in my.tracer
90
+
91
+ Method Selectors:
92
+
93
+ sleep # any instance or class method named sleep
94
+ String#gsub # specific instance method
95
+ Process.pid # specific class method
96
+ Dir. # any class methods in Dir
97
+ Fixnum# # any instance methods of Fixnum
98
+
99
+ Trace Expressions:
100
+
101
+ method(self) # value of self at method invocation
102
+ method(@ivar) # value of given instance variable
103
+ method(arg1, arg2) # value of argument local variables
104
+ method(self.attr) # value of arbitrary ruby expression
105
+ method(__source__) # source file/line of callsite
106
+
107
+
108
+ All Options:\n
109
+
110
+ EOS
111
+ opt :exec,
112
+ "spawn new ruby process and attach to it",
113
+ :type => :strings,
114
+ :short => nil
115
+
116
+ opt :pid,
117
+ "pid of the ruby process to trace",
118
+ :type => :ints,
119
+ :short => '-p'
120
+
121
+ opt :ps,
122
+ "find any matching processes to trace",
123
+ :type => :string,
124
+ :short => nil
125
+
126
+ opt :firehose,
127
+ "show all method calls",
128
+ :short => '-f'
129
+
130
+ opt :slow,
131
+ "watch for method calls slower than 250 milliseconds",
132
+ :default => 250,
133
+ :short => '-s'
134
+
135
+ opt :slowcpu,
136
+ "watch for method calls slower than 250 milliseconds (cpu time only)",
137
+ :default => 250,
138
+ :short => nil
139
+
140
+ opt :slow_methods,
141
+ "method(s) to restrict --slow to",
142
+ :type => :strings
143
+
144
+ opt :methods,
145
+ "method(s) to trace (valid formats: sleep String#gsub Process.pid Kernel# Dir.)",
146
+ :type => :strings,
147
+ :short => '-m'
148
+
149
+ opt :gc,
150
+ "trace garbage collections"
151
+
152
+ opt :start_time,
153
+ "show start time for each method call",
154
+ :short => '-t'
155
+
156
+ opt :no_duration,
157
+ "hide time spent in each method call",
158
+ :default => false,
159
+ :short => '-n'
160
+
161
+ opt :output,
162
+ "write trace to filename",
163
+ :type => String,
164
+ :short => '-o'
165
+
166
+ opt :append,
167
+ "append to output file instead of overwriting",
168
+ :short => '-a'
169
+
170
+ opt :prefix,
171
+ "prefix nested method calls with N spaces",
172
+ :default => 2,
173
+ :short => '-r'
174
+
175
+ opt :config,
176
+ "config file",
177
+ :type => :strings,
178
+ :short => '-c'
179
+
180
+ opt :devmode,
181
+ "assume the ruby process is reloading classes and methods"
182
+
183
+ opt :fork,
184
+ "fork a copy of the process for debugging (so you can attach gdb.rb)"
185
+
186
+ opt :eval,
187
+ "evaluate a ruby expression in the process",
188
+ :type => String,
189
+ :short => '-e'
190
+
191
+ opt :backtrace,
192
+ "get lines from the current backtrace in the process",
193
+ :type => :int
194
+
195
+ opt :wait,
196
+ "seconds to wait before attaching to process",
197
+ :default => 0,
198
+ :short => nil
199
+
200
+ opt :timeout,
201
+ "seconds to wait before giving up on attach/detach",
202
+ :default => 5
203
+ end
204
+
205
+ opts = Trollop.with_standard_exception_handling(parser) do
206
+ raise Trollop::HelpNeeded if ARGV.empty?
207
+ parser.stop_on '--exec'
208
+ parser.parse(ARGV)
209
+ end
210
+
211
+ if ARGV.first == '--exec'
212
+ ARGV.shift
213
+ opts[:exec_given] = true
214
+ opts[:exec] = ARGV.dup
215
+ ARGV.clear
216
+ end
217
+
218
+ unless %w[ fork eval backtrace slow slowcpu firehose methods config gc ].find{ |n| opts[:"#{n}_given"] }
219
+ $stderr.puts "Error: --slow, --slowcpu, --gc, --firehose, --methods or --config required."
220
+ $stderr.puts "Try --help for help."
221
+ exit(-1)
222
+ end
223
+
224
+ if opts[:fork_given] and opts[:pid].size != 1
225
+ parser.die :fork, '(can only be invoked with one pid)'
226
+ end
227
+
228
+ if opts[:exec_given]
229
+ if opts[:pid_given]
230
+ parser.die :exec, '(cannot exec and attach to pid)'
231
+ end
232
+ if opts[:fork_given]
233
+ parser.die :fork, '(cannot fork inside newly execed process)'
234
+ end
235
+ end
236
+
237
+ methods, smethods = [], []
238
+
239
+ if opts[:methods_given]
240
+ methods += opts[:methods]
241
+ end
242
+ if opts[:slow_methods_given]
243
+ smethods += opts[:slow_methods]
244
+ end
245
+
246
+ if opts[:config_given]
247
+ Array(opts[:config]).each do |config|
248
+ file = [
249
+ config,
250
+ File.expand_path("../../tracers/#{config}.tracer", __FILE__)
251
+ ].find{ |f| File.exists?(f) }
252
+
253
+ unless file
254
+ parser.die :config, '(file does not exist)'
255
+ end
256
+
257
+ File.readlines(file).each do |line|
258
+ line.strip!
259
+ next if line =~ /^#/
260
+ next if line.empty?
261
+
262
+ methods << line
263
+ end
264
+ end
265
+ end
266
+
267
+ tracee = nil
268
+
269
+ if opts[:ps_given]
270
+ list = `ps aux`.split("\n")
271
+ filtered = list.grep(Regexp.new opts[:ps])
272
+ filtered.reject! do |line|
273
+ line =~ /^\w+\s+(#{Process.pid}|#{Process.ppid})\s+/ # cannot trace self
274
+ end
275
+
276
+ if filtered.size > 0
277
+ max_len = filtered.size.to_s.size
278
+
279
+ STDERR.puts "*** found #{filtered.size} processes matching #{opts[:ps].inspect}"
280
+ filtered.each_with_index do |line, i|
281
+ STDERR.puts " [#{(i+1).to_s.rjust(max_len)}] #{line.strip}"
282
+ end
283
+ STDERR.puts " [#{'0'.rjust(max_len)}] all #{filtered.size} processes"
284
+
285
+ while true
286
+ STDERR.sync = true
287
+ STDERR.print "*** trace which processes? (0/1,4): "
288
+
289
+ begin
290
+ input = gets
291
+ rescue Interrupt
292
+ exit 1
293
+ end
294
+
295
+ if input =~ /^(\d+,?)+$/
296
+ if input.strip == '0'
297
+ pids = filtered.map do |line|
298
+ line.split[1].to_i
299
+ end
300
+ else
301
+ indices = input.split(',').map(&:to_i)
302
+ pids = indices.map do |i|
303
+ if i > 0 and line = filtered[i-1]
304
+ line.split[1].to_i
305
+ end
306
+ end
307
+ end
308
+
309
+ unless pids.include?(nil)
310
+ opts[:pid] = pids
311
+ break
312
+ end
313
+ end
314
+ end
315
+ else
316
+ STDERR.puts "*** could not find any processes matching #{opts[:ps].inspect}"
317
+ exit 1
318
+ end
319
+ end
320
+
321
+ if opts[:exec_given]
322
+ tracee = fork{
323
+ Process.setsid
324
+ ENV['RUBYOPT'] = "-r#{File.expand_path('../../lib/rbtrace',__FILE__)}"
325
+ exec(*opts[:exec])
326
+ }
327
+ STDERR.puts "*** spawned child #{tracee}: #{opts[:exec].inspect[1..-2]}"
328
+
329
+ if (secs = opts[:wait]) > 0
330
+ STDERR.puts "*** waiting #{secs} seconds for child to boot up"
331
+ sleep secs
332
+ end
333
+
334
+ elsif opts[:pid].size <= 1
335
+ tracee = opts[:pid].first
336
+
337
+ else
338
+ tracers = []
339
+
340
+ opts[:pid].each do |pid|
341
+ if child = fork
342
+ tracers << child
343
+ else
344
+ Process.setpgrp
345
+ STDIN.reopen '/dev/null'
346
+ $0 = "rbtrace -p #{pid} (parent: #{Process.ppid})"
347
+
348
+ opts[:output] += ".#{pid}" if opts[:output]
349
+ tracee = pid
350
+
351
+ # fall through and start tracing
352
+ break
353
+ end
354
+ end
355
+
356
+ if tracee.nil?
357
+ # this is the parent
358
+ while true
359
+ begin
360
+ break if tracers.empty?
361
+ if pid = Process.wait
362
+ tracers.delete(pid)
363
+ end
364
+ rescue Interrupt, SignalException
365
+ STDERR.puts "*** waiting on child tracers: #{tracers.inspect}"
366
+ tracers.each do |pid|
367
+ begin
368
+ Process.kill 'INT', pid
369
+ rescue Errno::ESRCH
370
+ end
371
+ end
372
+ end
373
+ end
374
+
375
+ exit!
376
+ end
377
+ end
378
+
379
+ if out = opts[:output]
380
+ output = File.open(out, opts[:append] ? 'a+' : 'w')
381
+ output.sync = true
382
+ end
383
+
384
+ begin
385
+ begin
386
+ tracer = RBTracer.new(tracee)
387
+ rescue ArgumentError => e
388
+ parser.die :pid, "(#{e.message})"
389
+ end
390
+
391
+ if opts[:fork_given]
392
+ pid = tracer.fork
393
+ STDERR.puts "*** forked off a busy looping copy at #{pid} (make sure to kill -9 it when you're done)"
394
+
395
+ elsif opts[:backtrace_given]
396
+ num = opts[:backtrace]
397
+ code = "caller.first(#{num}).join('|')"
398
+
399
+ if res = tracer.eval(code)
400
+ tracer.puts res[1..-2].split('|').join("\n ")
401
+ end
402
+
403
+ elsif opts[:eval_given]
404
+ if res = tracer.eval(code = opts[:eval])
405
+ tracer.puts ">> #{code}"
406
+ tracer.puts "=> #{res}"
407
+ end
408
+
409
+ else
410
+ tracer.out = output if output
411
+ tracer.timeout = opts[:timeout] if opts[:timeout] > 0
412
+ tracer.prefix = ' ' * opts[:prefix]
413
+ tracer.show_time = opts[:start_time]
414
+ tracer.show_duration = !opts[:no_duration]
415
+
416
+ tracer.devmode if opts[:devmode_given]
417
+ tracer.gc if opts[:gc_given]
418
+
419
+ if opts[:firehose_given]
420
+ tracer.firehose
421
+ else
422
+ tracer.add(methods) if methods.any?
423
+ if opts[:slow_given] || opts[:slowcpu_given]
424
+ tracer.watch(opts[:slowcpu_given] ? opts[:slowcpu] : opts[:slow], opts[:slowcpu_given])
425
+ tracer.add_slow(smethods) if smethods.any?
426
+ end
427
+ end
428
+ begin
429
+ tracer.recv_loop
430
+ rescue Interrupt, SignalException
431
+ end
432
+ end
433
+ ensure
434
+ if tracer
435
+ tracer.detach
436
+ end
437
+
438
+ if opts[:exec_given]
439
+ STDERR.puts "*** waiting on spawned child #{tracee}"
440
+ Process.kill 'TERM', tracee
441
+ Process.waitpid(tracee)
442
+ end
443
+ end
444
+ end
445
+ end
@@ -0,0 +1,17 @@
1
+ class String
2
+ alias :bytesize :size
3
+ end unless ''.respond_to?(:bytesize)
4
+
5
+ module FFI::LastError
6
+ Errnos = Errno::constants.map(&Errno.method(:const_get)).inject({}) do |hash, c|
7
+ hash[ c.const_get(:Errno) ] = c
8
+ hash
9
+ end
10
+
11
+ def self.exception
12
+ Errnos[error]
13
+ end
14
+ def self.raise(msg=nil)
15
+ Kernel.raise exception, msg
16
+ end
17
+ end