rbtrace 0.4.2 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|