#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Christopher Brown (<cb@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require_relative "config"
require_relative "mixin/params_validate"
require "chef-utils/dsl/path_sanity" unless defined?(ChefUtils::DSL::PathSanity)
require_relative "log"
require_relative "deprecated"
require_relative "server_api"
require_relative "api_client"
require_relative "api_client/registration"
require_relative "node"
require_relative "role"
require_relative "file_cache"
require_relative "run_context"
require_relative "runner"
require_relative "run_status"
require_relative "cookbook/cookbook_collection"
require_relative "cookbook/file_vendor"
require_relative "cookbook/file_system_file_vendor"
require_relative "cookbook/remote_file_vendor"
require_relative "event_dispatch/dispatcher"
require_relative "event_loggers/base"
require_relative "event_loggers/windows_eventlog"
require_relative "exceptions"
require_relative "formatters/base"
require_relative "formatters/doc"
require_relative "formatters/minimal"
require_relative "version"
require_relative "action_collection"
require_relative "resource_reporter"
require_relative "data_collector"
require_relative "run_lock"
require_relative "policy_builder"
require_relative "request_id"
require_relative "platform/rebooter"
require_relative "mixin/deprecation"
require "chef-utils" unless defined?(ChefUtils::CANARY)
require "ohai" unless defined?(Ohai::System)
require "rbconfig"
require_relative "dist"
require "forwardable" unless defined?(Forwardable)

class Chef
  # == Chef::Client
  # The main object in a Chef run. Preps a Chef::Node and Chef::RunContext,
  # syncs cookbooks if necessary, and triggers convergence.
  class Client
    extend Chef::Mixin::Deprecation

    extend Forwardable
    #
    # The status of the Chef run.
    #
    # @return [Chef::RunStatus]
    #
    attr_reader :run_status

    #
    # The run context of the Chef run.
    #
    # @return [Chef::RunContext]
    #
    attr_reader :run_context

    #
    # The node represented by this client.
    #
    # @return [Chef::Node]
    #
    def node
      run_status.node
    end

    def node=(value)
      run_status.node = value
    end

    #
    # The ohai system used by this client.
    #
    # @return [Ohai::System]
    #
    attr_reader :ohai

    #
    # The runner used to converge.
    #
    # @return [Chef::Runner]
    #
    attr_accessor :runner

    #
    # Extra node attributes that were applied to the node.
    #
    # @return [Hash]
    #
    attr_reader :json_attribs

    #
    # The event dispatcher for the Chef run, including any configured output
    # formatters and event loggers.
    #
    # @return [EventDispatch::Dispatcher]
    #
    # @see Chef::Formatters
    # @see Chef::Config#formatters
    # @see Chef::Config#stdout
    # @see Chef::Config#stderr
    # @see Chef::Config#force_logger
    # @see Chef::Config#force_formatter
    # TODO add stdout, stderr, and default formatters to Chef::Config so the
    # defaults aren't calculated here.  Remove force_logger and force_formatter
    # from this code.
    # @see Chef::EventLoggers
    # @see Chef::Config#disable_event_logger
    # @see Chef::Config#event_loggers
    # @see Chef::Config#event_handlers
    #
    attr_reader :events

    attr_reader :logger

    def_delegator :@run_context, :transport_connection

    #
    # Creates a new Chef::Client.
    #
    # @param json_attribs [Hash] Node attributes to layer into the node when it is
    #   fetched.
    # @param args [Hash] Options:
    # @option args [Array<RunList::RunListItem>] :override_runlist A runlist to
    #   use instead of the node's embedded run list.
    # @option args [Array<String>] :specific_recipes A list of recipe file paths
    #   to load after the run list has been loaded.
    #
    def initialize(json_attribs = nil, args = {})
      @json_attribs = json_attribs || {}
      @logger = args.delete(:logger) || Chef::Log.with_child

      @ohai = Ohai::System.new(logger: logger)

      event_handlers = configure_formatters + configure_event_loggers
      event_handlers += Array(Chef::Config[:event_handlers])

      @events = EventDispatch::Dispatcher.new(*event_handlers)
      # @todo it seems like a bad idea to be deletin' other peoples' hashes.
      @override_runlist = args.delete(:override_runlist)
      @specific_recipes = args.delete(:specific_recipes)
      @run_status = Chef::RunStatus.new(nil, events)

      if new_runlist = args.delete(:runlist)
        @json_attribs["run_list"] = new_runlist
      end
    end

    #
    # Do a full run for this Chef::Client.
    #
    # Locks the run while doing its job.
    #
    # Fires run_start before doing anything and fires run_completed or
    # run_failed when finished.  Also notifies client listeners of run_started
    # at the beginning of Compile, and run_completed_successfully or run_failed
    # when all is complete.
    #
    # Phase 1: Setup
    # --------------
    # Gets information about the system and the run we are doing.
    #
    # 1. Run ohai to collect system information.
    # 2. Register / connect to the Chef server (unless in solo mode).
    # 3. Retrieve the node (or create a new one).
    # 4. Merge in json_attribs, Chef::Config.environment, and override_run_list.
    #
    # @see #run_ohai
    # @see #load_node
    # @see #build_node
    # @see Chef::Config#lockfile
    # @see Chef::RunLock#acquire
    #
    # Phase 2: Compile
    # ----------------
    # Decides *what* we plan to converge by compiling recipes.
    #
    # 1. Sync required cookbooks to the local cache.
    # 2. Load libraries from all cookbooks.
    # 3. Load attributes from all cookbooks.
    # 4. Load LWRPs from all cookbooks.
    # 5. Load resource definitions from all cookbooks.
    # 6. Load recipes in the run list.
    # 7. Load recipes from the command line.
    #
    # @see #setup_run_context Syncs and compiles cookbooks.
    # @see Chef::CookbookCompiler#compile
    #
    # Phase 3: Converge
    # -----------------
    # Brings the system up to date.
    #
    # 1. Converge the resources built from recipes in Phase 2.
    # 2. Save the node.
    # 3. Reboot if we were asked to.
    #
    # @see #converge_and_save
    # @see Chef::Runner
    #
    # @return Always returns true.
    #
    def run
      start_profiling

      runlock = RunLock.new(Chef::Config.lockfile)
      # TODO feels like acquire should have its own block arg for this
      runlock.acquire
      # don't add code that may fail before entering this section to be sure to release lock
      begin
        runlock.save_pid

        events.register(Chef::DataCollector::Reporter.new(events))
        events.register(Chef::ActionCollection.new(events))

        run_status.run_id = request_id = Chef::RequestID.instance.request_id

        @run_context = Chef::RunContext.new
        run_context.events = events
        run_status.run_context = run_context

        events.run_start(Chef::VERSION, run_status)

        logger.info("*** #{Chef::Dist::PRODUCT} #{Chef::VERSION} ***")
        logger.info("Platform: #{RUBY_PLATFORM}")
        logger.info "#{Chef::Dist::CLIENT.capitalize} pid: #{Process.pid}"
        logger.info "Targeting node: #{Chef::Config.target_mode.host}" if Chef::Config.target_mode?
        logger.debug("#{Chef::Dist::CLIENT.capitalize} request_id: #{request_id}")
        ENV["PATH"] = ChefUtils::DSL::PathSanity.sanitized_path if Chef::Config[:enforce_path_sanity]

        if Chef::Config.target_mode?
          get_ohai_data_remotely
        else
          run_ohai
        end

        unless Chef::Config[:solo_legacy_mode]
          register

          # create and save the rest objects in the run_context
          run_context.rest = rest
          run_context.rest_clean = rest_clean

          events.register(Chef::ResourceReporter.new(rest_clean))
        end

        load_node

        build_node

        run_status.start_clock
        logger.info("Starting #{Chef::Dist::PRODUCT} Run for #{node.name}")
        run_started

        do_windows_admin_check

        Chef.resource_handler_map.lock!
        Chef.provider_handler_map.lock!

        setup_run_context

        load_required_recipe(@rest, run_context) unless Chef::Config[:solo_legacy_mode]

        converge_and_save(run_context)

        run_status.stop_clock
        logger.info("#{Chef::Dist::PRODUCT} Run complete in #{run_status.elapsed_time} seconds")
        run_completed_successfully
        events.run_completed(node, run_status)

        # keep this inside the main loop to get exception backtraces
        end_profiling

        # rebooting has to be the last thing we do, no exceptions.
        Chef::Platform::Rebooter.reboot_if_needed!(node)
      rescue Exception => run_error
        # CHEF-3336: Send the error first in case something goes wrong below and we don't know why
        logger.trace("Re-raising exception: #{run_error.class} - #{run_error.message}\n#{run_error.backtrace.join("\n  ")}")
        # If we failed really early, we may not have a run_status yet. Too early for these to be of much use.
        if run_status
          run_status.stop_clock
          run_status.exception = run_error
          run_failed
        end
        events.run_failed(run_error, run_status)
        Chef::Application.debug_stacktrace(run_error)
        raise run_error
      ensure
        Chef::RequestID.instance.reset_request_id
        @run_status = nil
        runlock.release
      end

      true
    end

    #
    # Private API
    # @todo make this stuff protected or private
    #

    # @api private
    def configure_formatters
      formatters_for_run.map do |formatter_name, output_path|
        if output_path.nil?
          Chef::Formatters.new(formatter_name, STDOUT_FD, STDERR_FD)
        else
          io = File.open(output_path, "a+")
          io.sync = true
          Chef::Formatters.new(formatter_name, io, io)
        end
      end
    end

    # @api private
    def formatters_for_run
      if Chef::Config.formatters.empty?
        [default_formatter]
      else
        Chef::Config.formatters
      end
    end

    # @api private
    def default_formatter
      if !Chef::Config[:force_logger] || Chef::Config[:force_formatter]
        [:doc]
      else
        [:null]
      end
    end

    # @api private
    def configure_event_loggers
      if Chef::Config.disable_event_logger
        []
      else
        Chef::Config.event_loggers.map do |evt_logger|
          case evt_logger
          when Symbol
            Chef::EventLoggers.new(evt_logger)
          when Class
            evt_logger.new
          else
          end
        end
      end
    end

    # Standard rest object for talking to the Chef Server
    #
    # FIXME: Can we drop this and only use the rest_clean object?  Did I add rest_clean
    # only out of some cant-break-a-minor-version paranoia?
    #
    # @api private
    def rest
      @rest ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], client_name: node_name,
                                    signing_key_filename: Chef::Config[:client_key])
    end

    # A rest object with validate_utf8 set to false.  This will not throw exceptions
    # on non-UTF8 strings in JSON but will sanitize them so that e.g. POSTs will
    # never fail.  Cannot be configured on a request-by-request basis, so we carry
    # around another rest object for it.
    #
    # @api private
    def rest_clean
      @rest_clean ||=
        Chef::ServerAPI.new(Chef::Config[:chef_server_url], client_name: node_name,
                            signing_key_filename: Chef::Config[:client_key], validate_utf8: false)
    end

    #
    # Callback to fire notifications that the Chef run is starting
    #
    # @api private
    #
    def run_started
      self.class.run_start_notifications.each do |notification|
        notification.call(run_status)
      end
      events.run_started(run_status)
    end

    #
    # Callback to fire notifications that the run completed successfully
    #
    # @api private
    #
    def run_completed_successfully
      success_handlers = self.class.run_completed_successfully_notifications
      success_handlers.each do |notification|
        notification.call(run_status)
      end
    end

    #
    # Callback to fire notifications that the Chef run failed
    #
    # @api private
    #
    def run_failed
      failure_handlers = self.class.run_failed_notifications
      failure_handlers.each do |notification|
        notification.call(run_status)
      end
    end

    #
    # Instantiates a Chef::Node object, possibly loading the node's prior state
    # when using chef-client. Sets Chef.node to the new node.
    #
    # @return [Chef::Node] The node object for this Chef run
    #
    # @see Chef::PolicyBuilder#load_node
    #
    # @api private
    #
    def load_node
      policy_builder.load_node
      run_status.node = policy_builder.node
      Chef.set_node(policy_builder.node)
      node
    end

    #
    # Mutates the `node` object to prepare it for the chef run.
    #
    # @return [Chef::Node] The updated node object
    #
    # @see Chef::PolicyBuilder#build_node
    #
    # @api private
    #
    def build_node
      policy_builder.build_node
      run_status.node = node
      node
    end

    #
    # Sync cookbooks to local cache.
    #
    # TODO this appears to be unused.
    #
    # @see Chef::PolicyBuilder#sync_cookbooks
    #
    # @api private
    #
    def sync_cookbooks
      policy_builder.sync_cookbooks
    end

    #
    # Sets up the run context.
    #
    # @see Chef::PolicyBuilder#setup_run_context
    #
    # @return The newly set up run context
    #
    # @api private
    def setup_run_context
      @run_context = policy_builder.setup_run_context(specific_recipes, run_context)
      assert_cookbook_path_not_empty(run_context)
      run_status.run_context = run_context # backcompat for chefspec
      run_context
    end

    #
    # Adds a required recipe as specified by the Chef Server
    #
    # @return The modified run context
    #
    # @api private
    #
    # TODO: @rest doesn't appear to be used anywhere outside
    # of client.register except for here. If it's common practice
    # to create your own rest client, perhaps we should do that
    # here but it seems more appropriate to reuse one that we
    # know is already created. for ease of testing, we'll pass
    # the existing rest client in as a parameter
    #
    def load_required_recipe(rest, run_context)
      required_recipe_contents = rest.get("required_recipe")
      logger.info("Required Recipe found, loading it")
      Chef::FileCache.store("required_recipe", required_recipe_contents)
      required_recipe_file = Chef::FileCache.load("required_recipe", false)

      # TODO: add integration tests with resource reporting turned on
      #       (presumably requires changes to chef-zero)
      #
      # Chef::Recipe.new takes a cookbook name and a recipe name along
      # with the run context. These names are eventually used in the
      # resource reporter, and if the cookbook name cannot be found in the
      # cookbook collection then we will fail with an exception. Cases where
      # we currently also fail:
      #   - specific recipes
      #   - chef-apply would fail if resource reporting was enabled
      #
      recipe = Chef::Recipe.new(nil, nil, run_context)
      recipe.from_file(required_recipe_file)
      run_context
    rescue Net::HTTPClientException => e
      case e.response
      when Net::HTTPNotFound
        logger.trace("Required Recipe not configured on the server, skipping it")
      else
        raise
      end
    end

    #
    # The PolicyBuilder strategy for figuring out run list and cookbooks.
    #
    # @return [Chef::PolicyBuilder::Policyfile, Chef::PolicyBuilder::ExpandNodeObject]
    #
    # @api private
    #
    def policy_builder
      @policy_builder ||= Chef::PolicyBuilder::Dynamic.new(node_name, ohai.data, json_attribs, override_runlist, events)
    end

    #
    # Save the updated node to Chef.
    #
    # Does not save if we are in solo mode or using override_runlist.
    #
    # @see Chef::Node#save
    # @see Chef::Config#solo
    #
    # @api private
    #
    def save_updated_node
      if Chef::Config[:solo_legacy_mode]
        # nothing to do
      elsif policy_builder.temporary_policy?
        logger.warn("Skipping final node save because override_runlist was given")
      else
        logger.debug("Saving the current state of node #{node_name}")
        node.save
      end
    end

    #
    # Populate the minimal ohai attributes defined in #run_ohai with data train collects.
    #
    # Eventually ohai may support colleciton of data.
    #
    def get_ohai_data_remotely
      ohai.data[:fqdn] = if transport_connection.respond_to?(:hostname)
                           transport_connection.hostname
                         else
                           Chef::Config[:target_mode][:host]
                         end
      if transport_connection.respond_to?(:os)
        ohai.data[:platform] = transport_connection.os.name
        ohai.data[:platform_version] = transport_connection.os.release
        ohai.data[:os] = transport_connection.os.family_hierarchy[1]
        ohai.data[:platform_family] = transport_connection.os.family
      end
      # train does not collect these specifically
      # ohai.data[:machinename] = nil
      # ohai.data[:hostname] = nil
      # ohai.data[:os_version] = nil # kernel version

      ohai.data[:ohai_time] = Time.now.to_f
      events.ohai_completed(node)
    end

    #
    # Run ohai plugins.  Runs all ohai plugins unless minimal_ohai is specified.
    #
    # Sends the ohai_completed event when finished.
    #
    # @see Chef::EventDispatcher#
    # @see Chef::Config#minimal_ohai
    #
    # @api private
    #
    def run_ohai
      filter = Chef::Config[:minimal_ohai] ? %w{fqdn machinename hostname platform platform_version ohai_time os os_version init_package} : nil
      ohai.all_plugins(filter)
      events.ohai_completed(node)
    end

    #
    # Figure out the node name we are working with.
    #
    # It tries these, in order:
    # - Chef::Config.node_name
    # - ohai[:fqdn]
    # - ohai[:machinename]
    # - ohai[:hostname]
    #
    # @raise [Chef::Exceptions::CannotDetermineNodeName] If the node name is not
    #   set and cannot be determined via ohai.
    #
    # @see Chef::Config#node_name
    #
    # @api private
    #
    def node_name
      name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:machinename] || ohai[:hostname]
      Chef::Config[:node_name] = name

      raise Chef::Exceptions::CannotDetermineNodeName unless name

      name
    end

    #
    # Determine our private key and set up the connection to the Chef server.
    #
    # Skips registration and fires the `skipping_registration` event if
    # Chef::Config.client_key is unspecified or already exists.
    #
    # If Chef::Config.client_key does not exist, we register the client with the
    # Chef server and fire the registration_start and registration_completed events.
    #
    # @return [Chef::ServerAPI] The server connection object.
    #
    # @see Chef::Config#chef_server_url
    # @see Chef::Config#client_key
    # @see Chef::ApiClient::Registration#run
    # @see Chef::EventDispatcher#skipping_registration
    # @see Chef::EventDispatcher#registration_start
    # @see Chef::EventDispatcher#registration_completed
    # @see Chef::EventDispatcher#registration_failed
    #
    # @api private
    #
    def register(client_name = node_name, config = Chef::Config)
      if !config[:client_key]
        events.skipping_registration(client_name, config)
        logger.trace("Client key is unspecified - skipping registration")
      elsif File.exists?(config[:client_key])
        events.skipping_registration(client_name, config)
        logger.trace("Client key #{config[:client_key]} is present - skipping registration")
      else
        events.registration_start(node_name, config)
        logger.info("Client key #{config[:client_key]} is not present - registering")
        Chef::ApiClient::Registration.new(node_name, config[:client_key]).run
        events.registration_completed
      end
    rescue Exception => e
      # TODO this should probably only ever fire if we *started* registration.
      # Move it to the block above.
      # TODO: munge exception so a semantic failure message can be given to the
      # user
      events.registration_failed(client_name, e, config)
      raise
    end

    #
    # Converges all compiled resources.
    #
    # Fires the converge_start, converge_complete and converge_failed events.
    #
    # If the exception `:end_client_run_early` is thrown during convergence, it
    # does not mark the run complete *or* failed, and returns `nil`
    #
    # @param run_context The run context.
    #
    # @raise Any converge exception
    #
    # @see Chef::Runner#converge
    # @see Chef::EventDispatch#converge_start
    # @see Chef::EventDispatch#converge_complete
    # @see Chef::EventDispatch#converge_failed
    #
    # @api private
    #
    def converge(run_context)
      catch(:end_client_run_early) do
        begin
          events.converge_start(run_context)
          logger.debug("Converging node #{node_name}")
          @runner = Chef::Runner.new(run_context)
          @runner.converge
          events.converge_complete
        rescue Exception => e
          events.converge_failed(e)
          raise e
        end
      end
    end

    # Converge the node via and then save it if successful.
    #
    # If converge() raises it is important that save_updated_node is bypassed.
    #
    # @param run_context [Chef::RunContext] The run context.
    # @raise Any converge or node save exception
    #
    # @api private
    #
    def converge_and_save(run_context)
      converge(run_context)
      save_updated_node
    end

    #
    # Expands the run list.
    #
    # @return [Chef::RunListExpansion] The expanded run list.
    #
    # @see Chef::PolicyBuilder#expand_run_list
    #
    def expanded_run_list
      policy_builder.expand_run_list
    end

    #
    # Check if the user has Administrator privileges on windows.
    #
    # Throws an error if the user is not an admin, and
    # `Chef::Config.fatal_windows_admin_check` is true.
    #
    # @raise [Chef::Exceptions::WindowsNotAdmin] If the user is not an admin.
    #
    # @see Chef::platform#windows?
    # @see Chef::Config#fatal_windows_admin_check
    #
    # @api private
    #
    def do_windows_admin_check
      if ChefUtils.windows?
        logger.trace("Checking for administrator privileges....")

        if !has_admin_privileges?
          message = "#{Chef::Dist::CLIENT} doesn't have administrator privileges on node #{node_name}."
          if Chef::Config[:fatal_windows_admin_check]
            logger.fatal(message)
            logger.fatal("fatal_windows_admin_check is set to TRUE.")
            raise Chef::Exceptions::WindowsNotAdmin, message
          else
            logger.warn("#{message} This might cause unexpected resource failures.")
          end
        else
          logger.trace("#{Chef::Dist::CLIENT} has administrator privileges on node #{node_name}.")
        end
      end
    end

    # Notification registration
    class<<self
      #
      # Add a listener for the 'client run started' event.
      #
      # @param notification_block The callback (takes |run_status| parameter).
      # @yieldparam [Chef::RunStatus] run_status The run status.
      #
      def when_run_starts(&notification_block)
        run_start_notifications << notification_block
      end

      #
      # Add a listener for the 'client run success' event.
      #
      # @param notification_block The callback (takes |run_status| parameter).
      # @yieldparam [Chef::RunStatus] run_status The run status.
      #
      def when_run_completes_successfully(&notification_block)
        run_completed_successfully_notifications << notification_block
      end

      #
      # Add a listener for the 'client run failed' event.
      #
      # @param notification_block The callback (takes |run_status| parameter).
      # @yieldparam [Chef::RunStatus] run_status The run status.
      #
      def when_run_fails(&notification_block)
        run_failed_notifications << notification_block
      end

      #
      # Clears all listeners for client run status events.
      #
      # Primarily for testing purposes.
      #
      # @api private
      #
      def clear_notifications
        @run_start_notifications = nil
        @run_completed_successfully_notifications = nil
        @run_failed_notifications = nil
      end

      #
      # TODO These seem protected to me.
      #

      #
      # Listeners to be run when the client run starts.
      #
      # @return [Array<Proc>]
      #
      # @api private
      #
      def run_start_notifications
        @run_start_notifications ||= []
      end

      #
      # Listeners to be run when the client run completes successfully.
      #
      # @return [Array<Proc>]
      #
      # @api private
      #
      def run_completed_successfully_notifications
        @run_completed_successfully_notifications ||= []
      end

      #
      # Listeners to be run when the client run fails.
      #
      # @return [Array<Proc>]
      #
      # @api private
      #
      def run_failed_notifications
        @run_failed_notifications ||= []
      end
    end

    #
    # IO stream that will be used as 'STDOUT' for formatters. Formatters are
    # configured during `initialize`, so this provides a convenience for
    # setting alternative IO stream during tests.
    #
    # @api private
    #
    STDOUT_FD = STDOUT

    #
    # IO stream that will be used as 'STDERR' for formatters. Formatters are
    # configured during `initialize`, so this provides a convenience for
    # setting alternative IO stream during tests.
    #
    # @api private
    #
    STDERR_FD = STDERR

    private

    attr_reader :override_runlist
    attr_reader :specific_recipes

    def profiling_prereqs!
      require "ruby-prof"
    rescue LoadError
      raise "You must have the ruby-prof gem installed in order to use --profile-ruby"
    end

    def start_profiling
      return unless Chef::Config[:profile_ruby]

      profiling_prereqs!
      RubyProf.start
    end

    def end_profiling
      return unless Chef::Config[:profile_ruby]

      profiling_prereqs!
      path = Chef::FileCache.create_cache_path("graph_profile.out", false)
      File.open(path, "w+") do |file|
        RubyProf::GraphPrinter.new(RubyProf.stop).print(file, {})
      end
      logger.warn("Ruby execution profile dumped to #{path}")
    end

    def empty_directory?(path)
      !File.exists?(path) || (Dir.entries(path).size <= 2)
    end

    def is_last_element?(index, object)
      object.is_a?(Array) ? index == object.size - 1 : true
    end

    def assert_cookbook_path_not_empty(run_context)
      if Chef::Config[:solo_legacy_mode]
        # Check for cookbooks in the path given
        # Chef::Config[:cookbook_path] can be a string or an array
        # if it's an array, go through it and check each one, raise error at the last one if no files are found
        cookbook_paths = Array(Chef::Config[:cookbook_path])
        logger.trace "Loading from cookbook_path: #{cookbook_paths.map { |path| File.expand_path(path) }.join(", ")}"
        if cookbook_paths.all? { |path| empty_directory?(path) }
          msg = "None of the cookbook paths set in Chef::Config[:cookbook_path], #{cookbook_paths.inspect}, contain any cookbooks"
          logger.fatal(msg)
          raise Chef::Exceptions::CookbookNotFound, msg
        end
      else
        logger.warn("Node #{node_name} has an empty run list.") if run_context.node.run_list.empty?
      end
    end

    def has_admin_privileges?
      require_relative "win32/security"

      Chef::ReservedNames::Win32::Security.has_admin_privileges?
    end
  end
end

# HACK cannot load this first, but it must be loaded.
require_relative "cookbook_loader"
require_relative "cookbook_version"
require_relative "cookbook/synchronizer"
