diff --git a/exe/3scale b/exe/3scale index 9b98c989..168aebbb 100755 --- a/exe/3scale +++ b/exe/3scale @@ -4,4 +4,4 @@ require '3scale_toolbox' args = ARGV.clone -ThreeScaleToolbox::CLI.run args +exit(ThreeScaleToolbox::CLI.run(args)) diff --git a/lib/3scale_toolbox/cli.rb b/lib/3scale_toolbox/cli.rb index 375d974b..cec05f8d 100644 --- a/lib/3scale_toolbox/cli.rb +++ b/lib/3scale_toolbox/cli.rb @@ -1,3 +1,5 @@ +require '3scale_toolbox/cli/error_handler' + module ThreeScaleToolbox::CLI def self.root_command ThreeScaleToolbox::Commands::ThreeScaleCommand @@ -11,10 +13,31 @@ def self.load_builtin_commands ThreeScaleToolbox::Commands::BUILTIN_COMMANDS.each(&method(:add_command)) end + def self.install_signal_handlers + # Set exit handler + %w[INT TERM].each do |signal| + Signal.trap(signal) do + puts + exit!(0) + end + end + + # Set stack trace dump handler + if !defined?(RUBY_ENGINE) || RUBY_ENGINE != 'jruby' + Signal.trap('USR1') do + puts 'Caught USR1; dumping a stack trace' + puts caller.map { |i| " #{i}" }.join("\n") + end + end + end + def self.run(args) - load_builtin_commands - ThreeScaleToolbox.load_plugins - # TODO handle error - root_command.build_command.run args + install_signal_handlers + err = ErrorHandler.error_watchdog do + load_builtin_commands + ThreeScaleToolbox.load_plugins + root_command.build_command.run args + end + err.nil? ? 0 : 1 end end diff --git a/lib/3scale_toolbox/cli/error_handler.rb b/lib/3scale_toolbox/cli/error_handler.rb new file mode 100644 index 00000000..6bbddc46 --- /dev/null +++ b/lib/3scale_toolbox/cli/error_handler.rb @@ -0,0 +1,120 @@ +require 'json' + +module ThreeScaleToolbox + module CLI + class ErrorHandler + def self.error_watchdog + new.error_watchdog { yield } + end + + # Catches errors and prints nice diagnostic messages + def error_watchdog + # Run + yield + rescue StandardError, ScriptError => e + handle_error e + e + else + nil + end + + private + + def handle_error(error) + if expected_error?(error) + warn + warn "\e[1m\e[31mError: #{error.message}\e[0m" + else + print_error(error) + end + end + + def expected_error?(error) + case error + when ThreeScaleToolbox::Error + true + else + false + end + end + + def print_error(error) + write_error(error, $stderr) + + File.open('crash.log', 'w') do |io| + write_verbose_error(error, io) + end + + write_section_header($stderr, 'Detailed information') + warn + warn 'A detailed crash log has been written to ./crash.log.' + end + + def write_error(error, stream) + write_error_message(error, stream) + write_stack_trace(error, stream) + end + + def write_error_message(error, stream) + write_section_header(stream, 'Message') + stream.puts "\e[1m\e[31m#{error.class}: #{error.message}\e[0m" + end + + def write_stack_trace(error, stream) + write_section_header(stream, 'Backtrace') + stream.puts error.backtrace + end + + def write_version_information(stream) + write_section_header(stream, 'Version Information') + stream.puts ThreeScaleToolbox::VERSION + end + + def write_system_information(stream) + write_section_header(stream, 'System Information') + stream.puts Etc.uname.to_json + end + + def write_installed_gems(stream) + write_section_header(stream, 'Installed gems') + gems_and_versions.each do |g| + stream.puts " #{g.first} #{g.last.join(', ')}" + end + end + + def write_load_paths(stream) + write_section_header(stream, 'Load paths') + $LOAD_PATH.each_with_index do |i, index| + stream.puts " #{index}. #{i}" + end + end + + def write_verbose_error(error, stream) + stream.puts "Crashlog created at #{Time.now}" + + write_error_message(error, stream) + write_stack_trace(error, stream) + write_version_information(stream) + write_system_information(stream) + write_installed_gems(stream) + write_load_paths(stream) + end + + def gems_and_versions + gems = {} + Gem::Specification.find_all.sort_by { |s| [s.name, s.version] }.each do |spec| + gems[spec.name] ||= [] + gems[spec.name] << spec.version.to_s + end + gems + end + + def write_section_header(stream, title) + stream.puts + + stream.puts "===== #{title.upcase}:" + stream.puts + end + end + end +end diff --git a/spec/unit/error_handler_spec.rb b/spec/unit/error_handler_spec.rb new file mode 100644 index 00000000..75340718 --- /dev/null +++ b/spec/unit/error_handler_spec.rb @@ -0,0 +1,55 @@ +require '3scale_toolbox' + +RSpec.describe ThreeScaleToolbox::CLI::ErrorHandler do + include_context :temp_dir + + context '#error_watchdog' do + def raise_runtime_error + raise 'some error' + end + + def raise_toolbox_error + raise ThreeScaleToolbox::Error, 'some error' + end + + context 'raises expected error' do + it 'error is shown on stderr' do + Dir.chdir(tmp_dir) do + expect do + subject.error_watchdog { raise_toolbox_error } + end.to output(/some error/).to_stderr + expect(File).not_to exist('crash.log') + end + end + + it 'returns error' do + expect( + subject.error_watchdog { raise_toolbox_error } + ).to be + end + end + + context 'raises unexpected error' do + it 'crash.log is generated' do + Dir.chdir(tmp_dir) do + expect do + subject.error_watchdog { raise_runtime_error } + end.to output(/some error/).to_stderr + expect(File).to exist('crash.log') + end + end + + it 'returns error' do + expect( + subject.error_watchdog { raise_runtime_error } + ).to be + end + end + + context 'Does not raise error' do + it 'returns true' do + expect(subject.error_watchdog {}).to be_nil + end + end + end +end