diff --git a/extensions/WSLExtension/Constants.cs b/extensions/WSLExtension/Constants.cs index 007076617..34b0098c1 100644 --- a/extensions/WSLExtension/Constants.cs +++ b/extensions/WSLExtension/Constants.cs @@ -20,7 +20,8 @@ public static class Constants public const string WindowsTerminalPackageFamilyName = "Microsoft.WindowsTerminal_8wekyb3d8bbwe"; public const string WslExe = "wsl.exe"; public const string WslTemplateSubfolderName = "WslTemplates"; - + public const string WslKernelPackageStoreId = "9P9TQF7MRM4R"; + public const string WSLPackageFamilyName = "MicrosoftCorporationII.WindowsSubsystemForLinux_8wekyb3d8bbwe"; public const string DefaultWslLogoPath = @"ms-appx:///WslAssets/wslLinux.png"; public const string WslLogoPathFormat = @"ms-appx:///WslAssets/{0}"; public const string KnownDistributionsLocalYamlLocation = @"ms-appx:///DistributionDefinitions/DistributionDefinition.yaml"; diff --git a/extensions/WSLExtension/Contracts/IWslManager.cs b/extensions/WSLExtension/Contracts/IWslManager.cs index 76e16887b..b3f450704 100644 --- a/extensions/WSLExtension/Contracts/IWslManager.cs +++ b/extensions/WSLExtension/Contracts/IWslManager.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Windows.ApplicationModel.Store.Preview.InstallControl; using WSLExtension.DistributionDefinitions; using WSLExtension.Models; @@ -52,4 +53,10 @@ public interface IWslManager /// This is a wrapper for /// public bool IsDistributionRunning(string distributionName); + + /// Installs the WSL kernel package from the Microsoft store if it is not already installed. + public Task InstallWslKernelPackageAsync(Action? statusUpdateCallback, CancellationToken cancellationToken); + + /// Provides subscribers with download/installation progress for Microsoft store app installs. + public event EventHandler? WslInstallationEventHandler; } diff --git a/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs index 956743dd1..46508ef7e 100644 --- a/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs +++ b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs @@ -24,7 +24,7 @@ public class DistributionDefinition public string? WindowsTerminalProfileGuid { get; set; } - public string? StoreAppId { get; set; } + public string StoreAppId { get; set; } = string.Empty; [JsonPropertyName("Amd64")] public bool IsAmd64Supported { get; set; } diff --git a/extensions/WSLExtension/Models/RegisterAndInstallDistributionSession.cs b/extensions/WSLExtension/Models/RegisterAndInstallDistributionSession.cs index c51a05dc1..bb2ac0cc5 100644 --- a/extensions/WSLExtension/Models/RegisterAndInstallDistributionSession.cs +++ b/extensions/WSLExtension/Models/RegisterAndInstallDistributionSession.cs @@ -76,7 +76,7 @@ public IAsyncOperation OnAction(string action, string i var shouldEndSession = false; var adaptiveCardStateNotRecognizedError = _stringResource.GetLocalized("AdaptiveCardStateNotRecognizedError"); - var actionPayload = Json.ToObject(action); + var actionPayload = Helpers.Json.ToObject(action); if (actionPayload == null) { _log.Error($"Actions in Adaptive card action Json not recognized: {action}"); diff --git a/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs b/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs index 3fb15d103..593cd89c3 100644 --- a/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs +++ b/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs @@ -1,11 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Runtime.InteropServices.WindowsRuntime; using Microsoft.Windows.DevHome.SDK; using Serilog; +using Windows.ApplicationModel.Store.Preview.InstallControl; using Windows.Foundation; using WSLExtension.Contracts; using WSLExtension.DistributionDefinitions; +using static HyperVExtension.Helpers.BytesHelper; +using static WSLExtension.Constants; namespace WSLExtension.Models; @@ -13,7 +17,7 @@ public class WslInstallDistributionOperation : ICreateComputeSystemOperation { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WslInstallDistributionOperation)); - private readonly string _preparingToInstall; + private readonly string _wslCreationProcessStart; private readonly string _waitingToComplete; @@ -21,6 +25,8 @@ public class WslInstallDistributionOperation : ICreateComputeSystemOperation private readonly string _installationSuccessful; + private const uint IndeterminateProgressPercentage = 0U; + private readonly TimeSpan _threeSecondDelayInSeconds = TimeSpan.FromSeconds(3); private readonly DistributionDefinition _definition; @@ -37,7 +43,7 @@ public WslInstallDistributionOperation( _definition = distributionDefinition; _stringResource = stringResource; _wslManager = wslManager; - _preparingToInstall = GetLocalizedString("WSLPrepareInstall", _definition.FriendlyName); + _wslCreationProcessStart = GetLocalizedString("WSLCreationProcessStart", _definition.FriendlyName); _waitingToComplete = GetLocalizedString("WSLWaitingToCompleteInstallation", _definition.FriendlyName); _installationFailedTimeout = GetLocalizedString("WSLInstallationFailedTimeOut", _definition.FriendlyName); @@ -52,27 +58,33 @@ private string GetLocalizedString(string resourcekey, string value) public IAsyncOperation StartAsync() { - return Task.Run(async () => + return AsyncInfo.Run(async (cancellationToken) => { try { var startTime = DateTime.UtcNow; _log.Information($"Starting installation for {_definition.Name}"); - Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(_preparingToInstall, 0)); // Cancel waiting for install if the distribution hasn't been installed after 10 minutes. CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token, cancellationToken); cancellationTokenSource.CancelAfter(TimeSpan.FromMinutes(10)); + StatusUpdateCallback(_wslCreationProcessStart); + _wslManager.WslInstallationEventHandler += OnInstallChanged; + + // 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; - _wslManager.InstallDistribution(_definition.Name); Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(_waitingToComplete, 0)); while (!cancellationTokenSource.IsCancellationRequested) { // 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); + await Task.Delay(_threeSecondDelayInSeconds, cancellationToken); registeredDistribution = await _wslManager.GetInformationOnRegisteredDistributionAsync(_definition.Name); if ((registeredDistribution != null) && @@ -93,11 +105,102 @@ public IAsyncOperation StartAsync() } catch (Exception ex) { - _log.Error(ex, $"Unable to install {_definition.FriendlyName} due to exception"); + _log.Error(ex, $"Unable to install and register {_definition.FriendlyName} due to exception"); var errorMsg = _stringResource.GetLocalized("WSLInstallationFailedWithException", _definition.FriendlyName, ex.Message); return new CreateComputeSystemResult(ex, errorMsg, ex.Message); } - }).AsAsyncOperation(); + finally + { + _wslManager.WslInstallationEventHandler -= OnInstallChanged; + } + }); + } + + private void StatusUpdateCallback(string progressText) + { + StatusUpdateCallback(progressText, IndeterminateProgressPercentage); + } + + private void StatusUpdateCallback(string progressText, uint progressPercent) + { + try + { + Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(progressText, progressPercent)); + } + catch (Exception ex) + { + _log.Error(ex, "Failed to provide progress back to Dev Home"); + } + } + + private void OnInstallChanged(object? sender, AppInstallItem args) + { + var packageName = _definition.FriendlyName; + + if (!_definition.StoreAppId.Equals(args.ProductId, StringComparison.OrdinalIgnoreCase)) + { + // If we're not downloading/installing the wsl distribution with the provided productId + // then check if the Linux kernel package is being downloaded/installed. + if (!WslKernelPackageStoreId.Equals(args.ProductId, StringComparison.OrdinalIgnoreCase)) + { + // The AppInstallItem isn't the selected distribution nor is it the kernel package. + return; + } + + packageName = _stringResource.GetLocalized("WslKernelPackageName"); + } + + var status = args.GetCurrentStatus(); + var itemInstallState = status.InstallState; + var progressText = GetLocalizedString("AppInstallPending", packageName); + var progressPercent = IndeterminateProgressPercentage; + + switch (itemInstallState) + { + case AppInstallState.Pending: + break; + case AppInstallState.Starting: + progressText = GetLocalizedString("AppInstallStarting", packageName); + break; + case AppInstallState.Downloading: + progressText = GetTextForByteTransfer("AppInstallDownloading", packageName, status); + progressPercent = (uint)status.PercentComplete; + break; + case AppInstallState.Installing: + progressText = GetLocalizedString("AppInstalling", packageName); + break; + case AppInstallState.Completed: + progressText = GetLocalizedString("AppInstallComplete", packageName); + break; + case AppInstallState.Canceled: + progressText = GetLocalizedString("AppInstallCancelled", packageName); + break; + case AppInstallState.Paused: + progressText = GetLocalizedString("AppInstallPaused", packageName); + break; + case AppInstallState.Error: + progressText = GetLocalizedString("AppInstallError", packageName); + break; + case AppInstallState.PausedLowBattery: + progressText = GetLocalizedString("AppInstallPausedLowBattery", packageName); + break; + case AppInstallState.PausedWiFiRecommended: + case AppInstallState.PausedWiFiRequired: + progressText = GetLocalizedString("AppInstallPausedWiFi", packageName); + break; + case AppInstallState.ReadyToDownload: + progressText = GetLocalizedString("AppInstallReadyToDownload", packageName); + break; + } + + StatusUpdateCallback(progressText, progressPercent); + } + + private string GetTextForByteTransfer(string resourceKey, string packageName, AppInstallStatus status) + { + var bytesReceivedSoFar = ConvertBytesToString(status.BytesDownloaded); + var totalBytesToReceive = ConvertBytesToString(status.DownloadSizeInBytes); + return _stringResource.GetLocalized(resourceKey, packageName, $"{bytesReceivedSoFar} / {totalBytesToReceive}"); } public event TypedEventHandler? ActionRequired diff --git a/extensions/WSLExtension/Program.cs b/extensions/WSLExtension/Program.cs index 0b4873ba3..21f1107bd 100644 --- a/extensions/WSLExtension/Program.cs +++ b/extensions/WSLExtension/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using DevHome.Services.Core.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -91,9 +92,13 @@ private static void BuildHostContainer() }). ConfigureServices((context, services) => { + // Add Serilog logging for ILogger. + services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true)); + // Services services.AddHttpClient(); services.AddWslExtensionServices(); + services.AddCore(); }). Build(); } diff --git a/extensions/WSLExtension/Services/WslManager.cs b/extensions/WSLExtension/Services/WslManager.cs index 76c816d5a..9e244669e 100644 --- a/extensions/WSLExtension/Services/WslManager.cs +++ b/extensions/WSLExtension/Services/WslManager.cs @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using DevHome.Services.Core.Contracts; using Serilog; +using Windows.ApplicationModel.Store.Preview.InstallControl; using Windows.System.Threading; -using WSLExtension.ClassExtensions; using WSLExtension.Contracts; using WSLExtension.DistributionDefinitions; using WSLExtension.Helpers; @@ -12,7 +13,7 @@ namespace WSLExtension.Services; -public class WslManager : IWslManager +public class WslManager : IWslManager, IDisposable { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WslManager)); @@ -28,20 +29,35 @@ public class WslManager : IWslManager private readonly List _registeredWslDistributions = new(); + private readonly IMicrosoftStoreService _microsoftStoreService; + + private readonly IStringResource _stringResource; + + private readonly SemaphoreSlim _wslKernelPackageInstallLock = new(1, 1); + public event EventHandler>? DistributionStateSyncEventHandler; private Dictionary? _distributionDefinitionsMap; private ThreadPoolTimer? _timerForUpdatingDistributionStates; + private bool _disposed; + + public event EventHandler? WslInstallationEventHandler; + public WslManager( IWslServicesMediator wslServicesMediator, WslRegisteredDistributionFactory wslDistributionFactory, - IDistributionDefinitionHelper distributionDefinitionHelper) + IDistributionDefinitionHelper distributionDefinitionHelper, + IMicrosoftStoreService microsoftStoreService, + IStringResource stringResource) { _wslRegisteredDistributionFactory = wslDistributionFactory; _wslServicesMediator = wslServicesMediator; _definitionHelper = distributionDefinitionHelper; + _microsoftStoreService = microsoftStoreService; + _stringResource = stringResource; + _microsoftStoreService.ItemStatusChanged += OnInstallChanged; StartDistributionStatePolling(); } @@ -134,6 +150,35 @@ public void TerminateDistribution(string distributionName) _wslServicesMediator.TerminateDistribution(distributionName); } + /// + public async Task InstallWslKernelPackageAsync(Action? statusUpdateCallback, CancellationToken cancellationToken) + { + // Regardless of how many WSL distributions are being installed. Only one thread should be allowed to install the + // WSL kernel package if it isn't already installed. + await _wslKernelPackageInstallLock.WaitAsync(cancellationToken); + try + { + statusUpdateCallback?.Invoke(_stringResource.GetLocalized("WslKernelPackageInstallationCheck")); + if (!_packageHelper.IsPackageInstalled(WSLPackageFamilyName)) + { + // If not installed, we'll install it from the store. + statusUpdateCallback?.Invoke(_stringResource.GetLocalized("InstallingWslKernelPackage")); + + cancellationToken.ThrowIfCancellationRequested(); + if (!await _microsoftStoreService.TryInstallPackageAsync(WslKernelPackageStoreId)) + { + throw new InvalidDataException("Failed to install the Wsl kernel package"); + } + } + + statusUpdateCallback?.Invoke(_stringResource.GetLocalized("WslKernelPackageInstalled")); + } + finally + { + _wslKernelPackageInstallLock.Release(); + } + } + /// /// Retrieves information about all registered distributions on the machine and fills in any missing data /// that is needed for them to be shown in Dev Home's UI. E.g logo images. @@ -179,4 +224,31 @@ private void StartDistributionStatePolling() }, _oneMinutePollingInterval); } + + private void OnInstallChanged(object sender, AppInstallManagerItemEventArgs args) + { + var installItem = args.Item; + + WslInstallationEventHandler?.Invoke(this, installItem); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + _log.Debug("Disposing WslManager"); + if (disposing) + { + _wslKernelPackageInstallLock.Dispose(); + } + } + + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } } diff --git a/extensions/WSLExtension/Services/WslServicesMediator.cs b/extensions/WSLExtension/Services/WslServicesMediator.cs index 7d3e7d5ed..d2843e4b9 100644 --- a/extensions/WSLExtension/Services/WslServicesMediator.cs +++ b/extensions/WSLExtension/Services/WslServicesMediator.cs @@ -32,6 +32,12 @@ public WslServicesMediator(IProcessCreator creator) /// public HashSet GetAllNamesOfRunningDistributions() { + // Only attempt to get the running distributions if the kernel package is installed. + if (_packageHelper.GetPackageFromPackageFamilyName(WSLPackageFamilyName) is null) + { + return new HashSet(); + } + var processData = _processCreator.CreateProcessWithoutWindowAndWaitForExit(WslExe, ListAllRunningDistributions); // wsl.exe returns an error code when there are no distributions running. But in that case diff --git a/extensions/WSLExtension/Strings/en-US/Resources.resw b/extensions/WSLExtension/Strings/en-US/Resources.resw index cf89c0b8c..240569470 100644 --- a/extensions/WSLExtension/Strings/en-US/Resources.resw +++ b/extensions/WSLExtension/Strings/en-US/Resources.resw @@ -60,7 +60,7 @@ Action passed to the extension was not recognized. View the extension logs for more information - Error text to show when we don't recognize the adaptive card action that was passed to the extension + Error text to show when the extension doesn't recognize the adaptive card action that was passed to it by Dev Home Next @@ -116,38 +116,102 @@ Unable to terminate distribution due to error: {0} - {Locked="{0}"} Error text for when we couldn't terminate a distribution. {0} is the error message + {Locked="{0}"} Error text for when the extension isn't able to terminate a distribution. {0} is the error message Unable to unregister distribution due to error: {0} - {Locked="{0}"} Error text for when we couldn't unregister a distribution. {0} is the error message + {Locked="{0}"} Error text for when the extension isn't able to unregister a distribution. {0} is the error message Unable to launch a session into the distribution due to error: {0} - {Locked="{0}"} Error text for when we couldn't launch into the distribution. {0} is the error message + {Locked="{0}"} Error text for when the extension isn't able to launch a distribution. {0} is the error message - - Preparing to install and register {0} - {Locked="{0}"} Text message for when an installation of a wsl distribution start. {0} is the name of the distribution + + 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 + 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 we failed to install a wsl distribution. {0} is the name of the distribution and {1} is the error message. + {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 we failed to install a wsl distribution. {0} is the name of the distribution + {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"} + + + Checking if the Windows Subsystem for Linux kernel package is installed + {Locked="Windows", "Linux"} - Successfully installed and registered {0} - {Locked="{0}"} Text message to display when we successfully installed a wsl distribution. {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 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. + {Locked="Windows", "WSL", "aka.ms/wslinstall", "Dev Home"} Text message to display when the virtual machine platform Windows optional component is not enabled. + + + Installation of the {0} package is pending + {Locked="{0}"} Text to display when an app installation is pending. {0} is the name of the package + + + Installation of the {0} package is now starting + {Locked="{0}"} Text to display when an app installation is starting. {0} is the name of the package + + + Downloading the {0} package {1} + {Locked="{0}", "{1}"} Text to display when an app is downloading. {0} is the name of the package and {1} is the progress of the download e.g "10MB / 200MB" + + + Installing the {0} package + {Locked="{0}"} Text to display when an app is installing. + + + Installation of the {0} package completed successfully + {Locked="{0}"} Text to display when an app installation has completed. {0} is the name of the package + + + Installation of the {0} package is paused + {Locked="{0}"} Text to display when an app installation is paused without a reason. {0} is the name of the package + + + Installation of the {0} package is paused due to low battery + {Locked="{0}"} Text to display when an app installation is paused due to low battery. {0} is the name of the package + + + Installation of the {0} package is paused. Please connect to a wifi network. + {Locked="{0}"} Text to display when an app installation is paused due to the user needing to connect their PC to wifi. {0} is the name of the package + + + Installation of the {0} package was Canceled + {Locked="{0}"} Text to display when an app installation is Canceled. {0} is the name of the package + + + Installation of the {0} package failed. '{1}' + {Locked="{0}", "{1}"} Text to display when an app installation failed. {0} is the name of the package and {1} is the error message + + + The {0} package is now ready to download + {Locked="{0}"} Text to display when an app is ready to download. {0} is the name of the package + + + Windows Subsystem for Linux + {Locked="Windows", "Linux"} Name of the WSL package name + + + The Windows Subsystem for Linux kernel package is installed + {Locked="Windows", "Linux"} Text to display when the wsl package is installed. \ No newline at end of file diff --git a/extensions/WSLExtension/WSLExtension.csproj b/extensions/WSLExtension/WSLExtension.csproj index 2fcb07411..e526048ae 100644 --- a/extensions/WSLExtension/WSLExtension.csproj +++ b/extensions/WSLExtension/WSLExtension.csproj @@ -50,6 +50,11 @@ + + + + + Always