Skip to content

Commit

Permalink
Update creation so Dev Home no longer brings up the wsl.exe window du…
Browse files Browse the repository at this point in the history
…ring creation (#3794)

* update creation so Dev Home no longer brings up the wsl.exe cmd window
to install and register wsl distributions.

* Update extensions/WSLExtension/Contracts/IWslManager.cs

Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com>

---------

Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com>
  • Loading branch information
bbonaby and krschau committed Sep 9, 2024
1 parent 4810c85 commit 0ec0885
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 68 deletions.
12 changes: 10 additions & 2 deletions extensions/WSLExtension/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
// <launcher>.exe executable file. Where <launcher> 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";
Expand Down
9 changes: 5 additions & 4 deletions extensions/WSLExtension/Contracts/IWslManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ public interface IWslManager
/// </summary>
void LaunchDistribution(string distributionName);

/// <summary> Installs a new WSL distribution.
/// This is a wrapper for <see cref="IWslServicesMediator.InstallDistribution(string)"/>
/// </summary>
void InstallDistribution(string distributionName);
/// <summary> Installs a new WSL distribution from the Microsoft store.</summary>
public Task InstallDistributionPackageAsync(
DistributionDefinition definition,
Action<string>? statusUpdateCallback,
CancellationToken cancellationToken);

/// <summary> Terminates all sessions for a new WSL distribution.
/// This is a wrapper for <see cref="IWslServicesMediator.TerminateDistribution(string)"/>
Expand Down
5 changes: 3 additions & 2 deletions extensions/WSLExtension/Contracts/IWslServicesMediator.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;
using WSLExtension.Models;

namespace WSLExtension.Contracts;
Expand Down Expand Up @@ -31,8 +32,8 @@ public interface IWslServicesMediator
/// <summary> Launches a new WSL process with the provided distribution. </summary>
void LaunchDistribution(string distributionName);

/// <summary> Installs and registers a new distribution on the machine. </summary>
void InstallDistribution(string distributionName);
/// <summary> Installs and registers a distribution on the machine. </summary>
void InstallAndRegisterDistribution(Package distributionPackage);

/// <summary> Terminates all running WSL sessions for the provided distribution on the machine. </summary>
void TerminateDistribution(string distributionName);
Expand Down
14 changes: 10 additions & 4 deletions extensions/WSLExtension/DevHomeProviders/WslProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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)
{
}
}
45 changes: 12 additions & 33 deletions extensions/WSLExtension/Models/WslInstallDistributionOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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);
}

Expand All @@ -62,7 +53,6 @@ public IAsyncOperation<CreateComputeSystemResult> 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.
Expand All @@ -75,33 +65,22 @@ public IAsyncOperation<CreateComputeSystemResult> 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)
{
Expand Down
2 changes: 1 addition & 1 deletion extensions/WSLExtension/Models/WslProcessData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
95 changes: 86 additions & 9 deletions extensions/WSLExtension/Services/WslManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> _distributionsBeingInstalled = new();

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

private Dictionary<string, DistributionDefinition>? _distributionDefinitionsMap;
Expand Down Expand Up @@ -90,15 +94,25 @@ public async Task<List<DistributionDefinition>> GetAllDistributionsAvailableToIn
var registeredDistributionsMap = await GetInformationOnAllRegisteredDistributionsAsync();
var distributionsToListOnCreationPage = new List<DistributionDefinition>();
_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.
Expand Down Expand Up @@ -138,10 +152,58 @@ public void LaunchDistribution(string distributionName)
_wslServicesMediator.LaunchDistribution(distributionName);
}

/// <inheritdoc cref="IWslManager.InstallDistribution"/>
public void InstallDistribution(string distributionName)
/// <inheritdoc cref="IWslManager.InstallDistributionPackageAsync"/>
public async Task InstallDistributionPackageAsync(
DistributionDefinition definition,
Action<string>? 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);
}
}
}

/// <inheritdoc cref="IWslManager.TerminateDistribution"/>
Expand Down Expand Up @@ -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);
}
Expand Down
Loading

0 comments on commit 0ec0885

Please sign in to comment.