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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NmQ5ZDFlYWZkMTFjNGFhNzc1ZTNiM2RjNDAxYjQwMmYzMDcxYzEwOQ==
4
+ Y2Y5ZDY3NmVmZWY1ODA5YWVjYzJmNmI2ZDI0NWM3OTMwYmM0M2VkMA==
5
5
  data.tar.gz: !binary |-
6
- N2Q2M2YxMzE3N2JhNTA0MmE3NzU1OWMwODY3MGFjMDRkZGM4YzVlMg==
6
+ NWVhZWRkOGJhNzE3NTE4ZjRiM2U1YTcyOGFiMzJiODE0ZDc0MmRlYQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MzQ1NzUyZDgxMzM1OWZiZTg2OThhZjZiNmIyZDdjNTI4YzJjYzgxYmQ5MWVk
10
- NWQxMWU0MWY1MzlmNmM1MmE5M2VmMzkxOGRkOTI1M2U1YWU3YWZmZTQzMjA4
11
- ZDJiZjAyM2FlNWFlNDQ4OWNhN2QxNzM5OTMzYjdmZTM5MmE3ZWM=
9
+ YzM0Yjk4YjViNjgwNTk1N2Y5NzY5YzBiMjg3N2MzZGE5NjIwMjU3Zjg4NzQz
10
+ MGNmNjM0MzUzNGUwZWVlNDI1MGVkZTlkNDQwZmJjYzliMTlmZTY2OGNhYzdh
11
+ NjBiNmNjOWZjNmU4MjJjMTU5MTkwYTk0N2ExYzU0ZWFkNTBmNjM=
12
12
  data.tar.gz: !binary |-
13
- YTUxNzAyMDQyZThkMjJlOGNhNTliNjE0NjYwYjI0NjFhZjZlOTNlZTRmYjhi
14
- MWFhZmJlNzVmNGY4MzY4OTlkYjQ0ZWJlNDEzMmNjYTcxMTgzOWVjODA5ZDky
15
- N2Q3ZjQ5NDIyMTJkNTc5ZWYzMzUyMGY4YTc1YzI0OGE3ZmFiYTc=
13
+ ODg5ZGI2NTEyZGM4Y2I1ZjcwYTQ2YzA5NWZkMTFhNDNlNWI2MTlkYWE3ZWU3
14
+ MGZhZTcxMWQyZmZkNDFkNmU3NGExZDAyMmU4NmNlNTU1OTIxYjIzNGRjNzUw
15
+ OTdlMmU5YTFjYjg1NGJmOTNiMjI0MTQ4ZjI5MDg1ZTBlODZkNGM=
data/.gitignore CHANGED
@@ -1 +1,3 @@
1
1
  *.gem
2
+ .bundle
3
+ ext/src/msgpack*
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rbtrace (0.4.2)
4
+ rbtrace (0.4.3)
5
5
  ffi (>= 1.0.6)
6
6
  msgpack (>= 0.4.3)
7
7
  trollop (>= 1.16.2)
data/bin/rbtrace CHANGED
@@ -1,1080 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- require 'socket'
3
- require 'fileutils'
4
2
  require 'rubygems'
5
- require 'ffi'
6
- require 'msgpack'
7
- require 'trollop'
3
+ require 'rbtrace/cli'
8
4
 
9
- class String
10
- alias :bytesize :size
11
- end unless ''.respond_to?(:bytesize)
12
-
13
- module FFI::LastError
14
- Errnos = Errno::constants.map(&Errno.method(:const_get)).inject({}) do |hash, c|
15
- hash[ c.const_get(:Errno) ] = c
16
- hash
17
- end
18
-
19
- def self.exception
20
- Errnos[error]
21
- end
22
- def self.raise(msg=nil)
23
- Kernel.raise exception, msg
24
- end
25
- end
26
-
27
- module MsgQ
28
- extend FFI::Library
29
- ffi_lib FFI::CURRENT_PROCESS
30
-
31
- class EventMsg < FFI::Struct
32
- BUF_SIZE = RUBY_PLATFORM =~ /linux/ ? 256 : 120
33
- IPC_NOWAIT = 004000
34
-
35
- layout :mtype, :long,
36
- :buf, [:char, BUF_SIZE]
37
-
38
- def self.send_cmd(q, str)
39
- msg = EventMsg.new
40
- msg[:mtype] = 1
41
- msg[:buf].to_ptr.put_string(0, str)
42
-
43
- ret = MsgQ.msgsnd(q, msg, BUF_SIZE, 0)
44
- FFI::LastError.raise if ret == -1
45
- end
46
-
47
- def self.recv_cmd(q, block=true)
48
- MsgQ.rb_enable_interrupt if RUBY_VERSION > '1.9' && RUBY_VERSION < '2.0'
49
-
50
- msg = EventMsg.new
51
- ret = MsgQ.msgrcv(q, msg, BUF_SIZE, 0, block ? 0 : IPC_NOWAIT)
52
- if ret == -1
53
- if !block and [Errno::EAGAIN, Errno::ENOMSG].include?(FFI::LastError.exception)
54
- return nil
55
- end
56
-
57
- FFI::LastError.raise
58
- end
59
-
60
- msg[:buf].to_ptr.read_string_length(BUF_SIZE)
61
- ensure
62
- MsgQ.rb_disable_interrupt if RUBY_VERSION > '1.9' && RUBY_VERSION < '2.0'
63
- end
64
- end
65
-
66
- attach_function :msgget, [:int, :int], :int
67
- attach_function :msgrcv, [:int, EventMsg.ptr, :size_t, :long, :int], :int
68
- attach_function :msgsnd, [:int, EventMsg.ptr, :size_t, :int], :int
69
-
70
- if RUBY_VERSION > '1.9' && RUBY_VERSION < '2.0'
71
- attach_function :rb_enable_interrupt, [], :void
72
- attach_function :rb_disable_interrupt, [], :void
73
- end
74
- end
75
-
76
- class RBTracer
77
- # Suggest increasing the maximum number of bytes allowed on
78
- # a message queue to 1MB.
79
- #
80
- # This defaults to 16k on Linux, and is hardcoded to 2k in OSX kernel.
81
- #
82
- # Returns nothing.
83
- def self.check_msgmnb
84
- if File.exists?(msgmnb = "/proc/sys/kernel/msgmnb")
85
- curr = File.read(msgmnb).to_i
86
- max = 1024*1024
87
- cmd = "sysctl kernel.msgmnb=#{max}"
88
-
89
- if curr < max
90
- if Process.uid == 0
91
- STDERR.puts "*** running `#{cmd}` for you to prevent losing events (currently: #{curr} bytes)"
92
- system(cmd)
93
- else
94
- STDERR.puts "*** run `sudo #{cmd}` to prevent losing events (currently: #{curr} bytes)"
95
- end
96
- end
97
- end
98
- end
99
-
100
- # Look for any message queues pairs (pid/-pid) that no longer have an
101
- # associated process alive, and remove them.
102
- #
103
- # Returns nothing.
104
- def self.cleanup_queues
105
- if (pids = `ps ax -o pid`.split("\n").map{ |p| p.strip.to_i }).any?
106
- ipcs = `ipcs -q`.split("\n").grep(/^(q|0x)/).map{ |line| line[/(0x[a-f0-9]+)/,1] }
107
- ipcs.each do |ipci|
108
- next if ipci.match(/^0xf/)
109
-
110
- qi = ipci.to_i(16)
111
- qo = 0xffffffff - qi + 1
112
- ipco = "0x#{qo.to_s(16)}"
113
-
114
- if ipcs.include?(ipco) and !pids.include?(qi)
115
- STDERR.puts "*** removing stale message queue pair: #{ipci}/#{ipco}"
116
- system("ipcrm -Q #{ipci} -Q #{ipco}")
117
- end
118
- end
119
- end
120
- end
121
-
122
- # Public: The Fixnum pid of the traced process.
123
- attr_reader :pid
124
-
125
- # Public: The IO where tracing output is written (default: STDOUT).
126
- attr_accessor :out
127
-
128
- # Public: The timeout before giving up on attaching/detaching to a process.
129
- attr_accessor :timeout
130
-
131
- # The String prefix used on nested method calls (default: ' ').
132
- attr_accessor :prefix
133
-
134
- # The Boolean flag for showing how long method calls take (default: true).
135
- attr_accessor :show_duration
136
-
137
- # The Boolean flag for showing the timestamp when method calls start (default: false).
138
- attr_accessor :show_time
139
-
140
- # Create a new tracer
141
- #
142
- # pid - The String of Fixnum process id
143
- #
144
- # Returns a tracer.
145
- def initialize(pid)
146
- begin
147
- raise ArgumentError unless pid
148
- @pid = pid.to_i
149
- raise ArgumentError unless @pid > 0
150
- Process.kill(0, @pid)
151
- rescue TypeError, ArgumentError
152
- raise ArgumentError, 'pid required'
153
- rescue Errno::ESRCH
154
- raise ArgumentError, 'invalid pid'
155
- rescue Errno::EPERM
156
- raise ArgumentError, 'could not signal process, are you running as root?'
157
- end
158
-
159
- path = "/tmp/rbtrace-#{@pid}.sock"
160
- @sock = Socket.new Socket::AF_UNIX, Socket::SOCK_DGRAM, 0
161
- @sockaddr = Socket.pack_sockaddr_un(path)
162
- @sock.bind(@sockaddr)
163
- FileUtils.chmod 0666, path
164
- at_exit{ FileUtils.rm(path) if File.exists?(path) }
165
-
166
- 5.times do
167
- signal
168
- sleep 0.15 # wait for process to create msgqs
169
-
170
- @qo = MsgQ.msgget(-pid, 0666)
171
-
172
- break if @qo > -1
173
- end
174
-
175
- if @qo == -1
176
- raise ArgumentError, 'pid is not listening for messages, did you `require "rbtrace"`'
177
- end
178
-
179
- @klasses = {}
180
- @methods = {}
181
- @tracers = Hash.new{ |h,k|
182
- h[k] = {
183
- :query => nil,
184
- :times => [],
185
- :names => [],
186
- :exprs => {},
187
- :last => false,
188
- :arglist => false
189
- }
190
- }
191
- @max_nesting = @last_nesting = @nesting = 0
192
- @last_tracer = nil
193
-
194
- @timeout = 5
195
-
196
- @out = STDOUT
197
- @out.sync = true
198
- @prefix = ' '
199
- @printed_newline = true
200
-
201
- @show_time = false
202
- @show_duration = true
203
- @watch_slow = false
204
-
205
- attach
206
- end
207
-
208
- # Watch for method calls slower than a threshold.
209
- #
210
- # msec - The Fixnum threshold in milliseconds
211
- #
212
- # Returns nothing.
213
- def watch(msec, cpu_only=false)
214
- @watch_slow = true
215
- send_cmd(cpu_only ? :watchcpu : :watch, msec)
216
- end
217
-
218
- # Turn on the firehose (show all method calls).
219
- #
220
- # Returns nothing.
221
- def firehose
222
- send_cmd(:firehose)
223
- end
224
-
225
- # Turn on dev mode.
226
- #
227
- # Returns nothing.
228
- def devmode
229
- send_cmd(:devmode)
230
- end
231
-
232
- # Fork the process and return the copy's pid.
233
- #
234
- # Returns a Fixnum pid.
235
- def fork
236
- send_cmd(:fork)
237
- if wait('for fork', 30){ !!@forked_pid }
238
- @forked_pid
239
- else
240
- STDERR.puts '*** timed out waiting for fork'
241
- end
242
- end
243
-
244
- # Evaluate some ruby code.
245
- #
246
- # Returns the String result.
247
- def eval(code)
248
- if (err = valid_syntax?(code)) != true
249
- raise ArgumentError, "#{err.class} for expression #{code.inspect}"
250
- end
251
-
252
- send_cmd(:eval, code)
253
-
254
- if wait('for eval response', 15){ !!@eval_result }
255
- @eval_result
256
- else
257
- STDERR.puts '*** timed out waiting for eval response'
258
- end
259
- end
260
-
261
- # Turn on GC tracing.
262
- #
263
- # Returns nothing.
264
- def gc
265
- send_cmd(:gc)
266
- end
267
-
268
- # Restrict slow tracing to a specific list of methods.
269
- #
270
- # methods - The String or Array of method selectors.
271
- #
272
- # Returns nothing.
273
- def add_slow(methods)
274
- add(methods, true)
275
- end
276
-
277
- # Add tracers for the given list of methods.
278
- #
279
- # methods - The String or Array of method selectors to trace.
280
- #
281
- # Returns nothing.
282
- def add(methods, slow=false)
283
- Array(methods).each do |func|
284
- func = func.strip
285
- next if func.empty?
286
-
287
- if func =~ /^(.+?)\((.+)\)$/
288
- name, args = $1, $2
289
- args = args.split(',').map{ |a| a.strip }
290
- end
291
-
292
- send_cmd(:add, name || func, slow)
293
-
294
- if args and args.any?
295
- args.each do |arg|
296
- if (err = valid_syntax?(arg)) != true
297
- raise ArgumentError, "#{err.class} for expression #{arg.inspect} in method #{func.inspect}"
298
- end
299
- if arg =~ /^@/ and arg !~ /^@[_a-z][_a-z0-9]+$/i
300
- # arg[0]=='@' means ivar, but if this is an expr
301
- # we can hack a space in front so it gets eval'd instead
302
- arg = " #{arg}"
303
- end
304
- send_cmd(:addexpr, arg)
305
- end
306
- end
307
- end
308
- end
309
-
310
- # Attach to the process.
311
- #
312
- # Returns nothing.
313
- def attach
314
- send_cmd(:attach, Process.pid)
315
- if wait('to attach'){ @attached == true }
316
- STDERR.puts "*** attached to process #{pid}"
317
- else
318
- raise ArgumentError, 'process already being traced?'
319
- end
320
- end
321
-
322
- # Detach from the traced process.
323
- #
324
- # Returns nothing.
325
- def detach
326
- begin
327
- send_cmd(:detach)
328
- rescue Errno::ESRCH
329
- end
330
-
331
- newline
332
-
333
- if wait('to detach cleanly'){ @attached == false }
334
- newline
335
- STDERR.puts "*** detached from process #{pid}"
336
- else
337
- newline
338
- STDERR.puts "*** could not detach cleanly from process #{pid}"
339
- end
340
- rescue Errno::EINVAL, Errno::EIDRM
341
- newline
342
- STDERR.puts "*** process #{pid} is gone"
343
- # STDERR.puts "*** #{$!.inspect}"
344
- # STDERR.puts $!.backtrace.join("\n ")
345
- rescue Interrupt, SignalException
346
- retry
347
- end
348
-
349
- # Process events from the traced process.
350
- #
351
- # Returns nothing.
352
- def recv_loop
353
- while true
354
- # block until a message arrives
355
- process_line(recv_cmd)
356
-
357
- # process any remaining messages
358
- recv_lines
359
- end
360
- rescue Errno::EINVAL, Errno::EIDRM
361
- # process went away
362
- end
363
-
364
- # Process events from the traced process, without blocking if
365
- # there is nothing to do. This is a useful way to drain the buffer
366
- # so messages do not accumulate in kernel land.
367
- #
368
- # Returns nothing.
369
- def recv_lines
370
- 50.times do
371
- break unless line = recv_cmd(false)
372
- process_line(line)
373
- end
374
- end
375
-
376
- def puts(arg=nil)
377
- @printed_newline = true
378
- arg ? @out.puts(arg) : @out.puts
379
- end
380
-
381
- private
382
-
383
- def signal
384
- Process.kill 'URG', @pid
385
- end
386
-
387
- # Process incoming events until either a timeout or a condition becomes true.
388
- #
389
- # time - The Fixnum timeout in seconds.
390
- # block - The Block that is checked every 50ms until it returns true.
391
- #
392
- # Returns true when the condition was met, or false on a timeout.
393
- def wait(reason, time=@timeout)
394
- wait = 0.05 # polling interval
395
-
396
- (time/wait).to_i.times do
397
- begin
398
- recv_lines
399
- sleep(wait)
400
- begin
401
- signal
402
- rescue Errno::ESRCH
403
- break
404
- end
405
- time -= wait
406
-
407
- return true if yield
408
- rescue Interrupt
409
- STDERR.puts "*** waiting #{reason} (#{time.to_i}s left)"
410
- retry
411
- end
412
- end
413
-
414
- false
415
- end
416
-
417
- def send_cmd(*cmd)
418
- begin
419
- msg = cmd.to_msgpack
420
- raise ArgumentError, 'command is too long' if msg.bytesize > MsgQ::EventMsg::BUF_SIZE
421
- MsgQ::EventMsg.send_cmd(@qo, msg)
422
- rescue Errno::EINTR
423
- retry
424
- end
425
- signal
426
- recv_lines
427
- end
428
-
429
- def recv_cmd(block=true)
430
- if block
431
- @sock.recv(65536)
432
- else
433
- @sock.recv_nonblock(65536)
434
- end
435
- rescue Errno::EAGAIN
436
- nil
437
- end
438
-
439
- def valid_syntax?(code)
440
- begin
441
- Kernel.eval("#{code}\nBEGIN {return true}", nil, 'rbtrace_expression', 0)
442
- rescue Exception => e
443
- e
444
- end
445
- end
446
-
447
- def print(arg)
448
- @printed_newline = false
449
- @out.print(arg)
450
- end
451
-
452
- def newline
453
- puts unless @printed_newline
454
- @printed_newline = true
455
- end
456
-
457
- def parse_cmd(line)
458
- unpacker = MessagePack::Unpacker.new
459
- unpacker.feed(line)
460
-
461
- obj = nil
462
- unpacker.each{|o| obj = o; break }
463
- obj
464
- end
465
-
466
- def process_line(line)
467
- return unless cmd = parse_cmd(line)
468
- event = cmd.shift
469
-
470
- case event
471
- when 'during_gc'
472
- sleep 0.01
473
- signal
474
- return
475
-
476
- when 'attached'
477
- tracer_pid, = *cmd
478
- if tracer_pid != Process.pid
479
- STDERR.puts "*** process #{pid} is already being traced (#{tracer_pid} != #{Process.pid})"
480
- exit!(-1)
481
- end
482
-
483
- @attached = true
484
- return
485
-
486
- when 'detached'
487
- tracer_pid, = *cmd
488
- if tracer_pid != Process.pid
489
- STDERR.puts "*** process #{pid} detached #{tracer_pid}, but we are #{Process.pid}"
490
- else
491
- @attached = false
492
- end
493
-
494
- return
495
- end
496
-
497
- unless @attached
498
- STDERR.puts "*** got #{event} before attaching"
499
- return
500
- end
501
-
502
- case event
503
- when 'forked'
504
- pid, = *cmd
505
- @forked_pid = pid
506
-
507
- when 'evaled'
508
- res, = *cmd
509
- @eval_result = res
510
-
511
- when 'mid'
512
- mid, name = *cmd
513
- @methods[mid] = name
514
-
515
- when 'klass'
516
- kid, name = *cmd
517
- @klasses[kid] = name
518
-
519
- when 'add'
520
- tracer_id, query = *cmd
521
- if tracer_id == -1
522
- STDERR.puts "*** unable to add tracer for #{query}"
523
- else
524
- @tracers.delete(tracer_id)
525
- @tracers[tracer_id][:query] = query
526
- end
527
-
528
- when 'newexpr'
529
- tracer_id, expr_id, expr = *cmd
530
- tracer = @tracers[tracer_id]
531
-
532
- if expr_id > -1
533
- tracer[:exprs][expr_id] = expr.strip
534
- end
535
-
536
- when 'exprval'
537
- tracer_id, expr_id, val = *cmd
538
-
539
- tracer = @tracers[tracer_id]
540
- expr = tracer[:exprs][expr_id]
541
-
542
- if tracer[:arglist]
543
- print ', '
544
- else
545
- print '('
546
- end
547
-
548
- print "#{expr}="
549
- print val
550
- tracer[:arglist] = true
551
-
552
- when 'call','ccall'
553
- time, tracer_id, mid, is_singleton, klass = *cmd
554
-
555
- tracer = @tracers[tracer_id]
556
- klass = @klasses[klass]
557
- name = klass ? "#{klass}#{ is_singleton ? '.' : '#' }" : ''
558
- name += @methods[mid] || '(unknown)'
559
-
560
- tracer[:times] << time
561
- tracer[:names] << name
562
-
563
- if @last_tracer and @last_tracer[:arglist]
564
- print ')'
565
- @last_tracer[:arglist] = false
566
- end
567
- newline
568
- if @show_time
569
- t = Time.at(time/1_000_000)
570
- print t.strftime("%H:%M:%S.")
571
- print "%06d " % (time - t.to_f*1_000_000).round
572
- end
573
- print @prefix*@nesting if @nesting > 0
574
- print name
575
-
576
- @nesting += 1
577
- @max_nesting = @nesting if @nesting > @max_nesting
578
- @last_nesting = @nesting
579
- @last_tracer = tracer
580
- tracer[:last] = "#{name}:#{@nesting-1}"
581
-
582
- when 'return','creturn'
583
- time, tracer_id = *cmd
584
- tracer = @tracers[tracer_id]
585
-
586
- @nesting -= 1 if @nesting > 0
587
-
588
- if start = tracer[:times].pop
589
- name = tracer[:names].pop
590
- diff = time - start
591
- @last_tracer[:arglist] = false if @last_tracer and @last_tracer[:last] != "#{name}:#{@nesting}"
592
-
593
- print ')' if @last_tracer and @last_tracer[:arglist]
594
-
595
- unless tracer == @last_tracer and @last_tracer[:last] == "#{name}:#{@nesting}"
596
- newline
597
- print ' '*16 if @show_time
598
- print @prefix*@nesting if @nesting > 0
599
- print name
600
- end
601
- print ' <%f>' % (diff/1_000_000.0) if @show_duration
602
- newline
603
-
604
- if @nesting == 0 and @max_nesting > 1
605
- # unless tracer == @last_tracer and @last_tracer[:last] == name
606
- puts
607
- # end
608
- end
609
- end
610
-
611
- tracer[:arglist] = false
612
- @last_nesting = @nesting
613
-
614
- when 'slow', 'cslow'
615
- time, diff, nesting, mid, is_singleton, klass = *cmd
616
-
617
- klass = @klasses[klass]
618
- name = klass ? "#{klass}#{ is_singleton ? '.' : '#' }" : ''
619
- name += @methods[mid] || '(unknown)'
620
-
621
- newline
622
- nesting = @nesting if @nesting > 0
623
-
624
- if @show_time
625
- t = Time.at(time/1_000_000)
626
- print t.strftime("%H:%M:%S.")
627
- print "%06d " % (time - t.to_f*1_000_000).round
628
- end
629
-
630
- print @prefix*nesting if nesting > 0
631
- print name
632
- if @show_duration
633
- print ' '
634
- print "<%f>" % (diff/1_000_000.0)
635
- end
636
- puts
637
- puts if nesting == 0 and @max_nesting > 1
638
-
639
- @max_nesting = nesting if nesting > @max_nesting
640
- @last_nesting = nesting
641
-
642
- when 'gc_start'
643
- time, = *cmd
644
- @gc_start = time
645
- print 'garbage_collect'
646
-
647
- when 'gc_end'
648
- time, = *cmd
649
- diff = time - @gc_start
650
- # if @gc_mark
651
- # mark = ((@gc_mark - @gc_start) * 100.0 / diff).to_i
652
- # print '(mark: %d%%, sweep: %d%%)' % [mark, 100-mark]
653
- # end
654
- print ' <%f>' % (diff/1_000_000.0) if @show_duration
655
- @gc_start = nil
656
- newline
657
-
658
- when 'gc'
659
- time, = *cmd
660
- @gc_mark = time
661
-
662
- unless @gc_start
663
- newline
664
- if @show_time
665
- t = Time.at(time/1_000_000)
666
- print t.strftime("%H:%M:%S.")
667
- print "%06d " % (time - t.to_f*1_000_000).round
668
- end
669
- print @prefix*@last_nesting if @last_nesting > 0
670
- print "garbage_collect"
671
- puts if @watch_slow
672
- end
673
-
674
- else
675
- puts "unknown event #{event}: #{cmd.inspect}"
676
-
677
- end
678
- rescue => e
679
- STDERR.puts "error on #{event}: #{cmd.inspect}"
680
- raise e
681
- end
682
-
683
- def self.run
684
- check_msgmnb
685
- cleanup_queues
686
-
687
- parser = Trollop::Parser.new do
688
- version <<-EOS
689
- rbtrace: like strace, but for ruby code
690
- version 0.4.0
691
- (c) 2013 Aman Gupta (tmm1)
692
- http://github.com/tmm1/rbtrace
693
- EOS
694
-
695
- banner <<-EOS
696
- rbtrace shows you method calls happening inside another ruby process in real time.
697
-
698
- to use rbtrace, simply `require "rbtrace"` in your ruby app.
699
-
700
- for examples and more information, see http://github.com/tmm1/rbtrace
701
-
702
- Usage:
703
-
704
- rbtrace --exec <CMD> # run and trace <CMD>
705
- rbtrace --pid <PID+> # trace the given process(es)
706
- rbtrace --ps <CMD> # look for running <CMD> processes to trace
707
-
708
- rbtrace -o <FILE> # write output to file
709
- rbtrace -t # show method call start time
710
- rbtrace -n # hide duration of each method call
711
- rbtrace -r 3 # use 3 spaces to nest method calls
712
-
713
- Tracers:
714
-
715
- rbtrace --firehose # trace all method calls
716
- rbtrace --slow=250 # trace method calls slower than 250ms
717
- rbtrace --methods a b c # trace calls to given methods
718
- rbtrace --gc # trace garbage collections
719
-
720
- rbtrace -c io # trace common input/output functions
721
- rbtrace -c eventmachine # trace common eventmachine functions
722
- rbtrace -c my.tracer # trace all methods listed in my.tracer
723
-
724
- Method Selectors:
725
-
726
- sleep # any instance or class method named sleep
727
- String#gsub # specific instance method
728
- Process.pid # specific class method
729
- Dir. # any class methods in Dir
730
- Fixnum# # any instance methods of Fixnum
731
-
732
- Trace Expressions:
733
-
734
- method(self) # value of self at method invocation
735
- method(@ivar) # value of given instance variable
736
- method(arg1, arg2) # value of argument local variables
737
- method(self.attr) # value of arbitrary ruby expression
738
- method(__source__) # source file/line of callsite
739
-
740
-
741
- All Options:\n
742
-
743
- EOS
744
- opt :exec,
745
- "spawn new ruby process and attach to it",
746
- :type => :strings,
747
- :short => nil
748
-
749
- opt :pid,
750
- "pid of the ruby process to trace",
751
- :type => :ints,
752
- :short => '-p'
753
-
754
- opt :ps,
755
- "find any matching processes to trace",
756
- :type => :string,
757
- :short => nil
758
-
759
- opt :firehose,
760
- "show all method calls",
761
- :short => '-f'
762
-
763
- opt :slow,
764
- "watch for method calls slower than 250 milliseconds",
765
- :default => 250,
766
- :short => '-s'
767
-
768
- opt :slowcpu,
769
- "watch for method calls slower than 250 milliseconds (cpu time only)",
770
- :default => 250,
771
- :short => nil
772
-
773
- opt :slow_methods,
774
- "method(s) to restrict --slow to",
775
- :type => :strings
776
-
777
- opt :methods,
778
- "method(s) to trace (valid formats: sleep String#gsub Process.pid Kernel# Dir.)",
779
- :type => :strings,
780
- :short => '-m'
781
-
782
- opt :gc,
783
- "trace garbage collections"
784
-
785
- opt :start_time,
786
- "show start time for each method call",
787
- :short => '-t'
788
-
789
- opt :no_duration,
790
- "hide time spent in each method call",
791
- :default => false,
792
- :short => '-n'
793
-
794
- opt :output,
795
- "write trace to filename",
796
- :type => String,
797
- :short => '-o'
798
-
799
- opt :append,
800
- "append to output file instead of overwriting",
801
- :short => '-a'
802
-
803
- opt :prefix,
804
- "prefix nested method calls with N spaces",
805
- :default => 2,
806
- :short => '-r'
807
-
808
- opt :config,
809
- "config file",
810
- :type => :strings,
811
- :short => '-c'
812
-
813
- opt :devmode,
814
- "assume the ruby process is reloading classes and methods"
815
-
816
- opt :fork,
817
- "fork a copy of the process for debugging (so you can attach gdb.rb)"
818
-
819
- opt :eval,
820
- "evaluate a ruby expression in the process",
821
- :type => String,
822
- :short => '-e'
823
-
824
- opt :backtrace,
825
- "get lines from the current backtrace in the process",
826
- :type => :int
827
-
828
- opt :wait,
829
- "seconds to wait before attaching to process",
830
- :default => 0,
831
- :short => nil
832
-
833
- opt :timeout,
834
- "seconds to wait before giving up on attach/detach",
835
- :default => 5
836
- end
837
-
838
- opts = Trollop.with_standard_exception_handling(parser) do
839
- raise Trollop::HelpNeeded if ARGV.empty?
840
- parser.stop_on '--exec'
841
- parser.parse(ARGV)
842
- end
843
-
844
- if ARGV.first == '--exec'
845
- ARGV.shift
846
- opts[:exec_given] = true
847
- opts[:exec] = ARGV.dup
848
- ARGV.clear
849
- end
850
-
851
- unless %w[ fork eval backtrace slow slowcpu firehose methods config gc ].find{ |n| opts[:"#{n}_given"] }
852
- $stderr.puts "Error: --slow, --slowcpu, --gc, --firehose, --methods or --config required."
853
- $stderr.puts "Try --help for help."
854
- exit(-1)
855
- end
856
-
857
- if opts[:fork_given] and opts[:pid].size != 1
858
- parser.die :fork, '(can only be invoked with one pid)'
859
- end
860
-
861
- if opts[:exec_given]
862
- if opts[:pid_given]
863
- parser.die :exec, '(cannot exec and attach to pid)'
864
- end
865
- if opts[:fork_given]
866
- parser.die :fork, '(cannot fork inside newly execed process)'
867
- end
868
- end
869
-
870
- methods, smethods = [], []
871
-
872
- if opts[:methods_given]
873
- methods += opts[:methods]
874
- end
875
- if opts[:slow_methods_given]
876
- smethods += opts[:slow_methods]
877
- end
878
-
879
- if opts[:config_given]
880
- Array(opts[:config]).each do |config|
881
- file = [
882
- config,
883
- File.expand_path("../../tracers/#{config}.tracer", __FILE__)
884
- ].find{ |f| File.exists?(f) }
885
-
886
- unless file
887
- parser.die :config, '(file does not exist)'
888
- end
889
-
890
- File.readlines(file).each do |line|
891
- line.strip!
892
- next if line =~ /^#/
893
- next if line.empty?
894
-
895
- methods << line
896
- end
897
- end
898
- end
899
-
900
- tracee = nil
901
-
902
- if opts[:ps_given]
903
- list = `ps aux`.split("\n")
904
- filtered = list.grep(Regexp.new opts[:ps])
905
- filtered.reject! do |line|
906
- line =~ /^\w+\s+(#{Process.pid}|#{Process.ppid})\s+/ # cannot trace self
907
- end
908
-
909
- if filtered.size > 0
910
- max_len = filtered.size.to_s.size
911
-
912
- STDERR.puts "*** found #{filtered.size} processes matching #{opts[:ps].inspect}"
913
- filtered.each_with_index do |line, i|
914
- STDERR.puts " [#{(i+1).to_s.rjust(max_len)}] #{line.strip}"
915
- end
916
- STDERR.puts " [#{'0'.rjust(max_len)}] all #{filtered.size} processes"
917
-
918
- while true
919
- STDERR.sync = true
920
- STDERR.print "*** trace which processes? (0/1,4): "
921
-
922
- begin
923
- input = gets
924
- rescue Interrupt
925
- exit 1
926
- end
927
-
928
- if input =~ /^(\d+,?)+$/
929
- if input.strip == '0'
930
- pids = filtered.map do |line|
931
- line.split[1].to_i
932
- end
933
- else
934
- indices = input.split(',').map(&:to_i)
935
- pids = indices.map do |i|
936
- if i > 0 and line = filtered[i-1]
937
- line.split[1].to_i
938
- end
939
- end
940
- end
941
-
942
- unless pids.include?(nil)
943
- opts[:pid] = pids
944
- break
945
- end
946
- end
947
- end
948
- else
949
- STDERR.puts "*** could not find any processes matching #{opts[:ps].inspect}"
950
- exit 1
951
- end
952
- end
953
-
954
- if opts[:exec_given]
955
- tracee = fork{
956
- Process.setsid
957
- ENV['RUBYOPT'] = "-r#{File.expand_path('../../lib/rbtrace',__FILE__)}"
958
- exec(*opts[:exec])
959
- }
960
- STDERR.puts "*** spawned child #{tracee}: #{opts[:exec].inspect[1..-2]}"
961
-
962
- if (secs = opts[:wait]) > 0
963
- STDERR.puts "*** waiting #{secs} seconds for child to boot up"
964
- sleep secs
965
- end
966
-
967
- elsif opts[:pid].size <= 1
968
- tracee = opts[:pid].first
969
-
970
- else
971
- tracers = []
972
-
973
- opts[:pid].each do |pid|
974
- if child = fork
975
- tracers << child
976
- else
977
- Process.setpgrp
978
- STDIN.reopen '/dev/null'
979
- $0 = "rbtrace -p #{pid} (parent: #{Process.ppid})"
980
-
981
- opts[:output] += ".#{pid}" if opts[:output]
982
- tracee = pid
983
-
984
- # fall through and start tracing
985
- break
986
- end
987
- end
988
-
989
- if tracee.nil?
990
- # this is the parent
991
- while true
992
- begin
993
- break if tracers.empty?
994
- if pid = Process.wait
995
- tracers.delete(pid)
996
- end
997
- rescue Interrupt, SignalException
998
- STDERR.puts "*** waiting on child tracers: #{tracers.inspect}"
999
- tracers.each do |pid|
1000
- begin
1001
- Process.kill 'INT', pid
1002
- rescue Errno::ESRCH
1003
- end
1004
- end
1005
- end
1006
- end
1007
-
1008
- exit!
1009
- end
1010
- end
1011
-
1012
- if out = opts[:output]
1013
- output = File.open(out, opts[:append] ? 'a+' : 'w')
1014
- output.sync = true
1015
- end
1016
-
1017
- begin
1018
- begin
1019
- tracer = RBTracer.new(tracee)
1020
- rescue ArgumentError => e
1021
- parser.die :pid, "(#{e.message})"
1022
- end
1023
-
1024
- if opts[:fork_given]
1025
- pid = tracer.fork
1026
- STDERR.puts "*** forked off a busy looping copy at #{pid} (make sure to kill -9 it when you're done)"
1027
-
1028
- elsif opts[:backtrace_given]
1029
- num = opts[:backtrace]
1030
- code = "caller.first(#{num}).join('|')"
1031
-
1032
- if res = tracer.eval(code)
1033
- tracer.puts res[1..-2].split('|').join("\n ")
1034
- end
1035
-
1036
- elsif opts[:eval_given]
1037
- if res = tracer.eval(code = opts[:eval])
1038
- tracer.puts ">> #{code}"
1039
- tracer.puts "=> #{res}"
1040
- end
1041
-
1042
- else
1043
- tracer.out = output if output
1044
- tracer.timeout = opts[:timeout] if opts[:timeout] > 0
1045
- tracer.prefix = ' ' * opts[:prefix]
1046
- tracer.show_time = opts[:start_time]
1047
- tracer.show_duration = !opts[:no_duration]
1048
-
1049
- tracer.devmode if opts[:devmode_given]
1050
- tracer.gc if opts[:gc_given]
1051
-
1052
- if opts[:firehose_given]
1053
- tracer.firehose
1054
- else
1055
- tracer.add(methods) if methods.any?
1056
- if opts[:slow_given] || opts[:slowcpu_given]
1057
- tracer.watch(opts[:slowcpu_given] ? opts[:slowcpu] : opts[:slow], opts[:slowcpu_given])
1058
- tracer.add_slow(smethods) if smethods.any?
1059
- end
1060
- end
1061
- begin
1062
- tracer.recv_loop
1063
- rescue Interrupt, SignalException
1064
- end
1065
- end
1066
- ensure
1067
- if tracer
1068
- tracer.detach
1069
- end
1070
-
1071
- if opts[:exec_given]
1072
- STDERR.puts "*** waiting on spawned child #{tracee}"
1073
- Process.kill 'TERM', tracee
1074
- Process.waitpid(tracee)
1075
- end
1076
- end
1077
- end
1078
- end
1079
-
1080
- RBTracer.run
5
+ RBTraceCLI.run