# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require "log4r"

module Vagrant
  module Action
    module Builtin
      # This class creates a multi-process lock using `flock`. The lock
      # is active for the remainder of the middleware stack.
      class Lock
        def initialize(app, env, options=nil)
          @app     = app
          @logger  = Log4r::Logger.new("vagrant::action::builtin::lock")
          @options ||= options || {}
          raise ArgumentError, "Please specify a lock path" if !@options[:path]
          raise ArgumentError, "Please specify an exception." if !@options[:exception]
        end

        def call(env)
          lock_path = @options[:path]
          lock_path = lock_path.call(env) if lock_path.is_a?(Proc)

          env_key   = "has_lock_#{lock_path}"

          if !env[env_key]
            # If we already have the key in our environment we assume the
            # lock is held by our middleware stack already and we allow
            # nesting.
            File.open(lock_path, "w+") do |f|
              # The file locking fails only if it returns "false." If it
              # succeeds it returns a 0, so we must explicitly check for
              # the proper error case.
              @logger.info("Locking: #{lock_path}")
              if f.flock(File::LOCK_EX | File::LOCK_NB) === false
                exception = @options[:exception]
                exception = exception.call(env) if exception.is_a?(Proc)
                raise exception
              end

              # Set that we gained the lock and call deeper into the
              # middleware, but make sure we UNSET the lock when we leave.
              begin
                env[env_key] = true
                @app.call(env)
              ensure
                @logger.info("Unlocking: #{lock_path}")
                env[env_key] = false
                f.flock(File::LOCK_UN)
              end
            end
          else
            # Just call up the middleware because we already hold the lock
            @app.call(env)
          end
        end
      end
    end
  end
end
