rbtrace 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
- data/.gitignore +2 -0
- data/Gemfile.lock +1 -1
- data/bin/rbtrace +2 -1077
- data/lib/rbtrace/cli.rb +445 -0
- data/lib/rbtrace/core_ext.rb +17 -0
- data/lib/rbtrace/msgq.rb +50 -0
- data/lib/rbtrace/rbtracer.rb +570 -0
- data/rbtrace.gemspec +1 -1
- data/test.sh +1 -1
- metadata +6 -2
data/lib/rbtrace/cli.rb
ADDED
|
@@ -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
|