Skip to content

Commit

Permalink
Allow wsl extension to install the wsl kernel package before attempti…
Browse files Browse the repository at this point in the history
…ng to install a distribution (#3743)

* add initial code

* update so we only query running distributions when kernel package is installed

* update based on comments and merge with main
  • Loading branch information
bbonaby authored Sep 5, 2024
1 parent 4feeb45 commit d64f108
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 27 deletions.
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)
{
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

0 comments on commit d64f108

Please sign in to comment.