diff --git a/DevHome.sln b/DevHome.sln
index 6ab2a0efdb..7c9dfcd078 100644
--- a/DevHome.sln
+++ b/DevHome.sln
@@ -159,6 +159,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{E768
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.Services.DesiredStateConfiguration", "services\DevHome.Services.DesiredStateConfiguration\DevHome.Services.DesiredStateConfiguration.csproj", "{D7A1C2CE-36B1-43A8-9BA6-1EE2CF24479F}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WSLExtension", "WSLExtension", "{73D1E84F-56CC-412B-BF2B-FA692BF6B396}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WSLExtension", "extensions\WSLExtension\WSLExtension.csproj", "{B6153EEA-EADE-4BAA-B47D-6B48205C6696}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug_FailFast|arm64 = Debug_FailFast|arm64
@@ -1057,6 +1061,24 @@ Global
{D7A1C2CE-36B1-43A8-9BA6-1EE2CF24479F}.Release|x64.Build.0 = Release|x64
{D7A1C2CE-36B1-43A8-9BA6-1EE2CF24479F}.Release|x86.ActiveCfg = Release|x86
{D7A1C2CE-36B1-43A8-9BA6-1EE2CF24479F}.Release|x86.Build.0 = Release|x86
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug_FailFast|arm64.ActiveCfg = Debug|ARM64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug_FailFast|arm64.Build.0 = Debug|ARM64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug_FailFast|x64.ActiveCfg = Debug|x64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug_FailFast|x64.Build.0 = Debug|x64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug_FailFast|x86.ActiveCfg = Debug|x86
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug_FailFast|x86.Build.0 = Debug|x86
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug|arm64.ActiveCfg = Debug|ARM64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug|arm64.Build.0 = Debug|ARM64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug|x64.ActiveCfg = Debug|x64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug|x64.Build.0 = Debug|x64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug|x86.ActiveCfg = Debug|x86
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Debug|x86.Build.0 = Debug|x86
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Release|arm64.ActiveCfg = Release|ARM64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Release|arm64.Build.0 = Release|ARM64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Release|x64.ActiveCfg = Release|x64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Release|x64.Build.0 = Release|x64
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Release|x86.ActiveCfg = Release|x86
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1117,6 +1139,8 @@ Global
{8FB1EF90-B693-4A2A-A7F2-44ECA499D769} = {E0A15760-487A-4CCB-8093-DE6FF3C4BC23}
{E768781A-D1F7-4C03-B46D-E76354FAB587} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD}
{D7A1C2CE-36B1-43A8-9BA6-1EE2CF24479F} = {E0A15760-487A-4CCB-8093-DE6FF3C4BC23}
+ {73D1E84F-56CC-412B-BF2B-FA692BF6B396} = {DCAF188B-60C3-4EDB-8049-BAA927FBCD7D}
+ {B6153EEA-EADE-4BAA-B47D-6B48205C6696} = {73D1E84F-56CC-412B-BF2B-FA692BF6B396}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {030B5641-B206-46BB-BF71-36FF009088FA}
diff --git a/common/Environments/Styles/HorizontalCardStyles.xaml b/common/Environments/Styles/HorizontalCardStyles.xaml
index 7a9e98c2b8..db6109d5e2 100644
--- a/common/Environments/Styles/HorizontalCardStyles.xaml
+++ b/common/Environments/Styles/HorizontalCardStyles.xaml
@@ -238,4 +238,12 @@
+
diff --git a/common/Helpers/CommonConstants.cs b/common/Helpers/CommonConstants.cs
index 9e3c5c86a2..e1238da23e 100644
--- a/common/Helpers/CommonConstants.cs
+++ b/common/Helpers/CommonConstants.cs
@@ -5,7 +5,9 @@ namespace DevHome.Common.Helpers;
public static class CommonConstants
{
- public const string HyperVExtensionClassId = "F8B26528-976A-488C-9B40-7198FB425C9E";
+ public const string HyperVExtensionClassId = "F8B26528-976A-488C-9B40-7198FB425C9E";
+
+ public const string WSLExtensionClassId = "121253AB-BA5D-4E73-99CF-25A2CB8BF173";
public const string HyperVWindowsOptionalFeatureName = "Microsoft-Hyper-V";
}
diff --git a/common/Views/AdaptiveCardViews/AdaptiveCardResourceTemplates.xaml b/common/Views/AdaptiveCardViews/AdaptiveCardResourceTemplates.xaml
index d43ba0d760..90f6183f2c 100644
--- a/common/Views/AdaptiveCardViews/AdaptiveCardResourceTemplates.xaml
+++ b/common/Views/AdaptiveCardViews/AdaptiveCardResourceTemplates.xaml
@@ -4,19 +4,32 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cardModels="using:DevHome.Common.DevHomeAdaptiveCards.CardModels"
- xmlns:controls="using:CommunityToolkit.WinUI.Controls">
+ xmlns:controls="using:CommunityToolkit.WinUI.Controls"
+ xmlns:local="using:DevHome.Common.Views.AdaptiveCardViews"
+ xmlns:converters="using:CommunityToolkit.WinUI.Converters">
-
+
+
-
+
+ 40
+
+
diff --git a/extensions/WSLExtension/ClassExtensions/IHostExtensions.cs b/extensions/WSLExtension/ClassExtensions/IHostExtensions.cs
new file mode 100644
index 0000000000..9cc1fe9e41
--- /dev/null
+++ b/extensions/WSLExtension/ClassExtensions/IHostExtensions.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace WSLExtension.ClassExtensions;
+
+public static class IHostExtensions
+{
+ ///
+ public static T CreateInstance(this IHost host, params object[] parameters)
+ {
+ return ActivatorUtilities.CreateInstance(host.Services, parameters);
+ }
+
+ ///
+ /// Gets the service object for the specified type, or throws an exception
+ /// if type was not registered.
+ ///
+ /// Service type
+ /// Host object
+ /// Service object
+ /// Throw an exception if the specified
+ /// type is not registered
+ public static T GetService(this IHost host)
+ where T : class
+ {
+ if (host.Services.GetService(typeof(T)) is not T service)
+ {
+ throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
+ }
+
+ return service;
+ }
+}
diff --git a/extensions/WSLExtension/ClassExtensions/ResourceExtensions.cs b/extensions/WSLExtension/ClassExtensions/ResourceExtensions.cs
new file mode 100644
index 0000000000..4ae87ea97e
--- /dev/null
+++ b/extensions/WSLExtension/ClassExtensions/ResourceExtensions.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Windows.ApplicationModel.Resources;
+
+namespace WSLExtension.ClassExtensions;
+
+public static class ResourceExtensions
+{
+ private static readonly ResourceLoader _resourceLoader = new();
+
+ public static string GetLocalized(this string resourceKey) => _resourceLoader.GetString(resourceKey);
+}
diff --git a/extensions/WSLExtension/ClassExtensions/ServiceExtensions.cs b/extensions/WSLExtension/ClassExtensions/ServiceExtensions.cs
new file mode 100644
index 0000000000..942f86613d
--- /dev/null
+++ b/extensions/WSLExtension/ClassExtensions/ServiceExtensions.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Windows.DevHome.SDK;
+using WSLExtension.Contracts;
+using WSLExtension.DevHomeProviders;
+using WSLExtension.DistributionDefinitions;
+using WSLExtension.Models;
+using WSLExtension.Services;
+
+namespace WSLExtension.ClassExtensions;
+
+public static class ServiceExtensions
+{
+ public static IServiceCollection AddWslExtensionServices(this IServiceCollection services)
+ {
+ // Services
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // Factory delegate to create WslComputeSystems
+ services.AddSingleton(
+ serviceProvider => wslDistribution => ActivatorUtilities.CreateInstance(serviceProvider, wslDistribution));
+
+ return services;
+ }
+}
diff --git a/extensions/WSLExtension/ClassExtensions/StringExtensions.cs b/extensions/WSLExtension/ClassExtensions/StringExtensions.cs
new file mode 100644
index 0000000000..a233ab45cd
--- /dev/null
+++ b/extensions/WSLExtension/ClassExtensions/StringExtensions.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Globalization;
+
+namespace WSLExtension.ClassExtensions;
+
+public static class StringExtensions
+{
+ public static string FormatArgs(this string source, params object[] args)
+ {
+ return string.Format(CultureInfo.InvariantCulture, source, args);
+ }
+}
diff --git a/extensions/WSLExtension/Constants.cs b/extensions/WSLExtension/Constants.cs
new file mode 100644
index 0000000000..cd07902e9b
--- /dev/null
+++ b/extensions/WSLExtension/Constants.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace WSLExtension;
+
+public static class Constants
+{
+#if CANARY_BUILD
+ public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome.Canary/Files/WslAssets/wslLinux.png";
+#elif STABLE_BUILD
+ public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome/Files/WslAssets/wslLinux.png";
+#else
+ public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome.Dev/Files/WslAssets/wslLinux.png";
+#endif
+
+ // Common unlocalized strings used for WSL extension
+ public const string WslProviderDisplayName = "Microsoft WSL";
+ public const string WslProviderId = "Microsoft.WSL";
+ public const string WindowsTerminalShimExe = "wt.exe";
+ public const string WindowsTerminalPackageFamilyName = "Microsoft.WindowsTerminal_8wekyb3d8bbwe";
+ public const string WslExe = "wsl.exe";
+ public const string WslTemplateSubfolderName = "WslTemplates";
+
+ 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";
+ public const string KnownDistributionsWebJsonLocation = @"https://aka.ms/wsldistributionsjson";
+
+ // Wsl registry location for registered distributions.
+ public const string WslRegistryLocation = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss";
+
+ // Wsl registry data names within a distribution location.
+ public const string PackageFamilyRegistryName = "PackageFamilyName";
+ public const string DistributionRegistryName = "DistributionName";
+ public const string DefaultDistributionRegistryName = "DefaultDistribution";
+ public const string WslVersion = "Version";
+ public const string WslState = "State";
+ public const int WslVersion1 = 1;
+ public const int WslVersion2 = 2;
+ public const int WslExeExitSuccess = 0;
+
+ // Launch terminal with specific profile and log the user into their home directory in the login shell
+ // Note: this opens a new terminal window in the UI
+ public static string LaunchDistributionInTerminalWithProfile { get; } = "--profile {0} -- wsl --shell-type login --cd ~ --distribution {1}";
+
+ // Launch without using a Terminal profile and log the user into their home directory using the login shell
+ // Note: this opens a new terminal window in the UI
+ public static string LaunchDistributionInTerminalWithNoProfile { get; } = "wsl --shell-type login --cd ~ --distribution {0}";
+
+ // Launch into the wsl process without terminal and log the user into their home directory using the login shell
+ // Note: this opens a new terminal window in the UI
+ public static string LaunchDistributionWithoutTerminal { get; } = "--shell-type login --cd ~ --distribution {0}";
+
+ // Arguments to unregister a wsl distribution from a machine using wsl.exe
+ public const string UnregisterDistributionArgs = "--unregister {0}";
+
+ // 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 using Terminal
+ // Note: this opens a new terminal window in the UI
+ public const string InstallDistributionWithTerminal = "wsl --install --distribution {0}";
+
+ // Arguments to download, install and register a wsl distribution without using terminal
+ // Note: this opens a cmd window in the UI
+ public const string InstallDistributionWithoutTerminal = "--install --distribution {0}";
+
+ // 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/IDistributionDefinitionHelper.cs b/extensions/WSLExtension/Contracts/IDistributionDefinitionHelper.cs
new file mode 100644
index 0000000000..562c971074
--- /dev/null
+++ b/extensions/WSLExtension/Contracts/IDistributionDefinitionHelper.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using WSLExtension.DistributionDefinitions;
+
+namespace WSLExtension.Contracts;
+
+///
+/// Provides definition information about all the WSL distributions that can be found at
+/// .
+///
+public interface IDistributionDefinitionHelper
+{
+ ///
+ /// Retrieves a list of objects that contain metadata about WSL distributions that can be
+ /// installed from the wsl.exe executable.
+ ///
+ ///
+ /// A Dictionary where the key is the name of the distribution and the value is its
+ /// metadata.
+ ///
+ public Task> GetDistributionDefinitionsAsync();
+}
diff --git a/extensions/WSLExtension/Contracts/IProcessCreator.cs b/extensions/WSLExtension/Contracts/IProcessCreator.cs
new file mode 100644
index 0000000000..a18bf217f6
--- /dev/null
+++ b/extensions/WSLExtension/Contracts/IProcessCreator.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using WSLExtension.Models;
+
+namespace WSLExtension.Contracts;
+
+///
+/// Interface used to create processes throughout the WSL extension.
+///
+public interface IProcessCreator
+{
+ ///
+ /// Creates and starts a new process that opens in a new Window. Note:
+ /// The process is started and the method does not wait until the process
+ /// has exited before returning.
+ ///
+ /// The name of the executable to start
+ /// The arguments that will be passed to the executable at process startup
+ public void CreateProcessWithWindow(string fileName, string arguments);
+
+ ///
+ /// Creates and starts a new process without opening a window. Note: execution is blocked
+ /// until the process exits.
+ ///
+ /// The name of the executable to start
+ /// The arguments that will be passed to the executable at process startup
+ /// The meta data associated with the exited process.
+ /// E.g StdOutput, StdError and its exit code.
+ ///
+ public WslProcessData CreateProcessWithoutWindowAndWaitForExit(string fileName, string arguments);
+}
diff --git a/extensions/WSLExtension/Contracts/IStringResource.cs b/extensions/WSLExtension/Contracts/IStringResource.cs
new file mode 100644
index 0000000000..7feec156dd
--- /dev/null
+++ b/extensions/WSLExtension/Contracts/IStringResource.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace WSLExtension.Contracts;
+
+public interface IStringResource
+{
+ public string GetLocalized(string key, params object[] args);
+}
diff --git a/extensions/WSLExtension/Contracts/IWslManager.cs b/extensions/WSLExtension/Contracts/IWslManager.cs
new file mode 100644
index 0000000000..eadaf4b014
--- /dev/null
+++ b/extensions/WSLExtension/Contracts/IWslManager.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using WSLExtension.DistributionDefinitions;
+using WSLExtension.Models;
+
+namespace WSLExtension.Contracts;
+
+///
+/// Used to interact between the WSL dev environment compute systems and the WSL mediator.
+///
+public interface IWslManager
+{
+ public event EventHandler>? DistributionStateSyncEventHandler;
+
+ /// Gets a list of all registered WSL distributions on the machine.
+ public Task> GetAllRegisteredDistributionsAsync();
+
+ ///
+ /// Gets a list of objects that each contain metadata about a wsl distribution
+ /// that is not currently registered on the machine and is available to install.
+ ///
+ public Task> GetAllDistributionsAvailableToInstallAsync();
+
+ ///
+ /// Gets a list of objects that each contain information about a WSL distribution that is already
+ /// registered on the machine.
+ ///
+ public Task GetInformationOnRegisteredDistributionAsync(string distributionName);
+
+ ///
+ /// Unregisters a WSL distribution. This is a wrapper for
+ ///
+ void UnregisterDistribution(string distributionName);
+
+ /// Launches a new WSL distribution.
+ /// This is a wrapper for
+ ///
+ void LaunchDistribution(string distributionName, string? windowsTerminalProfile);
+
+ /// Installs a new WSL distribution.
+ /// This is a wrapper for
+ ///
+ void InstallDistribution(string distributionName);
+
+ /// Terminates all sessions for a new WSL distribution.
+ /// This is a wrapper for
+ ///
+ void TerminateDistribution(string distributionName);
+
+ /// Gets a boolean indicating whether the WSL distribution is currently running.
+ /// This is a wrapper for
+ ///
+ public bool IsDistributionRunning(string distributionName);
+}
diff --git a/extensions/WSLExtension/Contracts/IWslServicesMediator.cs b/extensions/WSLExtension/Contracts/IWslServicesMediator.cs
new file mode 100644
index 0000000000..2915dea08f
--- /dev/null
+++ b/extensions/WSLExtension/Contracts/IWslServicesMediator.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using WSLExtension.Models;
+
+namespace WSLExtension.Contracts;
+
+///
+/// Used to interact between the WSL service on the machine and
+/// the wsl extension itself.
+///
+public interface IWslServicesMediator
+{
+ ///
+ /// Gets a set of all currently running distributions. Note: each WSL distribution
+ /// name is unique.
+ ///
+ public HashSet GetAllNamesOfRunningDistributions();
+
+ ///
+ /// Gets a list of the registered distributions on the machine.
+ ///
+ public List GetAllRegisteredDistributions();
+
+ ///
+ /// Unregisters a WSL distribution from the WSL service. Note: This is the same as deleting the
+ /// distribution and any of its associated data.
+ ///
+ void UnregisterDistribution(string distributionName);
+
+ /// Launches a new WSL process with the provided distribution.
+ void LaunchDistribution(string distributionName, string? windowsTerminalProfile);
+
+ /// Installs and registers a new distribution on the machine.
+ void InstallDistribution(string distributionName);
+
+ /// Terminates all running WSL sessions for the provided distribution on the machine.
+ void TerminateDistribution(string distributionName);
+
+ /// Checks whether the provided WSL distribution is currently running
+ /// True only if the distribution is running. False otherwise.
+ public bool IsDistributionRunning(string distributionName);
+}
diff --git a/extensions/WSLExtension/DevHomeProviders/WslProvider.cs b/extensions/WSLExtension/DevHomeProviders/WslProvider.cs
new file mode 100644
index 0000000000..1762a8269e
--- /dev/null
+++ b/extensions/WSLExtension/DevHomeProviders/WslProvider.cs
@@ -0,0 +1,100 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using Microsoft.Windows.DevHome.SDK;
+using Serilog;
+using Windows.Foundation;
+using WSLExtension.Contracts;
+using WSLExtension.Models;
+using static WSLExtension.Constants;
+
+namespace WSLExtension.DevHomeProviders;
+
+/// Provides functionality to enumerate and install WSL distributions
+public class WslProvider : IComputeSystemProvider
+{
+ private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WslProvider));
+
+ private readonly IStringResource _stringResource;
+
+ private readonly IWslManager _wslManager;
+
+ public string DisplayName => WslProviderDisplayName;
+
+ public Uri Icon { get; }
+
+ public string Id => WslProviderId;
+
+ public ComputeSystemProviderOperations SupportedOperations => ComputeSystemProviderOperations.CreateComputeSystem;
+
+ public WslProvider(IStringResource stringResource, IWslManager wslManager)
+ {
+ _stringResource = stringResource;
+ _wslManager = wslManager;
+ Icon = new(ExtensionIcon);
+ }
+
+ ///
+ /// Creates and returns the adaptive card session that will appear in the create environment creation UX in Dev Home.
+ ///
+ public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForDeveloperId(IDeveloperId developerId, ComputeSystemAdaptiveCardKind sessionKind)
+ {
+ var definitions = _wslManager.GetAllDistributionsAvailableToInstallAsync().GetAwaiter().GetResult();
+ return new ComputeSystemAdaptiveCardResult(new RegisterAndInstallDistributionSession(definitions, _stringResource));
+ }
+
+ ///
+ /// Creates the operation that when started will install and register the WSL distribution.
+ ///
+ public ICreateComputeSystemOperation? CreateCreateComputeSystemOperation(IDeveloperId? developerId, string inputJson)
+ {
+ try
+ {
+ var deserializedObject = JsonSerializer.Deserialize(inputJson, typeof(WslInstallationUserInput));
+ var wslInstallationUserInput =
+ 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);
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Failed to create the compute system creation operation. InputJson: {inputJson}");
+
+ // Dev Home will handle null values as failed operations. We can't throw because this is an out of proc
+ // COM call, so we'll lose the error information. We'll log the error and return null.
+ return null;
+ }
+ }
+
+ public IAsyncOperation GetComputeSystemsAsync(IDeveloperId developerId)
+ {
+ return Task.Run(async () =>
+ {
+ try
+ {
+ var computeSystems = await _wslManager.GetAllRegisteredDistributionsAsync();
+
+ _log.Information($"Successfully retrieved all wsl distributions");
+ return new ComputeSystemsResult(computeSystems);
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Failed to retrieve all wsl distributions");
+ return new ComputeSystemsResult(ex, ex.Message, ex.Message);
+ }
+ }).AsAsyncOperation();
+ }
+
+ public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForComputeSystem(
+ IComputeSystem computeSystem,
+ ComputeSystemAdaptiveCardKind sessionKind)
+ {
+ var notImplementedException = new NotImplementedException($"Method not implemented by WSL Compute System Provider");
+ return new ComputeSystemAdaptiveCardResult(notImplementedException, notImplementedException.Message, notImplementedException.Message);
+ }
+}
diff --git a/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs
new file mode 100644
index 0000000000..956743dd1d
--- /dev/null
+++ b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+
+namespace WSLExtension.DistributionDefinitions;
+
+///
+/// Represents a definition of a WSL distribution. This metadata is used
+/// to deserialize the WSL DistributionInfo.json file located in
+/// . It is also
+/// used to deserialize the WSL definitions located in the local
+/// file.
+///
+public class DistributionDefinition
+{
+ public string FriendlyName { get; set; } = string.Empty;
+
+ public string Name { get; set; } = string.Empty;
+
+ public string LogoFile { get; set; } = string.Empty;
+
+ public string Base64StringLogo { get; set; } = string.Empty;
+
+ public string? WindowsTerminalProfileGuid { get; set; }
+
+ public string? StoreAppId { get; set; }
+
+ [JsonPropertyName("Amd64")]
+ public bool IsAmd64Supported { get; set; }
+
+ [JsonPropertyName("Arm64")]
+ public bool IsArm64Supported { get; set; }
+
+ public string PackageFamilyName { get; set; } = string.Empty;
+
+ public string Publisher { get; set; } = string.Empty;
+}
diff --git a/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.yaml b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.yaml
new file mode 100644
index 0000000000..8824e88616
--- /dev/null
+++ b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinition.yaml
@@ -0,0 +1,74 @@
+# This is used to map a distributions name to it logo file in the WslAssets folder. These distribution names
+# should be kept in sync with the distributionInfo.json file here: https://aka.ms/wsldistributionsjson.
+# Note: Cela has approved the usage of these image assets in Dev Home as they are used for non-marketing purposes.
+---
+# See: https://www.debian.org/logos/ for trademark information.
+- Name: Debian
+ LogoFile: debian.png
+ Publisher: The Debian Project
+
+# See: https://www.kali.org/docs/policy/trademark/ for trademark information.
+- Name: kali-linux
+ LogoFile: kali.png
+ Publisher: Kali Linux
+
+# See: https://en.opensuse.org/openSUSE:Trademark_guidelines#Redistributing_openSUSE_Without_Modifications for trademark information.
+- Name: openSUSE-Leap-15.5
+ LogoFile: opensuse.png
+ Publisher: SUSE
+
+# See: https://en.opensuse.org/openSUSE:Trademark_guidelines#Redistributing_openSUSE_Without_Modifications for trademark information.
+- Name: openSUSE-Tumbleweed
+ LogoFile: opensuse.png
+ Publisher: SUSE
+
+ # We don't store oracle logos in Dev Home.
+- Name: OracleLinux_7_9
+ Publisher: Oracle America, Inc.
+
+- Name: OracleLinux_8_7
+ Publisher: Oracle America, Inc.
+
+- Name: OracleLinux_9_1
+ Publisher: Oracle America, Inc.
+
+# See: https://en.opensuse.org/openSUSE:Trademark_guidelines#Redistributing_openSUSE_Without_Modifications for trademark information.
+- Name: SUSE-Linux-Enterprise-Server-15-SP4
+ LogoFile: openuse-enterprise.png
+ Publisher: SUSE
+
+# logo can be found: https://github.com/openSUSE/artwork/blob/master/logos/buttons/button-colour.png
+# See: https://en.opensuse.org/openSUSE:Trademark_guidelines#Redistributing_openSUSE_Without_Modifications for trademark information.
+- Name: SUSE-Linux-Enterprise-15-SP5
+ LogoFile: openuse-enterprise.png
+ Publisher: SUSE
+
+# See heading 6 at: https://ubuntu.com/legal/intellectual-property-policy for trademark information.
+- Name: Ubuntu
+ WindowsTerminalProfileGuid: "{51855cb2-8cce-5362-8f54-464b92b32386}"
+ LogoFile: ubuntu.png
+ Publisher: Canonical Group Limited
+
+# See heading 6 at: https://ubuntu.com/legal/intellectual-property-policy for trademark information.
+- Name: Ubuntu-18.04
+ WindowsTerminalProfileGuid: "{24a0533e-913b-5f8c-a5cb-6be85a4c9e70}"
+ LogoFile: ubuntu.png
+ Publisher: Canonical Group Limited
+
+# See heading 6 at: https://ubuntu.com/legal/intellectual-property-policy for trademark information.
+- Name: Ubuntu-20.04
+ WindowsTerminalProfileGuid: "{4dd1e689-b517-5f39-947d-78e8a8bdf958}"
+ LogoFile: ubuntu.png
+ Publisher: Canonical Group Limited
+
+# See heading 6 at: https://ubuntu.com/legal/intellectual-property-policy for trademark information.
+- Name: Ubuntu-22.04
+ WindowsTerminalProfileGuid: "{e5a83caa-4c73-52b3-ae6b-bc438d721ef9}"
+ LogoFile: ubuntu.png
+ Publisher: Canonical Group Limited
+
+# See heading 6 at: https://ubuntu.com/legal/intellectual-property-policy for trademark information.
+- Name: Ubuntu-24.04
+ WindowsTerminalProfileGuid: "{acbafd15-cbbb-5bb3-8a61-bed446ff4b83}"
+ LogoFile: ubuntu.png
+ Publisher: Canonical Group Limited
diff --git a/extensions/WSLExtension/DistributionDefinitions/DistributionDefinitionHelper.cs b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinitionHelper.cs
new file mode 100644
index 0000000000..9b31040add
--- /dev/null
+++ b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinitionHelper.cs
@@ -0,0 +1,142 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using Serilog;
+using Windows.Storage;
+using WSLExtension.ClassExtensions;
+using WSLExtension.Contracts;
+using WSLExtension.Helpers;
+using YamlDotNet.Serialization;
+using static WSLExtension.Constants;
+
+namespace WSLExtension.DistributionDefinitions;
+
+///
+/// Provides definition information about all the WSL distributions that can be found at
+/// .
+///
+public class DistributionDefinitionHelper : IDistributionDefinitionHelper, IDisposable
+{
+ private readonly ILogger _log = Log.ForContext("SourceContext", nameof(DistributionDefinitionHelper));
+
+ private readonly IHttpClientFactory _httpClientFactory;
+
+ private readonly PackageHelper _packageHelper = new();
+
+ private readonly Architecture _osArchitecture;
+
+ private readonly SemaphoreSlim _definitionsLock = new(1, 1);
+
+ private readonly JsonSerializerOptions _jsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ private readonly Dictionary _distributionDefinitionsMap = new();
+
+ private bool _disposedValue;
+
+ public DistributionDefinitionHelper(IHttpClientFactory httpClientFactory)
+ {
+ _httpClientFactory = httpClientFactory;
+ _osArchitecture = RuntimeInformation.OSArchitecture;
+ }
+
+ ///
+ public async Task> GetDistributionDefinitionsAsync()
+ {
+ await _definitionsLock.WaitAsync();
+
+ try
+ {
+ // Get the update to date distribution definitions from WSL GitHub repository.
+ // We use definitions from the web as our single source of truth, these web definitions are the
+ // same that are used in the command wsl.exe --list --online.
+ var client = _httpClientFactory.CreateClient();
+ var distributionDefinitionsJson = await client.GetStringAsync(KnownDistributionsWebJsonLocation);
+ var webDefinitions = JsonSerializer.Deserialize(distributionDefinitionsJson, _jsonOptions);
+
+ foreach (var definition in webDefinitions!.Values)
+ {
+ // Only supported distributions for this machine.
+ if (ShouldAddDistribution(definition))
+ {
+ _distributionDefinitionsMap[definition.Name] = definition;
+ }
+ }
+
+ // Merge the local distribution information we have stored in DistributionDefinition.yaml with the one above.
+ var uri = new Uri(KnownDistributionsLocalYamlLocation);
+ var storageFile = await StorageFile.GetFileFromApplicationUriAsync(uri);
+ var localYamlDefinitionsFile = await FileIO.ReadTextAsync(storageFile);
+ var localYamlDefinitions = BuildYamlDeserializer().Deserialize>(localYamlDefinitionsFile);
+ foreach (var localYamlDefinition in localYamlDefinitions)
+ {
+ // Ignore distributions that we have in the local yaml file but are no longer present in the web file.
+ if (!_distributionDefinitionsMap.TryGetValue(localYamlDefinition.Name, out var definitionFromWeb))
+ {
+ continue;
+ }
+
+ definitionFromWeb.Publisher = localYamlDefinition.Publisher;
+ definitionFromWeb.WindowsTerminalProfileGuid = localYamlDefinition.WindowsTerminalProfileGuid;
+
+ // Only add a logo to the definition we got from the web if the definition in the local yaml file
+ // has one.
+ if (!string.IsNullOrEmpty(localYamlDefinition.LogoFile))
+ {
+ // Update the logo with the base64 string representation so we can show it as a thumbnail.
+ var logoFilePath = WslLogoPathFormat.FormatArgs(localYamlDefinition.LogoFile);
+ definitionFromWeb.Base64StringLogo = await _packageHelper.GetBase64StringFromLogoPathAsync(logoFilePath);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Unable to retrieve all definitions for known distributions");
+ }
+
+ return _distributionDefinitionsMap;
+ }
+
+ private bool ShouldAddDistribution(DistributionDefinition distribution)
+ {
+ if (_osArchitecture == Architecture.Arm64)
+ {
+ return distribution.IsArm64Supported;
+ }
+ else if (_osArchitecture == Architecture.X64)
+ {
+ return distribution.IsAmd64Supported;
+ }
+
+ return false;
+ }
+
+ private IDeserializer BuildYamlDeserializer()
+ {
+ return new DeserializerBuilder().IgnoreUnmatchedProperties().Build();
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ _definitionsLock.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+ }
+}
diff --git a/extensions/WSLExtension/DistributionDefinitions/DistributionDefinitions.cs b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinitions.cs
new file mode 100644
index 0000000000..7568bd3a3c
--- /dev/null
+++ b/extensions/WSLExtension/DistributionDefinitions/DistributionDefinitions.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+
+namespace WSLExtension.DistributionDefinitions;
+
+///
+/// Used when deserializing json file at
+///
+public class DistributionDefinitions
+{
+ [JsonPropertyName("Distributions")]
+ public List Values { get; set; } = new();
+}
diff --git a/extensions/WSLExtension/Exceptions/AdaptiveCardInvalidActionException.cs b/extensions/WSLExtension/Exceptions/AdaptiveCardInvalidActionException.cs
new file mode 100644
index 0000000000..d48f20d2d2
--- /dev/null
+++ b/extensions/WSLExtension/Exceptions/AdaptiveCardInvalidActionException.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace WSLExtension.Exceptions;
+
+public class AdaptiveCardInvalidActionException : Exception
+{
+ public AdaptiveCardInvalidActionException(string message)
+ : base(message)
+ {
+ }
+}
diff --git a/extensions/WSLExtension/Exceptions/WslServicesMediatorException.cs b/extensions/WSLExtension/Exceptions/WslServicesMediatorException.cs
new file mode 100644
index 0000000000..4b64da0b66
--- /dev/null
+++ b/extensions/WSLExtension/Exceptions/WslServicesMediatorException.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace WSLExtension.Exceptions;
+
+public class WslServicesMediatorException : Exception
+{
+ public WslServicesMediatorException(string? message)
+ : base(message)
+ {
+ }
+}
diff --git a/extensions/WSLExtension/Helpers/AdaptiveCardActionPayload.cs b/extensions/WSLExtension/Helpers/AdaptiveCardActionPayload.cs
new file mode 100644
index 0000000000..fcd5ea4b88
--- /dev/null
+++ b/extensions/WSLExtension/Helpers/AdaptiveCardActionPayload.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace WSLExtension.Helpers;
+
+internal sealed class AdaptiveCardActionPayload
+{
+ public string? Id { get; set; }
+}
diff --git a/extensions/WSLExtension/Helpers/Json.cs b/extensions/WSLExtension/Helpers/Json.cs
new file mode 100644
index 0000000000..3e42ee7b87
--- /dev/null
+++ b/extensions/WSLExtension/Helpers/Json.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace WSLExtension.Helpers;
+
+public static class Json
+{
+ private static readonly JsonSerializerOptions _options = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.Never,
+ IncludeFields = true,
+ };
+
+ public static T? ToObject(string value)
+ {
+ if (typeof(T) == typeof(bool))
+ {
+ return (T)(object)bool.Parse(value);
+ }
+
+ return JsonSerializer.Deserialize(value, _options);
+ }
+}
diff --git a/extensions/WSLExtension/Helpers/Logging.cs b/extensions/WSLExtension/Helpers/Logging.cs
new file mode 100644
index 0000000000..7a91a1aea7
--- /dev/null
+++ b/extensions/WSLExtension/Helpers/Logging.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Windows.Storage;
+
+namespace WSLExtension.Helpers;
+
+public static class Logging
+{
+ public static readonly string LogFolderName = "Logs";
+
+ public static readonly string WslSubFolderName = "WSL";
+
+ private static readonly Lazy _logFolderRoot = new(() => Path.Combine(ApplicationData.Current.TemporaryFolder.Path, LogFolderName));
+
+ public static readonly string RootDevHomeLogFolder = _logFolderRoot.Value;
+
+ public static readonly string PathToWslLogFolder = Path.Combine(RootDevHomeLogFolder, WslSubFolderName);
+}
diff --git a/extensions/WSLExtension/Helpers/PackageHelper.cs b/extensions/WSLExtension/Helpers/PackageHelper.cs
new file mode 100644
index 0000000000..e58cfa2b44
--- /dev/null
+++ b/extensions/WSLExtension/Helpers/PackageHelper.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Runtime.InteropServices.WindowsRuntime;
+using Serilog;
+using Windows.ApplicationModel;
+using Windows.Foundation;
+using Windows.Management.Deployment;
+using Windows.Storage;
+using Windows.Storage.Streams;
+
+namespace WSLExtension.Helpers;
+
+public class PackageHelper
+{
+ private readonly ILogger _log = Log.ForContext("SourceContext", nameof(PackageHelper));
+
+ // Using 256 because the thumbnail in Dev Home's environments page
+ // is 64x64 but Windows packaged app logo sizes go up to 256.
+ // Getting the 256px image helps as it will mean the image will be rounded down instead
+ // of up, if there is a scaling change.
+ // See: https://learn.microsoft.com/windows/apps/design/style/iconography/app-icon-construction
+ private readonly Size _logoDimensions = new(256, 256);
+
+ private readonly PackageManager _packageManager = new();
+
+ public virtual bool IsPackageInstalled(string packageName)
+ {
+ var currentPackage = _packageManager.FindPackagesForUser(string.Empty, packageName).FirstOrDefault();
+ return currentPackage != null;
+ }
+
+ public async virtual Task GetPackageIconAsByteArrayAsync(string packageName)
+ {
+ // We'll use the first installed distribution for the package family to get the icon.
+ if (!(GetPackageFromPackageFamilyName(packageName) is Package package))
+ {
+ return [];
+ }
+
+ var stream = package.GetLogoAsRandomAccessStreamReference(_logoDimensions);
+ var logoStream = await stream.OpenReadAsync();
+
+ // Convert the stream to a byte array
+ var bytesArray = new byte[logoStream.Size];
+ await logoStream.ReadAsync(bytesArray.AsBuffer(), (uint)logoStream.Size, InputStreamOptions.None);
+ return bytesArray;
+ }
+
+ ///
+ /// Converts a path to a distributions logo in the DistributionDefinition.yaml file to its
+ /// base64 string representation.
+ ///
+ /// path to the logo file using the ms-appx:// schema
+ /// A base64 string that represents the logo.
+ public async virtual Task GetBase64StringFromLogoPathAsync(string logoFilePath)
+ {
+ try
+ {
+ var uri = new Uri(logoFilePath);
+ var storageFile = await StorageFile.GetFileFromApplicationUriAsync(uri);
+ var randomAccessStream = await storageFile.OpenReadAsync();
+
+ // Convert the stream to a byte array
+ var bytes = new byte[randomAccessStream.Size];
+ await randomAccessStream.ReadAsync(bytes.AsBuffer(), (uint)randomAccessStream.Size, InputStreamOptions.None);
+
+ return Convert.ToBase64String(bytes);
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Unable to get base64 string from logo file path: {logoFilePath}");
+ return string.Empty;
+ }
+ }
+
+ public virtual Package? GetPackageFromPackageFamilyName(string packageFamilyName)
+ {
+ return _packageManager.FindPackagesForUser(string.Empty, packageFamilyName).FirstOrDefault();
+ }
+}
diff --git a/extensions/WSLExtension/Models/RegisterAndInstallDistributionSession.cs b/extensions/WSLExtension/Models/RegisterAndInstallDistributionSession.cs
new file mode 100644
index 0000000000..c51a05dc17
--- /dev/null
+++ b/extensions/WSLExtension/Models/RegisterAndInstallDistributionSession.cs
@@ -0,0 +1,304 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Microsoft.Windows.DevHome.SDK;
+using Serilog;
+using Windows.ApplicationModel;
+using Windows.Foundation;
+using WSLExtension.Contracts;
+using WSLExtension.DistributionDefinitions;
+using WSLExtension.Exceptions;
+using WSLExtension.Helpers;
+using static WSLExtension.Constants;
+
+namespace WSLExtension.Models;
+
+public enum SessionState
+{
+ WslInstallationForm,
+ ReviewForm,
+}
+
+///
+/// Class used to send adaptive cards to Dev Home's machine create environment flow. It sends a list of available
+/// wsl distributions on the first page, and then the name and logo of the selected distribution on the review page.
+///
+public class RegisterAndInstallDistributionSession : IExtensionAdaptiveCardSession2
+{
+ private readonly ILogger _log = Log.ForContext("SourceContext", nameof(RegisterAndInstallDistributionSession));
+
+ private readonly PackageHelper _packageHelper = new();
+
+ private readonly string _adaptiveCardNextButtonId = "DevHomeMachineConfigurationNextButton";
+
+ private readonly string _pathToInitialCreationFormTemplate =
+ Path.Combine(Package.Current.EffectivePath, $@"{WslTemplateSubfolderName}\WslInstallationForm.json");
+
+ private readonly string _pathToReviewFormTemplate =
+ Path.Combine(Package.Current.EffectivePath, $@"{WslTemplateSubfolderName}\ReviewFormForWslInstallation.json");
+
+ private readonly List _availableDistributionsToInstall;
+
+ private readonly IStringResource _stringResource;
+
+ private string? _defaultWslLogo;
+
+ private IExtensionAdaptiveCard? _availableDistributionsAdaptiveCard;
+
+ public event TypedEventHandler? Stopped;
+
+ public string UserInputJson { get; private set; } = string.Empty;
+
+ public RegisterAndInstallDistributionSession(List availableDistributions, IStringResource stringResource)
+ {
+ _availableDistributionsToInstall = availableDistributions;
+ _stringResource = stringResource;
+ }
+
+ public ProviderOperationResult Initialize(IExtensionAdaptiveCard extensionUI)
+ {
+ _availableDistributionsAdaptiveCard = extensionUI;
+ _defaultWslLogo ??= _packageHelper.GetBase64StringFromLogoPathAsync(DefaultWslLogoPath).GetAwaiter().GetResult();
+
+ return GetWslInstallationFormAdaptiveCard();
+ }
+
+ public IAsyncOperation OnAction(string action, string inputs)
+ {
+ return Task.Run(() =>
+ {
+ try
+ {
+ ProviderOperationResult operationResult;
+ var shouldEndSession = false;
+ var adaptiveCardStateNotRecognizedError = _stringResource.GetLocalized("AdaptiveCardStateNotRecognizedError");
+
+ var actionPayload = Json.ToObject(action);
+ if (actionPayload == null)
+ {
+ _log.Error($"Actions in Adaptive card action Json not recognized: {action}");
+ var creationFormGenerationError = _stringResource.GetLocalized("AdaptiveCardUnRecognizedAction");
+ throw new AdaptiveCardInvalidActionException(creationFormGenerationError);
+ }
+
+ switch (_availableDistributionsAdaptiveCard?.State)
+ {
+ case "wslInstallationForm":
+ operationResult = HandleActionWhenFormInInitialState(actionPayload, inputs);
+ break;
+ case "reviewForm":
+ (operationResult, shouldEndSession) = HandleActionWhenFormInReviewState(actionPayload);
+ break;
+ default:
+ _log.Error($"No matching state found for: '{_availableDistributionsAdaptiveCard?.State}'." +
+ $"resetting state of adaptive card back to default.");
+ operationResult = GetWslInstallationFormAdaptiveCard();
+ break;
+ }
+
+ if (shouldEndSession)
+ {
+ // The session has now ended. We'll raise the Stopped event to notify anyone in Dev Home who was listening to this event,
+ // that the session has ended.
+ Stopped?.Invoke(
+ this,
+ new ExtensionAdaptiveCardSessionStoppedEventArgs(operationResult, UserInputJson));
+ }
+
+ return operationResult;
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Unable to process action request from Dev Home");
+ return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, ex.Message, ex.Message);
+ }
+ }).AsAsyncOperation();
+ }
+
+ ///
+ /// Loads the adaptive card template based on the session state.
+ ///
+ /// State of the adaptive card session
+ /// A Json string representing the adaptive card
+ public string LoadTemplate(SessionState state)
+ {
+ var pathToTemplate = state switch
+ {
+ SessionState.WslInstallationForm => _pathToInitialCreationFormTemplate,
+ SessionState.ReviewForm => _pathToReviewFormTemplate,
+ _ => _pathToInitialCreationFormTemplate,
+ };
+
+ return File.ReadAllText(pathToTemplate, Encoding.Default);
+ }
+
+ private ProviderOperationResult HandleActionWhenFormInInitialState(AdaptiveCardActionPayload actionPayload, string inputs)
+ {
+ ProviderOperationResult operationResult;
+ var actionButtonId = actionPayload.Id ?? string.Empty;
+
+ if (actionButtonId.Equals(_adaptiveCardNextButtonId, StringComparison.OrdinalIgnoreCase))
+ {
+ // if OnAction's state is initialCreationForm, then the user has selected a VM gallery image and is ready to review the form.
+ // we'll also keep the original user input so we can pass it back to Dev Home once the session ends.
+ UserInputJson = inputs;
+ operationResult = GetWslReviewFormAdaptiveCardAsync(inputs);
+ }
+ else
+ {
+ operationResult = GetWslInstallationFormAdaptiveCard();
+ }
+
+ return operationResult;
+ }
+
+ private (ProviderOperationResult, bool) HandleActionWhenFormInReviewState(AdaptiveCardActionPayload actionPayload)
+ {
+ ProviderOperationResult operationResult;
+ var shouldEndSession = false;
+ var actionButtonId = actionPayload.Id ?? string.Empty;
+
+ if (actionButtonId.Equals(_adaptiveCardNextButtonId, StringComparison.OrdinalIgnoreCase))
+ {
+ // if OnAction's state is reviewForm, then the user has reviewed the form and Dev Home has started the creation process.
+ // we'll show the same form to the user in Dev Homes summary page.
+ shouldEndSession = true;
+ operationResult = GetWslReviewFormAdaptiveCardAsync(UserInputJson);
+ }
+ else
+ {
+ operationResult = GetWslInstallationFormAdaptiveCard();
+ }
+
+ return (operationResult, shouldEndSession);
+ }
+
+ ///
+ /// Creates the initial form that will be displayed to the user. It will be a list of settings cards from the
+ /// Windows community toolkit that can be displayed in Dev Homes UI. This will be display on the initial
+ /// create environments page once the user selects the wsl extension and clicks the next button.
+ ///
+ /// Result of the operation
+ private ProviderOperationResult GetWslInstallationFormAdaptiveCard()
+ {
+ try
+ {
+ // Create the JSON array for the available wsl distributions that can be installed and
+ // add the data for each one. These will be display in the initial creation form.
+ var jsonArrayOfAvailableDistributions = new JsonArray();
+ var primaryButtonForCreationFlowText = _stringResource.GetLocalized("PrimaryButtonLabelForCreationFlow");
+ var secondaryButtonForCreationFlowText = _stringResource.GetLocalized("SecondaryButtonLabelForCreationFlow");
+ var settingsCardLabel = _stringResource.GetLocalized("SettingsCardLabel", _availableDistributionsToInstall.Count);
+ var noDistributionsFound = _stringResource.GetLocalized("NoDistributionsFoundAvailable");
+
+ // Add information about all found distributions so we can use them in the settings cards
+ foreach (var distribution in _availableDistributionsToInstall)
+ {
+ var base64Logo =
+ string.IsNullOrEmpty(distribution.Base64StringLogo) ? _defaultWslLogo : distribution.Base64StringLogo;
+
+ var dataJson = new JsonObject
+ {
+ { "Header", distribution.FriendlyName },
+ { "HeaderIcon", base64Logo },
+ { "PublisherName", distribution.Publisher },
+ };
+
+ jsonArrayOfAvailableDistributions.Add(dataJson);
+ }
+
+ // Make sure we show the error message for when there are no distributions available to install
+ var noDistributionErrorData = new JsonArray
+ {
+ new JsonObject
+ {
+ { "NoDistributionsFoundError", noDistributionsFound },
+ { "NoDistributionsFoundErrorVisibility", _availableDistributionsToInstall.Count == 0 },
+ },
+ };
+
+ var templateData =
+ $"{{\"PrimaryButtonLabelForCreationFlow\" : \"{primaryButtonForCreationFlowText}\"," +
+ $"\"SecondaryButtonLabelForCreationFlow\" : \"{secondaryButtonForCreationFlowText}\"," +
+ $"\"SettingsCardLabel\": \"{settingsCardLabel}\"," +
+ $"\"NoDistributionErrorData\": {noDistributionErrorData.ToJsonString()}," +
+ $"\"AvailableDistributions\" : {jsonArrayOfAvailableDistributions.ToJsonString()}" +
+ $"}}";
+
+ var template = LoadTemplate(SessionState.WslInstallationForm);
+
+ return _availableDistributionsAdaptiveCard!.Update(template, templateData, "wslInstallationForm");
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Unable to create wsl creation form due to error");
+ return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, ex.Message, ex.Message);
+ }
+ }
+
+ ///
+ /// Creates the review form that will be displayed to the user. This will be an adaptive card that
+ /// is displayed in Dev Homes setup flow review page.
+ ///
+ /// Result of the operation
+ private ProviderOperationResult GetWslReviewFormAdaptiveCardAsync(string inputJson)
+ {
+ try
+ {
+ var deserializedObject = JsonSerializer.Deserialize(inputJson, typeof(WslInstallationUserInput));
+
+ if (!(deserializedObject is WslInstallationUserInput inputForWslInstallation))
+ {
+ throw new InvalidOperationException($"Json deserialization failed for input Json: {inputJson}");
+ }
+
+ var distribution = _availableDistributionsToInstall[inputForWslInstallation.SelectedDistributionIndex];
+ var distributionLabel = _stringResource.GetLocalized("DistributionNameLabel");
+ var publisherLabel = _stringResource.GetLocalized("ReviewPagePublisherLabel");
+ var primaryButtonForCreationFlowText = _stringResource.GetLocalized("PrimaryButtonLabelForCreationFlow");
+ var secondaryButtonForCreationFlowText = _stringResource.GetLocalized("SecondaryButtonLabelForCreationFlow");
+ var extensionLabel = _stringResource.GetLocalized("ExtensionLabel");
+ var base64Logo = string.IsNullOrEmpty(distribution.Base64StringLogo) ? _defaultWslLogo : distribution.Base64StringLogo;
+
+ var reviewFormData = new JsonObject
+ {
+ { "ProviderName", WslProviderDisplayName },
+ { "NewEnvironmentName", distribution.FriendlyName },
+ { "DistributionName", distribution.Name },
+ { "DistributionNameLabel", distributionLabel },
+ { "PublisherName", distribution.Publisher },
+ { "ReviewPagePublisherLabel", publisherLabel },
+ { "ExtensionLabel", extensionLabel },
+ { "DistributionImage", $"data:image/png;base64,{base64Logo}" },
+ { "PrimaryButtonLabelForCreationFlow", primaryButtonForCreationFlowText },
+ { "SecondaryButtonLabelForCreationFlow", secondaryButtonForCreationFlowText },
+ { "DistributionImageLogoAltText", _stringResource.GetLocalized("DistributionLogoAltText", distribution.FriendlyName) },
+ };
+
+ // Add friendly name of the selected distribution to the original user input so Dev Home
+ // can show its name on the Environments page when the environment is being created.
+ UserInputJson = new JsonObject
+ {
+ { "NewEnvironmentName", distribution.FriendlyName },
+ { "SelectedDistributionIndex", $"{inputForWslInstallation.SelectedDistributionIndex}" },
+ }.ToJsonString();
+
+ return _availableDistributionsAdaptiveCard!.Update(
+ LoadTemplate(SessionState.ReviewForm),
+ reviewFormData.ToJsonString(),
+ "reviewForm");
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Unable to create wsl review form due to error");
+ return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, ex.Message, ex.Message);
+ }
+ }
+
+ public void Dispose()
+ {
+ }
+}
diff --git a/extensions/WSLExtension/Models/WslComputeSystem.cs b/extensions/WSLExtension/Models/WslComputeSystem.cs
new file mode 100644
index 0000000000..d5369f34e8
--- /dev/null
+++ b/extensions/WSLExtension/Models/WslComputeSystem.cs
@@ -0,0 +1,341 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Data;
+using Microsoft.Windows.DevHome.SDK;
+using Serilog;
+using Windows.ApplicationModel;
+using Windows.Foundation;
+using WSLExtension.Contracts;
+using WSLExtension.Helpers;
+using static System.Convert;
+using static WSLExtension.Constants;
+
+namespace WSLExtension.Models;
+
+public delegate WslComputeSystem WslRegisteredDistributionFactory(WslRegisteredDistribution distribution);
+
+public class WslComputeSystem : IComputeSystem
+{
+ private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WslComputeSystem));
+
+ private readonly PackageHelper _packageHelper = new();
+
+ private readonly string _versionLabel;
+
+ private readonly string _defaultDistributionLabel;
+
+ private readonly string _defaultDistributionValue;
+
+ private readonly string _packageVersionLabel;
+
+ private readonly string _publisherLabel;
+
+ private readonly object _lock = new();
+
+ private readonly IStringResource _stringResource;
+
+ private readonly IWslManager _wslManager;
+
+ private readonly WslRegisteredDistribution _distribution;
+
+ private readonly Package? _curPackage;
+
+ public IDeveloperId? AssociatedDeveloperId { get; set; }
+
+ public string AssociatedProviderId { get; set; } = WslProviderId;
+
+ public string DisplayName { get; set; }
+
+ private ComputeSystemState _curState;
+
+ public event TypedEventHandler? StateChanged;
+
+ ///
+ /// Gets or sets the Id of the compute system.All WSL distribution names are unique
+ /// so we will use them as the Id for the compute system.
+ ///
+ public string Id { get; set; }
+
+ public string SupplementalDisplayName { get; set; } = string.Empty;
+
+ public ComputeSystemOperations SupportedOperations
+ {
+ get
+ {
+ if (GetState() == ComputeSystemState.Stopped)
+ {
+ return ComputeSystemOperations.Delete;
+ }
+
+ return ComputeSystemOperations.Delete | ComputeSystemOperations.Terminate;
+ }
+ }
+
+ public WslComputeSystem(
+ IStringResource stringResource,
+ WslRegisteredDistribution distribution,
+ IWslManager wslManager)
+ {
+ _stringResource = stringResource;
+ _distribution = distribution;
+ _wslManager = wslManager;
+ DisplayName = distribution.FriendlyName;
+ _curPackage = _packageHelper.GetPackageFromPackageFamilyName(_distribution.PackageFamilyName!);
+
+ // Use display name of package if there is no friendly name for this distribution
+ if (string.IsNullOrEmpty(DisplayName) && _curPackage != null)
+ {
+ DisplayName = _curPackage.DisplayName;
+ }
+
+ // Use the unique name of the distribution as its supplemental name as long as its not the same
+ // as the display name above.
+ if (!DisplayName.Equals(distribution.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ SupplementalDisplayName = distribution.Name;
+ }
+
+ Id = distribution.Name;
+ _wslManager.DistributionStateSyncEventHandler += OnStateSyncRequested;
+ _versionLabel = _stringResource.GetLocalized("WSLVersionLabel");
+ _defaultDistributionLabel = _stringResource.GetLocalized("WSLDefaultDistributionLabel");
+ _defaultDistributionValue = _stringResource.GetLocalized("WSLDefaultDistributionValue");
+ _publisherLabel = _stringResource.GetLocalized("WSLPublisherLabel");
+ _packageVersionLabel = _stringResource.GetLocalized("WSLPackageVersionLabel");
+ _curState = _wslManager.IsDistributionRunning(Id) ? ComputeSystemState.Running : ComputeSystemState.Stopped;
+ }
+
+ private void OnStateSyncRequested(object? sender, HashSet runningDistributions)
+ {
+ var newState = runningDistributions.Contains(Id)
+ ? ComputeSystemState.Running
+ : ComputeSystemState.Stopped;
+
+ UpdateState(newState);
+ }
+
+ private void UpdateState(ComputeSystemState newState)
+ {
+ lock (_lock)
+ {
+ if (_curState != newState)
+ {
+ _curState = newState;
+ StateChanged?.Invoke(this, newState);
+ }
+ }
+ }
+
+ private ComputeSystemState GetState()
+ {
+ lock (_lock)
+ {
+ return _curState;
+ }
+ }
+
+ public void RemoveSubscriptions()
+ {
+ _wslManager.DistributionStateSyncEventHandler -= OnStateSyncRequested;
+ }
+
+ public IAsyncOperation GetStateAsync()
+ {
+ return Task.Run(() =>
+ {
+ try
+ {
+ return new ComputeSystemStateResult(GetState());
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Unable to get state for Distribution: {Id}");
+ return new ComputeSystemStateResult(ex, ex.Message, ex.Message);
+ }
+ }).AsAsyncOperation();
+ }
+
+ public IAsyncOperation TerminateAsync(string options)
+ {
+ return Task.Run(() =>
+ {
+ try
+ {
+ UpdateState(ComputeSystemState.Stopping);
+ _wslManager.TerminateDistribution(Id);
+ UpdateState(ComputeSystemState.Stopped);
+ return new ComputeSystemOperationResult();
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Unable to terminate all sessions for Distribution: {Id}");
+ UpdateState(ComputeSystemState.Unknown);
+ return GetErrorResult(ex, "WSLTerminateError");
+ }
+ }).AsAsyncOperation();
+ }
+
+ public IAsyncOperation DeleteAsync(string options)
+ {
+ return Task.Run(() =>
+ {
+ try
+ {
+ UpdateState(ComputeSystemState.Deleting);
+ _wslManager.UnregisterDistribution(Id);
+ UpdateState(ComputeSystemState.Deleted);
+ return new ComputeSystemOperationResult();
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Unable to unregister Distribution: {Id}");
+ UpdateState(ComputeSystemState.Unknown);
+ return GetErrorResult(ex, "WSLUnregisterError");
+ }
+ }).AsAsyncOperation();
+ }
+
+ public IAsyncOperation GetComputeSystemThumbnailAsync(string options)
+ {
+ return Task.Run(async () =>
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(_distribution.Base64StringLogo))
+ {
+ // No logo for this distribution so we'll use its PackageFamily logo
+ var packageFamilyLogo =
+ await _packageHelper.GetPackageIconAsByteArrayAsync(_distribution.PackageFamilyName!);
+
+ if (packageFamilyLogo == null)
+ {
+ // Couldn't find package family logo, so instead use the default WSL logo.
+ var defaultLogoBase64 = await _packageHelper.GetBase64StringFromLogoPathAsync(DefaultWslLogoPath);
+ return new ComputeSystemThumbnailResult(FromBase64String(defaultLogoBase64));
+ }
+
+ return new ComputeSystemThumbnailResult(packageFamilyLogo);
+ }
+
+ // This is a known distribution. Use the logo we have for this distribution defined in
+ // the DistributionDefinitions.yaml
+ return new ComputeSystemThumbnailResult(FromBase64String(_distribution.Base64StringLogo!));
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Unable to get thumbnail for Distribution: {Id}");
+ return new ComputeSystemThumbnailResult(ex, ex.Message, ex.Message);
+ }
+ }).AsAsyncOperation();
+ }
+
+ public IAsyncOperation> GetComputeSystemPropertiesAsync(string options)
+ {
+ return Task.Run(() =>
+ {
+ try
+ {
+ var properties = new List();
+ if (_distribution.Version != null)
+ {
+ properties.Add(ComputeSystemProperty.CreateCustom(_distribution.Version.Value, _versionLabel, null));
+ }
+
+ if (_distribution.IsDefaultDistribution)
+ {
+ properties.Add(ComputeSystemProperty.CreateCustom(_defaultDistributionValue, _defaultDistributionLabel, null));
+ }
+
+ if (_curPackage != null)
+ {
+ if (!string.IsNullOrEmpty(_curPackage.PublisherDisplayName))
+ {
+ properties.Add(ComputeSystemProperty.CreateCustom(_curPackage.PublisherDisplayName, _publisherLabel, null));
+ }
+
+ var version = _curPackage.Id.Version;
+ var versionInfo = $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
+ properties.Add(ComputeSystemProperty.CreateCustom(versionInfo, _packageVersionLabel, null));
+ }
+
+ return properties.AsEnumerable();
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Unable to get compute system properties for Distribution: {Id}");
+ return new List();
+ }
+ }).AsAsyncOperation();
+ }
+
+ public IAsyncOperation ConnectAsync(string options)
+ {
+ return Task.Run(() =>
+ {
+ try
+ {
+ UpdateState(ComputeSystemState.Starting);
+ _wslManager.LaunchDistribution(Id, _distribution.AssociatedTerminalProfileGuid);
+ UpdateState(ComputeSystemState.Running);
+ return new ComputeSystemOperationResult();
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Unable to launch Distribution: {Id}");
+ UpdateState(ComputeSystemState.Unknown);
+ return GetErrorResult(ex, "WSLLaunchError");
+ }
+ }).AsAsyncOperation();
+ }
+
+ private ComputeSystemOperationResult GetUnSupportedResult()
+ {
+ var notImplementedException = new NotImplementedException($"Method not implemented by WSL Compute Systems");
+ return new ComputeSystemOperationResult(notImplementedException, notImplementedException.Message, notImplementedException.Message);
+ }
+
+ public IApplyConfigurationOperation? CreateApplyConfigurationOperation(string configuration)
+ {
+ // Dev Home will handle null values as failed operations. We can't throw because this is an out of proc
+ // COM call, so we'll lose the error information. We'll log the error and return null.
+ return null;
+ }
+
+ private ComputeSystemOperationResult GetErrorResult(Exception ex, string resourceKey)
+ {
+ var displayMsg = _stringResource.GetLocalized(resourceKey, ex.Message);
+ return new ComputeSystemOperationResult(ex, displayMsg, ex.Message);
+ }
+
+ // Unsupported IComputeSystem methods
+ public IAsyncOperation SaveAsync(string options) =>
+ Task.FromResult(GetUnSupportedResult()).AsAsyncOperation();
+
+ public IAsyncOperation PauseAsync(string options) =>
+ Task.FromResult(GetUnSupportedResult()).AsAsyncOperation();
+
+ public IAsyncOperation ResumeAsync(string options) =>
+ Task.FromResult(GetUnSupportedResult()).AsAsyncOperation();
+
+ public IAsyncOperation CreateSnapshotAsync(string options) =>
+ Task.FromResult(GetUnSupportedResult()).AsAsyncOperation();
+
+ public IAsyncOperation RevertSnapshotAsync(string options) =>
+ Task.FromResult(GetUnSupportedResult()).AsAsyncOperation();
+
+ public IAsyncOperation DeleteSnapshotAsync(string options) =>
+ Task.FromResult(GetUnSupportedResult()).AsAsyncOperation();
+
+ public IAsyncOperation ModifyPropertiesAsync(string inputJson) =>
+ Task.FromResult(GetUnSupportedResult()).AsAsyncOperation();
+
+ public IAsyncOperation StartAsync(string options) =>
+ Task.FromResult(GetUnSupportedResult()).AsAsyncOperation();
+
+ public IAsyncOperation ShutDownAsync(string options) =>
+ Task.FromResult(GetUnSupportedResult()).AsAsyncOperation();
+
+ public IAsyncOperation RestartAsync(string options) =>
+ Task.FromResult(GetUnSupportedResult()).AsAsyncOperation();
+}
diff --git a/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs b/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs
new file mode 100644
index 0000000000..858de3ca55
--- /dev/null
+++ b/extensions/WSLExtension/Models/WslInstallDistributionOperation.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Windows.DevHome.SDK;
+using Serilog;
+using Windows.Foundation;
+using WSLExtension.Contracts;
+using WSLExtension.DistributionDefinitions;
+
+namespace WSLExtension.Models;
+
+public class WslInstallDistributionOperation : ICreateComputeSystemOperation
+{
+ private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WslInstallDistributionOperation));
+
+ private readonly string _preparingToInstall;
+
+ private readonly string _waitingToComplete;
+
+ private readonly string _installationFailedTimeout;
+
+ private readonly string _installationSuccessful;
+
+ private readonly TimeSpan _threeSecondDelayInSeconds = TimeSpan.FromSeconds(3);
+
+ private readonly DistributionDefinition _definition;
+
+ private readonly IStringResource _stringResource;
+
+ private readonly IWslManager _wslManager;
+
+ public WslInstallDistributionOperation(
+ DistributionDefinition distributionDefinition,
+ IStringResource stringResource,
+ IWslManager wslManager)
+ {
+ _definition = distributionDefinition;
+ _stringResource = stringResource;
+ _wslManager = wslManager;
+ _preparingToInstall = GetLocalizedString("WSLPrepareInstall", _definition.FriendlyName);
+ _waitingToComplete = GetLocalizedString("WSLWaitingToCompleteInstallation", _definition.FriendlyName);
+
+ _installationFailedTimeout = GetLocalizedString("WSLInstallationFailedTimeOut", _definition.FriendlyName);
+
+ _installationSuccessful = GetLocalizedString("WSLInstallationCompletedSuccessfully", _definition.FriendlyName);
+ }
+
+ private string GetLocalizedString(string resourcekey, string value)
+ {
+ return _stringResource.GetLocalized(resourcekey, value);
+ }
+
+ public IAsyncOperation StartAsync()
+ {
+ return Task.Run(async () =>
+ {
+ try
+ {
+ var startTime = DateTime.UtcNow;
+ _log.Information($"Starting installation for {_definition.Name}");
+ Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(_preparingToInstall, 0));
+ _wslManager.InstallDistribution(_definition.Name);
+
+ // Cancel waiting for install if the distribution hasn't been installed after 10 minutes.
+ CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
+ cancellationTokenSource.CancelAfter(TimeSpan.FromMinutes(10));
+ WslRegisteredDistribution? registeredDistribution = null;
+ var distributionInstalledSuccessfully = false;
+
+ 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);
+ 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));
+ return new CreateComputeSystemResult(new WslComputeSystem(_stringResource, registeredDistribution!, _wslManager));
+ }
+
+ throw new TimeoutException(_installationFailedTimeout);
+ }
+ catch (Exception ex)
+ {
+ var errorMsg = _stringResource.GetLocalized("WSLInstallationFailedWithException", _definition.FriendlyName, ex.Message);
+ return new CreateComputeSystemResult(ex, errorMsg, ex.Message);
+ }
+ }).AsAsyncOperation();
+ }
+
+ public event TypedEventHandler? ActionRequired
+ {
+ add { }
+ remove { }
+ }
+
+ public event TypedEventHandler? Progress;
+}
diff --git a/extensions/WSLExtension/Models/WslInstallationUserInput.cs b/extensions/WSLExtension/Models/WslInstallationUserInput.cs
new file mode 100644
index 0000000000..4d5bbf19ff
--- /dev/null
+++ b/extensions/WSLExtension/Models/WslInstallationUserInput.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+
+namespace WSLExtension.Models;
+
+///
+/// Represents the user input for a Wsl install and registration operation.
+///
+public sealed class WslInstallationUserInput
+{
+ ///
+ /// Gets or sets the WSL distribution name for the creation operation.
+ /// Note: Do not rename this variable, as Dev Home looks for the name
+ /// 'NewEnvironmentName' in the user input Json in order to show the
+ /// name of the environment in the UI while its being created in the
+ /// CreateComputeSystemOperation object.
+ ///
+ public string NewEnvironmentName { get; set; } = string.Empty;
+
+ [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
+ public int SelectedDistributionIndex { get; set; }
+}
diff --git a/extensions/WSLExtension/Models/WslProcessData.cs b/extensions/WSLExtension/Models/WslProcessData.cs
new file mode 100644
index 0000000000..d3914de84a
--- /dev/null
+++ b/extensions/WSLExtension/Models/WslProcessData.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Globalization;
+using System.Text;
+using static WSLExtension.Constants;
+
+namespace WSLExtension.Models;
+
+///
+/// Represents metadata about a process that has exited.
+///
+public class WslProcessData
+{
+ public int ExitCode { get; }
+
+ public string StdOutput { get; } = string.Empty;
+
+ public string StdError { get; } = string.Empty;
+
+ public WslProcessData(int exitCode)
+ {
+ ExitCode = exitCode;
+ }
+
+ public WslProcessData(int exitCode, string stdOutput, string stdError)
+ {
+ ExitCode = exitCode;
+ StdOutput = stdOutput;
+ StdError = stdError;
+ }
+
+ public bool ExitedSuccessfully()
+ {
+ return ExitCode == WslExeExitSuccess && string.IsNullOrEmpty(StdError);
+ }
+
+ public override string ToString()
+ {
+ StringBuilder builder = new();
+ builder.AppendLine(CultureInfo.InvariantCulture, $"ExitCode: {ExitCode} ");
+ builder.AppendLine(CultureInfo.InvariantCulture, $"StdOutput: {StdOutput} ");
+ builder.AppendLine(CultureInfo.InvariantCulture, $"StdError: {StdError} ");
+ return builder.ToString();
+ }
+}
diff --git a/extensions/WSLExtension/Models/WslRegisteredDistribution.cs b/extensions/WSLExtension/Models/WslRegisteredDistribution.cs
new file mode 100644
index 0000000000..15141e82d1
--- /dev/null
+++ b/extensions/WSLExtension/Models/WslRegisteredDistribution.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using WSLExtension.DistributionDefinitions;
+using static Microsoft.Win32.Registry;
+using static WSLExtension.Constants;
+
+namespace WSLExtension.Models;
+
+///
+/// Represents information about a registered WSL distribution
+///
+public class WslRegisteredDistribution
+{
+ private const int InstalledState = 1;
+
+ public string FriendlyName { get; set; } = string.Empty;
+
+ public string Name { get; set; } = string.Empty;
+
+ public int? Version { get; set; }
+
+ public string? SubKeyName { get; set; }
+
+ public string? PackageFamilyName { get; set; }
+
+ public string? Base64StringLogo { get; set; }
+
+ public bool IsDefaultDistribution { get; set; }
+
+ public string Publisher { get; set; } = string.Empty;
+
+ public string? AssociatedTerminalProfileGuid { get; set; } = string.Empty;
+
+ public WslRegisteredDistribution(string distributionName)
+ {
+ Name = distributionName;
+ }
+
+ public WslRegisteredDistribution(DistributionDefinition distributionDistribution)
+ {
+ Name = distributionDistribution.Name;
+ FriendlyName = distributionDistribution.FriendlyName;
+ Base64StringLogo = distributionDistribution.Base64StringLogo;
+ AssociatedTerminalProfileGuid = distributionDistribution.WindowsTerminalProfileGuid;
+ }
+
+ public WslRegisteredDistribution(string distributionName, string? subKeyName, string? packageFamilyName, int? version)
+ {
+ Name = distributionName;
+ FriendlyName = Name;
+ SubKeyName = subKeyName;
+ Version = version;
+ PackageFamilyName = packageFamilyName;
+ }
+
+ ///
+ /// Uses the registry information about the distribution to determine if its fully registered or not.
+ ///
+ /// True only when distribution is fully registered. False otherwise.
+ public virtual bool IsDistributionFullyRegistered()
+ {
+ var distributionKey = CurrentUser.OpenSubKey($@"{WslRegistryLocation}\{SubKeyName}", false);
+
+ if (distributionKey == null)
+ {
+ return false;
+ }
+
+ var state = distributionKey?.GetValue(WslState) as int?;
+
+ // Any other state other than a 1 means the distribution is not fully installed yet.
+ return state == InstalledState;
+ }
+}
diff --git a/extensions/WSLExtension/Program.cs b/extensions/WSLExtension/Program.cs
new file mode 100644
index 0000000000..fe43049bee
--- /dev/null
+++ b/extensions/WSLExtension/Program.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Windows.AppLifecycle;
+using Microsoft.Windows.DevHome.SDK;
+using Serilog;
+using Windows.ApplicationModel.Activation;
+using WSLExtension.ClassExtensions;
+using WSLExtension.Services;
+using static WSLExtension.Helpers.Logging;
+
+namespace WSLExtension;
+
+public sealed class Program
+{
+ public static IHost? Host { get; set; }
+
+ [MTAThread]
+ public static void Main([System.Runtime.InteropServices.WindowsRuntime.ReadOnlyArray] string[] args)
+ {
+ // Set up Logging
+ Environment.SetEnvironmentVariable("WSL_LOGS_ROOT", PathToWslLogFolder);
+ var configuration = new ConfigurationBuilder().AddJsonFile("wsl_appsettings.json").Build();
+ Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
+
+ Log.Information($"Launched with args: {string.Join(' ', args.ToArray())}");
+
+ // Force the app to be single instanced.
+ // Get or register the main instance.
+ var mainInstance = AppInstance.FindOrRegisterForKey("mainInstance");
+ var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
+ if (!mainInstance.IsCurrent)
+ {
+ Log.Information("Not main instance, redirecting.");
+ mainInstance.RedirectActivationToAsync(activationArgs).AsTask().Wait();
+ Log.CloseAndFlush();
+ return;
+ }
+
+ // Build the host container before handling activation.
+ BuildHostContainer();
+
+ // Register for activation redirection.
+ AppInstance.GetCurrent().Activated += AppActivationRedirected;
+
+ if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
+ {
+ HandleCOMServerActivation();
+ }
+ else
+ {
+ Log.Warning("Not being launched as a ComServer... exiting.");
+ }
+
+ Log.CloseAndFlush();
+ }
+
+ private static void AppActivationRedirected(object? sender, AppActivationArguments activationArgs)
+ {
+ Log.Information($"Redirected with kind: {activationArgs.Kind}");
+
+ // Handle COM server.
+ if (activationArgs.Kind == ExtendedActivationKind.Launch)
+ {
+ var launchActivatedEventArgs = activationArgs.Data as ILaunchActivatedEventArgs;
+ var args = launchActivatedEventArgs?.Arguments.Split();
+
+ if (args?.Length > 0 && args[1] == "-RegisterProcessAsComServer")
+ {
+ Log.Information($"Activation COM Registration Redirect: {string.Join(' ', args.ToList())}");
+ HandleCOMServerActivation();
+ }
+ }
+ }
+
+ ///
+ /// Creates the host container for the WSLExtension server application. This can be used to register
+ /// services and other dependencies throughout the application.
+ ///
+ private static void BuildHostContainer()
+ {
+ Host = Microsoft.Extensions.Hosting.Host.
+ CreateDefaultBuilder().
+ UseContentRoot(AppContext.BaseDirectory).
+ UseDefaultServiceProvider((context, options) =>
+ {
+ options.ValidateOnBuild = true;
+ }).
+ ConfigureServices((context, services) =>
+ {
+ // Services
+ services.AddHttpClient();
+ services.AddWslExtensionServices();
+ }).
+ Build();
+ }
+
+ private static void HandleCOMServerActivation()
+ {
+ Log.Information("Activating COM Server");
+
+ // Register and run COM server.
+ // This could be called by either of the COM registrations, we will do them all to avoid deadlock and bind all on the extension's lifetime.
+ using var extensionServer = new ExtensionServer();
+ var wslExtension = Host!.GetService();
+
+ // We are instantiating extension instance once above, and returning it every time the callback in RegisterExtension below is called.
+ // This makes sure that only one instance of the extension is alive, which is returned every time the host asks for the IExtension object.
+ // If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate.
+ extensionServer.RegisterExtension(() => wslExtension, true);
+
+ // This will make the main thread wait until the event is signaled by the extension class.
+ // Since we have single instance of the extension object, we exit as soon as it is disposed.
+ wslExtension.ExtensionDisposedEvent.WaitOne();
+ Log.Information("Extension is disposed.");
+ }
+}
diff --git a/extensions/WSLExtension/Services/ProcessCreator.cs b/extensions/WSLExtension/Services/ProcessCreator.cs
new file mode 100644
index 0000000000..d9d743743a
--- /dev/null
+++ b/extensions/WSLExtension/Services/ProcessCreator.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Diagnostics;
+using System.Text;
+using WSLExtension.Contracts;
+using WSLExtension.Models;
+
+namespace WSLExtension.Services;
+
+public class ProcessCreator : IProcessCreator
+{
+ ///
+ public void CreateProcessWithWindow(string fileName, string arguments)
+ {
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = fileName,
+ Arguments = arguments,
+ UseShellExecute = true,
+ },
+ };
+
+ process.Start();
+ }
+
+ ///
+ public WslProcessData CreateProcessWithoutWindowAndWaitForExit(string fileName, string arguments)
+ {
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = fileName,
+ Arguments = arguments,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ StandardOutputEncoding = Encoding.Unicode,
+ },
+ };
+
+ process.Start();
+ var output = process.StandardOutput.ReadToEnd();
+ var errors = process.StandardError.ReadToEnd();
+
+ process.WaitForExit();
+ var exitCode = process.ExitCode;
+ return new WslProcessData(exitCode, output, errors);
+ }
+}
diff --git a/extensions/WSLExtension/Services/StringResource.cs b/extensions/WSLExtension/Services/StringResource.cs
new file mode 100644
index 0000000000..52abbd1869
--- /dev/null
+++ b/extensions/WSLExtension/Services/StringResource.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Globalization;
+using Microsoft.Windows.ApplicationModel.Resources;
+using WSLExtension.Contracts;
+
+namespace WSLExtension.Services;
+
+public class StringResource : IStringResource
+{
+ private readonly ResourceLoader _resourceLoader;
+
+ public StringResource()
+ {
+ _resourceLoader = new ResourceLoader("WSLExtensionServer.pri", "Resources");
+ }
+
+ public StringResource(string name, string path)
+ {
+ _resourceLoader = new ResourceLoader(name, path);
+ }
+
+ /// Gets the localized string of a resource key.
+ /// Resource key.
+ /// Placeholder arguments.
+ /// Localized value, or resource key if the value is empty or an exception occurred.
+ public string GetLocalized(string key, params object[] args)
+ {
+ string value;
+
+ try
+ {
+ value = _resourceLoader.GetString(key);
+ value = string.Format(CultureInfo.CurrentCulture, value, args);
+ }
+ catch
+ {
+ value = string.Empty;
+ }
+
+ return string.IsNullOrEmpty(value) ? key : value;
+ }
+}
diff --git a/extensions/WSLExtension/Services/WslExtension.cs b/extensions/WSLExtension/Services/WslExtension.cs
new file mode 100644
index 0000000000..6d443220c5
--- /dev/null
+++ b/extensions/WSLExtension/Services/WslExtension.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Windows.DevHome.SDK;
+using Serilog;
+using WSLExtension.ClassExtensions;
+
+namespace WSLExtension.Services;
+
+[ComVisible(true)]
+[Guid("121253AB-BA5D-4E73-99CF-25A2CB8BF173")]
+[ComDefaultInterface(typeof(IExtension))]
+public sealed class WslExtension : IExtension, IDisposable
+{
+ private readonly IComputeSystemProvider _computeSystemProvider;
+
+ private bool _disposed;
+
+ public WslExtension(IComputeSystemProvider computeSystemProvider)
+ {
+ _computeSystemProvider = computeSystemProvider;
+ }
+
+ ///
+ /// Gets the synchronization object that is used to prevent the main program from exiting
+ /// until the extension is disposed.
+ ///
+ public ManualResetEvent ExtensionDisposedEvent { get; } = new(false);
+
+ ///
+ /// Gets provider object for the specified provider type.
+ ///
+ ///
+ /// The provider type that the WSL extension may support. This is used to query the WSL
+ /// extension for whether it supports the provider type.
+ ///
+ ///
+ /// When the extension supports the ProviderType the object returned will not be null. However,
+ /// when the extension does not support the ProviderType the returned object will be null.
+ ///
+ public object? GetProvider(ProviderType providerType)
+ {
+ var log = Log.ForContext("SourceContext", nameof(WslExtension));
+ object? provider = null;
+ try
+ {
+ switch (providerType)
+ {
+ case ProviderType.ComputeSystem:
+ provider = _computeSystemProvider;
+ break;
+ default:
+ log.Information($"Unsupported provider: {providerType}");
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error(ex, $"Failed to get provider for provider type {providerType}");
+ }
+
+ return provider;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ ExtensionDisposedEvent.Set();
+ }
+
+ _disposed = true;
+ }
+}
diff --git a/extensions/WSLExtension/Services/WslManager.cs b/extensions/WSLExtension/Services/WslManager.cs
new file mode 100644
index 0000000000..dcf32b8da5
--- /dev/null
+++ b/extensions/WSLExtension/Services/WslManager.cs
@@ -0,0 +1,175 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Serilog;
+using Windows.System.Threading;
+using WSLExtension.ClassExtensions;
+using WSLExtension.Contracts;
+using WSLExtension.DistributionDefinitions;
+using WSLExtension.Helpers;
+using WSLExtension.Models;
+using static WSLExtension.Constants;
+
+namespace WSLExtension.Services;
+
+public class WslManager : IWslManager
+{
+ private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WslManager));
+
+ private readonly PackageHelper _packageHelper = new();
+
+ private readonly TimeSpan _oneMinutePollingInterval = TimeSpan.FromMinutes(1);
+
+ private readonly WslRegisteredDistributionFactory _wslRegisteredDistributionFactory;
+
+ private readonly IWslServicesMediator _wslServicesMediator;
+
+ private readonly IDistributionDefinitionHelper _definitionHelper;
+
+ private readonly List _registeredWslDistributions = new();
+
+ public event EventHandler>? DistributionStateSyncEventHandler;
+
+ private Dictionary? _distributionDefinitionsMap;
+
+ private ThreadPoolTimer? _timerForUpdatingDistributionStates;
+
+ public WslManager(
+ IWslServicesMediator wslServicesMediator,
+ WslRegisteredDistributionFactory wslDistributionFactory,
+ IDistributionDefinitionHelper distributionDefinitionHelper)
+ {
+ _wslRegisteredDistributionFactory = wslDistributionFactory;
+ _wslServicesMediator = wslServicesMediator;
+ _definitionHelper = distributionDefinitionHelper;
+ StartDistributionStatePolling();
+ }
+
+ ///
+ public async Task> GetAllRegisteredDistributionsAsync()
+ {
+ // The list of compute systems in Dev Home is being refreshed, so remove any old
+ // subscriptions
+ _registeredWslDistributions.ForEach(distribution => distribution.RemoveSubscriptions());
+ _registeredWslDistributions.Clear();
+
+ foreach (var distribution in await GetInformationOnAllRegisteredDistributionsAsync())
+ {
+ _registeredWslDistributions.Add(_wslRegisteredDistributionFactory(distribution.Value));
+ }
+
+ return _registeredWslDistributions;
+ }
+
+ ///
+ public async Task> GetAllDistributionsAvailableToInstallAsync()
+ {
+ var registeredDistributionsMap = await GetInformationOnAllRegisteredDistributionsAsync();
+ var distributionsToListOnCreationPage = new List();
+ _distributionDefinitionsMap ??= await _definitionHelper.GetDistributionDefinitionsAsync();
+ foreach (var distributionDefinition in _distributionDefinitionsMap.Values)
+ {
+ // filter out distribution definitions already registered on machine.
+ if (registeredDistributionsMap.TryGetValue(distributionDefinition.Name, out var _))
+ {
+ continue;
+ }
+
+ distributionsToListOnCreationPage.Add(distributionDefinition);
+ }
+
+ // Sort the list by distribution name in ascending order before sending it.
+ distributionsToListOnCreationPage.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
+ return distributionsToListOnCreationPage;
+ }
+
+ ///
+ public async Task GetInformationOnRegisteredDistributionAsync(string distributionName)
+ {
+ foreach (var registeredDistribution in (await GetInformationOnAllRegisteredDistributionsAsync()).Values)
+ {
+ if (distributionName.Equals(registeredDistribution.Name, StringComparison.Ordinal))
+ {
+ return registeredDistribution;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ public bool IsDistributionRunning(string distributionName)
+ {
+ return _wslServicesMediator.IsDistributionRunning(distributionName);
+ }
+
+ ///
+ public void UnregisterDistribution(string distributionName)
+ {
+ _wslServicesMediator.UnregisterDistribution(distributionName);
+ }
+
+ ///
+ public void LaunchDistribution(string distributionName, string? windowsTerminalProfile = null)
+ {
+ _wslServicesMediator.LaunchDistribution(distributionName, windowsTerminalProfile);
+ }
+
+ ///
+ public void InstallDistribution(string distributionName)
+ {
+ _wslServicesMediator.InstallDistribution(distributionName);
+ }
+
+ ///
+ public void TerminateDistribution(string distributionName)
+ {
+ _wslServicesMediator.TerminateDistribution(distributionName);
+ }
+
+ ///
+ /// 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.
+ ///
+ private async Task> GetInformationOnAllRegisteredDistributionsAsync()
+ {
+ _distributionDefinitionsMap ??= await _definitionHelper.GetDistributionDefinitionsAsync();
+ var distributions = new Dictionary();
+ foreach (var distribution in _wslServicesMediator.GetAllRegisteredDistributions())
+ {
+ // If this is a distribution we know about in DistributionDefinition.yaml add its friendly name and logo.
+ if (_distributionDefinitionsMap.TryGetValue(distribution.Name, out var knownDistributionInfo))
+ {
+ distribution.FriendlyName = knownDistributionInfo.FriendlyName;
+ distribution.Base64StringLogo = knownDistributionInfo.Base64StringLogo;
+ distribution.AssociatedTerminalProfileGuid = knownDistributionInfo.WindowsTerminalProfileGuid;
+ }
+
+ distributions.Add(distribution.Name, distribution);
+ }
+
+ return distributions;
+ }
+
+ ///
+ /// Raises an event once every minute so that the wsl compute systems state can be updated. Unfortunately there
+ /// are no WSL APIs to achieve this. Once an API is created that fires an event for state changes this can be
+ /// updated/removed.
+ ///
+ private void StartDistributionStatePolling()
+ {
+ _timerForUpdatingDistributionStates = ThreadPoolTimer.CreatePeriodicTimer(
+ (ThreadPoolTimer timer) =>
+ {
+ try
+ {
+ DistributionStateSyncEventHandler?.Invoke(this, _wslServicesMediator.GetAllNamesOfRunningDistributions());
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Unable to raise distribution sync event due to an error");
+ }
+ },
+ _oneMinutePollingInterval);
+ }
+}
diff --git a/extensions/WSLExtension/Services/WslServicesMediator.cs b/extensions/WSLExtension/Services/WslServicesMediator.cs
new file mode 100644
index 0000000000..3d83056b23
--- /dev/null
+++ b/extensions/WSLExtension/Services/WslServicesMediator.cs
@@ -0,0 +1,214 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Diagnostics;
+using Microsoft.Win32;
+using WSLExtension.ClassExtensions;
+using WSLExtension.Contracts;
+using WSLExtension.Exceptions;
+using WSLExtension.Helpers;
+using WSLExtension.Models;
+using static Microsoft.Win32.Registry;
+using static WSLExtension.Constants;
+
+namespace WSLExtension.Services;
+
+///
+/// Class to interact with WSL services either through the wsl.exe process
+/// or the registry.
+///
+public class WslServicesMediator : IWslServicesMediator
+{
+ private const int FirstIndex = 0;
+
+ private readonly PackageHelper _packageHelper = new();
+
+ private readonly IProcessCreator _processCreator;
+
+ public WslServicesMediator(IProcessCreator creator)
+ {
+ _processCreator = creator;
+ }
+
+ ///
+ public HashSet GetAllNamesOfRunningDistributions()
+ {
+ var processData = _processCreator.CreateProcessWithoutWindowAndWaitForExit(WslExe, ListAllRunningDistributions);
+
+ // wsl.exe returns an error code when there are no distributions running. But in that case
+ // it will send at least one line to standard output e.g "There are no running distributions."
+ if (!processData.ExitedSuccessfully() && string.IsNullOrEmpty(processData.StdOutput))
+ {
+ throw new WslServicesMediatorException($"Unable to get all running distribution data" +
+ $" process data: {processData}");
+ }
+
+ var distributions = new HashSet();
+ using var reader = new StringReader(processData.StdOutput);
+
+ // Results for executing wsl.exe --list --running.
+ // When no distributions are running the first line is localized: "There are no running distributions."
+ // So we can skip it.
+ // When there are distributions the first line is: "Windows Subsystem for Linux Distributions"
+ // The rest of the lines are the running distribution names e.g
+ // Debian (Default)
+ // OracleLinux_7_9
+ //
+ // Note: the distribution that's set up as the default, will contain (default) next to it. But for our purposes
+ // we don't need to read that part. We only need to first word of the space separated line. Distribution
+ // names cannot have spaces so we don't need to worry about that either.
+ reader.ReadLine();
+ while (reader.ReadLine() is { } line)
+ {
+ var spaceSeparatedArr = line.Split(" ");
+ distributions.Add(spaceSeparatedArr[FirstIndex]);
+ }
+
+ return distributions;
+ }
+
+ ///
+ public bool IsDistributionRunning(string distributionName)
+ {
+ return GetAllNamesOfRunningDistributions().Contains(distributionName);
+ }
+
+ ///
+ ///
+ /// Method enumerates through the WSL registry location subkey location
+ /// to retrieve information about each registered distribution.
+ ///
+ public List GetAllRegisteredDistributions()
+ {
+ var distributions = new List();
+ var linuxSubSystemKey = CurrentUser.OpenSubKey(WslRegistryLocation, false);
+
+ if (linuxSubSystemKey == null)
+ {
+ return new();
+ }
+
+ var defaultDistribution = linuxSubSystemKey.GetValue(DefaultDistributionRegistryName) as string;
+
+ foreach (var subKeyName in linuxSubSystemKey.GetSubKeyNames())
+ {
+ var subKey = linuxSubSystemKey.OpenSubKey(subKeyName);
+
+ if (subKey == null)
+ {
+ continue;
+ }
+
+ var distribution = BuildDistributionInfoFromRegistry(subKey);
+ if (string.IsNullOrEmpty(distribution.Name))
+ {
+ // distribution doesn't have a name. This would happen only if the users registry info
+ // was messed up. WSL would likely not function properly either in these cases.
+ continue;
+ }
+
+ // the last part of the registry subkey is the registered guid of the wsl distribution.
+ var distributionGuid = subKey.Name.Split('\\').LastOrDefault() ?? string.Empty;
+ if (!string.IsNullOrEmpty(defaultDistribution) &&
+ defaultDistribution.Equals(distributionGuid, StringComparison.OrdinalIgnoreCase))
+ {
+ distribution.IsDefaultDistribution = true;
+ }
+
+ distributions.Add(distribution);
+ }
+
+ return distributions;
+ }
+
+ private WslRegisteredDistribution BuildDistributionInfoFromRegistry(RegistryKey registryKey)
+ {
+ var regDistributionName = registryKey.GetValue(DistributionRegistryName) as string ?? string.Empty;
+
+ // The registry key name will be in the form of e.g:
+ // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Lxss\{387ef692-f68f-4f83-a1f1-30c891257bb6}
+ // We need the last segment {387ef692-f68f-4f83-a1f1-30c891257bb6}. Each guid subkey under Lxss is a
+ // registered WSL distribution and the WSL service uses these subkeys to store information about the
+ // distribution.
+ var lastSegmentOfRegistryKeyName = registryKey.Name.Split('\\').LastOrDefault();
+ var version = registryKey.GetValue(WslVersion) as int?;
+ var packageFamilyName = registryKey.GetValue(PackageFamilyRegistryName) as string;
+ return new WslRegisteredDistribution(regDistributionName, lastSegmentOfRegistryKeyName, packageFamilyName, version);
+ }
+
+ ///
+ public void UnregisterDistribution(string distributionName)
+ {
+ var processData = _processCreator.CreateProcessWithoutWindowAndWaitForExit(WslExe, UnregisterDistributionArgs.FormatArgs(distributionName));
+
+ if (!processData.ExitedSuccessfully())
+ {
+ throw new WslServicesMediatorException($"Unable to launch distribution {distributionName} : {processData}");
+ }
+ }
+
+ ///
+ public void LaunchDistribution(string distributionName, string? windowsTerminalProfile)
+ {
+ var executable = GetFileNameForProcessLaunch();
+
+ // Only launch with terminal if its installed
+ if (executable.Equals(WindowsTerminalShimExe, StringComparison.OrdinalIgnoreCase))
+ {
+ LaunchDistributionUsingTerminal(distributionName, windowsTerminalProfile);
+ return;
+ }
+
+ // Default to starting the wsl process directly and passing in its command line args
+ _processCreator.CreateProcessWithWindow(executable, LaunchDistributionWithoutTerminal.FormatArgs(distributionName));
+ }
+
+ private void LaunchDistributionUsingTerminal(string distributionName, string? windowsTerminalProfile)
+ {
+ var terminalArgs = LaunchDistributionInTerminalWithNoProfile.FormatArgs(distributionName);
+
+ if (!string.IsNullOrEmpty(windowsTerminalProfile))
+ {
+ // Launch into terminal with the specified profile and run wsl.exe in the console window
+ terminalArgs = LaunchDistributionInTerminalWithProfile.FormatArgs(windowsTerminalProfile, distributionName);
+ _processCreator.CreateProcessWithWindow(WindowsTerminalShimExe, terminalArgs);
+ }
+ else
+ {
+ // Launch into terminal and run wsl.exe in the console window without a profile
+ _processCreator.CreateProcessWithWindow(WindowsTerminalShimExe, terminalArgs);
+ }
+ }
+
+ ///
+ public void TerminateDistribution(string distributionName)
+ {
+ var processData = _processCreator.CreateProcessWithoutWindowAndWaitForExit(WslExe, TerminateDistributionArgs.FormatArgs(distributionName));
+
+ if (!processData.ExitedSuccessfully())
+ {
+ throw new WslServicesMediatorException($"Unable to terminate distribution {distributionName} : {processData}");
+ }
+ }
+
+ ///
+ public void InstallDistribution(string distributionName)
+ {
+ var executable = GetFileNameForProcessLaunch();
+
+ // Launch into terminal if its installed and run wsl.exe in the console window
+ if (executable.Equals(WindowsTerminalShimExe, StringComparison.OrdinalIgnoreCase))
+ {
+ _processCreator.CreateProcessWithWindow(executable, InstallDistributionWithTerminal.FormatArgs(distributionName));
+ return;
+ }
+
+ // Default to starting the wsl process directly and passing in its command line args
+ _processCreator.CreateProcessWithWindow(executable, InstallDistributionWithoutTerminal.FormatArgs(distributionName));
+ }
+
+ private string GetFileNameForProcessLaunch()
+ {
+ return _packageHelper.IsPackageInstalled(WindowsTerminalPackageFamilyName) ? WindowsTerminalShimExe : WslExe;
+ }
+}
diff --git a/extensions/WSLExtension/Strings/en-US/Resources.resw b/extensions/WSLExtension/Strings/en-US/Resources.resw
new file mode 100644
index 0000000000..09cc76c0f3
--- /dev/null
+++ b/extensions/WSLExtension/Strings/en-US/Resources.resw
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 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
+
+
+ Next
+ Text to display to the user about what the primary button does in the UI
+
+
+ Previous
+ Text to display to the user about what the secondary button does in the UI.
+
+
+ Choose a WSL distribution to install ({0} Available):
+ {Locked="WSL", "{0}"} Label text for a list of cards that appear in the UI. {0} is the number of distributions available
+
+
+ Distribution name:
+ label text for distribution name
+
+
+ Extension:
+ label text for the extensions name
+
+
+ Publisher:
+ text for publisher name label.
+
+
+ {0} distribution logo
+ {Locked="{0}"} The alt text for a logo in Dev Homes review page. {0} is the name of the distribution the user selected.
+
+
+ There are no distributions available to install at this time
+ Error text when there aren't any distributions to display in Dev Homes UX
+
+
+ Default distribution
+ label text for when a wsl distribution is the default distribution
+
+
+ Yes
+ text for the value that will appear in the UI when a wsl distribution is the default distribution
+
+
+ WSL Version
+ {Locked="WSL"} label text for the wsl version number of the distribution
+
+
+ Publisher
+ label text for the publisher of the wsl distribution
+
+
+ Package version
+ label text for the version of the package the wsl distribution was installed from
+
+
+ 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
+
+
+ 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
+
+
+ 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
+
+
+ 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
+
+
+ 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
+
+
+ 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.
+
+
+ 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
+
+
+ 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
+
+
\ No newline at end of file
diff --git a/extensions/WSLExtension/WSLExtension.csproj b/extensions/WSLExtension/WSLExtension.csproj
new file mode 100644
index 0000000000..befbfca2d6
--- /dev/null
+++ b/extensions/WSLExtension/WSLExtension.csproj
@@ -0,0 +1,104 @@
+
+
+
+
+ Exe
+ Debug;Release;Debug_FailFast
+
+
+ Exe
+ Debug;Release;Debug_FailFast
+
+
+ WinExe
+
+
+
+ enable
+ enable
+ x86;x64;arm64
+ win-x86;win-x64;win-arm64
+ WSLExtension.Program
+ WSLExtensionServer
+ $(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml
+
+
+ Dev
+ $(DefineConstants);CANARY_BUILD
+ $(DefineConstants);STABLE_BUILD
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+ Always
+
+
+
+
+ portable
+
+
+
+ portable
+
+
+
+ portable
+
+
+
+ portable
+
+
+
+ portable
+
+
+
+ portable
+
+
+
+ portable
+
+
+
+ portable
+
+
+
+ portable
+
+
diff --git a/extensions/WSLExtension/WslAssets/debian.png b/extensions/WSLExtension/WslAssets/debian.png
new file mode 100644
index 0000000000..059e1142a7
Binary files /dev/null and b/extensions/WSLExtension/WslAssets/debian.png differ
diff --git a/extensions/WSLExtension/WslAssets/kali.png b/extensions/WSLExtension/WslAssets/kali.png
new file mode 100644
index 0000000000..ebe30712aa
Binary files /dev/null and b/extensions/WSLExtension/WslAssets/kali.png differ
diff --git a/extensions/WSLExtension/WslAssets/opensuse.png b/extensions/WSLExtension/WslAssets/opensuse.png
new file mode 100644
index 0000000000..98469158ac
Binary files /dev/null and b/extensions/WSLExtension/WslAssets/opensuse.png differ
diff --git a/extensions/WSLExtension/WslAssets/openuse-enterprise.png b/extensions/WSLExtension/WslAssets/openuse-enterprise.png
new file mode 100644
index 0000000000..6435b68903
Binary files /dev/null and b/extensions/WSLExtension/WslAssets/openuse-enterprise.png differ
diff --git a/extensions/WSLExtension/WslAssets/ubuntu.png b/extensions/WSLExtension/WslAssets/ubuntu.png
new file mode 100644
index 0000000000..3e21ef2298
Binary files /dev/null and b/extensions/WSLExtension/WslAssets/ubuntu.png differ
diff --git a/extensions/WSLExtension/WslAssets/wslLinux.png b/extensions/WSLExtension/WslAssets/wslLinux.png
new file mode 100644
index 0000000000..780ef02f6d
Binary files /dev/null and b/extensions/WSLExtension/WslAssets/wslLinux.png differ
diff --git a/extensions/WSLExtension/WslTemplates/ReviewFormForWslInstallation.json b/extensions/WSLExtension/WslTemplates/ReviewFormForWslInstallation.json
new file mode 100644
index 0000000000..4df8d58c1a
--- /dev/null
+++ b/extensions/WSLExtension/WslTemplates/ReviewFormForWslInstallation.json
@@ -0,0 +1,102 @@
+{
+ "type": "AdaptiveCard",
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+ "version": "1.6",
+ "body": [
+ {
+ "type": "ColumnSet",
+ "columns": [
+ {
+ "type": "Column",
+ "width": "auto",
+ "spacing": "medium",
+ "verticalContentAlignment": "center",
+ "items": [
+ {
+ "type": "TextBlock",
+ "text": "${ExtensionLabel}",
+ "wrap": true,
+ "size": "medium"
+ },
+ {
+ "type": "TextBlock",
+ "text": "${ProviderName}",
+ "wrap": true,
+ "size": "medium"
+ }
+ ]
+ },
+ {
+ "type": "Column",
+ "width": "auto",
+ "spacing": "extraLarge",
+ "verticalContentAlignment": "center",
+ "items": [
+ {
+ "type": "Image",
+ "url": "${DistributionImage}",
+ "altText": "${DistributionImageLogoAltText}",
+ "height": "48px"
+ }
+ ]
+ },
+ {
+ "type": "Column",
+ "width": "auto",
+ "spacing": "medium",
+ "verticalContentAlignment": "center",
+ "items": [
+ {
+ "type": "TextBlock",
+ "text": "${ReviewPagePublisherLabel}",
+ "wrap": true,
+ "size": "medium"
+ },
+ {
+ "type": "TextBlock",
+ "text": "${PublisherName}",
+ "wrap": true,
+ "size": "medium"
+ }
+ ]
+ },
+ {
+ "type": "Column",
+ "width": "auto",
+ "spacing": "extraLarge",
+ "verticalContentAlignment": "center",
+ "items": [
+ {
+ "type": "TextBlock",
+ "text": "${DistributionNameLabel}",
+ "wrap": true,
+ "size": "medium"
+ },
+ {
+ "type": "TextBlock",
+ "text": "${NewEnvironmentName}",
+ "wrap": true,
+ "size": "medium"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "ActionSet",
+ "id": "DevHomeTopLevelActionSet",
+ "actions": [
+ {
+ "id": "DevHomeMachineConfigurationNextButton",
+ "type": "Action.Submit",
+ "title": "${PrimaryButtonLabelForCreationFlow}"
+ },
+ {
+ "id": "DevHomeMachineConfigurationPreviousButton",
+ "type": "Action.Submit",
+ "title": "${SecondaryButtonLabelForCreationFlow}"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/WSLExtension/WslTemplates/WslInstallationForm.json b/extensions/WSLExtension/WslTemplates/WslInstallationForm.json
new file mode 100644
index 0000000000..1cd52f7ecb
--- /dev/null
+++ b/extensions/WSLExtension/WslTemplates/WslInstallationForm.json
@@ -0,0 +1,62 @@
+{
+ "type": "AdaptiveCard",
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+ "version": "1.5",
+ "body": [
+ {
+ "type": "Container",
+ "items": [
+ {
+ "type": "DevHome.SettingsCardChoiceSet",
+ "id": "SelectedDistributionIndex",
+ "label": "${SettingsCardLabel}",
+ "isRequired": true,
+ "devHomeSettingsCards": [
+ {
+ "type": "DevHome.SettingsCard",
+ "$data": "${AvailableDistributions}",
+ "devHomeSettingsCardDescription": "${PublisherName}",
+ "devHomeSettingsCardHeader": "${Header}",
+ "devHomeSettingsCardHeaderIcon": "${HeaderIcon}"
+ }
+ ]
+ },
+ {
+ "type": "Container",
+ "$data": "${NoDistributionErrorData}",
+ "isVisible": "${NoDistributionsFoundErrorVisibility}",
+ "verticalContentAlignment": "center",
+ "spacing": "extraLarge",
+ "height": "stretch",
+ "items": [
+ {
+ "type": "TextBlock",
+ "text": "${NoDistributionsFoundError}",
+ "size": "large",
+ "weight": "bolder",
+ "wrap": true,
+ "horizontalAlignment": "center",
+ "height": "stretch"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "ActionSet",
+ "id": "DevHomeTopLevelActionSet",
+ "actions": [
+ {
+ "id": "DevHomeMachineConfigurationNextButton",
+ "type": "Action.Submit",
+ "title": "${PrimaryButtonLabelForCreationFlow}"
+ },
+ {
+ "id": "DevHomeMachineConfigurationPreviousButton",
+ "type": "Action.Submit",
+ "title": "${SecondaryButtonLabelForCreationFlow}"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/WSLExtension/wsl_appsettings.json b/extensions/WSLExtension/wsl_appsettings.json
new file mode 100644
index 0000000000..03e5779060
--- /dev/null
+++ b/extensions/WSLExtension/wsl_appsettings.json
@@ -0,0 +1,31 @@
+{
+ "Serilog": {
+ "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Debug" ],
+ "MinimumLevel": "Debug",
+ "WriteTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}",
+ "restrictedToMinimumLevel": "Debug"
+ }
+ },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "%WSL_LOGS_ROOT%\\wsl.dhlog",
+ "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}",
+ "restrictedToMinimumLevel": "Information",
+ "rollingInterval": "Day"
+ }
+ },
+ {
+ "Name": "Debug"
+ }
+ ],
+ "Enrich": [ "FromLogContext" ],
+ "Properties": {
+ "SourceContext": "WSLExtension"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DevHome.csproj b/src/DevHome.csproj
index 59a349bae3..6d93a712dc 100644
--- a/src/DevHome.csproj
+++ b/src/DevHome.csproj
@@ -84,6 +84,7 @@
+
diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest
index 66de1e44c5..a23db92a59 100644
--- a/src/Package.appxmanifest
+++ b/src/Package.appxmanifest
@@ -66,6 +66,13 @@
+
+
+
+
+
+
+
@@ -107,6 +114,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Strings/en-us/Resources.resw b/src/Strings/en-us/Resources.resw
index 7088cf4e97..caac4a8a8b 100644
--- a/src/Strings/en-us/Resources.resw
+++ b/src/Strings/en-us/Resources.resw
@@ -1,17 +1,17 @@
-
@@ -310,4 +310,12 @@
Hyper-V Extension{Locked="Hyper-V"} Extension Display Name
+
+ Windows Subsystem for Linux Extension
+ {Locked="Windows", "Linux"} Extension Display Name
+
+
+ Windows Subsystem for Linux
+ {Locked="Windows", "Linux"} Extension Description
+
\ No newline at end of file
diff --git a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs
index 1e9d3740c5..8dfe018f26 100644
--- a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs
+++ b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs
@@ -53,6 +53,9 @@ public partial class ComputeSystemViewModel : ComputeSystemCardBase, IRecipient<
[ObservableProperty]
private bool _shouldShowDotOperations;
+ [ObservableProperty]
+ private bool _shouldShowSplitButton;
+
private bool _disposedValue;
///
@@ -128,6 +131,8 @@ private async Task InitializeOperationDataAsync()
try
{
ShouldShowDotOperations = false;
+ ShouldShowSplitButton = false;
+
RegisterForAllOperationMessages(DataExtractor.FillDotButtonOperations(ComputeSystem, _mainWindow), DataExtractor.FillLaunchButtonOperations(ComputeSystem));
_ = Task.Run(async () =>
@@ -157,7 +162,11 @@ private async Task InitializeOperationDataAsync()
DotOperations.Add(data);
}
- ShouldShowDotOperations = true;
+ // Only show dot operations when there are items in the list.
+ ShouldShowDotOperations = DotOperations.Count > 0;
+
+ // Only show Launch split button with operations when there are items in the list.
+ ShouldShowSplitButton = LaunchOperations.Count > 0;
});
});
diff --git a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml
index 01e953aa0a..1fb634c345 100644
--- a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml
+++ b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml
@@ -35,17 +35,30 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs
index 02e22967ad..5efa729773 100644
--- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs
+++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs
@@ -19,6 +19,7 @@
using Windows.ApplicationModel;
using Windows.Data.Json;
using Windows.Storage;
+using static DevHome.Common.Helpers.CommonConstants;
namespace DevHome.ExtensionLibrary.ViewModels;
@@ -35,7 +36,8 @@ public partial class ExtensionLibraryViewModel : ObservableObject
// their class Ids to this set.
private readonly HashSet _internalClassIdsToBeShownInExtensionsPage = new()
{
- CommonConstants.HyperVExtensionClassId,
+ HyperVExtensionClassId,
+ WSLExtensionClassId,
};
public ObservableCollection StorePackagesList { get; set; }
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml
index 1fb724c57c..f558c3d559 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml
+++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml
@@ -18,6 +18,14 @@
+
+ 40
+