vaguerent/lib/vagrant/util/powershell.rb
Brian Cain e073153728
(#10099) Properly join commands from passed in array
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.
2018-08-14 09:30:11 -07:00

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