=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 require 'singleton' require 'arachni/reactor' module Arachni module Processes # Helper for managing processes. # # @author Tasos "Zapotek" Laskos class Manager include Singleton RUNNER = "#{File.dirname( __FILE__ )}/executables/base.rb" # @return [Array] PIDs of all running processes. attr_reader :pids def initialize @pids = [] @discard_output = true end # @param [Integer] pid # Adds a PID to the {#pids} and detaches the process. # # @return [Integer] `pid` def <<( pid ) @pids << pid Process.detach pid pid end # @param [Integer] pid # PID of the process to kill. def kill( pid ) Timeout.timeout 10 do while sleep 0.1 do begin Process.kill( Arachni.windows? ? 'KILL' : 'TERM', pid ) # Either kill was successful or we don't have enough perms or # we hit a reused PID for someone else's process, either way, # consider the process gone. rescue Errno::ESRCH, Errno::EPERM, # Don't kill ourselves. SignalException @pids.delete pid return end end end rescue Timeout::Error end # @param [Integer] pid # @return [Boolean] # `true` if the process is alive, `false` otherwise. def alive?( pid ) # Windows is not big on POSIX so try it its own way if possible. if Arachni.windows? begin alive = false wmi = WIN32OLE.connect( 'winmgmts://' ) processes = wmi.ExecQuery( "select ProcessId from win32_process where ProcessID='#{pid}'" ) processes.each do |proc| proc.ole_free alive = true end processes.ole_free wmi.ole_free return alive rescue WIN32OLERuntimeError end end !!(Process.kill( 0, pid ) rescue false) end # @param [Array] pids # PIDs of the process to {Arachni::Processes::Manager#kill}. def kill_many( pids ) pids.each { |pid| kill pid } end # Kills all {#pids processes}. def killall kill_many @pids.dup @pids.clear end # Stops the Reactor. def kill_reactor Reactor.stop rescue nil end # Overrides the default setting of discarding process outputs. def preserve_output @discard_output = false end def preserve_output? !discard_output? end def discard_output @discard_output = true end def discard_output? @discard_output end # @param [String] executable # Name of the executable Ruby script found in {OptionGroups::Paths#executables} # without the '.rb' extension. # @param [Hash] options # Options to pass to the script -- can be retrieved from `$options`. # # @return [Integer] # PID of the process. def spawn( executable, options = {} ) fork = options.delete(:fork) fork = false if fork.nil? stdin = options.delete(:stdin) stdout = options.delete(:stdout) stderr = options.delete(:stderr) new_pgroup = options.delete(:new_pgroup) spawn_options = {} if new_pgroup if Arachni.windows? spawn_options[:new_pgroup] = new_pgroup else spawn_options[:pgroup] = new_pgroup end end spawn_options[:in] = stdin if stdin spawn_options[:out] = stdout if stdout spawn_options[:err] = stderr if stderr options[:ppid] = Process.pid arachni_options = Options.to_h.merge( options.delete(:options) || {} ) # Paths are not included in RPC nor Hash representations as they're # considered local, in this case though they're necessary to provide # the same environment the processes. arachni_options[:paths] = Options.paths.to_h encoded_arachni_options = Base64.strict_encode64( Marshal.dump( arachni_options ) ) executable = "#{Options.paths.executables}/#{executable}.rb" encoded_options = Base64.strict_encode64( Marshal.dump( options ) ) argv = [executable, encoded_options] # Process.fork is faster, less stressful to the CPU and lets the parent # and child share the same RAM due to copy-on-write support on Ruby 2.0.0. # It is, however, not available when running on Windows nor JRuby so # have a fallback ready. if fork && Process.respond_to?( :fork ) pid = Process.fork do $stdin = spawn_options[:in] if spawn_options[:in] if spawn_options[:out] $stdout = spawn_options[:out] elsif discard_output? $stdout.reopen( Arachni.null_device, 'w' ) end if spawn_options[:err] $stderr = spawn_options[:err] elsif discard_output? $stderr.reopen( Arachni.null_device, 'w' ) end # Careful, Framework.reset will remove objects from Data # structures which off-load to disk, those files however belong # to our parent and should not be touched, thus, we remove # any references to them. Data.framework.page_queue.disk.clear Data.framework.url_queue.disk.clear Data.framework.rpc.distributed_page_queue.disk.clear # Provide a clean slate. Framework.reset Reactor.stop ENV['arachni_options'] = encoded_arachni_options ARGV.replace( argv ) load RUNNER end else # It's very, **VERY** important that we use this argument format as # it bypasses the OS shell and we can thus count on a 1-to-1 process # creation and that the PID we get will be for the actual process. pid = Process.spawn( { 'arachni_options' => encoded_arachni_options }, RbConfig.ruby, RUNNER, *(argv + [spawn_options]) ) end self << pid pid end def self.method_missing( sym, *args, &block ) if instance.respond_to?( sym ) instance.send( sym, *args, &block ) else super( sym, *args, &block ) end end def self.respond_to?( m ) super( m ) || instance.respond_to?( m ) end end end end