# Purpose: Setup and initialize the FR Debugger
#
# $Id: debugger.rb,v 1.33 2006/05/25 07:42:33 ljulliar Exp $
#
# Authors: Laurent Julliard <laurent AT moldus DOT org>
# Contributors:
#
# This file is part of the FreeRIDE project
#
# This application is free software; you can redistribute it and/or
# modify it under the terms of the Ruby license defined in the
# COPYING file.
#
# Copyright (c) 2002 Laurent Julliard. All rights reserved.
#
require 'drb'
require 'singleton'
require 'rubyide_tools_debugger/breakpoint'
require 'tempfile'
# if RUBY_PLATFORM =~ /(mswin32|mingw32)/
# require 'win32/process'
# require 'win32/open3'
# else
# require 'open3'
# end
module FreeRIDE; module GUI
DEBUG = false
##
# This module defines the FreeRIDE Debugger
#
class Debugger < Component
extend FreeBASE::StandardPlugin
def self.start(plugin)
# Manage the Debuggers in a pool. Although there can be only one
# debugger session active right now may be several will be allowed in the
# future
base_slot = plugin["/system/ui/components/Debugger"]
ComponentManager.new(plugin, base_slot, Debugger, 1)
# Create the Debug menu item and associate a command with it
# When the command is invoked create a new debugger session
# unless there is one already and start it
cmd_mgr = plugin['/system/ui/commands'].manager
cmd = cmd_mgr.add("App/Run/Debugger", "&Debugger") do |cmd_slot|
if plugin['/system/ui/current/Debugger'].is_link_slot?
debugger = plugin['/system/ui/current/Debugger']
if debugger.manager.running?
debugger.manager.show
else
debugger.manager.start
end
else
debugger = base_slot.manager.add
debugger.manager.start
end
end
cmd.availability = plugin['/system/ui/current'].has_child?('EditPane')
cmd.manage_availability do |command|
plugin['/system/ui/current'].subscribe do |event, slot|
if slot.name=="EditPane"
case event
when :notify_slot_link
command.availability=true
when :notify_slot_unlink
command.availability=false
end
end
end
end
# Insert the debugger menu item in the run menu and bind it
# to the F10 key
runmenu = plugin["/system/ui/components/MenuPane/Run_menu"].manager
runmenu.add_command("App/Run/Debugger")
key_mgr = plugin['/system/ui/keys'].manager
key_mgr.bind("/App/Run/Debugger", :F10)
# Now only is the plugin running
plugin.transition(FreeBASE::RUNNING)
end
include DRbUndumped
attr_reader :slot, :running, :debuggee, :plugin
##
# Instantiate a new debugger session . Only one session at a time for now
# Do not start it yet (see start)
#
def initialize(plugin, base_slot)
setup(plugin, base_slot)
@cmd_mgr = plugin["/system/ui/commands"].manager
@plugin['/system/ui/current'].link('Debugger',base_slot)
@action_queue = Array.new # must be initialized before starting
@plugin.log_info << "Debugger session created #{base_slot.path}"
end
##
# Prompt a message in the status bar
#
def status(msg)
@plugin['/system/ui/current/StatusBar/actions/prompt'].invoke(msg)
end
##
# Actually start the debugger session. Run the remote debugger,
# open a pipe for debugger command,....
#
def start
# if not the current Editpane is not a real edit pane then do nothing
return unless @plugin['/system/ui/current/EditPane'].managed?
# Check if the file is modified. if so must save it before debugging
ep = @plugin['/system/ui/current/EditPane']
file = ep.manager.filename
if @plugin.properties['save_before_running']
@cmd_mgr.command("App/File/SaveAll").invoke
else
if ep.manager.modified? || ep.manager.new?
answer = @cmd_mgr.command("App/Services/YesNoDialog").invoke("Save Changes before debug...", "You must first save the file before running the debugger. Save changes to '#{file}'?")
return unless answer == 'yes'
ep.manager.save
end
end
# update file name in case it was changed by a 'save as...'
file = ep.manager.filename
# see if we need to show the dialog box with configuration panel
if @plugin.properties['config_before_running']
# not implemented yet @actions['show_config'].invoke
end
# all ok show the debugger
show
# initialize variables
@loaded_files = Hash.new
@running = false
@action_queue = Array.new
# open the debugger command pipe
#open_pipe()
# start the remote debugger
# FIXME: in the future we should create "debugging profiles" where a user can
# indicate in a dialog box what ruby interpreter to use, what include dir, what module
# to include, command line arguments...
debuggee_file = File.join("#{@plugin.plugin_configuration.full_base_path}","debuggee.rb")
drb_file = File.join(@plugin['/system/properties/config/codebase'].data,'redist','drb','drb')
drb_file = 'drb'
ruby_path = @plugin.properties['path_to_ruby']
ruby_path = 'ruby' if (ruby_path == '' || ruby_path.nil?)
exec_dir = @plugin.properties['working_dir']
exec_dir = File.dirname(file) if (exec_dir == '' || exec_dir.nil?)
tmpfile = Tempfile.new("fr_dbg_"); tmpfile.close
exec_options = @plugin.properties['cmd_line_options']
exec_options = '' if (exec_options.nil?)
exec_args = exec_options + " #{tmpfile.path}"
# before running the command make sure that the ruby
# interpreter is here or ask the user to specify a path
unless FileTest.exist?(ruby_path)
@cmd_mgr.command('App/Services/MessageBox').invoke("Where is Ruby?",
"I can't find the default Ruby interpreter. Please configure the path to ruby in the Debugger/Run preference box")
end
command = "#{ruby_path} -C \"#{exec_dir}\" -r \"#{drb_file}\" -r \"#{debuggee_file}\" \"#{file}\" #{exec_args}"
if @plugin.properties['run_in_terminal']
if RUBY_PLATFORM =~ /(mswin32|mingw32)/
command = "start CMD /K "+command
else
command = "xterm -e "+command
end
end
#puts "command: #{command}"
@plugin.log_info << "Running debugger command: #{command}"
# On Windows build the popen3 method doesn't work because
# the fork() is not supported
#if RUBY_PLATFORM =~ /(mswin32|mingw32)/
@inp = @out = IO.popen(command,"w+")
@err = nil
#else
#@inp, @out, @err = Open3.popen3(command)
#end
begin
require 'timeout'
Timeout.timeout(5) {
while File.stat(tmpfile.path).zero?
sleep 0.1
end
}
# fetch Drb URI and process ID from temp file
tmpfile.open
debugUri, pid = tmpfile.gets.chomp.split(",")
tmpfile.close(true)
# connect to remote process
@debugSvr = DRb.start_service()
@debuggee = DRbObject.new(nil, debugUri)
debuggeeId = @debuggee.attach(self)
rescue
status("Debugger process aborted!!")
@cmd_mgr.command('App/Services/MessageBox').invoke('FATAL ERROR!', "#{$!} / Unexpected Error while launching the remote Ruby process with #{command}")
return
end
@pid = pid.to_i
if @plugin.properties['run_in_terminal']
@term_pid = @out.pid
end
@plugin.log_info << "Remote Debugger Started at URI : #{debugUri}, process id #{@pid}"
#puts "pid = #{@pid}, term_pid = #{@term_pid}"
# if the process was run through a terminal then the child pid is
# the one of the terminal not the one of the ruby process
t = Thread.new(@term_pid.nil? ? @pid : @term_pid, self) { |pid, dbg|
Process.waitpid(pid)
stop
}
trap( "INT" ) do
self.pause
#puts "Trap INT"
#@debuggee.signal( "INT" )
# @debugSvr.thread.join
end
@paused = true
# create the watchpoint and breakpoint manager (only after
# debuggee is running)
@brk_mgr = BreakpointManager.new(self)
@watch_mgr = WatchpointManager.new(self)
# attach process stdout and err to text console
@actions['attach_stderr'].invoke(@err) unless @err.nil?
@actions['attach_stdout'].invoke(@out)
@actions['attach_stdin'].invoke(@inp)
# we subscribe to the Edit Pane pool so that whenever a new edit pane is
# created we subscribe to the 'breakpoints' sub slot. This is where edit panes
# breakpoints addition/deletion events are posted. When these events
# are received we can act accordingly on the remote debugger
@plugin['/system/ui/components/EditPane'].subscribe { |event, slot|
if (event == :notify_slot_add && slot.parent.name == 'EditPane')
@brk_mgr.subscribe(slot)
end
}
# make sure that we also subscribe to existing edit panes if any
# also clear any error marker that may have been created in a previous
# session
@plugin['/system/ui/components/EditPane'].each_slot { |slot|
@brk_mgr.subscribe(slot)
slot['actions/clear_errorline'].invoke
}
# start GUI
@actions['start'].invoke
# get the list of watches memorized in the GUI and set them up
gui_idx=0
@actions['list_watchpoints'].invoke().each do |expr|
@watch_mgr.add(expr,gui_idx)
gui_idx = gui_idx.succ
end
# Warning about ouput bug
if !@plugin.properties['run_in_terminal'] && RUBY_PLATFORM =~ /(mswin32|mingw32)/
@actions['print_stderr'].invoke("*** WARNING *** Windows users should check the \"Run process in terminal\" check box in the Debugger Preferences\nto see STDOUT and STDERR output.\n")
end
# All good now!
@running = true
@plugin.log_info << "Debugger session started #{@base_slot.path}"
status("Debugger process started (#{debugUri}, process id #{@pid})")
end
##
# Stop the debugger session
#
def stop()
return unless @running
pause
@actions['detach_stderr'].invoke(@err) if @err
@actions['detach_stdout'].invoke(@out)
@actions['detach_stdin'].invoke(@inp)
show_debugline(@file,nil)
@brk_mgr.unsubscribe_all()
reset_loaded_file()
@running = false
@debuggee = SilentDebuggee.instance
@plugin.log_info << "Debugger session stopped #{@base_slot.path}"
status("Ruby Process Stopped (PID = #{@pid})")
end
##
# Show the line in the file the debugger is currently pointing to
# open the file if not already loaded in one of the Edit panes.
# If line is nil it removes the line marker, If file is nil do nothing
# If error is true then show the error marker on the line
#
def show_debugline(file,line,error=false)
return if file.nil?
ep_slot = @cmd_mgr.command("EditPane/FindFile").invoke(file)
if ep_slot.nil?
ep_slot = @cmd_mgr.command("App/File/Load").invoke(file)
end
ep_slot['actions/make_current'].invoke
if error
ep_slot['actions/show_errorline'].invoke(line)
else
ep_slot['actions/show_debugline'].invoke(line)
end
end
##
# Clear the highlighted line the debugger is currently pointing
# If error is true then clear the error marker
#
def clear_debugline(file, error=false)
show_debugline(file,nil,error)
end
##
# Return the line number (starting at 1) the cursor is on
# in the current edit pane
#
def cursor_line()
ep_slot = @plugin['/system/ui/current/EditPane']
line = ep_slot['actions/get_cursor_line'].invoke
return line
end
##
# add a file to the list of files loaded by the debugger
# subscribe to the corresponding edit pane breakpoints
# slot to be sure that we update the breakpoint
#
def add_loaded_file (file)
@loaded_files[file] = true
end
##
# check whether a given file is already loaded in the debugger are stored
#
def check_loaded_file(file)
@loaded_files.has_key? file
end
##
# Reset the list of files loaded in the debugger to empty
#
def reset_loaded_file
@loaded_files = Hash.new
end
##
# add a watch point
#
def add_watchpoint(expr, gui_idx)
@watch_mgr.add(expr,gui_idx)
end
##
# Delete a watch point
#
def delete_watchpoint(expr,gui_idx)
@watch_mgr.delete(expr,gui_idx)
end
##
# Run debugger up to where the cursor is. The only way to do this is
# to place a temporary breakpoint on the targeted line. Using "next nnn"
# command is not possible because it count down only executable lines
#
def run_to_cursor
line = cursor_line()
return if line == @line
file = @plugin['/system/ui/current/EditPane'].data
@brk_mgr.add(file, line, true)
send_command('cont')
end
##
# Make the edit pane and line where the execution point
# is visible
#
def show_exec_point
show_debugline(@file,@line)
end
##
# Pause the remote debugger
#
def pause
return if @paused
# - does not work when on a endless loop that is one line of Ruby
#@debuggee.signal("INT")
# catch exception in case process already killed
begin
if RUBY_PLATFORM =~ /(mswin32|mingw32)/
Process.kill(-2, @pid)
else
Process.kill("INT", @pid)
end
rescue
puts "Exception raised while sending KILLINT to process #{@pid}\n#{$!}"
end
@paused = true
end
##
# Resume the remote debugger
#
def resume
@paused = false
clear_debugline(@file) # clear debug line...
clear_debugline(@file,true) # ...and error line marker
send_command('cont')
end
##
# Check if the remote debugger session is paused waiting
# for a new command
#
# Return:: [Boolean] true if it debugger paused
#
def paused?
@paused
end
##
# Check if the debugger session is running
#
# Return:: [Boolean] true if it's running
#
def running?
running
end
##
# Show the debugger. Actually relay to the renderer
#
# Return:: none
#
def show
@actions['show'].invoke
end
##
# Clear the console ouput
def clear
@actions['clear'].invoke
end
##
# send a command to the remote debugger
def send_command(cmd)
@plugin.log_info << "Debug command: #{cmd}"
@action_queue.push(cmd)
@t.run if @t && @t.status
end
##
# Originally called by the remote debugger to print the debugger prompt and
# wait for the next end user command typed on the keyboard. In the FreeRIDE
# version it simply waits for the next debugger command
#
def prompt( str )
@paused = true
update_thread_list()
update_frame_list()
update_local_variables()
update_global_variables()
# The pipe approach doesn't work on Windows, so use the
# more portable Thread approach and suspend the Drb sub thread
# It'll be awaken by the send_command method
#
@t = Thread.current
Thread.stop if @action_queue.empty?
@action = @action_queue.pop
# some special cases that are not debugger command
case @action
when 'CLOSE'
@cmd = 'quit'
else
@cmd = @action
end
# @plugin.log_info << "Sending command to debugger: #{@cmd}"
@paused = false
return @cmd+"\n"
end
##
# Display Exception stack trace. This is called from the debuggee
# whenever an exception is caught
#
def printf_excn(excn_trace, ignored)
# TODO: In the future we should do some more clever things
# like allowing the user to click on each line of the stack trace
# and follow the exception history in the various files
# Display as if it was stderr
if ignored
@actions['print_stderr'].invoke(excn_trace[0].chomp+" (ignored by debugger)")
else
if excn_trace[0] =~ /(.*):(\d+):/ && !ignored
prev_file = @file
@file = $1
@line = $2.to_i
puts "File: #{@file}, line: #{@line}" if DEBUG
clear_debugline(prev_file) unless prev_file == @file
show_debugline(@file, @line, true)
# first time we are stopping in this file? Then keep track of it
add_loaded_file(@file) unless check_loaded_file(@file)
end
@actions['print_stderr'].invoke("*** Exception caught by debugger ***\n"+excn_trace.join)
end
end
##
# This is called from the debuggee whenever a breakpoint
# has been reached
#
def printf_breakpoint(idx, method, file, line)
brkpt = @brk_mgr[idx]
# if breakpoint is unknown to the breakpoint manager then it
# is a temporary breakpoint
if brkpt.nil?
status("Breakpoint reached at cursor")
else
status("Breakpoint reached at #{File.basename(brkpt.file)}, line #{brkpt.line}")
end
end
##
# This is called from the debuggee whenever a watchpoint
# has been reached
#
def printf_watchpoint(idx, method, file, line)
expr = @watch_mgr[idx].expr
status("Watchpoint reached at #{File.basename(file)}, line #{line} (#{expr})")
end
##
# This is called when the debugger says on which line it is (happens
# at each step and when the debugger is pausing
#
def printf_line(file, line)
prev_file = @file
@file = file
@line = line
puts "File: #{@file}, line: #{@line}" if DEBUG
clear_debugline(prev_file) unless prev_file == @file
show_debugline(@file, @line, false)
# first time we are stopping in this file? Then keep track of it
add_loaded_file(@file) unless check_loaded_file(@file)
end
def printf( *args )
# See debugger output
stdout.print "DBG>> " if DEBUG
stdout.printf( *args ) if DEBUG
end
def print( *args )
# See debugger output
stdout.print "DBG>> " if DEBUG
stdout.printf( *args ) if DEBUG
end
##
# Inform the debugger that the debuggee has just loaded a new file
# This method is called from the debuggee process
# For now we just set up breakpoints associated with this file
#
def file_loaded(file)
@brk_mgr.set_all(file)
end
##
# Update the local variable list and ask the renderer to reflect this in the UI
#
def update_local_variables()
lv_info = @debuggee.fr_local_variables()
@actions['update_local_var_list'].invoke(lv_info)
end
##
# Update the global variable list and ask the renderer to reflect this in the UI
#
def update_global_variables()
gv_info = @debuggee.fr_global_variables()
@actions['update_global_var_list'].invoke(gv_info)
end
def show_thread_list()
th_info = @debuggee.fr_thread_list_all()
end
##
# Update the thread list and ask the renderer to reflect this in the UI
#
def update_thread_list()
th_info = @debuggee.fr_thread_list_all()
@actions['update_thread_list'].invoke(th_info)
end
##
# Select a given thread in the debugged process
#
def select_thread(th_info)
#thread_no = @debuggee.fr_select_thread(th_info[0])
send_command("th switch #{th_info[0]}")
#STDERR.puts "ERROR!!! selected thread #{th_info[0]} could not be selected. Now #{thread_no}" if th_info[0] != thread_no
# after a thread change we must update the frame info
update_frame_list()
end
##
# Format the thread info in a string (used by the renderer among other things)
#
def format_thread(th_info)
"#{th_info[0]}- #{th_info[1]} #{th_info[3]} #{th_info[4]} #{File.basename(th_info[5])}:#{th_info[6]}"
end
##
# Update the frame list and ask the renderer to reflect this in the UI
#
def update_frame_list()
@fr_list = @debuggee.fr_frame_list_all()
@actions['update_frame_list'].invoke(@fr_list)
end
##
# Select a given frame in the debugged process
#
def select_frame(fr_info)
level = @debuggee.fr_select_frame(fr_info[0])
if fr_info[0] != level
error_msg = "ERROR!!! selected frame level #{fr_info[0]} could not be selected. Now #{level}"
@plugin.log_error << error_msg
else
@file = fr_info[1]
@line = fr_info[2].to_i
puts "File: #{@file}, line: #{@line}" if DEBUG
show_debugline(@file, @line)
# first time we are stopping in this file? Then keep track of it
add_loaded_file(@file) unless check_loaded_file(@file)
# update the local variables view
update_local_variables()
end
end
##
# Format the frame info in a string (used by the renderer among other things)
#
def format_frame(fr_info)
"#{fr_info[0]}- #{File.basename(fr_info[1])}:#{fr_info[2]} - #{fr_info[3]}"
end
##
# Evaluate an expression in the current context.
#
# Ouput: the inspected value (not the value itself)
def eval_expr(expr)
# rk: remove the leading and trailing quote
expr = @debuggee.fr_eval_expr(expr)
expr ? expr[1..-2] : "nil"
end
##
# Called from the remote debugger after a quit command
# has been received
#
def quit
# before stopping the Drb server make sure the Drb thread
# in charge of conveying the quit call from the remote debugger
# has its job done
Thread.new( Thread.current ) do | th |
while th.status == "run"
Thread.pass
end
stop()
#close() if @action == 'CLOSE'
# do this last because it kills the thread it runs in!!
@debugSvr.stop_service
@debuggee = SilentDebuggee.instance
end
end
private
def stdout
STDOUT
end
end # class Debugger
##
# Silent Debuggee class used whenever the
# debuggee process is stopped and we still want to
# avoid exception if a remote method is called
class SilentDebuggee
include Singleton
def fr_eval_expr(expr)
return '"can\'t eval - process stopped"'
end
def method_missing(method_id, *args)
# capture all the missing methods and do nothing
end
end
end; end
syntax highlighted by Code2HTML, v. 0.9.1