class Vagrant::Util::PowerShell

Executes PowerShell scripts.

This is primarily a convenience wrapper around Subprocess that properly sets powershell flags for you.

Constants

DEFAULT_VERSION_DETECTION_TIMEOUT

Number of seconds to wait while attempting to get powershell version

LOGGER
MINIMUM_REQUIRED_VERSION

NOTE: Version checks are only on Major

POWERSHELL_NAMES

Names of the powershell executable

POWERSHELL_PATHS

Paths to powershell executable

Public Class Methods

available?() click to toggle source

@return [Boolean] powershell executable available on PATH

# File lib/vagrant/util/powershell.rb, line 63
def self.available?
  !executable.nil?
end
executable() click to toggle source

@return [String|nil] a powershell executable, depending on environment

# File lib/vagrant/util/powershell.rb, line 30
def self.executable
  if !defined?(@_powershell_executable)
    # First start with detecting executable on configured path
    POWERSHELL_NAMES.detect do |psh|
      return @_powershell_executable = psh if Which.which(psh)
      psh += ".exe"
      return @_powershell_executable = psh if Which.which(psh)
    end

    # Now attempt with paths
    paths = POWERSHELL_PATHS.map do |ppath|
      result = Util::Subprocess.execute("cmd.exe", "/c", "echo #{ppath}")
      result.stdout.gsub("\"", "").strip if result.exit_code == 0
    end.compact

    paths.each do |psh_path|
      POWERSHELL_NAMES.each do |psh|
        path = File.join(psh_path, psh)
        return @_powershell_executable = path if Which.which(path)

        path += ".exe"
        return @_powershell_executable = path if Which.which(path)

        # Finally test the msys2 style path
        path = path.sub(/^([A-Za-z]):/, "/mnt/\\1")
        return @_powershell_executable = path if Which.which(path)
      end
    end
  end
  @_powershell_executable
end
execute(path, *args, **opts, &block) click to toggle source

Execute a powershell script.

@param [String] path Path to the PowerShell script to execute. @param [Array<String>] args Command arguments @param [Hash] opts Options passed to execute @option opts [Hash] :env Custom environment variables @return [Subprocess::Result]

# File lib/vagrant/util/powershell.rb, line 74
def self.execute(path, *args, **opts, &block)
  validate_install!
  if opts.delete(:sudo) || opts.delete(:runas)
    powerup_command(path, args, opts)
  else
    if mpath = opts.delete(:module_path)
      m_env = opts.fetch(:env, {})
      m_env["PSModulePath"] = "$env:PSModulePath+';#{mpath}'"
      opts[:env] = m_env
    end
    if env = opts.delete(:env)
      env = env.map{|k,v| "$env:#{k}=#{v}"}.join(";") + "; "
    end
    command = [
      executable,
      "-NoLogo",
      "-NoProfile",
      "-NonInteractive",
      "-ExecutionPolicy", "Bypass",
      "#{env}&('#{path}')",
      args
    ].flatten

    # Append on the options hash since Subprocess doesn't use
    # Ruby 2.0 style options yet.
    command << opts

    Subprocess.execute(*command, &block)
  end
end
execute_cmd(command, **opts) click to toggle source

Execute a powershell command.

@param [String] command PowerShell command to execute. @param [Hash] opts Extra options @option opts [Hash] :env Custom environment variables @return [nil, String] Returns nil if exit code is non-zero.

Returns stdout string if exit code is zero.
# File lib/vagrant/util/powershell.rb, line 112
def self.execute_cmd(command, **opts)
  validate_install!
  if mpath = opts.delete(:module_path)
    m_env = opts.fetch(:env, {})
    m_env["PSModulePath"] = "$env:PSModulePath+';#{mpath}'"
    opts[:env] = m_env
  end
  if env = opts.delete(:env)
    env = env.map{|k,v| "$env:#{k}=#{v}"}.join(";") + "; "
  end
  c = [
    executable,
    "-NoLogo",
    "-NoProfile",
    "-NonInteractive",
    "-ExecutionPolicy", "Bypass",
    "-Command",
    "#{env}#{command}"
  ].flatten.compact

  r = Subprocess.execute(*c)
  return nil if r.exit_code != 0
  return r.stdout.chomp
end
execute_inline(*command, **opts, &block) click to toggle source

Execute a powershell command and return a result

@param [String] command PowerShell command to execute. @param [Hash] opts A collection of options for subprocess::execute @option opts [Hash] :env Custom environment variables @param [Block] block Ruby block

# File lib/vagrant/util/powershell.rb, line 143
def self.execute_inline(*command, **opts, &block)
  validate_install!
  if mpath = opts.delete(:module_path)
    m_env = opts.fetch(:env, {})
    m_env["PSModulePath"] = "$env:PSModulePath+';#{mpath}'"
    opts[:env] = m_env
  end
  if env = opts.delete(:env)
    env = env.map{|k,v| "$env:#{k}=#{v}"}.join(";") + "; "
  end

  command = command.join(' ')

  c = [
    executable,
    "-NoLogo",
    "-NoProfile",
    "-NonInteractive",
    "-ExecutionPolicy", "Bypass",
    "-Command",
    "#{env}#{command}"
  ].flatten.compact
  c << opts

  Subprocess.execute(*c, &block)
end
powerup_command(path, args, opts) click to toggle source

Powerup the given command to perform privileged operations.

@param [String] path @param [Array<String>] args @return [Array<String>]

# File lib/vagrant/util/powershell.rb, line 227
def self.powerup_command(path, args, opts)
  Dir.mktmpdir("vagrant") do |dpath|
    all_args = [path] + args.flatten.map{ |a|
      a.gsub(/^['"](.+)['"]$/, "\\1")
    }
    arg_list = "\"" + all_args.join("\" \"") + "\""
    stdout = File.join(dpath, "stdout.txt")
    stderr = File.join(dpath, "stderr.txt")

    script = "& #{arg_list} ; exit $LASTEXITCODE;"
    script_content = Base64.strict_encode64(script.encode("UTF-16LE", "UTF-8"))

    # Wrap so we can redirect output to read later
    wrapper = "$p = Start-Process -FilePath powershell -ArgumentList @('-NoLogo', '-NoProfile', " \
      "'-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', '#{script_content}') " \
      "-PassThru -WindowStyle Hidden -Wait -RedirectStandardOutput '#{stdout}' -RedirectStandardError '#{stderr}'; " \
      "if($p){ exit $p.ExitCode; }else{ exit 1 }"
    wrapper_content = Base64.strict_encode64(wrapper.encode("UTF-16LE", "UTF-8"))

    powerup = "$p = Start-Process -FilePath powershell -ArgumentList @('-NoLogo', '-NoProfile', " \
      "'-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', '#{wrapper_content}') " \
      "-PassThru -WindowStyle Hidden -Wait -Verb RunAs; if($p){ exit $p.ExitCode; }else{ exit 1 }"

    cmd = [
      executable,
      "-NoLogo",
      "-NoProfile",
      "-NonInteractive",
      "-ExecutionPolicy", "Bypass",
      "-Command", powerup
    ]

    result = Subprocess.execute(*cmd.push(opts))
    r_stdout = result.stdout
    if File.exist?(stdout)
      r_stdout += File.read(stdout)
    end
    r_stderr = result.stderr
    if File.exist?(stderr)
      r_stderr += File.read(stderr)
    end

    Subprocess::Result.new(result.exit_code, r_stdout, r_stderr)
  end
end
reset!() click to toggle source

@private Reset the cached values for platform. This is not considered a public API and should only be used for testing.

# File lib/vagrant/util/powershell.rb, line 276
def self.reset!
  instance_variables.each(&method(:remove_instance_variable))
end
validate_install!() click to toggle source

Validates that powershell is installed, available, and at or above minimum required version

@return [Boolean] @raises []

# File lib/vagrant/util/powershell.rb, line 209
def self.validate_install!
  if !defined?(@_powershell_validation)
    raise Errors::PowerShellNotFound if !available?
    if version.to_i < MINIMUM_REQUIRED_VERSION
      raise Errors::PowerShellInvalidVersion,
        minimum_version: MINIMUM_REQUIRED_VERSION,
        installed_version: version ? version : "N/A"
    end
    @_powershell_validation = true
  end
  @_powershell_validation
end
version() click to toggle source

Returns the version of PowerShell that is installed.

@return [String]

# File lib/vagrant/util/powershell.rb, line 173
def self.version
  if !defined?(@_powershell_version)
    command = [
      executable,
      "-NoLogo",
      "-NoProfile",
      "-NonInteractive",
      "-ExecutionPolicy", "Bypass",
      "-Command",
      "Write-Output $PSVersionTable.PSVersion.Major"
    ].flatten

    version = nil
    timeout = ENV["VAGRANT_POWERSHELL_VERSION_DETECTION_TIMEOUT"].to_i
    if timeout < 1
      timeout = DEFAULT_VERSION_DETECTION_TIMEOUT
    end
    begin
      r = Subprocess.execute(*command,
        notify: [:stdout, :stderr],
        timeout: timeout,
      ) {|io_name,data| version = data}
    rescue Vagrant::Util::Subprocess::TimeoutExceeded
      LOGGER.debug("Timeout exceeded while attempting to determine version of Powershell.")
    end

    @_powershell_version = version
  end
  @_powershell_version
end