lib/guard/runner.rb
# frozen_string_literal: true
require "lumberjack"
require "guard/ui"
require "guard/watcher"
module Guard
# @private
# The runner is responsible for running all methods defined on each plugin.
#
class Runner
def initialize(session)
@session = session
@plugins = session.plugins
end
# Runs a Guard-task on all registered plugins.
#
# @param [Symbol] task the task to run
#
# @param [Hash] scope_hash either the Guard plugin or the group to run the task
# on
#
def run(task, scope_hash = [])
scopes, unknown = session.convert_scopes(scope_hash)
if unknown.any?
UI.info "Unknown scopes: #{unknown.join(', ')}"
end
Lumberjack.unit_of_work do
grouped_plugins = session.grouped_plugins(scopes)
grouped_plugins.each_value do |plugins|
_run_group_plugins(plugins) do |plugin|
_supervise(plugin, task) if plugin.respond_to?(task)
end
end
end
end
PLUGIN_FAILED = "%s has failed, other group's plugins will be skipped."
MODIFICATION_TASKS = %i(
run_on_modifications run_on_changes run_on_change
).freeze
ADDITION_TASKS = %i(run_on_additions run_on_changes run_on_change).freeze
REMOVAL_TASKS = %i(run_on_removals run_on_changes run_on_deletion).freeze
# Runs the appropriate tasks on all registered plugins
# based on the passed changes.
#
# @param [Array<String>] modified the modified paths.
# @param [Array<String>] added the added paths.
# @param [Array<String>] removed the removed paths.
#
def run_on_changes(modified, added, removed)
types = {
MODIFICATION_TASKS => modified,
ADDITION_TASKS => added,
REMOVAL_TASKS => removed
}
UI.clearable!
session.grouped_plugins.each_value do |plugins|
_run_group_plugins(plugins) do |plugin|
UI.clear
types.each do |tasks, unmatched_paths|
next if unmatched_paths.empty?
match_result = Watcher.match_files(plugin, unmatched_paths)
next if match_result.empty?
task = tasks.detect { |meth| plugin.respond_to?(meth) }
_supervise(plugin, task, match_result) if task
end
end
end
end
# Run a Guard plugin task, but remove the Guard plugin when his work leads
# to a system failure.
#
# When the Group has `:halt_on_fail` disabled, we've to catch
# `:task_has_failed` here in order to avoid an uncaught throw error.
#
# @param [Guard::Plugin] plugin guard the Guard to execute
# @param [Symbol] task the task to run
# @param [Array] args the arguments for the task
# @raise [:task_has_failed] when task has failed
#
def _supervise(plugin, task, *args)
catch self.class.stopping_symbol_for(plugin) do
plugin.hook("#{task}_begin", *args)
result = UI.options.with_progname(plugin.class.name) do
plugin.send(task, *args)
rescue Interrupt
throw(:task_has_failed)
end
plugin.hook("#{task}_end", result)
result
end
rescue ScriptError, StandardError
UI.error("#{plugin.class.name} failed to achieve its"\
" <#{task}>, exception was:" \
"\n#{$!.class}: #{$!.message}" \
"\n#{$!.backtrace.join("\n")}")
plugins.remove(plugin)
UI.info("\n#{plugin.class.name} has just been fired")
$!
end
# Returns the symbol that has to be caught when running a supervised task.
#
# @note If a Guard group is being run and it has the `:halt_on_fail`
# option set, this method returns :no_catch as it will be caught at the
# group level.
#
# @param [Guard::Plugin] guard the Guard plugin to execute
# @return [Symbol] the symbol to catch
#
def self.stopping_symbol_for(plugin)
plugin.group.options[:halt_on_fail] ? :no_catch : :task_has_failed
end
private
attr_reader :session, :plugins
def _run_group_plugins(plugins)
failed_plugin = nil
catch :task_has_failed do
plugins.each do |plugin|
failed_plugin = plugin
yield plugin
failed_plugin = nil
end
end
UI.info format(PLUGIN_FAILED, failed_plugin.class.name) if failed_plugin
end
end
end