Skip to content

Commit

Permalink
Add WSL extension to Dev Home (#3365)
Browse files Browse the repository at this point in the history
* add initial code for wsl

* add more changes

* add more changes

* add more changes

* add more changes

* add more changes

* add more changes

* complete changes for wsl extension

* fix build error about self-containing

* Remove console write log and use serilog in packagehelper

* fix launching console window when terminal not installed

* update based on comments

* update distribution definition helper based on comments

* update based on more comments
  • Loading branch information
bbonaby authored Jul 9, 2024
1 parent f2ec6c4 commit d606948
Show file tree
Hide file tree
Showing 55 changed files with 3,052 additions and 44 deletions.
24 changes: 24 additions & 0 deletions DevHome.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
8 changes: 8 additions & 0 deletions common/Environments/Styles/HorizontalCardStyles.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,12 @@
<Setter Property="Padding" Value="20,8,15,8" />
</Style>

<Style
x:Key="CardBodyLaunchButtonStyle"
TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Padding" Value="34,7,34,7" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
</Style>
</ResourceDictionary>
4 changes: 3 additions & 1 deletion common/Helpers/CommonConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
19 changes: 16 additions & 3 deletions common/Views/AdaptiveCardViews/AdaptiveCardResourceTemplates.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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">

<!--- Data templates for custom adaptive card renders can go here -->
<converters:EmptyObjectToObjectConverter x:Key="EmptyObjectToObjectConverter" NotEmptyValue="Visible" EmptyValue="Collapsed"/>

<!--- Data templates for custom adaptive card renders can go here -->
<DataTemplate x:Key="SettingsCardWithButtonThatLaunchesContentDialog" x:DataType="cardModels:DevHomeSettingsCard">
<ItemContainer AutomationProperties.Name="{Binding Header}">
<controls:SettingsCard
<ItemContainer.Resources>
<!--
Override SettingsCard default resources. This is used because the default
SettingsCardHeaderIconMaxSize value for settings cards is 20. This makes
the icons sent from extensions in the creation flow appear too small and pixelated.
So we override it here, to make sure they are bigger.
-->
<x:Double x:Key="SettingsCardHeaderIconMaxSize">40</x:Double>
</ItemContainer.Resources>
<controls:SettingsCard
x:Name="SettingsCardWithButtonToLaunchContentDialog"
Description="{Binding Description}"
Header="{Binding Header}"
HeaderIcon="{Binding HeaderIconImage}">
<Button
x:Name="LaunchDialog"
Visibility="{Binding NonSubmitActionElement, Converter={StaticResource EmptyObjectToObjectConverter}}"
Content="{Binding NonSubmitActionElement.ActionText}"
Command="{Binding NonSubmitActionElement.InvokeActionCommand}"
CommandParameter="{Binding ElementName=LaunchDialog}" />
Expand Down
36 changes: 36 additions & 0 deletions extensions/WSLExtension/ClassExtensions/IHostExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <inheritdoc cref="ActivatorUtilities.CreateInstance(IServiceProvider, Type, object[])"/>
public static T CreateInstance<T>(this IHost host, params object[] parameters)
{
return ActivatorUtilities.CreateInstance<T>(host.Services, parameters);
}

/// <summary>
/// Gets the service object for the specified type, or throws an exception
/// if type was not registered.
/// </summary>
/// <typeparam name="T">Service type</typeparam>
/// <param name="host">Host object</param>
/// <returns>Service object</returns>
/// <exception cref="ArgumentException">Throw an exception if the specified
/// type is not registered</exception>
public static T GetService<T>(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;
}
}
13 changes: 13 additions & 0 deletions extensions/WSLExtension/ClassExtensions/ResourceExtensions.cs
Original file line number Diff line number Diff line change
@@ -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);
}
34 changes: 34 additions & 0 deletions extensions/WSLExtension/ClassExtensions/ServiceExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<IWslServicesMediator, WslServicesMediator>();
services.AddSingleton<IDistributionDefinitionHelper, DistributionDefinitionHelper>();
services.AddSingleton<IStringResource, StringResource>();
services.AddSingleton<IComputeSystemProvider, WslProvider>();
services.AddSingleton<WslExtension>();
services.AddSingleton<IProcessCreator, ProcessCreator>();
services.AddSingleton<IWslManager, WslManager>();

// Factory delegate to create WslComputeSystems
services.AddSingleton<WslRegisteredDistributionFactory>(
serviceProvider => wslDistribution => ActivatorUtilities.CreateInstance<WslComputeSystem>(serviceProvider, wslDistribution));

return services;
}
}
14 changes: 14 additions & 0 deletions extensions/WSLExtension/ClassExtensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
70 changes: 70 additions & 0 deletions extensions/WSLExtension/Constants.cs
Original file line number Diff line number Diff line change
@@ -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";
}
23 changes: 23 additions & 0 deletions extensions/WSLExtension/Contracts/IDistributionDefinitionHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using WSLExtension.DistributionDefinitions;

namespace WSLExtension.Contracts;

/// <summary>
/// Provides definition information about all the WSL distributions that can be found at
/// <see cref="Constants.KnownDistributionsWebJsonLocation"/>.
/// </summary>
public interface IDistributionDefinitionHelper
{
/// <summary>
/// Retrieves a list of objects that contain metadata about WSL distributions that can be
/// installed from the wsl.exe executable.
/// </summary>
/// <returns>
/// A Dictionary where the key is the name of the distribution and the value is its
/// <see cref="DistributionDefinition"/> metadata.
/// </returns>
public Task<Dictionary<string, DistributionDefinition>> GetDistributionDefinitionsAsync();
}
32 changes: 32 additions & 0 deletions extensions/WSLExtension/Contracts/IProcessCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using WSLExtension.Models;

namespace WSLExtension.Contracts;

/// <summary>
/// Interface used to create processes throughout the WSL extension.
/// </summary>
public interface IProcessCreator
{
/// <summary>
/// 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.
/// </summary>
/// <param name="fileName">The name of the executable to start</param>
/// <param name="arguments">The arguments that will be passed to the executable at process startup</param>
public void CreateProcessWithWindow(string fileName, string arguments);

/// <summary>
/// Creates and starts a new process without opening a window. Note: execution is blocked
/// until the process exits.
/// </summary>
/// <param name="fileName">The name of the executable to start</param>
/// <param name="arguments">The arguments that will be passed to the executable at process startup</param>
/// <returns>The meta data associated with the exited process.
/// E.g StdOutput, StdError and its exit code.
/// </returns>
public WslProcessData CreateProcessWithoutWindowAndWaitForExit(string fileName, string arguments);
}
9 changes: 9 additions & 0 deletions extensions/WSLExtension/Contracts/IStringResource.cs
Original file line number Diff line number Diff line change
@@ -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);
}
55 changes: 55 additions & 0 deletions extensions/WSLExtension/Contracts/IWslManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using WSLExtension.DistributionDefinitions;
using WSLExtension.Models;

namespace WSLExtension.Contracts;

/// <summary>
/// Used to interact between the WSL dev environment compute systems and the WSL mediator.
/// </summary>
public interface IWslManager
{
public event EventHandler<HashSet<string>>? DistributionStateSyncEventHandler;

/// <summary> Gets a list of all registered WSL distributions on the machine.</summary>
public Task<List<WslComputeSystem>> GetAllRegisteredDistributionsAsync();

/// <summary>
/// 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.
/// </summary>
public Task<List<DistributionDefinition>> GetAllDistributionsAvailableToInstallAsync();

/// <summary>
/// Gets a list of objects that each contain information about a WSL distribution that is already
/// registered on the machine.
/// </summary>
public Task<WslRegisteredDistribution?> GetInformationOnRegisteredDistributionAsync(string distributionName);

/// <summary>
/// Unregisters a WSL distribution. This is a wrapper for <see cref="IWslServicesMediator.UnregisterDistribution(string)"/>
/// </summary>
void UnregisterDistribution(string distributionName);

/// <summary> Launches a new WSL distribution.
/// This is a wrapper for <see cref="IWslServicesMediator.LaunchDistribution"/>
/// </summary>
void LaunchDistribution(string distributionName, string? windowsTerminalProfile);

/// <summary> Installs a new WSL distribution.
/// This is a wrapper for <see cref="IWslServicesMediator.InstallDistribution(string)"/>
/// </summary>
void InstallDistribution(string distributionName);

/// <summary> Terminates all sessions for a new WSL distribution.
/// This is a wrapper for <see cref="IWslServicesMediator.TerminateDistribution(string)"/>
/// </summary>
void TerminateDistribution(string distributionName);

/// <summary> Gets a boolean indicating whether the WSL distribution is currently running.
/// This is a wrapper for <see cref="IWslServicesMediator.IsDistributionRunning(string)"/>
/// </summary>
public bool IsDistributionRunning(string distributionName);
}
Loading

0 comments on commit d606948

Please sign in to comment.