Prior to this commit, if the ssh-config command was invoked within cygwin or msys2, it would show a regular windows style path for private keys rather than a path that could be used within msys2 or cygwin. This commit updates that behavior by converting all of the private key paths to the proper msys2 or cygwin path if the platform is windows and the command was invoked from one of those two shells.
461 lines
16 KiB
Ruby
461 lines
16 KiB
Ruby
require "rbconfig"
|
|
require "shellwords"
|
|
require "tmpdir"
|
|
|
|
require "vagrant/util/subprocess"
|
|
require "vagrant/util/powershell"
|
|
require "vagrant/util/which"
|
|
|
|
module Vagrant
|
|
module Util
|
|
# This class just contains some platform checking code.
|
|
class Platform
|
|
class << self
|
|
def cygwin?
|
|
if !defined?(@_cygwin)
|
|
@_cygwin = ENV["VAGRANT_DETECTED_OS"].to_s.downcase.include?("cygwin") ||
|
|
platform.include?("cygwin") ||
|
|
ENV["OSTYPE"].to_s.downcase.include?("cygwin")
|
|
end
|
|
@_cygwin
|
|
end
|
|
|
|
def msys?
|
|
if !defined?(@_msys)
|
|
@_msys = ENV["VAGRANT_DETECTED_OS"].to_s.downcase.include?("msys") ||
|
|
platform.include?("msys") ||
|
|
ENV["OSTYPE"].to_s.downcase.include?("msys")
|
|
end
|
|
@_msys
|
|
end
|
|
|
|
def wsl?
|
|
if !defined?(@_wsl)
|
|
@_wsl = false
|
|
SilenceWarnings.silence! do
|
|
# Use PATH values to check for `/mnt/c` path indicative of WSL
|
|
if ENV.fetch("PATH", "").downcase.include?("/mnt/c")
|
|
# Validate WSL via uname output
|
|
uname = Subprocess.execute("uname", "-r")
|
|
if uname.exit_code == 0 && uname.stdout.downcase.include?("microsoft")
|
|
@_wsl = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
@_wsl
|
|
end
|
|
|
|
[:darwin, :bsd, :freebsd, :linux, :solaris].each do |type|
|
|
define_method("#{type}?") do
|
|
platform.include?(type.to_s)
|
|
end
|
|
end
|
|
|
|
def windows?
|
|
return @_windows if defined?(@_windows)
|
|
@_windows = %w[mingw mswin].any? { |t| platform.include?(t) }
|
|
return @_windows
|
|
end
|
|
|
|
# Checks if the user running Vagrant on Windows has administrative
|
|
# privileges.
|
|
#
|
|
# From: https://support.microsoft.com/en-us/kb/243330
|
|
# SID: S-1-5-19
|
|
#
|
|
# @return [Boolean]
|
|
def windows_admin?
|
|
return @_windows_admin if defined?(@_windows_admin)
|
|
|
|
@_windows_admin = -> {
|
|
ps_cmd = '(new-object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)'
|
|
output = Vagrant::Util::PowerShell.execute_cmd(ps_cmd)
|
|
return output == 'True'
|
|
}.call
|
|
|
|
return @_windows_admin
|
|
end
|
|
|
|
# Checks if the user running Vagrant on Windows is a member of the
|
|
# "Hyper-V Administrators" group.
|
|
#
|
|
# From: https://support.microsoft.com/en-us/kb/243330
|
|
# SID: S-1-5-32-578
|
|
# Name: BUILTIN\Hyper-V Administrators
|
|
#
|
|
# @return [Boolean]
|
|
def windows_hyperv_admin?
|
|
return @_windows_hyperv_admin if defined?(@_windows_hyperv_admin)
|
|
|
|
@_windows_hyperv_admin = -> {
|
|
ps_cmd = "[System.Security.Principal.WindowsIdentity]::GetCurrent().Groups | ForEach-Object { if ($_.Value -eq 'S-1-5-32-578'){ Write-Host 'true'; break }}"
|
|
output = Vagrant::Util::PowerShell.execute_cmd(ps_cmd)
|
|
return output == 'true'
|
|
}.call
|
|
|
|
return @_windows_hyperv_admin
|
|
end
|
|
|
|
# This takes any path and converts it from a Windows path to a
|
|
# Cygwin or msys style path.
|
|
#
|
|
# @param [String] path
|
|
# @return [String]
|
|
def cygwin_path(path)
|
|
begin
|
|
# We have to revert to the old env
|
|
# path here, otherwise it looks like
|
|
# msys2 ends up using the wrong cygpath
|
|
# binary and ends up with a `/cygdrive`
|
|
# when it doesn't exist in msys2
|
|
original_path_env = ENV['PATH']
|
|
ENV['PATH'] = ENV['VAGRANT_OLD_ENV_PATH']
|
|
cygpath = Vagrant::Util::Which.which("cygpath")
|
|
cygpath.gsub!("/", '\\')
|
|
process = Subprocess.execute(
|
|
cygpath, "-u", "-a", path.to_s)
|
|
return process.stdout.chomp
|
|
rescue Errors::CommandUnavailableWindows => e
|
|
# Sometimes cygpath isn't available (msys). Instead, do what we
|
|
# can with bash tricks.
|
|
process = Subprocess.execute(
|
|
"bash",
|
|
"--noprofile",
|
|
"--norc",
|
|
"-c", "cd #{Shellwords.escape(path)} && pwd")
|
|
return process.stdout.chomp
|
|
ensure
|
|
ENV['PATH'] = original_path_env
|
|
end
|
|
end
|
|
|
|
# Identical to cygwin_path for now
|
|
alias_method :msys_path, :cygwin_path
|
|
|
|
# This takes any path and converts it to a full-length Windows
|
|
# path on Windows machines in Cygwin.
|
|
#
|
|
# @return [String]
|
|
def cygwin_windows_path(path)
|
|
return path if !cygwin?
|
|
|
|
# Replace all "\" with "/", otherwise cygpath doesn't work.
|
|
path = path.gsub("\\", "/")
|
|
|
|
# Call out to cygpath and gather the result
|
|
process = Subprocess.execute("cygpath", "-w", "-l", "-a", path.to_s)
|
|
return process.stdout.chomp
|
|
end
|
|
|
|
# This checks if the filesystem is case sensitive. This is not a
|
|
# 100% correct check, since it is possible that the temporary
|
|
# directory runs a different filesystem than the root directory.
|
|
# However, this works in many cases.
|
|
def fs_case_sensitive?
|
|
return @_fs_case_sensitive if defined?(@_fs_case_sensitive)
|
|
@_fs_case_sensitive = Dir.mktmpdir("vagrant-fs-case-sensitive") do |dir|
|
|
tmp_file = File.join(dir, "FILE")
|
|
File.open(tmp_file, "w") do |f|
|
|
f.write("foo")
|
|
end
|
|
|
|
# The filesystem is case sensitive if the lowercased version
|
|
# of the filename is NOT reported as existing.
|
|
!File.file?(File.join(dir, "file"))
|
|
end
|
|
return @_fs_case_sensitive
|
|
end
|
|
|
|
# This expands the path and ensures proper casing of each part
|
|
# of the path.
|
|
def fs_real_path(path, **opts)
|
|
path = Pathname.new(File.expand_path(path))
|
|
|
|
if path.exist? && !fs_case_sensitive?
|
|
# If the path contains a Windows short path, then we attempt to
|
|
# expand. The require below is embedded here since it requires
|
|
# windows to work.
|
|
if windows? && path.to_s =~ /~\d(\/|\\)/
|
|
require_relative "windows_path"
|
|
path = Pathname.new(WindowsPath.longname(path.to_s))
|
|
end
|
|
|
|
# Build up all the parts of the path
|
|
original = []
|
|
while !path.root?
|
|
original.unshift(path.basename.to_s)
|
|
path = path.parent
|
|
end
|
|
|
|
# Traverse each part and join it into the resulting path
|
|
original.each do |single|
|
|
Dir.entries(path).each do |entry|
|
|
if entry.downcase == single.encode('filesystem').downcase
|
|
path = path.join(entry)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if windows?
|
|
# Fix the drive letter to be uppercase.
|
|
path = path.to_s
|
|
if path[1] == ":"
|
|
path[0] = path[0].upcase
|
|
end
|
|
|
|
path = Pathname.new(path)
|
|
end
|
|
|
|
path
|
|
end
|
|
|
|
# Converts a given path to UNC format by adding a prefix and converting slashes.
|
|
# @param [String] path Path to convert to UNC for Windows
|
|
# @return [String]
|
|
def windows_unc_path(path)
|
|
path = path.gsub("/", "\\")
|
|
|
|
# Convert to UNC path
|
|
if path =~ /^[a-zA-Z]:\\?$/
|
|
# If the path is just a drive letter, then return that as-is
|
|
path + "\\"
|
|
elsif path.start_with?("\\\\")
|
|
# If the path already starts with `\\` assume UNC and return as-is
|
|
path
|
|
else
|
|
"\\\\?\\" + path.gsub("/", "\\")
|
|
end
|
|
end
|
|
|
|
# Returns a boolean noting whether the terminal supports color.
|
|
# output.
|
|
def terminal_supports_colors?
|
|
return @_terminal_supports_colors if defined?(@_terminal_supports_colors)
|
|
@_terminal_supports_colors = -> {
|
|
if windows?
|
|
return true if ENV.key?("ANSICON")
|
|
return true if cygwin?
|
|
return true if ENV["TERM"] == "cygwin"
|
|
return false
|
|
end
|
|
|
|
return true
|
|
}.call
|
|
return @_terminal_supports_colors
|
|
end
|
|
|
|
def platform
|
|
return @_platform if defined?(@_platform)
|
|
@_platform = RbConfig::CONFIG["host_os"].downcase
|
|
return @_platform
|
|
end
|
|
|
|
# Determine if given path is within the WSL rootfs. Returns
|
|
# true if within the subsystem, or false if outside the subsystem.
|
|
#
|
|
# @param [String] path Path to check
|
|
# @return [Boolean] path is within subsystem
|
|
def wsl_path?(path)
|
|
wsl? && !path.to_s.downcase.start_with?("/mnt/")
|
|
end
|
|
|
|
# Convert a WSL path to the local Windows path. This is useful
|
|
# for conversion when calling out to Windows executables from
|
|
# the WSL
|
|
#
|
|
# @param [String, Pathname] path Path to convert
|
|
# @return [String]
|
|
def wsl_to_windows_path(path)
|
|
if wsl? && wsl_windows_access?
|
|
if wsl_path?(path)
|
|
parts = path.split("/")
|
|
parts.delete_if(&:empty?)
|
|
[wsl_windows_appdata_local, "lxss", *parts].join("\\")
|
|
else
|
|
path = path.to_s.sub("/mnt/", "")
|
|
parts = path.split("/")
|
|
parts.first << ":"
|
|
path = parts.join("\\")
|
|
path
|
|
end
|
|
else
|
|
path
|
|
end
|
|
end
|
|
|
|
# Takes a windows path and formats it to the
|
|
# 'unix' style (i.e. `/cygdrive/c` or `/c/`)
|
|
#
|
|
# @param [Pathname, String] path Path to convert
|
|
# @param [Hash] hash of arguments
|
|
# @return [String]
|
|
def format_windows_path(path, *args)
|
|
path = cygwin_path(path) if cygwin?
|
|
path = msys_path(path) if msys?
|
|
path = wsl_to_windows_path(path) if wsl?
|
|
if windows? || wsl?
|
|
path = windows_unc_path(path) if !args.include?(:disable_unc)
|
|
end
|
|
|
|
path
|
|
end
|
|
|
|
# Automatically convert a given path to a Windows path. Will only
|
|
# be applied if running on a Windows host. If running on Windows
|
|
# host within the WSL, the actual Windows path will be returned.
|
|
#
|
|
# @param [Pathname, String] path Path to convert
|
|
# @return [String]
|
|
def windows_path(path)
|
|
path = cygwin_windows_path(path)
|
|
path = wsl_to_windows_path(path)
|
|
if windows? || wsl?
|
|
path = windows_unc_path(path)
|
|
end
|
|
path
|
|
end
|
|
|
|
# Allow Vagrant to access Vagrant managed machines outside the
|
|
# Windows Subsystem for Linux
|
|
#
|
|
# @return [Boolean]
|
|
def wsl_windows_access?
|
|
if !defined?(@_wsl_windows_access)
|
|
@_wsl_windows_access = wsl? && ENV["VAGRANT_WSL_ENABLE_WINDOWS_ACCESS"]
|
|
end
|
|
@_wsl_windows_access
|
|
end
|
|
|
|
# The allowed windows system path Vagrant can manage from the Windows
|
|
# Subsystem for Linux
|
|
#
|
|
# @return [Pathname]
|
|
def wsl_windows_accessible_path
|
|
if !defined?(@_wsl_windows_accessible_path)
|
|
access_path = ENV["VAGRANT_WSL_WINDOWS_ACCESS_USER_HOME_PATH"]
|
|
if access_path.to_s.empty?
|
|
access_path = wsl_windows_home.gsub("\\", "/").sub(":", "")
|
|
access_path[0] = access_path[0].downcase
|
|
access_path = "/mnt/#{access_path}"
|
|
end
|
|
@_wsl_windows_accessible_path = Pathname.new(access_path)
|
|
end
|
|
@_wsl_windows_accessible_path
|
|
end
|
|
|
|
# Checks given path to determine if Vagrant is allowed to bypass checks
|
|
#
|
|
# @param [String] path Path to check
|
|
# @return [Boolean] Vagrant is allowed to bypass checks
|
|
def wsl_windows_access_bypass?(path)
|
|
wsl? && wsl_windows_access? &&
|
|
path.to_s.start_with?(wsl_windows_accessible_path.to_s)
|
|
end
|
|
|
|
# If running within the Windows Subsystem for Linux, this will provide
|
|
# simple setup to allow sharing of the user's VAGRANT_HOME directory
|
|
# within the subsystem
|
|
#
|
|
# @param [Environment] env
|
|
# @param [Logger] logger Optional logger to display information
|
|
def wsl_init(env, logger=nil)
|
|
if wsl?
|
|
if ENV["VAGRANT_WSL_ENABLE_WINDOWS_ACCESS"]
|
|
wsl_validate_matching_vagrant_versions!
|
|
shared_user = ENV["VAGRANT_WSL_WINDOWS_ACCESS_USER"]
|
|
if shared_user.to_s.empty?
|
|
shared_user = wsl_windows_username
|
|
end
|
|
if logger
|
|
logger.warn("Windows Subsystem for Linux detected. Allowing access to user: #{shared_user}")
|
|
logger.warn("Vagrant will be allowed to control Vagrant managed machines within the user's home path.")
|
|
end
|
|
if ENV["VAGRANT_HOME"] || ENV["VAGRANT_WSL_DISABLE_VAGRANT_HOME"]
|
|
logger.warn("VAGRANT_HOME environment variable already set. Not overriding!") if logger
|
|
else
|
|
home_path = wsl_windows_accessible_path.to_s
|
|
ENV["VAGRANT_HOME"] = File.join(home_path, ".vagrant.d")
|
|
if logger
|
|
logger.info("Overriding VAGRANT_HOME environment variable to configured windows user. (#{ENV["VAGRANT_HOME"]})")
|
|
end
|
|
true
|
|
end
|
|
else
|
|
if env.local_data_path.to_s.start_with?("/mnt/")
|
|
raise Vagrant::Errors::WSLVagrantAccessError
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Fetch the Windows username currently in use
|
|
#
|
|
# @return [String, Nil]
|
|
def wsl_windows_username
|
|
if !@_wsl_windows_username
|
|
result = Util::Subprocess.execute("cmd.exe", "/c", "echo %USERNAME%")
|
|
if result.exit_code == 0
|
|
@_wsl_windows_username = result.stdout.strip
|
|
end
|
|
end
|
|
@_wsl_windows_username
|
|
end
|
|
|
|
# Fetch the Windows user home directory
|
|
#
|
|
# @return [String, Nil]
|
|
def wsl_windows_home
|
|
if !@_wsl_windows_home
|
|
result = Util::Subprocess.execute("cmd.exe", "/c" "echo %USERPROFILE%")
|
|
if result.exit_code == 0
|
|
@_wsl_windows_home = result.stdout.gsub("\"", "").strip
|
|
end
|
|
end
|
|
@_wsl_windows_home
|
|
end
|
|
|
|
# Fetch the Windows user local app data directory
|
|
#
|
|
# @return [String, Nil]
|
|
def wsl_windows_appdata_local
|
|
if !@_wsl_windows_appdata_local
|
|
result = Util::Subprocess.execute("cmd.exe", "/c", "echo %LOCALAPPDATA%")
|
|
if result.exit_code == 0
|
|
@_wsl_windows_appdata_local = result.stdout.gsub("\"", "").strip
|
|
end
|
|
end
|
|
@_wsl_windows_appdata_local
|
|
end
|
|
|
|
# Confirm Vagrant versions installed within the WSL and the Windows system
|
|
# are the same. Raise error if they do not match.
|
|
def wsl_validate_matching_vagrant_versions!
|
|
valid = false
|
|
result = Util::Subprocess.execute("vagrant.exe", "version")
|
|
if result.exit_code == 0
|
|
windows_version = result.stdout.match(/Installed Version: (?<version>[\w.-]+)/)
|
|
if windows_version
|
|
windows_version = windows_version[:version].strip
|
|
valid = windows_version == Vagrant::VERSION
|
|
end
|
|
end
|
|
if !valid
|
|
raise Vagrant::Errors::WSLVagrantVersionMismatch,
|
|
wsl_version: Vagrant::VERSION,
|
|
windows_version: windows_version || "unknown"
|
|
end
|
|
end
|
|
|
|
# @private
|
|
# Reset the cached values for platform. This is not considered a public
|
|
# API and should only be used for testing.
|
|
def reset!
|
|
instance_variables.each(&method(:remove_instance_variable))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|