Merge pull request #9525 from chrisroberts/wsl-update

Support updated WSL implementation
This commit is contained in:
Chris Roberts 2018-02-28 12:12:17 -08:00 committed by GitHub
commit ae54756d1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 257 additions and 24 deletions

1
.gitignore vendored
View File

@ -23,6 +23,7 @@ tags
test/tmp/
vendor/
/exec
.ruby-bundle
# Documentation
_site/*

View File

@ -947,5 +947,9 @@ module Vagrant
class WSLVirtualBoxWindowsAccessError < VagrantError
error_key(:wsl_virtualbox_windows_access)
end
class WSLRootFsNotFoundError < VagrantError
error_key(:wsl_rootfs_not_found_error)
end
end
end

View File

@ -1,6 +1,8 @@
require "rbconfig"
require "shellwords"
require "tempfile"
require "tmpdir"
require "log4r"
require "vagrant/util/subprocess"
require "vagrant/util/powershell"
@ -291,6 +293,87 @@ module Vagrant
wsl? && !path.to_s.downcase.start_with?("/mnt/")
end
# Compute the path to rootfs of currently active WSL.
#
# @return [String] A path to rootfs of a current WSL instance.
def wsl_rootfs
return @_wsl_rootfs if defined?(@_wsl_rootfs)
if wsl?
# Mark our filesystem with a temporary file having an unique name.
marker = Tempfile.new(Time.now.to_i.to_s)
logger = Log4r::Logger.new("vagrant::util::platform::wsl")
# Check for lxrun installation first
lxrun_path = [wsl_windows_appdata_local, "lxss"].join("\\")
paths = [lxrun_path]
logger.debug("checking registry for WSL installation path")
paths += PowerShell.execute_cmd(
'(Get-ChildItem HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss ' \
'| ForEach-Object {Get-ItemProperty $_.PSPath}).BasePath').to_s.split("\r\n").map(&:strip)
paths.delete_if{|path| path.to_s.empty?}
paths.each do |path|
# Lowercase the drive letter, skip the next symbol (which is a
# colon from a Windows path) and convert path to UNIX style.
check_path = "/mnt/#{path[0, 1].downcase}#{path[2..-1].tr('\\', '/')}/rootfs"
logger.debug("checking `#{path}` for current WSL instance")
begin
# https://blogs.msdn.microsoft.com/wsl/2016/06/15/wsl-file-system-support
# Current WSL instance doesn't have an access to its mount from
# within itself despite all others are available. That's the
# hacky way we're using to determine current instance.
# For example we have three WSL instances:
# A -> C:\User\USER\AppData\Local\Packages\A\LocalState\rootfs
# B -> C:\User\USER\AppData\Local\Packages\B\LocalState\rootfs
# C -> C:\User\USER\AppData\Local\Packages\C\LocalState\rootfs
# If we're in "A" WSL at the moment, then its path will not be
# accessible since it's mounted for exactly the instance we're
# in. All others can be opened.
Dir.open(check_path) do |fs|
# A fallback for a case if our trick will stop working. For
# that we've created a temporary file with an unique name in
# a current WSL and now seeking it among all WSL.
if File.exist?("#{fs.path}/#{marker.path}")
@_wsl_rootfs = path
break
end
end
rescue Errno::EACCES
@_wsl_rootfs = path
# You can create and simultaneously run multiple WSL instances,
# comment out the "break", run this script within each one and
# it'll return only single value.
break
rescue Errno::ENOENT
# Warn about data discrepancy between Winreg and file system
# states. For the sake of justice, it's worth mentioning that
# it is possible only when someone will manually break WSL by
# removing a directory of its base path (kinda "stupid WSL
# uninstallation by removing hidden and system directory").
logger.warn("WSL instance at `#{path} is broken or no longer exists")
end
# All other exceptions have to be raised since they will mean
# something unpredictably terrible.
end
marker.close!
raise Vagrant::Errors::WSLRootFsNotFoundError if @_wsl_rootfs.nil?
end
# Attach the rootfs leaf to the path
if @_wsl_rootfs != lxrun_path
@_wsl_rootfs = "#{@_wsl_rootfs}\\rootfs"
end
logger.debug("detected `#{@_wsl_rootfs}` as current WSL instance")
@_wsl_rootfs
end
# Convert a WSL path to the local Windows path. This is useful
# for conversion when calling out to Windows executables from
# the WSL
@ -298,11 +381,17 @@ module Vagrant
# @param [String, Pathname] path Path to convert
# @return [String]
def wsl_to_windows_path(path)
if wsl? && wsl_windows_access?
if wsl? && wsl_windows_access? && !path.match(/^[a-zA-Z]:/)
if wsl_path?(path)
parts = path.split("/")
parts.delete_if(&:empty?)
[wsl_windows_appdata_local, "lxss", *parts].join("\\")
root_path = wsl_rootfs
# lxrun splits home separate so we need to account
# for it's specialness here when we build the path
if root_path.end_with?("lxss") && parts.first != "home"
root_path = "#{root_path}\\rootfs"
end
[root_path, *parts].join("\\")
else
path = path.to_s.sub("/mnt/", "")
parts = path.split("/")

View File

@ -13,12 +13,27 @@ module Vagrant
MINIMUM_REQUIRED_VERSION = 3
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"
# Try to use WSL interoperability if PowerShell is not symlinked to
# the container.
if Which.which(@_powershell_executable).nil? && Platform.wsl?
@_powershell_executable += ".exe"
if Which.which(@_powershell_executable).nil?
@_powershell_executable = nil
end
end
end
@_powershell_executable
end
# @return [Boolean] powershell executable available on PATH
def self.available?
if !defined?(@_powershell_available)
@_powershell_available = !!Which.which("powershell")
end
@_powershell_available
!executable.nil?
end
# Execute a powershell script.
@ -27,12 +42,11 @@ module Vagrant
# @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
command = [
"powershell",
executable,
"-NoLogo",
"-NoProfile",
"-NonInteractive",
@ -57,7 +71,7 @@ module Vagrant
def self.execute_cmd(command)
validate_install!
c = [
"powershell",
executable,
"-NoLogo",
"-NoProfile",
"-NonInteractive",
@ -77,7 +91,7 @@ module Vagrant
def self.version
if !defined?(@_powershell_version)
command = [
"powershell",
executable,
"-NoLogo",
"-NoProfile",
"-NonInteractive",

View File

@ -1629,6 +1629,10 @@ en:
Linux, please refer to the Vagrant documentation:
https://www.vagrantup.com/docs/other/wsl.html
wsl_rootfs_not_found_error: |-
Vagrant is unable to determine the location of this instance of the Windows
Subsystem for Linux. If this error persists it may be resolved by destroying
this subsystem and installing it again.
#-------------------------------------------------------------------------------
# Translations for config validation errors
#-------------------------------------------------------------------------------

View File

@ -4,8 +4,7 @@ require "vagrant/util/platform"
describe Vagrant::Util::Platform do
include_context "unit"
after{ described_class.reset! }
subject { described_class }
describe "#cygwin_path" do
@ -55,11 +54,6 @@ describe Vagrant::Util::Platform do
describe "#cygwin?" do
before do
allow(subject).to receive(:platform).and_return("test")
described_class.reset!
end
after do
described_class.reset!
end
around do |example|
@ -99,11 +93,6 @@ describe Vagrant::Util::Platform do
describe "#msys?" do
before do
allow(subject).to receive(:platform).and_return("test")
described_class.reset!
end
after do
described_class.reset!
end
around do |example|
@ -162,7 +151,6 @@ describe Vagrant::Util::Platform do
describe ".systemd?" do
before{ allow(subject).to receive(:windows?).and_return(false) }
after{ subject.reset! }
context "on windows" do
before{ expect(subject).to receive(:windows?).and_return(true) }
@ -223,10 +211,142 @@ describe Vagrant::Util::Platform do
end
it "should return false if disabled" do
Vagrant::Util::Platform.reset!
allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_return('Disabled')
expect(Vagrant::Util::Platform.windows_hyperv_enabled?).to be_falsey
end
end
context "within the WSL" do
before{ allow(subject).to receive(:wsl?).and_return(true) }
describe ".wsl_path?" do
it "should return true when path is not within /mnt" do
expect(subject.wsl_path?("/tmp")).to be(true)
end
it "should return false when path is within /mnt" do
expect(subject.wsl_path?("/mnt/c")).to be false
end
end
describe ".wsl_rootfs" do
let(:appdata_path){ "C:\\Custom\\Path" }
let(:registry_paths){ nil }
before do
allow(subject).to receive(:wsl_windows_appdata_local).and_return(appdata_path)
allow(Tempfile).to receive(:new).and_return(double("tempfile", path: "file.path", close!: true))
allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_return(registry_paths)
end
context "when no instance information is in the registry" do
before do
expect(Dir).to receive(:open).with(/.*Custom.*Path.*/).and_yield(double("path", path: appdata_path))
expect(File).to receive(:exist?).and_return(true)
end
it "should only check the lxrun path" do
expect(subject.wsl_rootfs).to include(appdata_path)
end
end
context "with instance information in the registry" do
let(:registry_paths) { ["C:\\Path1", "C:\\Path2"].join("\r\n") }
before do
allow(Dir).to receive(:open).and_yield(double("path", path: appdata_path))
allow(File).to receive(:exist?).and_return(false)
end
context "when no matches are detected" do
it "should check all paths given" do
expect(Dir).to receive(:open).and_yield(double("path", path: appdata_path)).exactly(3).times
expect(File).to receive(:exist?).and_return(false).exactly(3).times
expect{ subject.wsl_rootfs }.to raise_error(Vagrant::Errors::WSLRootFsNotFoundError)
end
it "should raise not found error" do
expect{ subject.wsl_rootfs }.to raise_error(Vagrant::Errors::WSLRootFsNotFoundError)
end
end
context "when file marker match found" do
let(:matching_path){ registry_paths.split("\r\n").last }
let(:matching_part){ matching_path.split("\\").last }
before do
allow(File).to receive(:exist?).with(/#{matching_part}/).and_return(true)
end
it "should return the matching path" do
expect(Dir).to receive(:open).with(/#{matching_part}/).and_yield(double("path", path: matching_part))
expect(subject.wsl_rootfs).to start_with(matching_path)
end
it "should return matching path when access error encountered" do
expect(Dir).to receive(:open).with(/#{matching_part}/).and_raise(Errno::EACCES)
expect(subject.wsl_rootfs).to start_with(matching_path)
end
end
end
end
describe ".wsl_to_windows_path" do
let(:path){ "/home/vagrant/test" }
context "when not within WSL" do
before{ allow(subject).to receive(:wsl?).and_return(false) }
it "should return the path unmodified" do
expect(subject.wsl_to_windows_path(path)).to eq(path)
end
end
context "when within WSL" do
before{ allow(subject).to receive(:wsl?).and_return(true) }
context "when windows access is not enabled" do
before{ allow(subject).to receive(:wsl_windows_access?).and_return(false) }
it "should return the path unmodified" do
expect(subject.wsl_to_windows_path(path)).to eq(path)
end
end
context "when windows access is enabled" do
let(:rootfs_path){ "C:\\WSL\\rootfs" }
before do
allow(subject).to receive(:wsl_windows_access?).and_return(true)
allow(subject).to receive(:wsl_rootfs).and_return(rootfs_path)
end
it "should generate expanded path when within WSL" do
expect(subject.wsl_to_windows_path(path)).to eq("#{rootfs_path}#{path.gsub("/", "\\")}")
end
it "should generate direct path when outside the WSL" do
expect(subject.wsl_to_windows_path("/mnt/c/vagrant")).to eq("c:\\vagrant")
end
it "should not modify path when already in windows format" do
expect(subject.wsl_to_windows_path("C:\\vagrant")).to eq("C:\\vagrant")
end
context "when within lxrun generated WSL instance" do
let(:rootfs_path){ "C:\\WSL\\lxss" }
it "should not include rootfs when accessing home" do
expect(subject.wsl_to_windows_path("/home/vagrant")).not_to include("rootfs")
end
it "should include rootfs when accessing non-home path" do
expect(subject.wsl_to_windows_path("/tmp/test")).to include("rootfs")
end
end
end
end
end
end
end

View File

@ -55,6 +55,7 @@ Gem::Specification.new do |s|
all_files = Dir.chdir(root_path) { Dir.glob("**/{*,.*}") }
all_files.reject! { |file| [".", ".."].include?(File.basename(file)) }
all_files.reject! { |file| file.start_with?("website/") }
all_files.reject! { |file| file.start_with?("test/") }
gitignore_path = File.join(root_path, ".gitignore")
gitignore = File.readlines(gitignore_path)
gitignore.map! { |line| line.chomp.strip }