Class: RBTracer

Inherits:
Object
  • Object
show all
Defined in:
lib/rbtrace/rbtracer.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pid) ⇒ RBTracer

Create a new tracer

pid - The String of Fixnum process id

Returns a tracer.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/rbtrace/rbtracer.rb', line 32

def initialize(pid)
  begin
    raise ArgumentError unless pid
    @pid = pid.to_i
    raise ArgumentError unless @pid > 0
    Process.kill(0, @pid)
  rescue TypeError, ArgumentError
    raise ArgumentError, 'pid required'
  rescue Errno::ESRCH
    raise ArgumentError, 'invalid pid'
  rescue Errno::EPERM
    raise ArgumentError, 'could not signal process, are you running as root?'
  end

  path = "/tmp/rbtrace-#{@pid}.sock"
  @sock = Socket.new Socket::AF_UNIX, Socket::SOCK_DGRAM, 0
  @sockaddr = Socket.pack_sockaddr_un(path)
  @sock.bind(@sockaddr)
  FileUtils.chmod 0666, path
  at_exit{ FileUtils.rm(path) if File.exists?(path) }

  5.times do
    signal
    sleep 0.15 # wait for process to create msgqs

    @qo = MsgQ.msgget(-@pid, 0666)

    break if @qo > -1
  end

  if @qo == -1
    raise ArgumentError, 'pid is not listening for messages, did you `require "rbtrace"`'
  end

  @klasses = {}
  @methods = {}
  @tracers = Hash.new{ |h,k|
    h[k] = {
      :query => nil,
      :times => [],
      :names => [],
      :exprs => {},
      :last => false,
      :arglist => false
    }
  }
  @max_nesting = @last_nesting = @nesting = 0
  @last_tracer = nil

  @timeout = 5

  @out = STDOUT
  @out.sync = true
  @prefix = '  '
  @printed_newline = true

  @show_time = false
  @show_duration = true
  @watch_slow = false

  attach
end

Instance Attribute Details

#out ⇒ Object

Public: The IO where tracing output is written (default: STDOUT).



13
14
15
# File 'lib/rbtrace/rbtracer.rb', line 13

def out
  @out
end

#pid ⇒ Object (readonly)

Public: The Fixnum pid of the traced process.



10
11
12
# File 'lib/rbtrace/rbtracer.rb', line 10

def pid
  @pid
end

#prefix ⇒ Object

The String prefix used on nested method calls (default: ' ').



19
20
21
# File 'lib/rbtrace/rbtracer.rb', line 19

def prefix
  @prefix
end

#show_duration ⇒ Object

The Boolean flag for showing how long method calls take (default: true).



22
23
24
# File 'lib/rbtrace/rbtracer.rb', line 22

def show_duration
  @show_duration
end

#show_time ⇒ Object

The Boolean flag for showing the timestamp when method calls start (default: false).



25
26
27
# File 'lib/rbtrace/rbtracer.rb', line 25

def show_time
  @show_time
end

#timeout ⇒ Object

Public: The timeout before giving up on attaching/detaching to a process.



16
17
18
# File 'lib/rbtrace/rbtracer.rb', line 16

def timeout
  @timeout
end

Instance Method Details

#add(methods, slow = false) ⇒ Object

Add tracers for the given list of methods.

methods - The String or Array of method selectors to trace.

Returns nothing.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/rbtrace/rbtracer.rb', line 169

def add(methods, slow=false)
  Array(methods).each do |func|
    func = func.strip
    next if func.empty?

    if func =~ /^(.+?)\((.+)\)$/
      name, args = $1, $2
      args = args.split(',').map{ |a| a.strip }
    end

    send_cmd(:add, name || func, slow)

    if args and args.any?
      args.each do |arg|
        if (err = valid_syntax?(arg)) != true
          raise ArgumentError, "#{err.class} for expression #{arg.inspect} in method #{func.inspect}"
        end
        if arg =~ /^@/ and arg !~ /^@[_a-z][_a-z0-9]+$/i
          # arg[0]=='@' means ivar, but if this is an expr
          # we can hack a space in front so it gets eval'd instead
          arg = " #{arg}"
        end
        send_cmd(:addexpr, arg)
      end
    end
  end
end

#add_slow(methods) ⇒ Object

Restrict slow tracing to a specific list of methods.

methods - The String or Array of method selectors.

Returns nothing.



160
161
162
# File 'lib/rbtrace/rbtracer.rb', line 160

def add_slow(methods)
  add(methods, true)
end

#attach ⇒ Object

Attach to the process.

Returns nothing.



200
201
202
203
204
205
206
207
# File 'lib/rbtrace/rbtracer.rb', line 200

def attach
  send_cmd(:attach, Process.pid)
  if wait('to attach'){ @attached == true }
    STDERR.puts "*** attached to process #{pid}"
  else
    raise ArgumentError, 'process already being traced?'
  end
end

#detach ⇒ Object

Detach from the traced process.

Returns nothing.



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/rbtrace/rbtracer.rb', line 212

def detach
  begin
    send_cmd(:detach)
  rescue Errno::ESRCH
  end

  newline

  if wait('to detach cleanly'){ @attached == false }
    newline
    STDERR.puts "*** detached from process #{pid}"
  else
    newline
    STDERR.puts "*** could not detach cleanly from process #{pid}"
  end
rescue Errno::EINVAL, Errno::EIDRM
  newline
  STDERR.puts "*** process #{pid} is gone"
  # STDERR.puts "*** #{$!.inspect}"
  # STDERR.puts $!.backtrace.join("\n  ")
rescue Interrupt, SignalException
  retry
end

#devmode ⇒ Object

Turn on dev mode.

Returns nothing.



115
116
117
# File 'lib/rbtrace/rbtracer.rb', line 115

def devmode
  send_cmd(:devmode)
end

#eval(code) ⇒ Object

Evaluate some ruby code.

Returns the String result.



134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/rbtrace/rbtracer.rb', line 134

def eval(code)
  if (err = valid_syntax?(code)) != true
    raise ArgumentError, "#{err.class} for expression #{code.inspect}"
  end

  send_cmd(:eval, code)

  if wait('for eval response', 15){ !!@eval_result }
    @eval_result
  else
    STDERR.puts '*** timed out waiting for eval response'
  end
end

#firehose ⇒ Object

Turn on the firehose (show all method calls).

Returns nothing.



108
109
110
# File 'lib/rbtrace/rbtracer.rb', line 108

def firehose
  send_cmd(:firehose)
end

#fork ⇒ Object

Fork the process and return the copy's pid.

Returns a Fixnum pid.



122
123
124
125
126
127
128
129
# File 'lib/rbtrace/rbtracer.rb', line 122

def fork
  send_cmd(:fork)
  if wait('for fork', 30){ !!@forked_pid }
    @forked_pid
  else
    STDERR.puts '*** timed out waiting for fork'
  end
end

#gc ⇒ Object

Turn on GC tracing.

Returns nothing.



151
152
153
# File 'lib/rbtrace/rbtracer.rb', line 151

def gc
  send_cmd(:gc)
end

#puts(arg = nil) ⇒ Object



263
264
265
266
# File 'lib/rbtrace/rbtracer.rb', line 263

def puts(arg=nil)
  @printed_newline = true
  arg ? @out.puts(arg) : @out.puts
end

#recv_lines ⇒ Object

Process events from the traced process, without blocking if there is nothing to do. This is a useful way to drain the buffer so messages do not accumulate in kernel land.

Returns nothing.



256
257
258
259
260
261
# File 'lib/rbtrace/rbtracer.rb', line 256

def recv_lines
  50.times do
    break unless line = recv_cmd(false)
    process_line(line)
  end
end

#recv_loop ⇒ Object

Process events from the traced process.

Returns nothing.



239
240
241
242
243
244
245
246
247
248
249
# File 'lib/rbtrace/rbtracer.rb', line 239

def recv_loop
  while true
    # block until a message arrives
    process_line(recv_cmd)

    # process any remaining messages
    recv_lines
  end
rescue Errno::EINVAL, Errno::EIDRM
  # process went away
end

#watch(msec, cpu_only = false) ⇒ Object

Watch for method calls slower than a threshold.

msec - The Fixnum threshold in milliseconds

Returns nothing.



100
101
102
103
# File 'lib/rbtrace/rbtracer.rb', line 100

def watch(msec, cpu_only=false)
  @watch_slow = true
  send_cmd(cpu_only ? :watchcpu : :watch, msec)
end