Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow wsl extension to install the wsl kernel package before attempting to install a distribution #3743

Merged
merged 4 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion extensions/WSLExtension/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
7 changes: 7 additions & 0 deletions extensions/WSLExtension/Contracts/IWslManager.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -52,4 +53,10 @@ public interface IWslManager
/// This is a wrapper for <see cref="IWslServicesMediator.IsDistributionRunning(string)"/>
/// </summary>
public bool IsDistributionRunning(string distributionName);

/// <summary> Installs the WSL kernel package from the Microsoft store if it is not already installed. </summary>
public Task InstallWslKernelPackageAsync(Action<string>? statusUpdateCallback, CancellationToken cancellationToken);

/// <summary> Provides subscribers with download/installation progress for Microsoft store app installs. </summary>
public event EventHandler<AppInstallItem>? WslInstallationEventHandler;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public IAsyncOperation<ProviderOperationResult> OnAction(string action, string i
var shouldEndSession = false;
var adaptiveCardStateNotRecognizedError = _stringResource.GetLocalized("AdaptiveCardStateNotRecognizedError");

var actionPayload = Json.ToObject<AdaptiveCardActionPayload>(action);
var actionPayload = Helpers.Json.ToObject<AdaptiveCardActionPayload>(action);
if (actionPayload == null)
{
_log.Error($"Actions in Adaptive card action Json not recognized: {action}");
Expand Down
119 changes: 111 additions & 8 deletions extensions/WSLExtension/Models/WslInstallDistributionOperation.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
// 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;

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;

private readonly string _installationFailedTimeout;

private readonly string _installationSuccessful;

private const uint IndeterminateProgressPercentage = 0U;

private readonly TimeSpan _threeSecondDelayInSeconds = TimeSpan.FromSeconds(3);

private readonly DistributionDefinition _definition;
Expand All @@ -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);
Expand All @@ -52,27 +58,33 @@ private string GetLocalizedString(string resourcekey, string value)

public IAsyncOperation<CreateComputeSystemResult> 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) &&
Expand All @@ -93,11 +105,102 @@ public IAsyncOperation<CreateComputeSystemResult> 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)
bbonaby marked this conversation as resolved.
Show resolved Hide resolved
{
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<ICreateComputeSystemOperation, CreateComputeSystemActionRequiredEventArgs>? ActionRequired
Expand Down
5 changes: 5 additions & 0 deletions extensions/WSLExtension/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
78 changes: 75 additions & 3 deletions extensions/WSLExtension/Services/WslManager.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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));

Expand All @@ -28,20 +29,35 @@ public class WslManager : IWslManager

private readonly List<WslComputeSystem> _registeredWslDistributions = new();

private readonly IMicrosoftStoreService _microsoftStoreService;

private readonly IStringResource _stringResource;

private readonly SemaphoreSlim _wslKernelPackageInstallLock = new(1, 1);

public event EventHandler<HashSet<string>>? DistributionStateSyncEventHandler;

private Dictionary<string, DistributionDefinition>? _distributionDefinitionsMap;

private ThreadPoolTimer? _timerForUpdatingDistributionStates;

private bool _disposed;

public event EventHandler<AppInstallItem>? 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();
}

Expand Down Expand Up @@ -134,6 +150,35 @@ public void TerminateDistribution(string distributionName)
_wslServicesMediator.TerminateDistribution(distributionName);
}

/// <inheritdoc cref="IWslManager.InstallWslKernelPackageAsync"/>
public async Task InstallWslKernelPackageAsync(Action<string>? 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();
}
}

/// <summary>
/// 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.
Expand Down Expand Up @@ -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);
}
}
6 changes: 6 additions & 0 deletions extensions/WSLExtension/Services/WslServicesMediator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ public WslServicesMediator(IProcessCreator creator)
/// <inheritdoc cref="IWslServicesMediator.GetAllNamesOfRunningDistributions"/>
public HashSet<string> GetAllNamesOfRunningDistributions()
{
// Only attempt to get the running distributions if the kernel package is installed.
if (_packageHelper.GetPackageFromPackageFamilyName(WSLPackageFamilyName) is null)
{
return new HashSet<string>();
}

var processData = _processCreator.CreateProcessWithoutWindowAndWaitForExit(WslExe, ListAllRunningDistributions);

// wsl.exe returns an error code when there are no distributions running. But in that case
Expand Down
Loading
Loading