|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2005 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 =begin edoc |
|
7 File implementing a debug client base module. |
|
8 =end |
|
9 |
|
10 if RUBY_VERSION < "1.9" |
|
11 $KCODE = 'UTF8' |
|
12 require 'jcode' |
|
13 end |
|
14 |
|
15 require 'socket' |
|
16 |
|
17 require 'DebugQuit' |
|
18 require 'DebugProtocol' |
|
19 require 'DebugClientCapabilities' |
|
20 require 'AsyncFile' |
|
21 require 'Config' |
|
22 require 'Completer' |
|
23 |
|
24 $DebugClientInstance = nil |
|
25 $debugging = false |
|
26 |
|
27 module DebugClientBase |
|
28 =begin edoc |
|
29 Module implementing the client side of the debugger. |
|
30 |
|
31 It provides access to the Ruby interpeter from a debugger running in another |
|
32 process. |
|
33 |
|
34 The protocol between the debugger and the client assumes that there will be |
|
35 a single source of debugger commands and a single source of Ruby |
|
36 statements. Commands and statement are always exactly one line and may be |
|
37 interspersed. |
|
38 |
|
39 The protocol is as follows. First the client opens a connection to the |
|
40 debugger and then sends a series of one line commands. A command is either |
|
41 >Load<, >Step<, >StepInto<, ... or a Ruby statement. |
|
42 See DebugProtocol.rb for a listing of valid protocol tokens. |
|
43 |
|
44 A Ruby statement consists of the statement to execute, followed (in a |
|
45 separate line) by >OK?<. If the statement was incomplete then the response |
|
46 is >Continue<. If there was an exception then the response is >Exception<. |
|
47 Otherwise the response is >OK<. The reason for the >OK?< part is to |
|
48 provide a sentinal (ie. the responding >OK<) after any possible output as a |
|
49 result of executing the command. |
|
50 |
|
51 The client may send any other lines at any other time which should be |
|
52 interpreted as program output. |
|
53 |
|
54 If the debugger closes the session there is no response from the client. |
|
55 The client may close the session at any time as a result of the script |
|
56 being debugged closing or crashing. |
|
57 |
|
58 <b>Note</b>: This module is meant to be mixed in by individual DebugClient classes. |
|
59 Do not use it directly. |
|
60 =end |
|
61 @@clientCapabilities = HasDebugger | HasInterpreter | HasShell | HasCompleter |
|
62 |
|
63 attr_accessor :passive, :traceRuby |
|
64 |
|
65 def initializeDebugClient |
|
66 =begin edoc |
|
67 Method to initialize the module |
|
68 =end |
|
69 |
|
70 # The context to run the debugged program in. |
|
71 @debugBinding = eval("def e4dc_DebugBinding; binding; end; e4dc_DebugBinding", |
|
72 TOPLEVEL_BINDING, |
|
73 __FILE__, |
|
74 __LINE__ - 3) |
|
75 |
|
76 # The context to run the shell commands in. |
|
77 @shellBinding = eval("def e4dc_ShellBinding; binding; end; e4dc_ShellBinding", |
|
78 TOPLEVEL_BINDING, |
|
79 __FILE__, |
|
80 __LINE__ - 3) |
|
81 |
|
82 # stack frames |
|
83 @frames = [] |
|
84 @framenr = 0 |
|
85 |
|
86 # The list of complete lines to execute. |
|
87 @buffer = '' |
|
88 @lineno = 0 |
|
89 |
|
90 # The list of regexp objects to filter variables against |
|
91 @globalsFilterObjects = [] |
|
92 @localsFilterObjects = [] |
|
93 |
|
94 @pendingResponse = ResponseOK |
|
95 @mainProcStr = nil # used for the passive mode |
|
96 @passive = false # used to indicate the passive mode |
|
97 @running = nil |
|
98 |
|
99 @readstream = nil |
|
100 @writestream = nil |
|
101 @errorstream = nil |
|
102 |
|
103 @variant = 'You should not see this' |
|
104 |
|
105 @completer = Completer.new(@shellBinding) |
|
106 end |
|
107 |
|
108 def handleException |
|
109 =begin edoc |
|
110 Private method called in the case of an exception |
|
111 |
|
112 It ensures that the debug server is informed of the raised exception. |
|
113 =end |
|
114 @pendingResponse = ResponseException |
|
115 end |
|
116 |
|
117 def sessionClose |
|
118 =begin edoc |
|
119 Privat method to close the session with the debugger and terminate. |
|
120 =end |
|
121 set_trace_func nil |
|
122 if $debugging |
|
123 $debugging = false |
|
124 @running = nil |
|
125 DEBUGGER__.context(DEBUGGER__.last_thread).step_quit() |
|
126 end |
|
127 |
|
128 # clean up asyncio |
|
129 disconnect() |
|
130 |
|
131 # make sure we close down our end of the socket |
|
132 # might be overkill as normally stdin, stdout and stderr |
|
133 # SHOULD be closed on exit, but it does not hurt to do it here |
|
134 @readstream.close() |
|
135 @writestream.close() |
|
136 @errorstream.close() |
|
137 |
|
138 # Ok, go away. |
|
139 exit() |
|
140 end |
|
141 |
|
142 def unhandled_exception(exc) |
|
143 =begin edoc |
|
144 Private method to report an unhandled exception. |
|
145 |
|
146 @param exc the exception object |
|
147 =end |
|
148 if SystemExit === exc |
|
149 if $debugging |
|
150 $debugging = false |
|
151 else |
|
152 progTerminated(exc.status) |
|
153 end |
|
154 return |
|
155 end |
|
156 |
|
157 # split the exception message |
|
158 msgParts = exc.message.split(":", 3) |
|
159 filename = File.expand_path(msgParts[0]) |
|
160 linenr = msgParts[1].to_i |
|
161 |
|
162 if ScriptError === exc |
|
163 msgParts = msgParts[2].split(":", 3) |
|
164 filename = msgParts[0].sub(/in `require'/, "") |
|
165 linenr = msgParts[1].to_i |
|
166 exclist = [""] |
|
167 exclist << [filename, linenr, 0] |
|
168 write("%s%s\n" % [ResponseSyntax, exclist.inspect]) |
|
169 return |
|
170 end |
|
171 |
|
172 exclist = ["unhandled %s" % exc.class, msgParts[2].sub(/in /, "")] |
|
173 exclist << [filename, linenr] |
|
174 |
|
175 # process the traceback |
|
176 frList = exc.backtrace |
|
177 frList.each do |frame| |
|
178 next if frame =~ /DebugClientBaseModule/ |
|
179 break if frame =~ /\(eval\)/ |
|
180 frameParts = frame.split(":", 3) |
|
181 filename = File.expand_path(frameParts[0]) |
|
182 linenr = frameParts[1].to_i |
|
183 next if [filename, linenr] == exclist[-1] |
|
184 exclist << [filename, linenr] |
|
185 end |
|
186 |
|
187 write("%s%s\n" % [ResponseException, exclist.inspect]) |
|
188 end |
|
189 |
|
190 def handleLine(line) |
|
191 =begin edoc |
|
192 Private method to handle the receipt of a complete line. |
|
193 |
|
194 It first looks for a valid protocol token at the start of the line. Thereafter |
|
195 it trys to execute the lines accumulated so far. |
|
196 |
|
197 @param line the received line |
|
198 =end |
|
199 |
|
200 # Remove any newline |
|
201 if line[-1] == "\n" |
|
202 line = line[1...-1] |
|
203 end |
|
204 |
|
205 ## STDOUT << line << "\n" ## debug |
|
206 |
|
207 eoc = line.index("<") |
|
208 |
|
209 if eoc and eoc >= 0 and line[0,1] == ">" |
|
210 # Get the command part and any argument |
|
211 cmd = line[0..eoc] |
|
212 arg = line[eoc+1...-1] |
|
213 |
|
214 case cmd |
|
215 when RequestOK |
|
216 write(@pendingResponse + "\n") |
|
217 @pendingResponse = ResponseOK |
|
218 return |
|
219 |
|
220 when RequestEnv |
|
221 # convert a Python stringified hash to a Ruby stringified hash |
|
222 arg.gsub!(/: u/, "=>") |
|
223 arg.gsub!(/u'/, "'") |
|
224 eval(arg).each do |key, value| |
|
225 if key[-1..-1] == "+" |
|
226 key = key[0..-2] |
|
227 if ENV[key] |
|
228 ENV[key] += value |
|
229 else |
|
230 ENV[key] = value |
|
231 end |
|
232 else |
|
233 ENV[key] = value |
|
234 end |
|
235 end |
|
236 return |
|
237 |
|
238 when RequestVariables |
|
239 frmnr, scope, filter = eval("[%s]" % arg.gsub(/u'/, "'").gsub(/u"/,'"')) |
|
240 dumpVariables(frmnr.to_i, scope.to_i, filter) |
|
241 return |
|
242 |
|
243 when RequestVariable |
|
244 var, frmnr, scope, filter = \ |
|
245 eval("[%s]" % arg.gsub(/u'/, "'").gsub(/u"/,'"')) |
|
246 dumpVariable(var, frmnr.to_i, scope.to_i, filter) |
|
247 return |
|
248 |
|
249 when RequestStep |
|
250 DEBUGGER__.context(DEBUGGER__.last_thread).stop_next() |
|
251 @eventExit = true |
|
252 return |
|
253 |
|
254 when RequestStepOver |
|
255 DEBUGGER__.context(DEBUGGER__.last_thread).step_over() |
|
256 @eventExit = true |
|
257 return |
|
258 |
|
259 when RequestStepOut |
|
260 DEBUGGER__.context(DEBUGGER__.last_thread).step_out() |
|
261 @eventExit = true |
|
262 return |
|
263 |
|
264 when RequestStepQuit |
|
265 set_trace_func nil |
|
266 wasDebugging = $debugging |
|
267 $debugging = false |
|
268 @running = nil |
|
269 if @passive |
|
270 progTerminated(42) |
|
271 else |
|
272 DEBUGGER__.context(DEBUGGER__.last_thread).step_quit() if wasDebugging |
|
273 end |
|
274 return |
|
275 |
|
276 when RequestContinue |
|
277 special = arg.to_i |
|
278 if special == 0 |
|
279 DEBUGGER__.context(DEBUGGER__.last_thread).step_continue() |
|
280 else |
|
281 # special == 1 means a continue while doing a step over |
|
282 # this occurs when an expception is raised doing a step over |
|
283 DEBUGGER__.context(DEBUGGER__.last_thread).step_over() |
|
284 end |
|
285 @eventExit = true |
|
286 return |
|
287 |
|
288 when RequestSetFilter |
|
289 scope, filterString = eval("[%s]" % arg) |
|
290 generateFilterObjects(scope.to_i, filterString) |
|
291 return |
|
292 |
|
293 when RequestLoad |
|
294 $debugging = true |
|
295 ARGV.clear() |
|
296 wd, fn, args, traceRuby = arg.split("|", -4) |
|
297 @traceRuby = traceRuby.to_i == 1 ? true : false |
|
298 ARGV.concat(eval(args.gsub(/u'/, "'").gsub(/u"/,'"'))) |
|
299 $:.insert(0, File.dirname(fn)) |
|
300 if wd == '' |
|
301 Dir.chdir($:[0]) |
|
302 else |
|
303 Dir.chdir(wd) |
|
304 end |
|
305 @running = fn |
|
306 command = "$0 = '%s'; require '%s'" % [fn, fn] |
|
307 set_trace_func proc { |event, file, line, id, binding_, klass, *rest| |
|
308 DEBUGGER__.context.trace_func(event, file, line, id, binding_, klass) |
|
309 } |
|
310 begin |
|
311 eval(command, @debugBinding) |
|
312 rescue DebugQuit |
|
313 # just ignore it |
|
314 rescue Exception => exc |
|
315 unhandled_exception(exc) |
|
316 ensure |
|
317 set_trace_func(nil) |
|
318 @running = nil |
|
319 end |
|
320 return |
|
321 |
|
322 when RequestRun |
|
323 $debugging = false |
|
324 ARGV.clear() |
|
325 wd, fn, args = arg.split("|", -3) |
|
326 ARGV.concat(eval(args.gsub(/u'/, "'").gsub(/u"/,'"'))) |
|
327 $:.insert(0, File.dirname(fn)) |
|
328 if wd == '' |
|
329 Dir.chdir($:[0]) |
|
330 else |
|
331 Dir.chdir(wd) |
|
332 end |
|
333 command = "$0 = '%s'; require '%s'" % [fn, fn] |
|
334 @frames = [] |
|
335 set_trace_func proc { |event, file, line, id, binding_, klass, *rest| |
|
336 trace_func(event, file, line, id, binding_, klass) |
|
337 } |
|
338 begin |
|
339 eval(command, @debugBinding) |
|
340 rescue SystemExit |
|
341 # ignore it |
|
342 rescue Exception => exc |
|
343 unhandled_exception(exc) |
|
344 ensure |
|
345 set_trace_func(nil) |
|
346 end |
|
347 return |
|
348 |
|
349 when RequestShutdown |
|
350 sessionClose() |
|
351 return |
|
352 |
|
353 when RequestBreak |
|
354 fn, line, temporary, set, cond = arg.split("@@") |
|
355 line = line.to_i |
|
356 set = set.to_i |
|
357 temporary = temporary.to_i == 1 ? true : false |
|
358 |
|
359 if set == 1 |
|
360 if cond == 'None' |
|
361 cond = nil |
|
362 end |
|
363 DEBUGGER__.context(DEBUGGER__.last_thread)\ |
|
364 .add_break_point(fn, line, temporary, cond) |
|
365 else |
|
366 DEBUGGER__.context(DEBUGGER__.last_thread)\ |
|
367 .delete_break_point(fn, line) |
|
368 end |
|
369 return |
|
370 |
|
371 when RequestBreakEnable |
|
372 fn, line, enable = arg.split(',') |
|
373 line = line.to_i |
|
374 enable = enable.to_i == 1 ? true : false |
|
375 DEBUGGER__.context(DEBUGGER__.last_thread)\ |
|
376 .enable_break_point(fn, line, enable) |
|
377 return |
|
378 |
|
379 when RequestBreakIgnore |
|
380 fn, line, count = arg.split(',') |
|
381 line = line.to_i |
|
382 count = count.to_i |
|
383 DEBUGGER__.context(DEBUGGER__.last_thread)\ |
|
384 .ignore_break_point(fn, line, count) |
|
385 return |
|
386 |
|
387 when RequestWatch |
|
388 cond, temporary, set = arg.split('@@') |
|
389 set = set.to_i |
|
390 temporary = temporary.to_i == 1 ? true : false |
|
391 |
|
392 if set == 1 |
|
393 DEBUGGER__.context(DEBUGGER__.last_thread)\ |
|
394 .add_watch_point(cond, temporary) |
|
395 else |
|
396 DEBUGGER__.context(DEBUGGER__.last_thread).delete_watch_point(cond) |
|
397 end |
|
398 return |
|
399 |
|
400 when RequestWatchEnable |
|
401 cond, enable = arg.split(',') |
|
402 enable = enable.to_i == 1 ? true : false |
|
403 DEBUGGER__.context(DEBUGGER__.last_thread)\ |
|
404 .enable_watch_point(cond, enable) |
|
405 return |
|
406 |
|
407 when RequestWatchIgnore |
|
408 cond, count = arg.split(',') |
|
409 count = count.to_i |
|
410 DEBUGGER__.context(DEBUGGER__.last_thread).ignore_watch_point(cond, count) |
|
411 return |
|
412 |
|
413 when RequestEval, RequestExec |
|
414 if not @running |
|
415 binding_ = @shellBinding |
|
416 else |
|
417 binding_ = DEBUGGER__.context(DEBUGGER__.last_thread).current_binding |
|
418 end |
|
419 write("\n") |
|
420 begin |
|
421 value = eval(arg, binding_) |
|
422 rescue Exception => exc |
|
423 list = [] |
|
424 list << "%s: %s\n" % \ |
|
425 [exc.class, exc.to_s.sub(/stdin:\d+:(in `.*':?)?/, '')] |
|
426 $@.each do |l| |
|
427 break if l =~ /e4dc_ShellBinding/ or l =~ /e4dc_DebugBinding/ or \ |
|
428 l =~ /:in `require'$/ |
|
429 list << "%s\n" % l.sub(/\(eval\)/, "(e3dc)") |
|
430 end |
|
431 list.each do |entry| |
|
432 write(entry) |
|
433 end |
|
434 else |
|
435 write("=> #{value.inspect}\n") |
|
436 write("#{ResponseOK}\n") |
|
437 end |
|
438 return |
|
439 |
|
440 when RequestBanner |
|
441 version = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]" |
|
442 write("%s('%s','%s','%s')\n" % \ |
|
443 [ResponseBanner, version, Socket.gethostname(), @variant]) |
|
444 return |
|
445 |
|
446 when RequestCapabilities |
|
447 write("%s%d, 'Ruby'\n" % \ |
|
448 [ResponseCapabilities, @@clientCapabilities]) |
|
449 return |
|
450 |
|
451 when RequestCompletion |
|
452 completionList(arg) |
|
453 return |
|
454 |
|
455 else |
|
456 puts "Got unsupported command %s.\n" % cmd |
|
457 return |
|
458 end |
|
459 end |
|
460 |
|
461 if @buffer |
|
462 @buffer << "\n" << line |
|
463 @lineno += 1 |
|
464 else |
|
465 @buffer = line.dup |
|
466 end |
|
467 |
|
468 # check for completeness |
|
469 if not canEval? |
|
470 @pendingResponse = ResponseContinue |
|
471 else |
|
472 command = @buffer.dup |
|
473 @buffer = "" |
|
474 begin |
|
475 res = "=> " |
|
476 if not @running |
|
477 res << eval(command, @shellBinding, "stdin", @lineno).inspect << "\n" |
|
478 else |
|
479 res << eval(command, |
|
480 DEBUGGER__.context(DEBUGGER__.last_thread).get_binding(@framenr), |
|
481 "stdin", @lineno).inspect << "\n" |
|
482 end |
|
483 write(res) |
|
484 rescue SystemExit => exc |
|
485 progTerminated(exc.status) |
|
486 rescue ScriptError, StandardError => exc |
|
487 list = [] |
|
488 list << "%s: %s\n" % \ |
|
489 [exc.class, exc.to_s.sub(/stdin:\d+:(in `.*':?)?/, '')] |
|
490 $@.each do |l| |
|
491 break if l =~ /e4dc_ShellBinding/ or l =~ /e4dc_DebugBinding/ or \ |
|
492 l =~ /:in `require'$/ |
|
493 list << "%s\n" % l.sub(/\(eval\)/, "(e3dc)") |
|
494 end |
|
495 list.each do |entry| |
|
496 write(entry) |
|
497 end |
|
498 handleException() |
|
499 end |
|
500 end |
|
501 end |
|
502 |
|
503 def canEval? |
|
504 =begin edoc |
|
505 Private method to check if the buffer's contents can be evaluated. |
|
506 |
|
507 @return flag indicating if an eval might succeed (boolean) |
|
508 =end |
|
509 indent = 0 |
|
510 if @buffer =~ /,\s*$/ |
|
511 return false |
|
512 end |
|
513 |
|
514 @buffer.split($/).each do |l| |
|
515 if l =~ /^\s*(class|module|def|if|unless|case|while|until|for|begin)\b[^_]/ |
|
516 indent += 1 |
|
517 end |
|
518 if l =~ /\s*do\s*(\|.*\|)?\s*$/ |
|
519 indent += 1 |
|
520 end |
|
521 if l =~ /^\s*end\s*$|^\s*end\b[^_]/ |
|
522 indent -= 1 |
|
523 end |
|
524 if l =~ /\{\s*(\|.*\|)?\s*$/ |
|
525 indent += 1 |
|
526 end |
|
527 if l =~ /^\s*\}/ |
|
528 indent -= 1 |
|
529 end |
|
530 end |
|
531 |
|
532 if indent > 0 |
|
533 return false |
|
534 end |
|
535 return true |
|
536 end |
|
537 |
|
538 def trace_func(event, file, line, id, binding_, klass) |
|
539 =begin edoc |
|
540 Method executed by the tracing facility. |
|
541 |
|
542 It is used to save the execution context of an exception. |
|
543 |
|
544 @param event the tracing event (String) |
|
545 @param file the name of the file being traced (String) |
|
546 @param line the line number being traced (int) |
|
547 @param id object id |
|
548 @param binding_ a binding object |
|
549 @param klass name of a class |
|
550 =end |
|
551 case event |
|
552 when 'line' |
|
553 if @frames[0] |
|
554 @frames[0] = binding_ |
|
555 else |
|
556 @frames.unshift binding_ |
|
557 end |
|
558 |
|
559 when 'call' |
|
560 @frames.unshift binding_ |
|
561 |
|
562 when 'class' |
|
563 @frames.unshift binding_ |
|
564 |
|
565 when 'return', 'end' |
|
566 @frames.shift |
|
567 |
|
568 when 'end' |
|
569 @frames.shift |
|
570 |
|
571 when 'raise' |
|
572 set_trace_func nil |
|
573 end |
|
574 end |
|
575 |
|
576 def write(s) |
|
577 =begin edoc |
|
578 Private method to write data to the output stream. |
|
579 |
|
580 @param s data to be written (string) |
|
581 =end |
|
582 @writestream.write(s) |
|
583 @writestream.flush() |
|
584 end |
|
585 |
|
586 def interact |
|
587 =begin edoc |
|
588 Private method to Interact with the debugger. |
|
589 =end |
|
590 setDescriptors(@readstream, @writestream) |
|
591 $DebugClientInstance = self |
|
592 |
|
593 if not @passive |
|
594 # At this point simulate an event loop. |
|
595 eventLoop() |
|
596 end |
|
597 end |
|
598 |
|
599 def eventLoop |
|
600 =begin edoc |
|
601 Private method implementing our event loop. |
|
602 =end |
|
603 @eventExit = nil |
|
604 |
|
605 while @eventExit == nil |
|
606 wrdy = [] |
|
607 |
|
608 if AsyncPendingWrite(@writestream) > 0 |
|
609 wrdy << @writestream.getSock() |
|
610 end |
|
611 |
|
612 if AsyncPendingWrite(@errorstream) > 0 |
|
613 wrdy << @errorstream.getSock() |
|
614 end |
|
615 |
|
616 rrdy, wrdy, xrdy = select([@readstream.getSock()], wrdy, []) |
|
617 |
|
618 if rrdy.include?(@readstream.getSock()) |
|
619 readReady(@readstream.fileno()) |
|
620 end |
|
621 |
|
622 if wrdy.include?(@writestream.getSock()) |
|
623 writeReady(@writestream.fileno()) |
|
624 end |
|
625 |
|
626 if wrdy.include?(@errorstream.getSock()) |
|
627 writeReady(@errorstream.fileno()) |
|
628 end |
|
629 end |
|
630 |
|
631 @eventExit = nil |
|
632 end |
|
633 |
|
634 def eventPoll |
|
635 =begin edoc |
|
636 Private method to poll for events like 'set break point'. |
|
637 =end |
|
638 |
|
639 # the choice of a ~0.5 second poll interval is arbitrary. |
|
640 lasteventpolltime = @lasteventpolltime ? @lasteventpolltime : Time.now |
|
641 now = Time.now |
|
642 if now - lasteventpolltime < 0.5 |
|
643 @lasteventpolltime = lasteventpolltime |
|
644 return |
|
645 else |
|
646 @lasteventpolltime = now |
|
647 end |
|
648 |
|
649 wrdy = [] |
|
650 |
|
651 if AsyncPendingWrite(@writestream) > 0 |
|
652 wrdy << @writestream.getSock() |
|
653 end |
|
654 |
|
655 if AsyncPendingWrite(@errorstream) > 0 |
|
656 wrdy << @errorstream.getSock() |
|
657 end |
|
658 |
|
659 rrdy, wrdy, xrdy = select([@readstream.getSock()], wrdy, [], 0) |
|
660 |
|
661 if rrdy == nil |
|
662 return |
|
663 end |
|
664 |
|
665 if rrdy.include?(@readstream.getSock()) |
|
666 readReady(@readstream.fileno()) |
|
667 end |
|
668 |
|
669 if wrdy.include?(@writestream.getSock()) |
|
670 writeReady(@writestream.fileno()) |
|
671 end |
|
672 |
|
673 if wrdy.include?(@errorstream.getSock()) |
|
674 writeReady(@errorstream.fileno()) |
|
675 end |
|
676 end |
|
677 |
|
678 def connectDebugger(port, remoteAddress=nil, redirect=true) |
|
679 =begin edoc |
|
680 Public method to establish a session with the debugger. |
|
681 |
|
682 It opens a network connection to the debugger, connects it to stdin, |
|
683 stdout and stderr and saves these file objects in case the application |
|
684 being debugged redirects them itself. |
|
685 |
|
686 @param port the port number to connect to (int) |
|
687 @param remoteAddress the network address of the debug server host (string) |
|
688 @param redirect flag indicating redirection of stdin, stdout and stderr (boolean) |
|
689 =end |
|
690 if remoteAddress == nil |
|
691 sock = TCPSocket.new(DebugAddress, port) |
|
692 else |
|
693 sock = TCPSocket.new(remoteAddress, port) |
|
694 end |
|
695 |
|
696 @readstream = AsyncFile.new(sock, "r", "stdin") |
|
697 @writestream = AsyncFile.new(sock, "w", "stdout") |
|
698 @errorstream = AsyncFile.new(sock, "w", "stderr") |
|
699 |
|
700 if redirect |
|
701 $stdin = @readstream |
|
702 $stdout = @writestream |
|
703 $stderr = @errorstream |
|
704 end |
|
705 end |
|
706 |
|
707 def progTerminated(status) |
|
708 =begin edoc |
|
709 Private method to tell the debugger that the program has terminated. |
|
710 |
|
711 @param status the return status |
|
712 =end |
|
713 if status == nil |
|
714 status = 0 |
|
715 else |
|
716 begin |
|
717 Integer(status) |
|
718 rescue |
|
719 status = 1 |
|
720 end |
|
721 end |
|
722 |
|
723 set_trace_func(nil) |
|
724 @running = nil |
|
725 write("%s%d\n" % [ResponseExit, status]) |
|
726 end |
|
727 |
|
728 def dumpVariables(frmnr, scope, filter) |
|
729 =begin edoc |
|
730 Private method to return the variables of a frame to the debug server. |
|
731 |
|
732 @param frmnr distance of frame reported on. 0 is the current frame (int) |
|
733 @param scope 1 to report global variables, 0 for local variables (int) |
|
734 @param filter the indices of variable types to be filtered (list of int) |
|
735 =end |
|
736 if $debugging |
|
737 if scope == 0 |
|
738 @framenr = frmnr |
|
739 end |
|
740 binding_, file, line, id = DEBUGGER__.context(DEBUGGER__.last_thread)\ |
|
741 .get_frame(frmnr) |
|
742 else |
|
743 binding_ = @frames[frmnr] |
|
744 end |
|
745 varlist = [scope] |
|
746 if scope >= 1 |
|
747 # dump global variables |
|
748 vlist = formatVariablesList(global_variables, binding_, scope, filter) |
|
749 elsif scope == 0 |
|
750 # dump local variables |
|
751 vlist = formatVariablesList(eval("local_variables", binding_), |
|
752 binding_, scope, filter) |
|
753 end |
|
754 varlist.concat(vlist) |
|
755 write("%s%s\n" % [ResponseVariables, varlist.inspect]) |
|
756 end |
|
757 |
|
758 def dumpVariable(var, frmnr, scope, filter) |
|
759 =begin edoc |
|
760 Private method to return the variables of a frame to the debug server. |
|
761 |
|
762 @param var list encoded name of the requested variable (list of strings) |
|
763 @param frmnr distance of frame reported on. 0 is the current frame (int) |
|
764 @param scope 1 to report global variables, 0 for local variables (int) |
|
765 @param filter the indices of variable types to be filtered (list of int) |
|
766 =end |
|
767 if $debugging |
|
768 binding_, file, line, id = DEBUGGER__.context(DEBUGGER__.last_thread)\ |
|
769 .get_frame(frmnr) |
|
770 else |
|
771 binding_ = @frames[frmnr] |
|
772 end |
|
773 varlist = [scope, var] |
|
774 i = 0 |
|
775 obj = nil |
|
776 keylist = nil |
|
777 formatSequences = false |
|
778 access = "" |
|
779 isDict = false |
|
780 |
|
781 while i < var.length |
|
782 if ["[]", "{}"].include?(var[i][-2..-1]) |
|
783 if i+1 == var.length |
|
784 keylist = [var[i][0..-3]] |
|
785 formatSequences = true |
|
786 if access.length == 0 |
|
787 access = "#{var[i][0..-3]}" |
|
788 else |
|
789 access << "[#{var[i][0..-3]}]" |
|
790 end |
|
791 if var[i][-2..-1] == "{}" |
|
792 isDict = true |
|
793 end |
|
794 break |
|
795 else |
|
796 if access.length == 0 |
|
797 access = "#{var[i][0..-3]}" |
|
798 else |
|
799 access << "[#{var[i][0..-3]}]" |
|
800 end |
|
801 end |
|
802 else |
|
803 if access.length != 0 |
|
804 access << "[#{var[i]}]" |
|
805 obj = eval(access, binding_) |
|
806 binding_ = obj.instance_eval{binding()} |
|
807 access = "" |
|
808 else |
|
809 obj = eval(var[i], binding_) |
|
810 binding_ = obj.instance_eval{binding()} |
|
811 end |
|
812 end |
|
813 i += 1 |
|
814 end |
|
815 if formatSequences |
|
816 bind = binding_ |
|
817 else |
|
818 bind = obj.instance_eval{binding()} |
|
819 end |
|
820 if not keylist |
|
821 keylist = obj.instance_variables |
|
822 access = nil |
|
823 else |
|
824 if access.length != 0 |
|
825 obj = eval(access, bind) |
|
826 end |
|
827 if isDict |
|
828 keylist = obj.keys() |
|
829 else |
|
830 keylist = Array.new(obj.length){|i| i} |
|
831 end |
|
832 end |
|
833 vlist = formatVariablesList(keylist, bind, scope, filter, |
|
834 excludeSelf=true, access=access) |
|
835 varlist.concat(vlist) |
|
836 write("%s%s\n" % [ResponseVariable, varlist.inspect]) |
|
837 end |
|
838 |
|
839 def formatVariablesList(keylist, binding_, scope, filter = [], |
|
840 excludeSelf = false, access = nil) |
|
841 =begin edoc |
|
842 Private method to produce a formated variables list. |
|
843 |
|
844 The binding passed in to it is scanned. Variables are |
|
845 only added to the list, if their type is not contained |
|
846 in the filter list and their name doesn't match any of the filter expressions. |
|
847 The formated variables list (a list of lists of 3 values) is returned. |
|
848 |
|
849 @param keylist keys of the dictionary |
|
850 @param binding_ the binding to be scanned |
|
851 @param scope 1 to filter using the globals filter, 0 using the locals filter (int). |
|
852 Variables are only added to the list, if their name do not match any of the |
|
853 filter expressions. |
|
854 @param filter the indices of variable types to be filtered. Variables are |
|
855 only added to the list, if their type is not contained in the filter |
|
856 list. |
|
857 @param excludeSelf flag indicating if the self object should be excluded from |
|
858 the listing (boolean) |
|
859 @param access String specifying the access path to (String) |
|
860 @return A list consisting of a list of formatted variables. Each variable |
|
861 entry is a list of three elements, the variable name, its type and |
|
862 value. |
|
863 =end |
|
864 varlist = [] |
|
865 if scope >= 1 |
|
866 patternFilterObjects = @globalsFilterObjects |
|
867 else |
|
868 patternFilterObjects = @localsFilterObjects |
|
869 begin |
|
870 obj = eval("self", binding_) |
|
871 rescue StandardError, ScriptError |
|
872 obj = nil |
|
873 end |
|
874 if not excludeSelf and obj.class != Object |
|
875 keylist << "self" |
|
876 end |
|
877 end |
|
878 |
|
879 keylist.each do |key| |
|
880 # filter based on the filter pattern |
|
881 matched = false |
|
882 patternFilterObjects.each do |pat| |
|
883 if pat.match(key) |
|
884 matched = true |
|
885 break |
|
886 end |
|
887 end |
|
888 next if matched |
|
889 |
|
890 begin |
|
891 if access |
|
892 if key.to_s == key |
|
893 key = "'%s'" % key |
|
894 else |
|
895 key = key.to_s |
|
896 end |
|
897 k = "#{access}[%s]" % key |
|
898 obj = eval(k, binding_) |
|
899 else |
|
900 obj = eval(key, binding_) |
|
901 end |
|
902 rescue NameError |
|
903 next |
|
904 end |
|
905 |
|
906 if obj or obj.class == NilClass or obj.class == FalseClass |
|
907 otype = obj.class.to_s |
|
908 if obj.inspect.nil? |
|
909 otype = "" |
|
910 oval = "" |
|
911 else |
|
912 oval = obj.inspect.gsub(/=>/,":") |
|
913 end |
|
914 else |
|
915 otype = "" |
|
916 oval = obj.inspect |
|
917 end |
|
918 |
|
919 next if inFilter?(filter, otype, oval) |
|
920 |
|
921 if oval.index("#<") == 0 |
|
922 addr = extractAddress(oval) |
|
923 oval = "<#{otype} object at #{addr}>" |
|
924 end |
|
925 if obj |
|
926 if obj.class == Array or obj.class == Hash |
|
927 oval = "%d" % obj.length() |
|
928 end |
|
929 end |
|
930 varlist << [key, otype, oval] |
|
931 end |
|
932 return varlist |
|
933 end |
|
934 |
|
935 def extractAddress(var) |
|
936 =begin edoc |
|
937 Private method to extract the address part of an object description. |
|
938 |
|
939 @param var object description (String) |
|
940 @return the address contained in the object description (String) |
|
941 =end |
|
942 m = var.match(/^#<.*?:([^:]*?) /) |
|
943 if m |
|
944 return m[1] |
|
945 else |
|
946 return "" |
|
947 end |
|
948 end |
|
949 |
|
950 def extractTypeAndAddress(var) |
|
951 =begin edoc |
|
952 Private method to extract the address and type parts of an object description. |
|
953 |
|
954 @param var object description (String) |
|
955 @return list containing the type and address contained in the object |
|
956 description (Array of two String) |
|
957 =end |
|
958 m = var.match(/^#<(.*?):(.*?) /) |
|
959 if m |
|
960 return [m[1], m[2]] |
|
961 else |
|
962 return ["", ""] |
|
963 end |
|
964 end |
|
965 |
|
966 def inFilter?(filter, otype, oval) |
|
967 =begin edoc |
|
968 Private method to check, if a variable is to be filtered based on its type. |
|
969 |
|
970 @param filter the indices of variable types to be filtered (Array of int. |
|
971 @param otype type of the variable to be checked (String) |
|
972 @param oval variable value to be checked (String) |
|
973 @return flag indicating, whether the variable should be filtered (boolean) |
|
974 =end |
|
975 cindex = ConfigVarTypeStrings.index(otype) |
|
976 if cindex == nil |
|
977 if oval.index("#<") == 0 |
|
978 if filter.include?(ConfigVarTypeStrings.index("instance")) |
|
979 return true |
|
980 else |
|
981 return false |
|
982 end |
|
983 elsif ['FalseClass', 'TrueClass'].include?(otype) |
|
984 if filter.include?(ConfigVarTypeStrings.index("bool")) |
|
985 return true |
|
986 else |
|
987 return false |
|
988 end |
|
989 else |
|
990 if filter.include?(ConfigVarTypeStrings.index("other")) |
|
991 return true |
|
992 else |
|
993 return false |
|
994 end |
|
995 end |
|
996 end |
|
997 if filter.include?(cindex) |
|
998 return true |
|
999 end |
|
1000 return false |
|
1001 end |
|
1002 |
|
1003 def generateFilterObjects(scope, filterString) |
|
1004 =begin edoc |
|
1005 Private method to convert a filter string to a list of filter objects. |
|
1006 |
|
1007 @param scope 1 to generate filter for global variables, 0 for local variables (int) |
|
1008 @param filterString string of filter patterns separated by ';' |
|
1009 =end |
|
1010 patternFilterObjects = [] |
|
1011 for pattern in filterString.split(';') |
|
1012 patternFilterObjects << Regexp.compile('^%s$' % pattern) |
|
1013 end |
|
1014 if scope == 1 |
|
1015 @globalsFilterObjects = patternFilterObjects[0..-1] |
|
1016 else |
|
1017 @localsFilterObjects = patternFilterObjects[0..-1] |
|
1018 end |
|
1019 end |
|
1020 |
|
1021 def completionList(text) |
|
1022 =begin edoc |
|
1023 Method used to handle the command completion request |
|
1024 |
|
1025 @param text the text to be completed (string) |
|
1026 =end |
|
1027 completions = @completer.complete(text).compact.sort.uniq |
|
1028 write("%s%s||%s\n" % [ResponseCompletion, completions.inspect, text]) |
|
1029 end |
|
1030 |
|
1031 def startProgInDebugger(progargs, wd = '', host = nil, |
|
1032 port = nil, exceptions = true, traceRuby = false, redirect=true) |
|
1033 =begin edoc |
|
1034 Method used to start the remote debugger. |
|
1035 |
|
1036 @param progargs commandline for the program to be debugged |
|
1037 (list of strings) |
|
1038 @param wd working directory for the program execution (string) |
|
1039 @param host hostname of the debug server (string) |
|
1040 @param port portnumber of the debug server (int) |
|
1041 @param exceptions flag to enable exception reporting of the IDE (boolean) |
|
1042 @param traceRuby flag to enable tracing into the Ruby library |
|
1043 @param redirect flag indicating redirection of stdin, stdout and stderr (boolean) |
|
1044 =end |
|
1045 $debugging = true |
|
1046 |
|
1047 if host == nil |
|
1048 host = ENV.fetch('ERICHOST', 'localhost') |
|
1049 end |
|
1050 if port == nil |
|
1051 port = ENV.fetch('ERICPORT', 42424) |
|
1052 end |
|
1053 |
|
1054 connectDebugger(port, host, redirect) #TCPSocket.gethostbyname(host)[3]) |
|
1055 DEBUGGER__.attach(self) |
|
1056 |
|
1057 @traceRuby = traceRuby |
|
1058 |
|
1059 fn = progargs.shift |
|
1060 fn = File.expand_path(fn) |
|
1061 |
|
1062 ARGV.clear() |
|
1063 ARGV.concat(progargs) |
|
1064 $:.insert(0, File.dirname(fn)) |
|
1065 if wd == '' |
|
1066 Dir.chdir($:[0]) |
|
1067 else |
|
1068 Dir.chdir(wd) |
|
1069 end |
|
1070 @running = fn |
|
1071 |
|
1072 @passive = true |
|
1073 write("%s%s|%d\n" % [PassiveStartup, @running, exceptions ? 1 : 0]) |
|
1074 interact() |
|
1075 |
|
1076 command = "$0 = '%s'; require '%s'" % [fn, fn] |
|
1077 set_trace_func proc { |event, file, line, id, binding_, klass, *rest| |
|
1078 DEBUGGER__.context.trace_func(event, file, line, id, binding_, klass) |
|
1079 } |
|
1080 begin |
|
1081 eval(command, @debugBinding) |
|
1082 rescue DebugQuit |
|
1083 # just ignore it |
|
1084 rescue Exception => exc |
|
1085 unhandled_exception(exc) |
|
1086 ensure |
|
1087 set_trace_func(nil) |
|
1088 @running = nil |
|
1089 end |
|
1090 # just a short delay because client might shut down too quickly otherwise |
|
1091 sleep(1) |
|
1092 end |
|
1093 |
|
1094 def main |
|
1095 =begin edoc |
|
1096 Public method implementing the main method. |
|
1097 =end |
|
1098 if ARGV.include?('--') |
|
1099 args = ARGV[0..-1] |
|
1100 host = nil |
|
1101 port = nil |
|
1102 wd = '' |
|
1103 traceRuby = false |
|
1104 exceptions = true |
|
1105 redirect = true |
|
1106 while args[0] |
|
1107 if args[0] == '-h' |
|
1108 host = args[1] |
|
1109 args.shift |
|
1110 args.shift |
|
1111 elsif args[0] == '-p' |
|
1112 port = args[1] |
|
1113 args.shift |
|
1114 args.shift |
|
1115 elsif args[0] == '-w' |
|
1116 wd = args[1] |
|
1117 args.shift |
|
1118 args.shift |
|
1119 elsif args[0] == '-t' |
|
1120 traceRuby = true |
|
1121 args.shift |
|
1122 elsif args[0] == '-e' |
|
1123 exceptions = false |
|
1124 args.shift |
|
1125 elsif args[0] == '-n' |
|
1126 redirect = false |
|
1127 args.shift |
|
1128 elsif args[0] == '--' |
|
1129 args.shift |
|
1130 break |
|
1131 else # unknown option |
|
1132 args.shift |
|
1133 end |
|
1134 end |
|
1135 if args.length == 0 |
|
1136 STDOUT << "No program given. Aborting!" |
|
1137 else |
|
1138 startProgInDebugger(args, wd, host, port, |
|
1139 exceptions = exceptions, traceRuby = traceRuby, |
|
1140 redirect = redirect) |
|
1141 end |
|
1142 else |
|
1143 if ARGV[0] == '--no-encoding' |
|
1144 # just ignore it, it's here to be compatible with python debugger |
|
1145 ARGV.shift |
|
1146 end |
|
1147 |
|
1148 begin |
|
1149 port = ARGV[0].to_i |
|
1150 rescue |
|
1151 port = -1 |
|
1152 end |
|
1153 |
|
1154 begin |
|
1155 redirect = ARGV[1].to_i |
|
1156 redirect = redirect == 1 ? true : false |
|
1157 rescue |
|
1158 redirect = true |
|
1159 end |
|
1160 |
|
1161 begin |
|
1162 remoteAddress = ARGV[2] |
|
1163 rescue |
|
1164 remoteAddress = nil |
|
1165 end |
|
1166 |
|
1167 ARGV.clear() |
|
1168 $:.insert(0,"") |
|
1169 if port >= 0 |
|
1170 connectDebugger(port, remoteAddress, redirect) |
|
1171 DEBUGGER__.attach(self) |
|
1172 interact() |
|
1173 end |
|
1174 end |
|
1175 end |
|
1176 end |