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