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.
@@ -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