=begin Copyright 2010-2022 Ecsypno This file is part of the Arachni Framework project and is subject to redistribution and commercial restrictions. Please see the Arachni Framework web site for more information on licensing and terms of use. =end module Arachni # The namespace under which all plugins exist. module Plugins end module Plugin class Error < Arachni::Error class UnsatisfiedDependency < Error end end # Holds and manages the {Plugins}. # # @author Tasos "Zapotek" Laskos class Manager < Arachni::Component::Manager include MonitorMixin # Namespace under which all plugins reside. NAMESPACE = Arachni::Plugins # Expressions matching default plugins. DEFAULT = %w(defaults/*) # @param [Arachni::Framework] framework # Framework instance. def initialize( framework ) super( framework.options.paths.plugins, NAMESPACE ) @framework = framework @jobs = {} end # Loads the default plugins. # # @see DEFAULT def load_default load DEFAULT end alias :load_defaults :load_default # @return [Array] # Components to load, by name. def default parse DEFAULT end alias :defaults :default # Runs each plug-in in its own thread. # # @raise [Error::UnsatisfiedDependency] # If the environment is {#sane_env? not sane}. def run print_status 'Preparing plugins...' schedule.each do |name, options| instance = create( name, options ) exception_jail do instance.prepare end rescue next @jobs[name] = Thread.new do exception_jail( false ) do Thread.current[:instance] = instance Thread.current[:instance].run Thread.current[:instance].clean_up end synchronize do @jobs.delete name end end end print_status '... done.' end # @return [Hash] # Sorted plugins (by priority) with their prepared options. # # @raise [Error::UnsatisfiedDependency] # If the environment is not {#sane_env? sane}. def schedule ordered = [] unordered = [] loaded.each do |name| ph = { name => self[name] } if (order = self[name].info[:priority]) ordered[order] ||= [] ordered[order] << ph else unordered << ph end end ordered << unordered ordered.flatten! ordered.compact! ordered.inject({}) do |h, ph| name = ph.keys.first plugin = ph.values.first if (ret = sane_env?( plugin )) != true deps = '' if !ret[:gem_errors].empty? print_bad "[#{name}] The following plug-in dependencies aren't satisfied:" ret[:gem_errors].each { |gem| print_bad "\t* #{gem}" } deps = ret[:gem_errors].join( ' ' ) print_bad 'Try installing them by running:' print_bad "\tgem install #{deps}" end fail Error::UnsatisfiedDependency, "Plug-in dependencies not met: #{name} -- #{deps}" end h[name.to_sym] = prepare_options( name, plugin, @framework.options.plugins[name] ) h end end # Checks whether or not the environment satisfies all plugin dependencies. # # @return [TrueClass, Hash] # `true` if the environment is sane, a hash with errors otherwise. def sane_env?( plugin ) gem_errors = [] plugin.gems.each do |gem| begin require gem rescue LoadError gem_errors << gem end end return { gem_errors: gem_errors } if !gem_errors.empty? true end def create( name, options = {} ) self[name].new( @framework, options ) end # Blocks until all plug-ins have finished executing. def block while busy? print_debug print_debug "Waiting on #{@jobs.size} plugins to finish:" print_debug job_names.join( ', ' ) print_debug synchronize do @jobs.select! { |_ ,j| j.alive? } end sleep 0.1 end nil end def suspend @jobs.dup.each do |name, job| next if !job.alive? plugin = job[:instance] state.store( name, data: plugin.suspend, options: plugin.options ) kill name end nil end def restore schedule.each do |name, options| @jobs[name] = Thread.new do exception_jail( false ) do if state.include? name Thread.current[:instance] = create( name, state[name][:options] ) Thread.current[:instance].restore state[name][:data] else Thread.current[:instance] = create( name, options ) Thread.current[:instance].prepare end Thread.current[:instance].run Thread.current[:instance].clean_up synchronize do @jobs.delete name end end end end return if @jobs.empty? print_status 'Waiting for plugins to settle...' sleep 1 nil end # @return [Bool] # `false` if all plug-ins have finished executing, `true` otherwise. def busy? @jobs.any? end # @return [Array] # Names of all running plug-ins. def job_names @jobs.keys end # @return [Hash{String=>Thread}] # All the running threads. def jobs @jobs end # Kills a plug-in by `name`. # # @param [String] name def kill( name ) synchronize do job = @jobs.delete( name.to_sym ) return true if job && job.kill end false end def killall synchronize do @jobs.values.each(&:kill) @jobs.clear true end end def state State.plugins end def data Data.plugins end def results data.results end def self.reset State.plugins.clear Data.plugins.clear remove_constants( NAMESPACE ) end def reset killall clear self.class.reset end end end end