Skip to content

Commit

Permalink
Added support for running the Ruby debugger (FATE#318421)
Browse files Browse the repository at this point in the history
- Allow running the Ruby debugger from the generic crash handler
  if the debugger is installed

- 3.1.47
  • Loading branch information
lslezak committed May 23, 2016
1 parent 3d5dcac commit c59d400
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .rubocop.yml
Expand Up @@ -67,3 +67,8 @@ Style/SingleLineBlockParams:
# Offense count: 1 (@__last_exception)
Style/TrivialAccessors:
Enabled: false

# the debugger invocation is deliberate here, it's not a forgotten leftover...
Lint/Debugger:
Exclude:
- src/ruby/yast/debugger.rb
8 changes: 8 additions & 0 deletions package/yast2-ruby-bindings.changes
@@ -1,3 +1,11 @@
-------------------------------------------------------------------
Mon May 23 12:30:17 UTC 2016 - lslezak@suse.cz

- Added support for running the Ruby debugger (FATE#318421)
- Allow running the Ruby debugger from the generic crash handler
if the debugger is installed
- 3.1.47

-------------------------------------------------------------------
Mon Mar 7 16:12:00 UTC 2016 - jreidinger@suse.com

Expand Down
2 changes: 1 addition & 1 deletion package/yast2-ruby-bindings.spec
Expand Up @@ -17,7 +17,7 @@


Name: yast2-ruby-bindings
Version: 3.1.46
Version: 3.1.47
Url: https://github.com/yast/yast-ruby-bindings
Release: 0
BuildRoot: %{_tmppath}/%{name}-%{version}-build
Expand Down
157 changes: 157 additions & 0 deletions src/ruby/yast/debugger.rb
@@ -0,0 +1,157 @@

require "yast"

module Yast
class Debugger
class << self
include Yast::Logger
include Yast::UIShortcuts

# Start the Ruby debugger. It handles the current UI mode and displays
# an user request if the debugger front-end needs to be started manually.
# @param [Boolean] remote if set to true the server is accesible from network.
# By default the debugger can connect only from the local machine, not from
# the network. If you need remote debugging then enable it.
# WARNING: There is no authentication, everybody can connect to
# the debugger! Use only in a trusted network as this is actually
# a backdoor to the system! For secure connection use SSH and start
# the debugger locally after connecting via SSH.
# @param [Fixnum] port the port number where the debugger server will
# listen to
# @param [Boolean] start_client autostart the debugger client
# (ignored in remote debugging)
# @example Start the debugger with default settings:
# require "yast/debugger"
# Yast::Debugger.start
# @example When using the debugger temporary you can use just simple:
# require "byebug"
# byebug
def start(remote: false, port: 3344, start_client: true)
begin
require "byebug"
require "byebug/core"
rescue LoadError
# catch loading error, the debugger is optional (might not be present)
Yast.import "Report"
Report.Error(format("Cannot load the Ruby debugger.\n" \
"Make sure '%s' Ruby gem is installed.") % "byebug")
return
end

popup = false

# do not start the server if it is already running
if Byebug.started?
log.warn "The debugger is already running at port #{Byebug.actual_port}"
log.warn "Skipping the server setup"
else
Yast.import "UI"
wait = false
if UI.TextMode || remote || !start_client
# in textmode or in remote mode ask the user to start
# the debugger client manually
UI.OpenDialog(Label(debugger_message(remote, port)))
popup = true
else
# in GUI open an xterm session with the debugger
start_gui_session(port)
# wait a bit to get the server ready (to avoid "Broken pipe" error)
# FIXME: looks like a race condition inside byebug itself...
wait = true
end

# start the server and wait for connection
start_server(remote, port, wait)

UI.CloseDialog if popup
end

# start the debugger session
byebug
# Now you can inspect the current state in the debugger,
# or use "next" to continue.
# Use "help" command to see the available commands, see more at
# https://github.com/deivid-rodriguez/byebug/blob/master/GUIDE.md
end

# is the Ruby debugger installed and can be loaded?
# @return [Boolean] true if the debugger is present
def installed?
require "byebug"
true
rescue LoadError
false
end

private

# starts the debugger server and waits for a cleint connection
# @param [Boolean] remote if set to true the server is accesible from network
# @param [Fixnum] port the port number used by the server
def start_server(remote, port, wait)
Byebug.wait_connection = true
host = remote ? "0.0.0.0" : "localhost"
log.info "Starting debugger server (#{host}:#{port}), waiting for connection..."
Byebug.start_server(host, port)
sleep(3) if wait
end

# starts a debugger session in xterm
# @param [Fixnum] port the port number to connect to
def start_gui_session(port)
job = fork do
# wait until the main thread starts the debugger and opens the port
# for listening
loop do
break if port_open?(port)
sleep(1)
end

# start the debugger client in an xterm session
exec "xterm", "-e", "byebug", "-R", "#{port}"
end

# detach the process, we do not wait for it so avoid zombies
Process.detach(job)
end

# compose the popup message describing how to manually connect to
# the running debugger
# @return [String] text
def debugger_message(remote, port)
waiting = "Waiting for the connection..."
if remote
# get the local IP addresses
require "socket"
remote_ips = Socket.ip_address_list.select { |a| a.ipv4? && !a.ipv4_loopback? }
cmds = remote_ips.map { |a| debugger_cmd(a.ip_address, port) }.join("\n")

"To connect to the debugger from a remote machine use this command:" \
"\n\n#{cmds}\n\n#{waiting}"
else
"To start the debugger switch to another console and run\n\n" \
"#{debugger_cmd(nil, port)}\n\n#{waiting}"
end
end

def debugger_cmd(host, port)
host_param = host ? "#{host}:" : ""
" byebug -R #{host_param}#{port}"
end

# is the target port open?
# @param [Fixnum] port the port number
# @return [Boolean] true if the port is open, false otherwise
def port_open?(port)
require "socket"

begin
TCPSocket.new("localhost", port).close
true
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
false
end
end
end
end
end
39 changes: 35 additions & 4 deletions src/ruby/yast/wfm.rb
Expand Up @@ -181,6 +181,21 @@ def self.call_builtin_wrapper(*args)
call_builtin(Regexp.last_match(1), Regexp.last_match(2).to_i, *args)
end

def self.ask_to_run_debugger?
Yast.import "Mode"
require "yast/debugger"

!Mode.auto && Debugger.installed?
end

# @param [Exception] e the caught exception
# @return [String] human readable exception description
def self.internal_error_msg(e)
"Internal error. Please report a bug report with logs.\n" \
"Details: #{e.message}\n" \
"Caller: #{e.backtrace.first}"
end

# @private wrapper to run client in ruby
def self.run_client(client)
Builtins.y2milestone "Call client %1", client
Expand All @@ -203,10 +218,26 @@ def self.run_client(client)
e.message,
e.backtrace
)
Yast.import "Report"
Report.Error "Internal error. Please report a bug report with logs.\n" \
"Details: #{e.message}\n" \
"Caller: #{e.backtrace.first}"

msg = internal_error_msg(e)

if ask_to_run_debugger?
log.info "Ruby debugger can be used for debugging"
Yast.import "Popup"
msg += "\n\nDo you want to start the Ruby debugger now\n" \
"and manually debug the issue?"

if Popup.YesNo(msg)
Debugger.start
# Now you can restart the client and watch it step-by-step with
# "next"/"step" commands or you can add some breakpoints into
# the code and use "continue".
retry
end
else
Yast.import "Report"
Report.Error(msg)
end
rescue Exception => e
Builtins.y2internal("Error reporting failed with '%1' and backtrace %2",
e.message,
Expand Down

0 comments on commit c59d400

Please sign in to comment.