Prior to this commit, the `Util::Powershell.execute_inline` method didn't properly join the passed in command and instead attempted to execute the array of strings. This commit updates that behavior to join the command array prior to inserting it into the full powershell command.
267 lines
8.8 KiB
Ruby
267 lines
8.8 KiB
Ruby
require "tmpdir"
|
|
require_relative "subprocess"
|
|
require_relative "which"
|
|
|
|
module Vagrant
|
|
module Util
|
|
# Executes PowerShell scripts.
|
|
#
|
|
# This is primarily a convenience wrapper around Subprocess that
|
|
# properly sets powershell flags for you.
|
|
class PowerShell
|
|
# NOTE: Version checks are only on Major
|
|
MINIMUM_REQUIRED_VERSION = 3
|
|
# Number of seconds to wait while attempting to get powershell version
|
|
DEFAULT_VERSION_DETECTION_TIMEOUT = 30
|
|
LOGGER = Log4r::Logger.new("vagrant::util::powershell")
|
|
|
|
# @return [String|nil] a powershell executable, depending on environment
|
|
def self.executable
|
|
if !defined?(@_powershell_executable)
|
|
@_powershell_executable = "powershell"
|
|
|
|
if Which.which(@_powershell_executable).nil?
|
|
# Try to use WSL interoperability if PowerShell is not symlinked to
|
|
# the container.
|
|
if Platform.wsl?
|
|
@_powershell_executable += ".exe"
|
|
|
|
if Which.which(@_powershell_executable).nil?
|
|
@_powershell_executable = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"
|
|
|
|
if Which.which(@_powershell_executable).nil?
|
|
@_powershell_executable = nil
|
|
end
|
|
end
|
|
else
|
|
@_powershell_executable = nil
|
|
end
|
|
end
|
|
end
|
|
@_powershell_executable
|
|
end
|
|
|
|
# @return [Boolean] powershell executable available on PATH
|
|
def self.available?
|
|
!executable.nil?
|
|
end
|
|
|
|
# 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]
|
|
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 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.
|
|
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 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
|
|
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
|
|
|
|
# Returns the version of PowerShell that is installed.
|
|
#
|
|
# @return [String]
|
|
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
|
|
|
|
# Validates that powershell is installed, available, and
|
|
# at or above minimum required version
|
|
#
|
|
# @return [Boolean]
|
|
# @raises []
|
|
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
|
|
|
|
# Powerup the given command to perform privileged operations.
|
|
#
|
|
# @param [String] path
|
|
# @param [Array<String>] args
|
|
# @return [Array<String>]
|
|
def self.powerup_command(path, args, opts)
|
|
Dir.mktmpdir("vagrant") do |dpath|
|
|
all_args = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", path] + args
|
|
arg_list = "@('" + all_args.join("', '") + "')"
|
|
stdout = File.join(dpath, "stdout.txt")
|
|
stderr = File.join(dpath, "stderr.txt")
|
|
exitcode = File.join(dpath, "exitcode.txt")
|
|
|
|
script = "$sp = Start-Process -FilePath powershell -ArgumentList #{arg_list} " \
|
|
"-PassThru -Wait -RedirectStandardOutput '#{stdout}' -RedirectStandardError '#{stderr}' -WindowStyle Hidden; " \
|
|
"if($sp){ Set-Content -Path '#{exitcode}' -Value $sp.ExitCode;exit $sp.ExitCode; }else{ exit 1 }"
|
|
|
|
# escape quotes so we can nest our script within a start-process
|
|
script.gsub!("'", "''")
|
|
|
|
cmd = [
|
|
"powershell",
|
|
"-NoLogo",
|
|
"-NoProfile",
|
|
"-NonInteractive",
|
|
"-ExecutionPolicy", "Bypass",
|
|
"-Command", "$p = Start-Process -FilePath powershell -ArgumentList " \
|
|
"@('-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', '#{script}') " \
|
|
"-PassThru -Wait -WindowStyle Hidden -Verb RunAs; if($p){ exit $p.ExitCode; }else{ exit 1 }"
|
|
]
|
|
|
|
result = Subprocess.execute(*cmd.push(opts))
|
|
if File.exist?(stdout)
|
|
r_stdout = File.read(stdout)
|
|
else
|
|
r_stdout = result.stdout
|
|
end
|
|
if File.exist?(stderr)
|
|
r_stderr = File.read(stderr)
|
|
else
|
|
r_stderr = result.stderr
|
|
end
|
|
|
|
code = 1
|
|
if File.exist?(exitcode)
|
|
code_txt = File.read(exitcode).strip
|
|
if code_txt.match(/^\d+$/)
|
|
code = code_txt.to_i
|
|
end
|
|
end
|
|
Subprocess::Result.new(code, r_stdout, r_stderr)
|
|
end
|
|
end
|
|
|
|
# @private
|
|
# Reset the cached values for platform. This is not considered a public
|
|
# API and should only be used for testing.
|
|
def self.reset!
|
|
instance_variables.each(&method(:remove_instance_variable))
|
|
end
|
|
end
|
|
end
|
|
end
|