debug 1.2.4 → 1.3.0

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.
data/lib/debug/server.rb CHANGED
@@ -15,6 +15,8 @@ module DEBUGGER__
15
15
  @q_ans = nil
16
16
  @unsent_messages = []
17
17
  @width = 80
18
+ @repl = true
19
+ @session = nil
18
20
  end
19
21
 
20
22
  class Terminate < StandardError
@@ -22,6 +24,7 @@ module DEBUGGER__
22
24
 
23
25
  def deactivate
24
26
  @reader_thread.raise Terminate
27
+ @reader_thread.join
25
28
  end
26
29
 
27
30
  def accept
@@ -36,9 +39,11 @@ module DEBUGGER__
36
39
  end
37
40
 
38
41
  def activate session, on_fork: false
42
+ @session = session
39
43
  @reader_thread = Thread.new do
40
44
  # An error on this thread should break the system.
41
45
  Thread.current.abort_on_exception = true
46
+ Thread.current.name = 'DEBUGGER__::Server::reader'
42
47
 
43
48
  accept do |server, already_connected: false|
44
49
  DEBUGGER__.warn "Connected."
@@ -52,7 +57,7 @@ module DEBUGGER__
52
57
  # flush unsent messages
53
58
  @unsent_messages.each{|m|
54
59
  @sock.puts m
55
- }
60
+ } if @repl
56
61
  @unsent_messages.clear
57
62
 
58
63
  @q_msg = Queue.new
@@ -68,6 +73,7 @@ module DEBUGGER__
68
73
  raise # should catch at outer scope
69
74
  rescue => e
70
75
  DEBUGGER__.warn "ReaderThreadError: #{e}"
76
+ pp e.backtrace
71
77
  ensure
72
78
  DEBUGGER__.warn "Disconnected."
73
79
  @sock = nil
@@ -103,23 +109,58 @@ module DEBUGGER__
103
109
 
104
110
  raise unless @sock.read(2) == "\r\n"
105
111
  self.extend(UI_DAP)
112
+ @repl = false
106
113
  dap_setup @sock.read($1.to_i)
114
+ when /^GET \/ HTTP\/1.1/
115
+ require_relative 'server_cdp'
116
+
117
+ self.extend(UI_CDP)
118
+ @repl = false
119
+ @web_sock = UI_CDP::WebSocket.new(@sock)
120
+ @web_sock.handshake
107
121
  else
108
122
  raise "Greeting message error: #{g}"
109
123
  end
110
124
  end
111
125
 
112
126
  def process
113
- while line = @sock.gets
127
+ while true
128
+ DEBUGGER__.info "sleep IO.select"
129
+ r = IO.select([@sock])
130
+ DEBUGGER__.info "wakeup IO.select"
131
+
132
+ line = @session.process_group.sync do
133
+ unless IO.select([@sock], nil, nil, 0)
134
+ DEBUGGER__.info "UI_Server can not read"
135
+ break :can_not_read
136
+ end
137
+ @sock.gets&.chomp.tap{|line|
138
+ DEBUGGER__.info "UI_Server received: #{line}"
139
+ }
140
+ end
141
+
142
+ next if line == :can_not_read
143
+
114
144
  case line
115
145
  when /\Apause/
116
146
  pause
117
- when /\Acommand ?(.+)/
118
- @q_msg << $1
119
- when /\Aanswer (.*)/
120
- @q_ans << $1
121
- when /\Awidth (.+)/
122
- @width = $1.to_i
147
+ when /\Acommand (\d+) (\d+) ?(.+)/
148
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
149
+
150
+ if $1.to_i == Process.pid
151
+ @width = $2.to_i
152
+ @q_msg << $3
153
+ else
154
+ raise "pid:#{Process.pid} but get #{line}"
155
+ end
156
+ when /\Aanswer (\d+) (.*)/
157
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
158
+
159
+ if $1.to_i == Process.pid
160
+ @q_ans << $2
161
+ else
162
+ raise "pid:#{Process.pid} but get #{line}"
163
+ end
123
164
  else
124
165
  STDERR.puts "unsupported: #{line}"
125
166
  exit!
@@ -135,6 +176,21 @@ module DEBUGGER__
135
176
  @width
136
177
  end
137
178
 
179
+ def sigurg_overridden? prev_handler
180
+ case prev_handler
181
+ when "SYSTEM_DEFAULT"
182
+ false
183
+ when Proc
184
+ if prev_handler.source_location[0] == __FILE__
185
+ false
186
+ else
187
+ true
188
+ end
189
+ else
190
+ true
191
+ end
192
+ end
193
+
138
194
  def setup_interrupt
139
195
  prev_handler = trap(:SIGURG) do
140
196
  # $stderr.puts "trapped SIGINT"
@@ -148,8 +204,8 @@ module DEBUGGER__
148
204
  end
149
205
  end
150
206
 
151
- if prev_handler != "SYSTEM_DEFAULT"
152
- DEBUGGER__.warn "SIGURG handler is overriddend by the debugger."
207
+ if sigurg_overridden?(prev_handler)
208
+ DEBUGGER__.warn "SIGURG handler is overridden by the debugger."
153
209
  end
154
210
  yield
155
211
  ensure
@@ -191,7 +247,7 @@ module DEBUGGER__
191
247
 
192
248
  def ask prompt
193
249
  sock do |s|
194
- s.puts "ask #{prompt}"
250
+ s.puts "ask #{Process.pid} #{prompt}"
195
251
  @q_ans.pop
196
252
  end
197
253
  end
@@ -220,13 +276,23 @@ module DEBUGGER__
220
276
 
221
277
  def readline prompt
222
278
  input = (sock do |s|
223
- s.puts "input"
279
+ if @repl
280
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
281
+ line = "input #{Process.pid}"
282
+ DEBUGGER__.info "send: #{line}"
283
+ s.puts line
284
+ end
224
285
  sleep 0.01 until @q_msg
286
+ @q_msg.pop.tap{|msg|
287
+ DEBUGGER__.info "readline: #{msg.inspect}"
288
+ }
289
+ end || 'continue')
225
290
 
226
- @q_msg.pop
227
- end || 'continue').strip
228
-
229
- input
291
+ if input.is_a?(String)
292
+ input.strip
293
+ else
294
+ input
295
+ end
230
296
  end
231
297
 
232
298
  def pause
@@ -244,6 +310,7 @@ module DEBUGGER__
244
310
 
245
311
  class UI_TcpServer < UI_ServerBase
246
312
  def initialize host: nil, port: nil
313
+ @addr = nil
247
314
  @host = host || CONFIG[:host] || '127.0.0.1'
248
315
  @port = port || begin
249
316
  port_str = CONFIG[:port] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
@@ -263,7 +330,24 @@ module DEBUGGER__
263
330
 
264
331
  begin
265
332
  Socket.tcp_server_sockets @host, @port do |socks|
266
- ::DEBUGGER__.warn "Debugger can attach via TCP/IP (#{socks.map{|e| e.local_address.inspect}})"
333
+ addr = socks[0].local_address.inspect_sockaddr # Change this part if `socks` are multiple.
334
+ rdbg = File.expand_path('../../exe/rdbg', __dir__)
335
+
336
+ DEBUGGER__.warn "Debugger can attach via TCP/IP (#{addr})"
337
+ DEBUGGER__.info <<~EOS
338
+ With rdbg, use the following command line:
339
+ #
340
+ # #{rdbg} --attach #{addr.split(':').join(' ')}
341
+ #
342
+ EOS
343
+
344
+ DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome'
345
+ With Chrome browser, type the following URL in the address-bar:
346
+
347
+ devtools://devtools/bundled/inspector.html?ws=#{addr}
348
+
349
+ EOS
350
+
267
351
  Socket.accept_loop(socks) do |sock, client|
268
352
  @client_addr = client
269
353
  yield @sock_for_fork = sock
@@ -298,6 +382,64 @@ module DEBUGGER__
298
382
  super()
299
383
  end
300
384
 
385
+ def vscode_setup
386
+ require 'tmpdir'
387
+ require 'json'
388
+ require 'fileutils'
389
+
390
+ dir = Dir.mktmpdir("ruby-debug-vscode-")
391
+ at_exit{
392
+ FileUtils.rm_rf dir
393
+ }
394
+ Dir.chdir(dir) do
395
+ Dir.mkdir('.vscode')
396
+ open('README.rb', 'w'){|f|
397
+ f.puts <<~MSG
398
+ # Wait for starting the attaching to the Ruby process
399
+ # This file will be removed at the end of the debuggee process.
400
+ #
401
+ # Note that vscode-rdbg extension is needed. Please install if you don't have.
402
+ MSG
403
+ }
404
+ open('.vscode/launch.json', 'w'){|f|
405
+ f.puts JSON.pretty_generate({
406
+ version: '0.2.0',
407
+ configurations: [
408
+ {
409
+ type: "rdbg",
410
+ name: "Attach with rdbg",
411
+ request: "attach",
412
+ rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
413
+ debugPort: @sock_path,
414
+ autoAttach: true,
415
+ }
416
+ ]
417
+ })
418
+ }
419
+ end
420
+
421
+ cmds = ['code', "#{dir}/", "#{dir}/README.rb"]
422
+ cmdline = cmds.join(' ')
423
+ ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/ #{dir}/README.rb"
424
+
425
+ STDERR.puts "Launching: #{cmdline}"
426
+ env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
427
+
428
+ unless system(env, *cmds)
429
+ DEBUGGER__.warn <<~MESSAGE
430
+ Can not invoke the command.
431
+ Use the command-line on your terminal (with modification if you need).
432
+
433
+ #{cmdline}
434
+
435
+ If your application is running on a SSH remote host, please try:
436
+
437
+ #{ssh_cmdline}
438
+
439
+ MESSAGE
440
+ end
441
+ end
442
+
301
443
  def accept
302
444
  super # for fork
303
445
 
@@ -310,6 +452,8 @@ module DEBUGGER__
310
452
  end
311
453
 
312
454
  ::DEBUGGER__.warn "Debugger can attach via UNIX domain socket (#{@sock_path})"
455
+ vscode_setup if CONFIG[:open_frontend] == 'vscode'
456
+
313
457
  Socket.unix_server_loop @sock_path do |sock, client|
314
458
  @sock_for_fork = sock
315
459
  @client_addr = client
@@ -0,0 +1,412 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'digest/sha1'
5
+ require 'base64'
6
+ require 'securerandom'
7
+
8
+ module DEBUGGER__
9
+ module UI_CDP
10
+ SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
11
+
12
+ class WebSocket
13
+ def initialize s
14
+ @sock = s
15
+ end
16
+
17
+ def handshake
18
+ CONFIG.set_config no_color: true
19
+
20
+ req = @sock.readpartial 4096
21
+ $stderr.puts '[>]' + req if SHOW_PROTOCOL
22
+
23
+ if req.match /^Sec-WebSocket-Key: (.*)\r\n/
24
+ accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
25
+ @sock.print "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
26
+ else
27
+ "Unknown request: #{req}"
28
+ end
29
+ end
30
+
31
+ def send **msg
32
+ msg = JSON.generate(msg)
33
+ frame = []
34
+ fin = 0b10000000
35
+ opcode = 0b00000001
36
+ frame << fin + opcode
37
+
38
+ mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
39
+ bytesize = msg.bytesize
40
+ if bytesize < 126
41
+ payload_len = bytesize
42
+ elsif bytesize < 2 ** 16
43
+ payload_len = 0b01111110
44
+ ex_payload_len = [bytesize].pack('n*').bytes
45
+ else
46
+ payload_len = 0b01111111
47
+ ex_payload_len = [bytesize].pack('Q>').bytes
48
+ end
49
+
50
+ frame << mask + payload_len
51
+ frame.push *ex_payload_len if ex_payload_len
52
+ frame.push *msg.bytes
53
+ @sock.print frame.pack 'c*'
54
+ end
55
+
56
+ def extract_data
57
+ first_group = @sock.getbyte
58
+ fin = first_group & 0b10000000 != 128
59
+ raise 'Unsupported' if fin
60
+ opcode = first_group & 0b00001111
61
+ raise "Unsupported: #{opcode}" unless opcode == 1
62
+
63
+ second_group = @sock.getbyte
64
+ mask = second_group & 0b10000000 == 128
65
+ raise 'The client must mask all frames' unless mask
66
+ payload_len = second_group & 0b01111111
67
+ # TODO: Support other payload_lengths
68
+ if payload_len == 126
69
+ payload_len = @sock.gets(2).unpack('n*')[0]
70
+ end
71
+
72
+ masking_key = []
73
+ 4.times { masking_key << @sock.getbyte }
74
+ unmasked = []
75
+ payload_len.times do |n|
76
+ masked = @sock.getbyte
77
+ unmasked << (masked ^ masking_key[n % 4])
78
+ end
79
+ JSON.parse unmasked.pack 'c*'
80
+ end
81
+ end
82
+
83
+ def send_response req, **res
84
+ if res.empty?
85
+ @web_sock.send id: req['id'], result: {}
86
+ else
87
+ @web_sock.send id: req['id'], result: res
88
+ end
89
+ end
90
+
91
+ def send_event method, **params
92
+ if params.empty?
93
+ @web_sock.send method: method, params: {}
94
+ else
95
+ @web_sock.send method: method, params: params
96
+ end
97
+ end
98
+
99
+ def process
100
+ loop do
101
+ req = @web_sock.extract_data
102
+ $stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL
103
+ bps = []
104
+
105
+ case req['method']
106
+
107
+ ## boot/configuration
108
+ when 'Page.getResourceTree'
109
+ abs = File.absolute_path($0)
110
+ src = File.read(abs)
111
+ send_response req,
112
+ frameTree: {
113
+ frame: {
114
+ id: SecureRandom.hex(16),
115
+ loaderId: SecureRandom.hex(16),
116
+ url: 'http://debuggee/',
117
+ securityOrigin: 'http://debuggee',
118
+ mimeType: 'text/plain' },
119
+ resources: [
120
+ ]
121
+ }
122
+ send_event 'Debugger.scriptParsed',
123
+ scriptId: abs,
124
+ url: "http://debuggee#{abs}",
125
+ startLine: 0,
126
+ startColumn: 0,
127
+ endLine: src.count('\n'),
128
+ endColumn: 0,
129
+ executionContextId: 1,
130
+ hash: src.hash
131
+ send_event 'Runtime.executionContextCreated',
132
+ context: {
133
+ id: SecureRandom.hex(16),
134
+ origin: "http://#{@addr}",
135
+ name: ''
136
+ }
137
+ when 'Debugger.getScriptSource'
138
+ s_id = req.dig('params', 'scriptId')
139
+ src = File.read(s_id)
140
+ send_response req, scriptSource: src
141
+ @q_msg << req
142
+ when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
143
+ 'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
144
+ 'Debugger.setBreakpointsActive', 'Runtime.releaseObjectGroup'
145
+ send_response req
146
+
147
+ ## control
148
+ when 'Debugger.resume'
149
+ @q_msg << 'c'
150
+ @q_msg << req
151
+ send_response req
152
+ send_event 'Debugger.resumed'
153
+ when 'Debugger.stepOver'
154
+ @q_msg << 'n'
155
+ @q_msg << req
156
+ send_response req
157
+ send_event 'Debugger.resumed'
158
+ when 'Debugger.stepInto'
159
+ @q_msg << 's'
160
+ @q_msg << req
161
+ send_response req
162
+ send_event 'Debugger.resumed'
163
+ when 'Debugger.stepOut'
164
+ @q_msg << 'fin'
165
+ @q_msg << req
166
+ send_response req
167
+ send_event 'Debugger.resumed'
168
+
169
+ # breakpoint
170
+ when 'Debugger.getPossibleBreakpoints'
171
+ s_id = req.dig('params', 'start', 'scriptId')
172
+ line = req.dig('params', 'lineNumber')
173
+ send_response req,
174
+ locations: [
175
+ { scriptId: s_id,
176
+ lineNumber: line,
177
+ }
178
+ ]
179
+ when 'Debugger.setBreakpointByUrl'
180
+ line = req.dig('params', 'lineNumber')
181
+ path = req.dig('params', 'url').match('http://debuggee(.*)')[1]
182
+ cond = req.dig('params', 'condition')
183
+ if cond != ''
184
+ bps << SESSION.add_line_breakpoint(path, line + 1, cond: cond)
185
+ else
186
+ bps << SESSION.add_line_breakpoint(path, line + 1)
187
+ end
188
+ send_response req,
189
+ breakpointId: (bps.size - 1).to_s,
190
+ locations: [
191
+ scriptId: path,
192
+ lineNumber: line
193
+ ]
194
+ when 'Debugger.removeBreakpoint'
195
+ b_id = req.dig('params', 'breakpointId')
196
+ @q_msg << "del #{b_id}"
197
+ send_response req
198
+
199
+ when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
200
+ @q_msg << req
201
+ end
202
+ end
203
+ end
204
+
205
+ ## Called by the SESSION thread
206
+
207
+ def readline prompt
208
+ @q_msg.pop || 'kill!'
209
+ end
210
+
211
+ def respond req, **result
212
+ send_response req, **result
213
+ end
214
+
215
+ def fire_event event, **result
216
+ if result.empty?
217
+ send_event event
218
+ else
219
+ send_event event, **result
220
+ end
221
+ end
222
+
223
+ def sock skip: false
224
+ yield $stderr
225
+ end
226
+
227
+ def puts result
228
+ # STDERR.puts "puts: #{result}"
229
+ # send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
230
+ end
231
+ end
232
+
233
+ class Session
234
+ def process_protocol_request req
235
+ case req['method']
236
+ when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
237
+ @tc << [:cdp, :backtrace, req]
238
+ when 'Debugger.evaluateOnCallFrame'
239
+ expr = req.dig('params', 'expression')
240
+ @tc << [:cdp, :evaluate, req, expr]
241
+ when 'Runtime.getProperties'
242
+ oid = req.dig('params', 'objectId')
243
+ case oid
244
+ when /(\d?):local/
245
+ @tc << [:cdp, :properties, req, $1.to_i]
246
+ when /\d?:script/
247
+ # TODO: Support a script type
248
+ @ui.respond req
249
+ return :retry
250
+ when /\d?:global/
251
+ # TODO: Support a global type
252
+ @ui.respond req
253
+ return :retry
254
+ end
255
+ end
256
+ end
257
+
258
+ def cdp_event args
259
+ type, req, result = args
260
+
261
+ case type
262
+ when :backtrace
263
+ result[:callFrames].each do |frame|
264
+ s_id = frame.dig(:location, :scriptId)
265
+ if File.exist?(s_id) && !@script_paths.include?(s_id)
266
+ src = File.read(s_id)
267
+ @ui.fire_event 'Debugger.scriptParsed',
268
+ scriptId: s_id,
269
+ url: frame[:url],
270
+ startLine: 0,
271
+ startColumn: 0,
272
+ endLine: src.count('\n'),
273
+ endColumn: 0,
274
+ executionContextId: @script_paths.size + 1,
275
+ hash: src.hash
276
+ @script_paths << s_id
277
+ end
278
+ end
279
+ result[:reason] = 'other'
280
+ @ui.fire_event 'Debugger.paused', **result
281
+ when :evaluate
282
+ @ui.respond req, result: result
283
+ when :properties
284
+ @ui.respond req, result: result
285
+ end
286
+ end
287
+ end
288
+
289
+ class ThreadClient
290
+ def process_cdp args
291
+ type = args.shift
292
+ req = args.shift
293
+
294
+ case type
295
+ when :backtrace
296
+ event! :cdp_result, :backtrace, req, {
297
+ callFrames: @target_frames.map.with_index{|frame, i|
298
+ path = frame.realpath || frame.path
299
+ if path.match /<internal:(.*)>/
300
+ abs = $1
301
+ else
302
+ abs = path
303
+ end
304
+
305
+ local_scope = {
306
+ callFrameId: SecureRandom.hex(16),
307
+ functionName: frame.name,
308
+ location: {
309
+ scriptId: abs,
310
+ lineNumber: frame.location.lineno - 1 # The line number is 0-based.
311
+ },
312
+ url: "http://debuggee#{abs}",
313
+ scopeChain: [
314
+ {
315
+ type: 'local',
316
+ object: {
317
+ type: 'object',
318
+ objectId: "#{i}:local"
319
+ }
320
+ },
321
+ {
322
+ type: 'script',
323
+ object: {
324
+ type: 'object',
325
+ objectId: "#{i}:script"
326
+ }
327
+ },
328
+ {
329
+ type: 'global',
330
+ object: {
331
+ type: 'object',
332
+ objectId: "#{i}:global"
333
+ }
334
+ }
335
+ ],
336
+ this: {
337
+ type: 'object'
338
+ }
339
+ }
340
+ }
341
+ }
342
+ when :evaluate
343
+ expr = args.shift
344
+ begin
345
+ result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
346
+ rescue Exception => e
347
+ result = e
348
+ end
349
+ event! :cdp_result, :evaluate, req, evaluate_result(result)
350
+ when :properties
351
+ fid = args.shift
352
+ frame = @target_frames[fid]
353
+ if b = frame.binding
354
+ vars = b.local_variables.map{|name|
355
+ v = b.local_variable_get(name)
356
+ variable(name, v)
357
+ }
358
+ vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
359
+ vars.unshift variable('%return', frame.return_value) if frame.has_return_value
360
+ vars.unshift variable('%self', b.receiver)
361
+ elsif lvars = frame.local_variables
362
+ vars = lvars.map{|var, val|
363
+ variable(var, val)
364
+ }
365
+ else
366
+ vars = [variable('%self', frame.self)]
367
+ vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
368
+ vars.push variable('%return', frame.return_value) if frame.has_return_value
369
+ end
370
+ event! :cdp_result, :properties, req, vars
371
+ end
372
+ end
373
+
374
+ def evaluate_result r
375
+ v = variable nil, r
376
+ v[:value]
377
+ end
378
+
379
+ def variable_ name, obj, type, use_short: true
380
+ {
381
+ name: name,
382
+ value: {
383
+ type: type,
384
+ value: DEBUGGER__.short_inspect(obj, use_short)
385
+ },
386
+ configurable: true,
387
+ enumerable: true
388
+ }
389
+ end
390
+
391
+ def variable name, obj
392
+ case obj
393
+ when Array, Hash, Range, NilClass, Time
394
+ variable_ name, obj, 'object'
395
+ when String
396
+ variable_ name, obj, 'string', use_short: false
397
+ when Class, Module, Struct
398
+ variable_ name, obj, 'function'
399
+ when TrueClass, FalseClass
400
+ variable_ name, obj, 'boolean'
401
+ when Symbol
402
+ variable_ name, obj, 'symbol'
403
+ when Float
404
+ variable_ name, obj, 'number'
405
+ when Integer
406
+ variable_ name, obj, 'number'
407
+ else
408
+ variable_ name, obj, 'undefined'
409
+ end
410
+ end
411
+ end
412
+ end