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