aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/breakpoint.rb
blob: d9925be600da4b90e8dcb5d465f80b48c7cc4e50 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
# The Breakpoint library provides the convenience of
# being able to inspect and modify state, diagnose
# bugs all via IRB by simply setting breakpoints in
# your applications by the call of a method.
#
# This library was written and is supported by me,
# Florian Gross. I can be reached at flgr@ccan.de
# and enjoy getting feedback about my libraries.
#
# The whole library (including breakpoint_client.rb
# and binding_of_caller.rb) is licensed under the
# same license that Ruby uses. (Which is currently
# either the GNU General Public License or a custom
# one that allows for commercial usage.) If you for
# some good reason need to use this under another
# license please contact me.

require 'irb'
# require 'binding_of_caller' <- Needs this
require 'drb'
require 'drb/acl'

module Breakpoint
  extend self

  # This will pop up an interactive ruby session at a
  # pre-defined break point in a Ruby application. In
  # this session you can examine the environment of
  # the break point.
  #
  # You can get a list of variables in the context using
  # local_variables via +local_variables+. You can then
  # examine their values by typing their names.
  #
  # You can have a look at the call stack via +caller+.
  #
  # The source code around the location where the breakpoint
  # was executed can be examined via +source_lines+. Its
  # argument specifies how much lines of context to display.
  # The default amount of context is 5 lines. Note that
  # the call to +source_lines+ can raise an exception when
  # it isn't able to read in the source code.
  #
  # breakpoints can also return a value. They will execute
  # a supplied block for getting a default return value.
  # A custom value can be returned from the session by doing
  # +throw(:debug_return, value)+.
  #
  # You can also give names to break points which will be
  # used in the message that is displayed upon execution 
  # of them.
  #
  # Here's a sample of how breakpoints should be placed:
  #
  #   class Person
  #     def initialize(name, age)
  #       @name, @age = name, age
  #       breakpoint("Person#initialize")
  #     end
  #
  #     attr_reader :age
  #     def name
  #       breakpoint("Person#name") { @name }
  #     end
  #   end
  #
  #   person = Person.new("Random Person", 23)
  #   puts "Name: #{person.name}"
  #
  # And here is a sample debug session:
  #
  #   Executing break point "Person#initialize" at file.rb:4 in `initialize'
  #   irb(#<Person:0x292fbe8>):001:0> local_variables
  #   => ["name", "age", "_", "__"]
  #   irb(#<Person:0x292fbe8>):002:0> [name, age]
  #   => ["Random Person", 23]
  #   irb(#<Person:0x292fbe8>):003:0> [@name, @age]
  #   => ["Random Person", 23]
  #   irb(#<Person:0x292fbe8>):004:0> self
  #   => #<Person:0x292fbe8 @age=23, @name="Random Person">
  #   irb(#<Person:0x292fbe8>):005:0> @age += 1; self
  #   => #<Person:0x292fbe8 @age=24, @name="Random Person">
  #   irb(#<Person:0x292fbe8>):006:0> exit
  #   Executing break point "Person#name" at file.rb:9 in `name'
  #   irb(#<Person:0x292fbe8>):001:0> throw(:debug_return, "Overriden name")
  #   Name: Overriden name
  #
  # Breakpoint sessions will automatically have a few
  # convenience methods available. See Breakpoint::CommandBundle
  # for a list of them.
  #
  # Breakpoints can also be used remotely over sockets.
  # This is implemented by running part of the IRB session
  # in the application and part of it in a special client.
  # You have to call Breakpoint.activate_drb to enable
  # support for remote breakpoints and then run
  # breakpoint_client.rb which is distributed with this
  # library. See the documentation of Breakpoint.activate_drb
  # for details.
  def breakpoint(id = nil, context = nil, &block)
    callstack = caller
    callstack.slice!(0, 3) if callstack.first["breakpoint"]
    file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures

    message = "Executing break point " + (id ? "#{id.inspect} " : "") +
              "at #{file}:#{line}" + (method ? " in `#{method}'" : "")

    if context then
      return handle_breakpoint(context, message, file, line, &block)
    end

    Binding.of_caller do |binding_context|
      handle_breakpoint(binding_context, message, file, line, &block)
    end
  end

  module CommandBundle #:nodoc:
    # Proxy to a Breakpoint client. Lets you directly execute code
    # in the context of the client.
    class Client#:nodoc:
      def initialize(eval_handler) # :nodoc:
        @eval_handler = eval_handler
      end

      instance_methods.each do |method|
        next if method[/^__.+__$/]
        undef_method method
      end

      # Executes the specified code at the client.
      def eval(code)
        @eval_handler.call(code)
      end

      # Will execute the specified statement at the client.
      def method_missing(method, *args)
        if args.empty?
          result = eval("#{method}")
        else
          result = eval("#{method}(*Marshal.load(#{Marshal.dump(args).inspect}))")
        end

        unless [true, false, nil].include?(result)
          result.extend(DRbUndumped) if result
        end

        return result
      end
    end

    # Returns the source code surrounding the location where the
    # breakpoint was issued.
    def source_lines(context = 5, return_line_numbers = false)
      lines = File.readlines(@__bp_file).map { |line| line.chomp }

      break_line = @__bp_line
      start_line = [break_line - context, 1].max
      end_line = break_line + context

      result = lines[(start_line - 1) .. (end_line - 1)]

      if return_line_numbers then
        return [start_line, break_line, result]
      else
        return result
      end
    end

    # Lets an object that will forward method calls to the breakpoint
    # client. This is useful for outputting longer things at the client
    # and so on. You can for example do these things:
    #
    #   client.puts "Hello" # outputs "Hello" at client console
    #   # outputs "Hello" into the file temp.txt at the client
    #   client.File.open("temp.txt", "w") { |f| f.puts "Hello" } 
    def client()
      if Breakpoint.use_drb? then
        Client.new(Breakpoint.drb_service.eval_handler)
      else
        Client.new(lambda { |code| eval(code, TOPLEVEL_BINDING) })
      end
    end
  end

  def handle_breakpoint(context, message, file = "", line = "", &block) # :nodoc:
    catch(:debug_return) do |value|
      eval(%{
        @__bp_file = #{file.inspect}
        @__bp_line = #{line}
        extend Breakpoint::CommandBundle
        extend DRbUndumped if self
      }, context) rescue nil

      if not use_drb? then
        puts message
        IRB.start(nil, IRB::WorkSpace.new(context))
      else
        @drb_service.add_breakpoint(context, message)
      end

      block.call if block
    end
  end

  # These exceptions will be raised on failed asserts
  # if Breakpoint.asserts_cause_exceptions is set to
  # true.
  class FailedAssertError < RuntimeError#:nodoc:
  end

  # This asserts that the block evaluates to true.
  # If it doesn't evaluate to true a breakpoint will
  # automatically be created at that execution point.
  #
  # You can disable assert checking in production
  # code by setting Breakpoint.optimize_asserts to
  # true. (It will still be enabled when Ruby is run
  # via the -d argument.)
  #
  # Example:
  #   person_name = "Foobar"
  #   assert { not person_name.nil? }
  #
  # Note: If you want to use this method from an
  # unit test, you will have to call it by its full
  # name, Breakpoint.assert.
  def assert(context = nil, &condition)
    return if Breakpoint.optimize_asserts and not $DEBUG
    return if yield

    callstack = caller
    callstack.slice!(0, 3) if callstack.first["assert"]
    file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures

    message = "Assert failed at #{file}:#{line}#{" in `#{method}'" if method}."

    if Breakpoint.asserts_cause_exceptions and not $DEBUG then
      raise(Breakpoint::FailedAssertError, message)
    end

    message += " Executing implicit breakpoint."

    if context then
      return handle_breakpoint(context, message, file, line)
    end

    Binding.of_caller do |context|
      handle_breakpoint(context, message, file, line)
    end
  end

  # Whether asserts should be ignored if not in debug mode.
  # Debug mode can be enabled by running ruby with the -d
  # switch or by setting $DEBUG to true.
  attr_accessor :optimize_asserts
  self.optimize_asserts = false

  # Whether an Exception should be raised on failed asserts
  # in non-$DEBUG code or not. By default this is disabled.
  attr_accessor :asserts_cause_exceptions
  self.asserts_cause_exceptions = false
  @use_drb = false

  attr_reader :drb_service # :nodoc:

  class DRbService # :nodoc:
    include DRbUndumped

    def initialize
      @handler = @eval_handler = @collision_handler = nil

      IRB.instance_eval { @CONF[:RC] = true }
      IRB.run_config
    end

    def collision
      sleep(0.5) until @collision_handler

      @collision_handler.call
    end

    def ping; end

    def add_breakpoint(context, message)
      workspace = IRB::WorkSpace.new(context)
      workspace.extend(DRbUndumped)

      sleep(0.5) until @handler

      @handler.call(workspace, message)
    end

    def register_handler(&block)
      @handler = block
    end

    def unregister_handler
      @handler = nil
    end

    attr_reader :eval_handler

    def register_eval_handler(&block)
      @eval_handler = block
    end

    def unregister_eval_handler
      @eval_handler = lambda { }
    end

    def register_collision_handler(&block)
      @collision_handler = block
    end

    def unregister_collision_handler
      @collision_handler = lambda { }
    end
  end

  # Will run Breakpoint in DRb mode. This will spawn a server
  # that can be attached to via the breakpoint-client command
  # whenever a breakpoint is executed. This is useful when you
  # are debugging CGI applications or other applications where
  # you can't access debug sessions via the standard input and
  # output of your application.
  #
  # You can specify an URI where the DRb server will run at.
  # This way you can specify the port the server runs on. The
  # default URI is druby://localhost:42531.
  #
  # Please note that breakpoints will be skipped silently in
  # case the DRb server can not spawned. (This can happen if
  # the port is already used by another instance of your
  # application on CGI or another application.)
  #
  # Also note that by default this will only allow access
  # from localhost. You can however specify a list of
  # allowed hosts or nil (to allow access from everywhere).
  # But that will still not protect you from somebody
  # reading the data as it goes through the net.
  #
  # A good approach for getting security and remote access
  # is setting up an SSH tunnel between the DRb service
  # and the client. This is usually done like this:
  #
  # $ ssh -L20000:127.0.0.1:20000 -R10000:127.0.0.1:10000 example.com
  # (This will connect port 20000 at the client side to port
  # 20000 at the server side, and port 10000 at the server
  # side to port 10000 at the client side.)
  #
  # After that do this on the server side: (the code being debugged)
  # Breakpoint.activate_drb("druby://127.0.0.1:20000", "localhost")
  #
  # And at the client side:
  # ruby breakpoint_client.rb -c druby://127.0.0.1:10000 -s druby://127.0.0.1:20000
  #
  # Running through such a SSH proxy will also let you use 
  # breakpoint.rb in case you are behind a firewall.
  #
  # Detailed information about running DRb through firewalls is
  # available at http://www.rubygarden.org/ruby?DrbTutorial
  def activate_drb(uri = nil, allowed_hosts = ['localhost', '127.0.0.1', '::1'], ignore_collisions = false) #:nodoc:

    return false if @use_drb

    uri ||= 'druby://localhost:42531'

    if allowed_hosts then
      acl = ["deny", "all"]

      Array(allowed_hosts).each do |host|
        acl += ["allow", host]
      end

      DRb.install_acl(ACL.new(acl))
    end

    @use_drb = true
    @drb_service = DRbService.new
    did_collision = false
    begin
      @service = DRb.start_service(uri, @drb_service)
    rescue Errno::EADDRINUSE
      if ignore_collisions then
        nil
      else
        # The port is already occupied by another
        # Breakpoint service. We will try to tell
        # the old service that we want its port.
        # It will then forward that request to the
        # user and retry.
        unless did_collision then
          DRbObject.new(nil, uri).collision
          did_collision = true
        end
        sleep(10)
        retry
      end
    end

    return true
  end

  # Deactivates a running Breakpoint service.
  def deactivate_drb #:nodoc:
    @service.stop_service unless @service.nil?
    @service = nil
    @use_drb = false
    @drb_service = nil
  end

  # Returns true when Breakpoints are used over DRb.
  # Breakpoint.activate_drb causes this to be true.
  def use_drb? #:nodoc:
    @use_drb == true
  end
end

module IRB # :nodoc:
  class << self; remove_method :start; end
  def self.start(ap_path = nil, main_context = nil, workspace = nil)
    $0 = File::basename(ap_path, ".rb") if ap_path

    # suppress some warnings about redefined constants
    old_verbose, $VERBOSE = $VERBOSE, nil
    IRB.setup(ap_path)
    $VERBOSE = old_verbose

    if @CONF[:SCRIPT] then
      irb = Irb.new(main_context, @CONF[:SCRIPT])
    else
      irb = Irb.new(main_context)
    end

    if workspace then
      irb.context.workspace = workspace
    end

    @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
    @CONF[:MAIN_CONTEXT] = irb.context

    old_sigint = trap("SIGINT") do
      irb.signal_handle
    end
    
    catch(:IRB_EXIT) do
      irb.eval_input
    end
  ensure
    trap("SIGINT", old_sigint)
  end

  class << self
    alias :old_CurrentContext :CurrentContext
    remove_method :CurrentContext
  end
  def IRB.CurrentContext
    if old_CurrentContext.nil? and Breakpoint.use_drb? then
      result = Object.new
      def result.last_value; end
      return result
    else
      old_CurrentContext
    end
  end

  class Context#:nodoc:
    alias :old_evaluate :evaluate
    def evaluate(line, line_no)
      if line.chomp == "exit" then
        exit
      else
        old_evaluate(line, line_no)
      end
    end
  end

  class WorkSpace#:nodoc:
    alias :old_evaluate :evaluate

    def evaluate(*args)
      if Breakpoint.use_drb? then
        result = old_evaluate(*args)
        if args[0] != :no_proxy and
          not [true, false, nil].include?(result)
        then
          result.extend(DRbUndumped) rescue nil
        end
        return result
      else
        old_evaluate(*args)
      end
    end
  end

  module InputCompletor#:nodoc:
    def self.eval(code, context, *more)
      # Big hack, this assumes that InputCompletor
      # will only call eval() when it wants code
      # to be executed in the IRB context.
      IRB.conf[:MAIN_CONTEXT].workspace.evaluate(:no_proxy, code, *more)
    end
  end
end

module DRb # :nodoc:
  class DRbObject#:nodoc:
    undef :inspect
    undef :clone
  end
end

# See Breakpoint.breakpoint
def breakpoint(id = nil, &block)
  Binding.of_caller do |context|
    Breakpoint.breakpoint(id, context, &block)
  end
end

# See Breakpoint.assert
def assert(&block)
  Binding.of_caller do |context|
    Breakpoint.assert(context, &block)
  end
end