diff --git a/extensions/WSLExtension/Constants.cs b/extensions/WSLExtension/Constants.cs index 34b0098c18..b6f98c42ea 100644 --- a/extensions/WSLExtension/Constants.cs +++ b/extensions/WSLExtension/Constants.cs @@ -30,6 +30,9 @@ public static class Constants // Wsl registry location for registered distributions. public const string WslRegistryLocation = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss"; + // Registry location for app paths + public const string AppPathsRegistryLocation = @"Software\Microsoft\Windows\CurrentVersion\App Paths"; + // Wsl registry data names within a distribution location. public const string PackageFamilyRegistryName = "PackageFamilyName"; public const string DistributionRegistryName = "DistributionName"; @@ -49,8 +52,13 @@ public static class Constants // Arguments to terminate all wsl sessions for a specific distribution using wsl.exe public const string TerminateDistributionArgs = "--terminate {0}"; - // Arguments to download, install and register a wsl distribution. - public const string InstallDistributionArgs = "--install --distribution {0}"; + // Arguments to installs and Register a wsl distribution using the distributions + // .exe executable file. Where is the WSL distributions name. + // See: https://github.com/microsoft/WSL-DistroLauncher?tab=readme-ov-file#contents + // for more information on the launcher executable. Using --root allows us to register + // the distribution without the command line session being hung waiting for the user to + // create a new username and password. + public const string InstallAndRegisterDistributionArgs = "install --root"; // Arguments to list of all running distributions on a machine using wsl.exe public const string ListAllRunningDistributions = "--list --running"; diff --git a/extensions/WSLExtension/Contracts/IWslManager.cs b/extensions/WSLExtension/Contracts/IWslManager.cs index b3f4507045..54ad9581f0 100644 --- a/extensions/WSLExtension/Contracts/IWslManager.cs +++ b/extensions/WSLExtension/Contracts/IWslManager.cs @@ -39,10 +39,11 @@ public interface IWslManager /// void LaunchDistribution(string distributionName); - /// Installs a new WSL distribution. - /// This is a wrapper for - /// - void InstallDistribution(string distributionName); + /// Installs a new WSL distribution from the Microsoft store. + public Task InstallDistributionPackageAsync( + DistributionDefinition definition, + Action? statusUpdateCallback, + CancellationToken cancellationToken); /// Terminates all sessions for a new WSL distribution. /// This is a wrapper for diff --git a/extensions/WSLExtension/Contracts/IWslServicesMediator.cs b/extensions/WSLExtension/Contracts/IWslServicesMediator.cs index fec0143246..7d0185e12e 100644 --- a/extensions/WSLExtension/Contracts/IWslServicesMediator.cs +++ b/extensions/WSLExtension/Contracts/IWslServicesMediator.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Windows.ApplicationModel; using WSLExtension.Models; namespace WSLExtension.Contracts; @@ -31,8 +32,8 @@ public interface IWslServicesMediator /// Launches a new WSL process with the provided distribution. void LaunchDistribution(string distributionName); - /// Installs and registers a new distribution on the machine. - void InstallDistribution(string distributionName); + /// Installs and registers a distribution on the machine. + void InstallAndRegisterDistribution(Package distributionPackage); /// Terminates all running WSL sessions for the provided distribution on the machine. void TerminateDistribution(string distributionName); diff --git a/extensions/WSLExtension/DevHomeProviders/WslProvider.cs b/extensions/WSLExtension/DevHomeProviders/WslProvider.cs index 514e7ecbeb..220b16b2e5 100644 --- a/extensions/WSLExtension/DevHomeProviders/WslProvider.cs +++ b/extensions/WSLExtension/DevHomeProviders/WslProvider.cs @@ -72,10 +72,16 @@ public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForDeveloperId(I deserializedObject as WslInstallationUserInput ?? throw new InvalidOperationException($"Json deserialization failed for input Json: {inputJson}"); var definitions = _wslManager.GetAllDistributionsAvailableToInstallAsync().GetAwaiter().GetResult(); - return new WslInstallDistributionOperation( - definitions[wslInstallationUserInput.SelectedDistributionIndex], - _stringResource, - _wslManager); + + // Make sure the distribution the user selected is still available. + var definition = definitions.SingleOrDefault(definition => definition.IsSameDistribution(wslInstallationUserInput.NewEnvironmentName)); + + if (definition == null) + { + throw new InvalidOperationException($"Couldn't find selected distribution with input {inputJson}"); + } + + return new WslInstallDistributionOperation(definition, _stringResource, _wslManager); } catch (Exception ex) { diff --git a/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs index 46508ef7e1..680d1502fe 100644 --- a/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs +++ b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs @@ -35,4 +35,19 @@ public class DistributionDefinition public string PackageFamilyName { get; set; } = string.Empty; public string Publisher { get; set; } = string.Empty; + + public bool IsSameDistribution(string name) + { + // Both name and friendly name should not be empty or null in the WSL distribution + // json. + if (string.IsNullOrEmpty(Name) || string.IsNullOrEmpty(FriendlyName)) + { + return false; + } + + // For distributions retrieved from the WSL distribution Json, both the name and + // friendly names are unique. + return Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + FriendlyName.Equals(name, StringComparison.OrdinalIgnoreCase); + } } diff --git a/extensions/WSLExtension/Exceptions/AppExecutionAliasNotFoundException.cs b/extensions/WSLExtension/Exceptions/AppExecutionAliasNotFoundException.cs new file mode 100644 index 0000000000..58feeb77b2 --- /dev/null +++ b/extensions/WSLExtension/Exceptions/AppExecutionAliasNotFoundException.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WSLExtension.Exceptions; + +public class AppExecutionAliasNotFoundException : Exception +{ + public AppExecutionAliasNotFoundException(string? message) + : base(message) + { + } +} diff --git a/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs b/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs index 593cd89c3a..fadbbc1eae 100644 --- a/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs +++ b/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs @@ -8,6 +8,7 @@ using Windows.Foundation; using WSLExtension.Contracts; using WSLExtension.DistributionDefinitions; +using WSLExtension.Exceptions; using static HyperVExtension.Helpers.BytesHelper; using static WSLExtension.Constants; @@ -19,16 +20,10 @@ public class WslInstallDistributionOperation : ICreateComputeSystemOperation private readonly string _wslCreationProcessStart; - private readonly string _waitingToComplete; - - private readonly string _installationFailedTimeout; - private readonly string _installationSuccessful; private const uint IndeterminateProgressPercentage = 0U; - private readonly TimeSpan _threeSecondDelayInSeconds = TimeSpan.FromSeconds(3); - private readonly DistributionDefinition _definition; private readonly IStringResource _stringResource; @@ -44,10 +39,6 @@ public WslInstallDistributionOperation( _stringResource = stringResource; _wslManager = wslManager; _wslCreationProcessStart = GetLocalizedString("WSLCreationProcessStart", _definition.FriendlyName); - _waitingToComplete = GetLocalizedString("WSLWaitingToCompleteInstallation", _definition.FriendlyName); - - _installationFailedTimeout = GetLocalizedString("WSLInstallationFailedTimeOut", _definition.FriendlyName); - _installationSuccessful = GetLocalizedString("WSLInstallationCompletedSuccessfully", _definition.FriendlyName); } @@ -62,7 +53,6 @@ public IAsyncOperation StartAsync() { try { - var startTime = DateTime.UtcNow; _log.Information($"Starting installation for {_definition.Name}"); // Cancel waiting for install if the distribution hasn't been installed after 10 minutes. @@ -75,33 +65,22 @@ public IAsyncOperation StartAsync() // Make sure the WSL kernel package is installed before attempting to install the selected distribution. await _wslManager.InstallWslKernelPackageAsync(StatusUpdateCallback, cancellationToken); - _wslManager.InstallDistribution(_definition.Name); - WslRegisteredDistribution? registeredDistribution = null; - var distributionInstalledSuccessfully = false; + await _wslManager.InstallDistributionPackageAsync(_definition, StatusUpdateCallback, cancellationToken); + var registeredDistribution = await _wslManager.GetInformationOnRegisteredDistributionAsync(_definition.Name); - Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(_waitingToComplete, 0)); - while (!cancellationTokenSource.IsCancellationRequested) + if (registeredDistribution != null && registeredDistribution.IsDistributionFullyRegistered()) { - // Wait in 3 second intervals before checking. Unfortunately there are no APIs to check for - // installation so we need to keep checking for its completion. - await Task.Delay(_threeSecondDelayInSeconds, cancellationToken); - registeredDistribution = await _wslManager.GetInformationOnRegisteredDistributionAsync(_definition.Name); - - if ((registeredDistribution != null) && - (distributionInstalledSuccessfully = registeredDistribution.IsDistributionFullyRegistered())) - { - break; - } - } - - _log.Information($"Ending installation for {_definition.Name}. Operation took: {DateTime.UtcNow - startTime}"); - if (distributionInstalledSuccessfully) - { - Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(_installationSuccessful, 100)); + StatusUpdateCallback(_installationSuccessful); return new CreateComputeSystemResult(new WslComputeSystem(_stringResource, registeredDistribution!, _wslManager)); } - throw new TimeoutException(_installationFailedTimeout); + throw new InvalidOperationException($"Failed to register {_definition.FriendlyName} distribution"); + } + catch (AppExecutionAliasNotFoundException ex) + { + _log.Error(ex, $"Unable to register {_definition.FriendlyName} due to app execution alias being absent from registry"); + var errorMsg = _stringResource.GetLocalized("WSLRegistrationFailedDueToNoAppExAlias", _definition.FriendlyName, ex.Message); + return new CreateComputeSystemResult(ex, errorMsg, ex.Message); } catch (Exception ex) { diff --git a/extensions/WSLExtension/Models/WslProcessData.cs b/extensions/WSLExtension/Models/WslProcessData.cs index d3914de84a..04f627f353 100644 --- a/extensions/WSLExtension/Models/WslProcessData.cs +++ b/extensions/WSLExtension/Models/WslProcessData.cs @@ -32,7 +32,7 @@ public WslProcessData(int exitCode, string stdOutput, string stdError) public bool ExitedSuccessfully() { - return ExitCode == WslExeExitSuccess && string.IsNullOrEmpty(StdError); + return ExitCode == WslExeExitSuccess; } public override string ToString() diff --git a/extensions/WSLExtension/Services/WslManager.cs b/extensions/WSLExtension/Services/WslManager.cs index 9e244669e8..3f80a42911 100644 --- a/extensions/WSLExtension/Services/WslManager.cs +++ b/extensions/WSLExtension/Services/WslManager.cs @@ -33,8 +33,12 @@ public class WslManager : IWslManager, IDisposable private readonly IStringResource _stringResource; + private readonly object _distributionInstallLock = new(); + private readonly SemaphoreSlim _wslKernelPackageInstallLock = new(1, 1); + private readonly HashSet _distributionsBeingInstalled = new(); + public event EventHandler>? DistributionStateSyncEventHandler; private Dictionary? _distributionDefinitionsMap; @@ -90,15 +94,25 @@ public async Task> GetAllDistributionsAvailableToIn var registeredDistributionsMap = await GetInformationOnAllRegisteredDistributionsAsync(); var distributionsToListOnCreationPage = new List(); _distributionDefinitionsMap ??= await _definitionHelper.GetDistributionDefinitionsAsync(); - foreach (var distributionDefinition in _distributionDefinitionsMap.Values) + + lock (_distributionInstallLock) { - // filter out distribution definitions already registered on machine. - if (registeredDistributionsMap.TryGetValue(distributionDefinition.Name, out var _)) + foreach (var distributionDefinition in _distributionDefinitionsMap.Values) { - continue; - } + // filter out distribution definitions already registered on machine. + if (registeredDistributionsMap.TryGetValue(distributionDefinition.Name, out var _)) + { + continue; + } - distributionsToListOnCreationPage.Add(distributionDefinition); + // filter out distributions that are currently being installed/registered. + if (_distributionsBeingInstalled.Contains(distributionDefinition.Name)) + { + continue; + } + + distributionsToListOnCreationPage.Add(distributionDefinition); + } } // Sort the list by distribution name in ascending order before sending it. @@ -138,10 +152,58 @@ public void LaunchDistribution(string distributionName) _wslServicesMediator.LaunchDistribution(distributionName); } - /// - public void InstallDistribution(string distributionName) + /// + public async Task InstallDistributionPackageAsync( + DistributionDefinition definition, + Action? statusUpdateCallback, + CancellationToken cancellationToken) { - _wslServicesMediator.InstallDistribution(distributionName); + lock (_distributionInstallLock) + { + if (_distributionsBeingInstalled.Contains(definition.Name)) + { + throw new InvalidOperationException("Distribution already being installed"); + } + + _distributionsBeingInstalled.Add(definition.Name); + } + + try + { + statusUpdateCallback?.Invoke(_stringResource.GetLocalized("DistributionPackageInstallationCheck", definition.FriendlyName)); + if (!_packageHelper.IsPackageInstalled(definition.PackageFamilyName)) + { + // Install it from the store. + statusUpdateCallback?.Invoke(_stringResource.GetLocalized("DistributionPackageInstallationStart", definition.FriendlyName)); + cancellationToken.ThrowIfCancellationRequested(); + if (!await _microsoftStoreService.TryInstallPackageAsync(definition.StoreAppId)) + { + throw new InvalidDataException($"Failed to install the {definition.Name} package"); + } + } + else + { + statusUpdateCallback?.Invoke(_stringResource.GetLocalized("DistributionPackageAlreadyInstalled", definition.FriendlyName)); + } + + var package = _packageHelper.GetPackageFromPackageFamilyName(definition.PackageFamilyName); + if (package == null) + { + throw new InvalidDataException($"Couldn't find the {definition.Name} package"); + } + + statusUpdateCallback?.Invoke(_stringResource.GetLocalized("WSLWaitingToCompleteRegistration", definition.FriendlyName)); + cancellationToken.ThrowIfCancellationRequested(); + _wslServicesMediator.InstallAndRegisterDistribution(package); + statusUpdateCallback?.Invoke(_stringResource.GetLocalized("WSLRegistrationCompletedSuccessfully", definition.FriendlyName)); + } + finally + { + lock (_distributionInstallLock) + { + _distributionsBeingInstalled.Remove(definition.Name); + } + } } /// @@ -228,6 +290,21 @@ private void StartDistributionStatePolling() private void OnInstallChanged(object sender, AppInstallManagerItemEventArgs args) { var installItem = args.Item; + var installationStatus = installItem.GetCurrentStatus(); + var itemInstallState = installationStatus.InstallState; + + lock (_distributionInstallLock) + { + if (_distributionsBeingInstalled.Contains(installItem.ProductId)) + { + if (itemInstallState == AppInstallState.Completed || + itemInstallState == AppInstallState.Canceled || + itemInstallState == AppInstallState.Error) + { + _distributionsBeingInstalled.Remove(installItem.ProductId); + } + } + } WslInstallationEventHandler?.Invoke(this, installItem); } diff --git a/extensions/WSLExtension/Services/WslServicesMediator.cs b/extensions/WSLExtension/Services/WslServicesMediator.cs index d2843e4b9a..a6f4dbb922 100644 --- a/extensions/WSLExtension/Services/WslServicesMediator.cs +++ b/extensions/WSLExtension/Services/WslServicesMediator.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.Win32; +using Windows.ApplicationModel; using WSLExtension.ClassExtensions; using WSLExtension.Contracts; using WSLExtension.Exceptions; @@ -24,9 +25,13 @@ public class WslServicesMediator : IWslServicesMediator private readonly IProcessCreator _processCreator; + private readonly string _distributionPackageExesLocation; + public WslServicesMediator(IProcessCreator creator) { _processCreator = creator; + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + _distributionPackageExesLocation = Path.Combine(localAppData, "Microsoft", "WindowsApps"); } /// @@ -172,11 +177,73 @@ public void TerminateDistribution(string distributionName) } } - /// - public void InstallDistribution(string distributionName) + /// + /// + /// Registers a wsl distribution by getting the details of its distribution launcher. Note: All WSL distributions found in the + /// Microsoft Store must be created via the WSL distribution launcher project found here: + /// https://github.com/microsoft/WSL-DistroLauncher. The WSL launcher allows WSL packages in the store to be registered with the WSL + /// service via the ".exe install" command. Where is the name of the distributions application exe. + /// These are always stored in %localappdata%\Microsoft\WindowsApps\ and only the applications + /// executable file is placed in this location. + /// + public void InstallAndRegisterDistribution(Package distributionPackage) { - _processCreator.CreateProcessWithWindow( - WslExe, - InstallDistributionArgs.FormatArgs(distributionName)); + var exeFileName = GetDistributionExecutableNameFromRegistry(distributionPackage.InstalledPath); + var exeLocalAppPath = $@"{_distributionPackageExesLocation}\{distributionPackage.Id.FamilyName}\{exeFileName}"; + var processData = _processCreator.CreateProcessWithoutWindowAndWaitForExit(exeLocalAppPath, InstallAndRegisterDistributionArgs); + + if (!processData.ExitedSuccessfully()) + { + throw new WslServicesMediatorException($"Unable to register {exeLocalAppPath} with WSL service : {processData}"); + } + } + + /// + /// Gets the executable name by using the app path registry location for the user. + /// + /// Absolute path to the distribution package's installation location + /// The executable name including the .exe part of the name + private string GetDistributionExecutableNameFromRegistry(string installationPath) + { + var appPathSubKey = CurrentUser.OpenSubKey(AppPathsRegistryLocation, false); + + if (appPathSubKey == null) + { + throw new WslServicesMediatorException($"Unable to access subkey {AppPathsRegistryLocation} to find {installationPath}"); + } + + foreach (var subKeyName in appPathSubKey.GetSubKeyNames()) + { + var subKey = appPathSubKey.OpenSubKey(subKeyName); + + if (subKey == null) + { + continue; + } + + // The subkey for the distribution contains a path value that points to the installation location of the distribution. + var appPath = subKey.GetValue("path") as string ?? string.Empty; + + if (!string.IsNullOrEmpty(appPath) && appPath[appPath.Length - 1] == '\\') + { + // Remove the ending backslash if one is found. + appPath = appPath.Substring(0, appPath.Length - 1); + } + + if (!appPath.Equals(installationPath, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (subKey.GetValue(null) is string absolutePathToExe) + { + // value will be in the form of: + // C:\Program Files\WindowsApps\46932SUSE.openSUSETumbleweed_24177.7.109.0_x64__022rs5jcyhyac\openSUSE-Tumbleweed.exe + // We only need the last part e.g openSUSE-Tumbleweed.exe. + return absolutePathToExe.Split('\\').Last(); + } + } + + throw new AppExecutionAliasNotFoundException($"App execution alias not found. No (Default) entry for {installationPath} in {AppPathsRegistryLocation}"); } } diff --git a/extensions/WSLExtension/Strings/en-US/Resources.resw b/extensions/WSLExtension/Strings/en-US/Resources.resw index 2405694700..718e9b9668 100644 --- a/extensions/WSLExtension/Strings/en-US/Resources.resw +++ b/extensions/WSLExtension/Strings/en-US/Resources.resw @@ -130,10 +130,6 @@ Starting the creation process for {0} {Locked="{0}"} Text for when a user starts the creation process for a specific wsl distribution. {0} is the name of the distribution - - Waiting for {0}'s installation to complete - {Locked="{0}"} Text message to display when a wsl distribution is installing. {0} is the name of the distribution - Waiting for the Windows Subsystem for Linux service to register {0}. This may take a few minutes. {Locked="Windows", "Linux", "{0}"} Text to display when a specific wsl distribution is being registered. {0} is the name of the distribution @@ -142,10 +138,6 @@ Unable to install and register {0} due to error: {1}. Try using wsl.exe to install it manually. See aka.ms/wslinstall {Locked="{0}", "{1}", "wsl.exe", "aka.ms/wslinstall"} Text message to display when the extension failed to install a wsl distribution. {0} is the name of the distribution and {1} is the error message. - - Installation timed out, unable to install and register {0}. Try using wsl.exe to install manually. See aka.ms/wslinstall - {Locked="{0}", "{wsl.exe}", "{aka.ms/wslinstall}"} Text message to display when the extension failed to install a wsl distribution. {0} is the name of the distribution - The Windows Subsystem for Linux kernel package is not installed. Starting installation. {Locked="Windows", "Linux"} @@ -154,10 +146,26 @@ Checking if the Windows Subsystem for Linux kernel package is installed {Locked="Windows", "Linux"} + + The {0} package is already installed. Attempting to register it with the Windows Subsystem for Linux service. + {Locked="{0}", "Windows", "Linux"} Text to display when a specific wsl distribution package is already installed. {0} is the name of the distribution + + + Checking if the {0} package is already installed + {Locked="{0}"} Text to display when the extension checks the users system to confirm if a specific wsl distribution package is already installed. {0} is the name of the distribution + + + The {0} package is not installed. Starting store installation. + {Locked="{0}"} Text to display when a specific wsl distribution package is not installed. {0} is the name of the distribution + Successfully installed {0} {Locked="{0}"} Text to display when the extension successfully installs a specific wsl distribution package onto a users machine. {0} is the name of the distribution + + Successfully registered {0} + {Locked="{0}"} Text to display when the extension successfully registers a specific wsl distribution. {0} is the name of the distribution + The Virtual Machine Platform feature is needed for the WSL extension to function properly. The feature can be enabled in Dev Home's Windows customization page, under the virtualization feature management option. Enabling the feature will require a reboot. For more information on WSL requirements visit aka.ms/wslinstall {Locked="Windows", "WSL", "aka.ms/wslinstall", "Dev Home"} Text message to display when the virtual machine platform Windows optional component is not enabled. @@ -214,4 +222,8 @@ The Windows Subsystem for Linux kernel package is installed {Locked="Windows", "Linux"} Text to display when the wsl package is installed. + + Failed to register {0} with the Windows Subsystem for Linux service due its app execution alias being turned off or missing. Please open the Windows Settings app and navigate to the app execution alias page to toggle the setting on for {0}, then try again. + {Locked="{0}", "{Windows}", "Linux"} Text to display when the registration of a specific wsl distribution failed due to its app execution alias setting being turned off. {0} is the name of the distribution. + \ No newline at end of file