class Nanoc::CLI::ErrorHandler

Catches errors and prints nice diagnostic messages, then exits.

@api private

Constants

GEM_NAMES

A hash that contains the name of the gem for a given required file. If a `#require` fails, the gem name is looked up in this hash.

Public Class Methods

disable() click to toggle source

Disables error handling. This is used by the test cases to prevent error from being handled by the CLI while tests are running.

# File lib/nanoc/cli/error_handler.rb, line 21
def self.disable
  @disabled = true
end
enable() click to toggle source

Re-enables error handling after it was disabled. This is used by the test cases to prevent error from being handled by the CLI while tests are running.

# File lib/nanoc/cli/error_handler.rb, line 28
def self.enable
  @disabled = false
end
handle_while(exit_on_error: true) { || ... } click to toggle source

Enables error handling in the given block.

@return [void]

# File lib/nanoc/cli/error_handler.rb, line 11
def self.handle_while(exit_on_error: true)
  if @disabled
    yield
  else
    new.handle_while(exit_on_error: exit_on_error) { yield }
  end
end
print_error(error) click to toggle source

Prints the given error to stderr. Includes message, possible resolution (see {#resolution_for}), compilation stack, backtrace, etc.

@param [Error] error The error that should be described

@return [void]

Public Instance Methods

handle_error(error, exit_on_error:) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 64
def handle_error(error, exit_on_error:)
  if trivial?(error)
    $stderr.puts
    $stderr.puts "Error: #{error.message}"
    resolution = resolution_for(error)
    if resolution
      $stderr.puts
      $stderr.puts resolution
    end
  else
    print_error(error)
  end
  exit(1) if exit_on_error
end
handle_while(exit_on_error:) { || ... } click to toggle source

Enables error handling in the given block. This method should not be called directly; use {Nanoc::CLI::ErrorHandler.handle_while} instead.

@return [void]

# File lib/nanoc/cli/error_handler.rb, line 36
def handle_while(exit_on_error:)
  # 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'
    begin
      Signal.trap('USR1') do
        puts 'Caught USR1; dumping a stack trace'
        puts caller.map { |i| "  #{i}" }.join("\n")
      end
    rescue ArgumentError
    end
  end

  # Run
  yield
rescue Interrupt
  exit(1)
rescue StandardError, ScriptError => e
  handle_error(e, exit_on_error: exit_on_error)
end
print_error(error) click to toggle source

Prints the given error to stderr. Includes message, possible resolution (see {#resolution_for}), compilation stack, backtrace, etc.

@param [Error] error The error that should be described

@return [void]

trivial?(error) click to toggle source

@api private

# File lib/nanoc/cli/error_handler.rb, line 146
def trivial?(error)
  case error
  when Nanoc::Core::TrivialError, Errno::EADDRINUSE
    true
  when LoadError
    GEM_NAMES.key?(gem_name_from_load_error(error))
  else
    false
  end
end
write_compact_error(error, stream) click to toggle source

Writes a compact representation of the error, suitable for a terminal, on the given stream (probably stderr).

@param [Error] error The error that should be described

@param [IO] stream The stream to write the description too

@return [void]

# File lib/nanoc/cli/error_handler.rb, line 113
def write_compact_error(error, stream)
  stream.puts
  stream.puts 'Captain! We’ve been hit!'

  write_error_message(stream, error)
  write_item_rep(stream, error)
  write_stack_trace(stream, error)

  stream.puts
  stream.puts 'A detailed crash log has been written to ./crash.log.'
end
write_verbose_error(error, stream) click to toggle source

Writes a verbose representation of the error on the given stream.

@param [Error] error The error that should be described

@param [IO] stream The stream to write the description too

@return [void]

# File lib/nanoc/cli/error_handler.rb, line 132
def write_verbose_error(error, stream)
  stream.puts "Crashlog created at #{Time.now}"

  write_error_message(stream, error, verbose: true)
  write_item_rep(stream, error, verbose: true)
  write_stack_trace(stream, error, verbose: true)
  write_version_information(stream, verbose: true)
  write_system_information(stream, verbose: true)
  write_installed_gems(stream, verbose: true)
  write_gemfile_lock(stream, verbose: true)
  write_load_paths(stream, verbose: true)
end

Protected Instance Methods

gem_name_from_load_error(error) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 249
def gem_name_from_load_error(error)
  matches = error.message.match(/(no such file to load|cannot load such file) -- ([^\s]+)/)
  return nil if matches.nil?

  GEM_NAMES[matches[2]]
end
gems_and_versions() click to toggle source

@return [Hash<String, Array>] A hash containing the gem names as keys and gem versions as value

# File lib/nanoc/cli/error_handler.rb, line 160
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
message_for_error(error) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 287
def message_for_error(error)
  case error
  when JsonSchema::AggregateError
    "\n" + error.errors.map { |e| "  * #{e.pointer}: #{e.message}" }.join("\n")
  else
    error.message
  end
end
resolution_for(error) click to toggle source

Attempts to find a resolution for the given error, or nil if no resolution can be automatically obtained.

@param [Error] error The error to find a resolution for

@return [String] The resolution for the given error

# File lib/nanoc/cli/error_handler.rb, line 219
    def resolution_for(error)
      error = unwrap_error(error)

      case error
      when LoadError
        gem_name = gem_name_from_load_error(error)

        if gem_name
          if using_bundler?
            <<~RES
              1. Add `gem '#{gem_name}'` to your Gemfile
              2. Run `bundle install`
              3. Re-run this command
            RES
          else
            "Install the '#{gem_name}' gem using `gem install #{gem_name}`."
          end
        end
      when RuntimeError
        if /^can't modify frozen/.match?(error.message)
          'You attempted to modify immutable data. Some data cannot ' \
          'be modified once compilation has started. Such data includes ' \
          'content and attributes of items and layouts, and filter arguments.'
        end
      when Errno::EADDRINUSE
        'There already is a server running. Either shut down that one, or ' \
        'specify a different port to run this server on.'
      end
    end
ruby_version() click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 260
def ruby_version
  RUBY_VERSION
end
unwrap_error(e) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 345
def unwrap_error(e)
  case e
  when Nanoc::Core::Errors::CompilationError
    e.unwrap
  else
    e
  end
end
using_bundler?() click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 256
def using_bundler?
  defined?(Bundler) && Bundler::SharedHelpers.in_bundle?
end
write_error_message(stream, error, verbose: false) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 273
def write_error_message(stream, error, verbose: false)
  write_section_header(stream, 'Message', verbose: verbose)

  error = unwrap_error(error)

  message = "#{error.class}: #{message_for_error(error)}"
  unless verbose
    message = "\e[1m\e[31m" + message + "\e[0m"
  end
  stream.puts message
  resolution = resolution_for(error)
  stream.puts resolution.to_s if resolution
end
write_gemfile_lock(stream, verbose: false) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 331
def write_gemfile_lock(stream, verbose: false)
  if File.exist?('Gemfile.lock')
    write_section_header(stream, 'Gemfile.lock', verbose: verbose)
    stream.puts File.read('Gemfile.lock')
  end
end
write_installed_gems(stream, verbose: false) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 324
def write_installed_gems(stream, verbose: false)
  write_section_header(stream, 'Installed gems', verbose: verbose)
  gems_and_versions.each do |g|
    stream.puts "  #{g.first} #{g.last.join(', ')}"
  end
end
write_item_rep(stream, error, verbose: false) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 296
def write_item_rep(stream, error, verbose: false)
  return unless error.is_a?(Nanoc::Core::Errors::CompilationError)

  write_section_header(stream, 'Item being compiled', verbose: verbose)

  item_rep = error.item_rep
  stream.puts "Current item: #{item_rep.item.identifier} (#{item_rep.name.inspect} representation)"
end
write_load_paths(stream, verbose: false) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 338
def write_load_paths(stream, verbose: false)
  write_section_header(stream, 'Load paths', verbose: verbose)
  $LOAD_PATH.each_with_index do |i, index|
    stream.puts "  #{index}. #{i}"
  end
end
write_section_header(stream, title, verbose: false) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 264
def write_section_header(stream, title, verbose: false)
  stream.puts

  if verbose
    stream.puts '===== ' + title.upcase + ':'
    stream.puts
  end
end
write_stack_trace(stream, error, verbose: false) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 305
def write_stack_trace(stream, error, verbose: false)
  write_section_header(stream, 'Stack trace', verbose: verbose)

  writer = Nanoc::CLI::StackTraceWriter.new(stream)
  writer.write(unwrap_error(error), verbose: verbose)
end
write_system_information(stream, verbose: false) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 317
def write_system_information(stream, verbose: false)
  uname = `uname -a`
  write_section_header(stream, 'System information', verbose: verbose)
  stream.puts uname
rescue Errno::ENOENT
end
write_version_information(stream, verbose: false) click to toggle source
# File lib/nanoc/cli/error_handler.rb, line 312
def write_version_information(stream, verbose: false)
  write_section_header(stream, 'Version information', verbose: verbose)
  stream.puts Nanoc::Core.version_information
end