diff --git a/DevHome.sln b/DevHome.sln index c02985c1ea..e9f8b34f28 100644 --- a/DevHome.sln +++ b/DevHome.sln @@ -146,6 +146,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.EnvironmentVariable EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.Telemetry.Native", "telemetry\DevHome.Telemetry.Native\DevHome.Telemetry.Native.vcxproj", "{8EB52F7D-D216-49FF-BF16-DE06E4695950}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WindowsSandboxExtension", "WindowsSandboxExtension", "{4ACF917D-B2CC-4CF2-8EE1-0EBBB52A69F0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsSandboxExtension", "extensions\WindowsSandboxExtension\WindowsSandboxExtension.csproj", "{118E20E8-FD8A-40CF-83A5-F912B9187787}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|arm64 = Debug|arm64 @@ -714,6 +718,18 @@ Global {8EB52F7D-D216-49FF-BF16-DE06E4695950}.Release|x64.Build.0 = Release|x64 {8EB52F7D-D216-49FF-BF16-DE06E4695950}.Release|x86.ActiveCfg = Release|Win32 {8EB52F7D-D216-49FF-BF16-DE06E4695950}.Release|x86.Build.0 = Release|Win32 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|arm64.ActiveCfg = Debug|arm64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|arm64.Build.0 = Debug|arm64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|x64.ActiveCfg = Debug|x64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|x64.Build.0 = Debug|x64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|x86.ActiveCfg = Debug|x86 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|x86.Build.0 = Debug|x86 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|arm64.ActiveCfg = Release|arm64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|arm64.Build.0 = Release|arm64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|x64.ActiveCfg = Release|x64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|x64.Build.0 = Release|x64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|x86.ActiveCfg = Release|x86 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -765,6 +781,8 @@ Global {DB3D0F2C-1A7F-44B4-B408-B21A56212985} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} {623998FD-B0A6-4980-95D5-A5072301CA10} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} {AF527EA4-6A24-4BD6-BC6E-A5863DC3489C} = {623998FD-B0A6-4980-95D5-A5072301CA10} + {4ACF917D-B2CC-4CF2-8EE1-0EBBB52A69F0} = {DCAF188B-60C3-4EDB-8049-BAA927FBCD7D} + {118E20E8-FD8A-40CF-83A5-F912B9187787} = {4ACF917D-B2CC-4CF2-8EE1-0EBBB52A69F0} {FAB6FAA7-ADF4-4B65-9831-0C819915E6E1} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} {19C08C57-7C22-48C9-9B6C-FAAF1FCC65E7} = {FAB6FAA7-ADF4-4B65-9831-0C819915E6E1} {1317314E-9BDD-4F1C-A76F-22121637A091} = {FAB6FAA7-ADF4-4B65-9831-0C819915E6E1} diff --git a/extensions/WindowsSandboxExtension/Assets/windows-sandbox-icon.png b/extensions/WindowsSandboxExtension/Assets/windows-sandbox-icon.png new file mode 100644 index 0000000000..73ca5be9d2 Binary files /dev/null and b/extensions/WindowsSandboxExtension/Assets/windows-sandbox-icon.png differ diff --git a/extensions/WindowsSandboxExtension/Assets/windows-sandbox-thumbnail.jpg b/extensions/WindowsSandboxExtension/Assets/windows-sandbox-thumbnail.jpg new file mode 100644 index 0000000000..6569fdd746 Binary files /dev/null and b/extensions/WindowsSandboxExtension/Assets/windows-sandbox-thumbnail.jpg differ diff --git a/extensions/WindowsSandboxExtension/Constants.cs b/extensions/WindowsSandboxExtension/Constants.cs new file mode 100644 index 0000000000..167c5dfa8d --- /dev/null +++ b/extensions/WindowsSandboxExtension/Constants.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WindowsSandboxExtension; + +internal sealed class Constants +{ + public const string WindowsSandboxExe = "WindowsSandbox.exe"; + public const string ProviderDisplayName = "Windows Sandbox"; + public const string ProviderId = "Microsoft.WindowsSandbox"; + public const string Thumbnail = "ms-appx:///Assets/windows-sandbox-thumbnail.jpg"; + + // We use different icon locations for different builds. Note these are ms-resource URIs, but are used by Dev Home to load the providers icon. + // from the extension package. Extensions that implement the IComputeSystemProvider interface must provide a provider icon in this format. + // Dev Home will use SHLoadIndirectString (https://learn.microsoft.com/windows/win32/api/shlwapi/nf-shlwapi-shloadindirectstring) to load the + // location of the icon from the extension package.Once it gets this location, it will load the icon from the path and display it in the UI. + // Icons should be located in an extension resource.pri file which is generated at build time. + // See the MakePri.exe documentation for how you can view what is in the resource.pri file, so you can find the location of your icon. + // https://learn.microsoft.com/windows/uwp/app-resources/makepri-exe-command-options. (use MakePri.exe in a VS Developer Command Prompt or + // Powershell window) +#if CANARY_BUILD + public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome.Canary/Files/Assets/windows-sandbox-icon.png"; +#elif STABLE_BUILD + public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome/Files/Assets/windows-sandbox-icon.png"; +#else + public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome.Dev/Files/Assets/windows-sandbox-icon.png"; +#endif +} diff --git a/extensions/WindowsSandboxExtension/Helpers/DependencyChecker.cs b/extensions/WindowsSandboxExtension/Helpers/DependencyChecker.cs new file mode 100644 index 0000000000..cb76ef75c6 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Helpers/DependencyChecker.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Management; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; +using Windows.Management.Deployment; +using WinRT; + +namespace WindowsSandboxExtension.Helpers; + +internal sealed class DependencyChecker +{ + private const string OptionalComponentName = "Containers-DisposableClientVM"; + private const string PackageFamilyName = "MicrosoftWindows.WindowsSandbox_cw5n1h2txyewy"; + + public static bool IsOptionalComponentEnabled() + { + var searcher = new ManagementObjectSearcher($"SELECT InstallState FROM Win32_OptionalFeature WHERE Name = '{OptionalComponentName}'"); + var collection = searcher.Get(); + + foreach (ManagementObject instance in collection) + { + if (instance["InstallState"] != null) + { + var state = Convert.ToInt32(instance.GetPropertyValue("InstallState"), CultureInfo.InvariantCulture); + + // 1 means the feature is enabled + return state == 1; + } + } + + // Return false if the feature is not found + return false; + } + + public static bool IsNewWindowsSandboxExtensionInstalled() + { + PackageManager packageManager = new PackageManager(); + + var securityId = WindowsIdentity.GetCurrent().Owner?.ToString(); + var packages = packageManager.FindPackagesForUser(securityId, PackageFamilyName); + + return packages.Any(); + } +} diff --git a/extensions/WindowsSandboxExtension/Helpers/Logging.cs b/extensions/WindowsSandboxExtension/Helpers/Logging.cs new file mode 100644 index 0000000000..8821497401 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Helpers/Logging.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Storage; + +namespace WindowsSandboxExtension.Helpers; + +public class Logging +{ + public static readonly string LogExtension = ".dhlog"; + + public static readonly string LogFolderName = "Logs"; + + public static readonly string DefaultLogFileName = "WindowsSandbox"; + + private static readonly Lazy _logFolderRoot = new(() => Path.Combine(ApplicationData.Current.TemporaryFolder.Path, LogFolderName)); + + public static readonly string LogFolderRoot = _logFolderRoot.Value; +} diff --git a/extensions/WindowsSandboxExtension/Helpers/Resources.cs b/extensions/WindowsSandboxExtension/Helpers/Resources.cs new file mode 100644 index 0000000000..315be43064 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Helpers/Resources.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Windows.ApplicationModel.Resources; +using Serilog; + +namespace WindowsSandboxExtension.Helpers; + +internal sealed class Resources +{ + private static ResourceLoader? _resourceLoader; + + public static string GetResource(string identifier, ILogger? log = null) + { + try + { + if (_resourceLoader == null) + { + var path = ResourceLoader.GetDefaultResourceFilePath(); + _resourceLoader = new ResourceLoader(path); + } + + return _resourceLoader.GetString(identifier); + } + catch (Exception ex) + { + log?.Error(ex, $"Failed loading resource: {identifier}"); + + // If we fail, load the original identifier so it is obvious which resource is missing. + return identifier; + } + } +} diff --git a/extensions/WindowsSandboxExtension/NativeMethods.txt b/extensions/WindowsSandboxExtension/NativeMethods.txt new file mode 100644 index 0000000000..0caa4406b1 --- /dev/null +++ b/extensions/WindowsSandboxExtension/NativeMethods.txt @@ -0,0 +1 @@ +SetForegroundWindow \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Program.cs b/extensions/WindowsSandboxExtension/Program.cs new file mode 100644 index 0000000000..54f5b83f6f --- /dev/null +++ b/extensions/WindowsSandboxExtension/Program.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +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 WindowsSandboxExtension.Helpers; +using WindowsSandboxExtension.Providers; +using WinRT; + +namespace WindowsSandboxExtension; + +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("DEVHOME_LOGS_ROOT", Path.Join(Helpers.Logging.LogFolderRoot, "WindowsSandbox")); + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings_WindowsSandbox.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, Microsoft.Windows.AppLifecycle.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 Windows Sandbox Extension 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.AddSingleton(); + services.AddSingleton(); + }). + Build(); + } + + private static void HandleCOMServerActivation() + { + Debug.Assert(Host != null, "Host is null"); + 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 Microsoft.Windows.DevHome.SDK.ExtensionServer(); + var windowsSandboxExtension = Host.Services.GetRequiredService(typeof(WindowsSandboxExtension)).As(); + + // 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(() => windowsSandboxExtension, true); + + // This will make the main thread wait until the event is signalled by the extension class. + // Since we have single instance of the extension object, we exit as soon as it is disposed. + windowsSandboxExtension.ExtensionDisposedEvent.WaitOne(); + Log.Information($"Extension is disposed."); + } +} diff --git a/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-arm64.pubxml b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-arm64.pubxml new file mode 100644 index 0000000000..b2f119ed57 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-arm64.pubxml @@ -0,0 +1,17 @@ + + + + + FileSystem + arm64 + win-arm64 + true + False + False + True + True + True + + \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x64.pubxml b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x64.pubxml new file mode 100644 index 0000000000..0c9801b313 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x64.pubxml @@ -0,0 +1,17 @@ + + + + + FileSystem + x64 + win-x64 + true + False + False + True + True + True + + \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x86.pubxml b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x86.pubxml new file mode 100644 index 0000000000..84817dc699 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x86.pubxml @@ -0,0 +1,17 @@ + + + + + FileSystem + x86 + win-x86 + true + False + False + True + True + True + + \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Properties/launchSettings.json b/extensions/WindowsSandboxExtension/Properties/launchSettings.json new file mode 100644 index 0000000000..20a32d9c87 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "WindowsSandboxExtension": { + "commandName": "Project", + "commandLineArgs": "-RegisterProcessAsComServer" + } + } +} \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Providers/WindowsSandboxComputeSystem.cs b/extensions/WindowsSandboxExtension/Providers/WindowsSandboxComputeSystem.cs new file mode 100644 index 0000000000..b80dd7303c --- /dev/null +++ b/extensions/WindowsSandboxExtension/Providers/WindowsSandboxComputeSystem.cs @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Diagnostics; +using System.Runtime.InteropServices.WindowsRuntime; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Foundation; +using Windows.Storage; +using Windows.Storage.Streams; +using Windows.Win32; +using Windows.Win32.Foundation; +using WindowsSandboxExtension.Helpers; +using WindowsSandboxExtension.Telemetry; + +using Timer = System.Timers.Timer; + +namespace WindowsSandboxExtension.Providers; + +public class WindowsSandboxComputeSystem : IComputeSystem, IDisposable +{ + private const long ByteSizeGB = 1024 * 1024 * 1024; + private const long DefaultMemorySizeInBytes = 4 * ByteSizeGB; + private const long DefaultStorageSizeInBytes = 80 * ByteSizeGB; + + private readonly Guid _id = Guid.NewGuid(); + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WindowsSandboxProvider)); + private readonly object _windowsSandboxStartLock = new(); + + private Process? _windowsSandboxExeProcess; + private ComputeSystemState _state = ComputeSystemState.Stopped; + + private ComputeSystemState State + { + get => _state; + + set + { + _state = value; + StateChanged?.Invoke(this, value); + } + } + + public string AssociatedProviderId => Constants.ProviderId; + + public string DisplayName => Resources.GetResource("WindowsSandboxDisplayName", _log); + + public string Id => _id.ToString(); + + public string SupplementalDisplayName => string.Empty; + + public ComputeSystemOperations SupportedOperations => State switch + { + ComputeSystemState.Running => ComputeSystemOperations.Terminate, + _ => ComputeSystemOperations.None, + }; + + public IDeveloperId? AssociatedDeveloperId => null; + + public event TypedEventHandler? StateChanged; + + public IAsyncOperation GetComputeSystemThumbnailAsync(string options) + { + return Task.Run(async () => + { + var uri = new Uri(Constants.Thumbnail); + 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 new ComputeSystemThumbnailResult(bytes); + }).AsAsyncOperation(); + } + + public IAsyncOperation> GetComputeSystemPropertiesAsync(string options) + { + return Task.Run(() => + { + var properties = new List + { + ComputeSystemProperty.Create(ComputeSystemPropertyKind.CpuCount, Environment.ProcessorCount), + ComputeSystemProperty.Create(ComputeSystemPropertyKind.AssignedMemorySizeInBytes, DefaultMemorySizeInBytes), + ComputeSystemProperty.Create(ComputeSystemPropertyKind.StorageSizeInBytes, DefaultStorageSizeInBytes), + }; + + return properties.AsEnumerable(); + }).AsAsyncOperation(); + } + + public IAsyncOperation GetStateAsync() + { + return Task.Run(() => + { + return new ComputeSystemStateResult(State); + }).AsAsyncOperation(); + } + + public IAsyncOperation ConnectAsync(string options) + { + return Task.Run(() => + { + try + { + // Windows Sandbox is not running. + if (_windowsSandboxExeProcess == null || _windowsSandboxExeProcess.HasExited) + { + State = ComputeSystemState.Starting; + + var system32Path = Environment.GetFolderPath(Environment.SpecialFolder.System); + var windowsSandboxExePath = Path.Combine(system32Path, Constants.WindowsSandboxExe); + + _windowsSandboxExeProcess = new(); + _windowsSandboxExeProcess.StartInfo.FileName = windowsSandboxExePath; + _windowsSandboxExeProcess.EnableRaisingEvents = true; + _windowsSandboxExeProcess.Exited += WindowsSandboxProcessExited; + + State = ComputeSystemState.Running; + TraceLogging.StartingWindowsSandbox(); + + _windowsSandboxExeProcess.Start(); + + PInvoke.SetForegroundWindow((HWND)_windowsSandboxExeProcess.MainWindowHandle); + } + + BringWindowsSandboxClientToForeground(); + + return new ComputeSystemOperationResult(); + } + catch (Exception ex) + { + State = ComputeSystemState.Unknown; + + _log.Error(ex, "Failed to start Windows Sandbox"); + TraceLogging.ExceptionThrown(ex); + + return new ComputeSystemOperationResult( + ex, + Resources.GetResource("WindowsSandboxFailedToStart", _log), + "Failed to start Windows Sandbox"); + } + }).AsAsyncOperation(); + } + + private void WindowsSandboxProcessExited(object? sender, EventArgs e) + { + State = ComputeSystemState.Stopped; + _windowsSandboxExeProcess?.Dispose(); + _windowsSandboxExeProcess = null; + } + + private Process? GetWindowsSandboxClientProcess() + { + return Process.GetProcessesByName("WindowsSandboxClient").FirstOrDefault(); + } + + private void BringWindowsSandboxClientToForeground() + { + var clientProcess = GetWindowsSandboxClientProcess(); + var windowHandle = clientProcess?.MainWindowHandle ?? IntPtr.Zero; + + PInvoke.SetForegroundWindow((HWND)windowHandle); + } + + public IAsyncOperation TerminateAsync(string options) + { + return Task.Run(() => + { + try + { + if (_windowsSandboxExeProcess == null || _windowsSandboxExeProcess.HasExited) + { + State = ComputeSystemState.Stopped; + return new ComputeSystemOperationResult(); + } + + GetWindowsSandboxClientProcess()?.Kill(); + _windowsSandboxExeProcess.Kill(); + + return new ComputeSystemOperationResult(); + } + catch (Exception ex) + { + State = ComputeSystemState.Unknown; + + _log.Error(ex, "Failed to terminate Windows Sandbox"); + TraceLogging.ExceptionThrown(ex); + + return new ComputeSystemOperationResult( + ex, + Resources.GetResource("FailedToTerminateWindowsSandbox", _log), + "Failed to terminate Windows Sandbox"); + } + }).AsAsyncOperation(); + } + + private IAsyncOperation NotImplemntedComputeSystemOperation() + { + NotImplementedException ex = new("This operation is not implemented."); + ComputeSystemOperationResult result = new(ex, Resources.GetResource("NotImplemented", _log), ex.Message); + + return Task.FromResult(result).AsAsyncOperation(); + } + + public IApplyConfigurationOperation? CreateApplyConfigurationOperation(string configuration) + { + return null; + } + + public IAsyncOperation CreateSnapshotAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation DeleteAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation DeleteSnapshotAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation ModifyPropertiesAsync(string inputJson) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation PauseAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation RestartAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation ResumeAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation RevertSnapshotAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation SaveAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation ShutDownAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation StartAsync(string options) => NotImplemntedComputeSystemOperation(); + + public void Dispose() + { + _windowsSandboxExeProcess?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/extensions/WindowsSandboxExtension/Providers/WindowsSandboxProvider.cs b/extensions/WindowsSandboxExtension/Providers/WindowsSandboxProvider.cs new file mode 100644 index 0000000000..fcf662b925 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Providers/WindowsSandboxProvider.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Foundation; +using WindowsSandboxExtension.Helpers; + +namespace WindowsSandboxExtension.Providers; + +internal sealed class WindowsSandboxProvider : IComputeSystemProvider +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WindowsSandboxProvider)); + + public string DisplayName => Constants.ProviderDisplayName; + + public Uri Icon => new(Constants.ExtensionIcon); + + public string Id => Constants.ProviderId; + + public ComputeSystemProviderOperations SupportedOperations => ComputeSystemProviderOperations.None; + + public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForComputeSystem(IComputeSystem computeSystem, ComputeSystemAdaptiveCardKind sessionKind) + { + return new ComputeSystemAdaptiveCardResult( + new NotImplementedException(), + Resources.GetResource("NotImplemented", _log), + "Create Windows Sandbox compute system is not implemented."); + } + + public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForDeveloperId(IDeveloperId developerId, ComputeSystemAdaptiveCardKind sessionKind) + { + return new ComputeSystemAdaptiveCardResult( + new NotImplementedException(), + Resources.GetResource("NotImplemented", _log), + "Developer Id Adaptive Card session is not implmented for Windows Sandbox."); + } + + public ICreateComputeSystemOperation? CreateCreateComputeSystemOperation(IDeveloperId developerId, string inputJson) + { + return null; + } + + public IAsyncOperation GetComputeSystemsAsync(IDeveloperId developerId) + { + return Task.Run(() => + { + List list = new(); + list.Add(new WindowsSandboxComputeSystem()); + + return new ComputeSystemsResult(list); + }).AsAsyncOperation(); + } +} diff --git a/extensions/WindowsSandboxExtension/Strings/en-US/Resources.resw b/extensions/WindowsSandboxExtension/Strings/en-US/Resources.resw new file mode 100644 index 0000000000..c968db3346 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Strings/en-US/Resources.resw @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Failed to terminate Windows Sandbox + {Locked="Windows Sandbox"} Failed to terminate display message. + + + This operation is not yet implemented for Windows Sandbox + + + Windows Sandbox Environment + {Locked="Windows Sandbox"} The diplay name used for the default Windows Sandbox compute system. + + + Failed to start Windows Sandbox + {Locked="Windows Sandbox"} Failed to start display message. + + \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Telemetry/TelemetryEventSource.cs b/extensions/WindowsSandboxExtension/Telemetry/TelemetryEventSource.cs new file mode 100644 index 0000000000..40a98a0a48 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Telemetry/TelemetryEventSource.cs @@ -0,0 +1,343 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#if TELEMETRYEVENTSOURCE_USE_NUGET +using Microsoft.Diagnostics.Tracing; +#else +using System.Diagnostics.Tracing; +#endif +using System; +using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute; + +#pragma warning disable 3021 // 'type' does not need a CLSCompliant attribute + +namespace Microsoft.Diagnostics.Telemetry +{ + /// + /// + /// An EventSource with extra methods and constants commonly used in Microsoft's + /// TraceLogging-based ETW. This class inherits from EventSource, and is exactly + /// the same as EventSource except that it always enables + /// EtwSelfDescribingEventFormat and never uses traits. It also provides several + /// constants and helpers commonly used by Microsoft code. + /// + /// + /// Different versions of this class use different provider traits. The provider + /// traits in this class are empty. As a result, providers using this class will + /// not join any ETW Provider Groups and will not be given any special treatment + /// by group-sensitive ETW listeners. + /// + /// + /// When including this class in your project, you may define the following + /// conditional-compilation symbols to adjust the default behaviors: + /// + /// + /// TELEMETRYEVENTSOURCE_USE_NUGET - use Microsoft.Diagnostics.Tracing instead + /// of System.Diagnostics.Tracing. + /// + /// + /// TELEMETRYEVENTSOURCE_PUBLIC - define TelemetryEventSource as public instead + /// of internal. + /// + /// +#if TELEMETRYEVENTSOURCE_PUBLIC + public +#else + internal +#endif + class TelemetryEventSource + : EventSource + { + /// + /// Keyword 0x0000100000000000 is reserved for future definition. Do + /// not use keyword 0x0000100000000000 in Microsoft-style ETW. + /// + public const EventKeywords Reserved44Keyword = (EventKeywords)0x0000100000000000; + + /// + /// Add TelemetryKeyword to eventSourceOptions.Keywords to indicate that + /// an event is for general-purpose telemetry. + /// This keyword should not be combined with MeasuresKeyword or + /// CriticalDataKeyword. + /// + public const EventKeywords TelemetryKeyword = (EventKeywords)0x0000200000000000; + + /// + /// Add MeasuresKeyword to eventSourceOptions.Keywords to indicate that + /// an event is for understanding measures and reporting scenarios. + /// This keyword should not be combined with TelemetryKeyword or + /// CriticalDataKeyword. + /// + public const EventKeywords MeasuresKeyword = (EventKeywords)0x0000400000000000; + + /// + /// Add CriticalDataKeyword to eventSourceOptions.Keywords to indicate that + /// an event powers user experiences or is critical to business intelligence. + /// This keyword should not be combined with TelemetryKeyword or + /// MeasuresKeyword. + /// + public const EventKeywords CriticalDataKeyword = (EventKeywords)0x0000800000000000; + + /// + /// Add CostDeferredLatency to eventSourceOptions.Tags to indicate that an event + /// should try to upload over free networks for a period of time before resorting + /// to upload over costed networks. + /// + public const EventTags CostDeferredLatency = (EventTags)0x040000; + + /// + /// Add CoreData to eventSourceOptions.Tags to indicate that an event + /// contains high priority "core data". + /// + public const EventTags CoreData = (EventTags)0x00080000; + + /// + /// Add InjectXToken to eventSourceOptions.Tags to indicate that an XBOX + /// identity token should be injected into the event before the event is + /// uploaded. + /// + public const EventTags InjectXToken = (EventTags)0x00100000; + + /// + /// Add RealtimeLatency to eventSourceOptions.Tags to indicate that an event + /// should be transmitted in real time (via any available connection). + /// + public const EventTags RealtimeLatency = (EventTags)0x0200000; + + /// + /// Add NormalLatency to eventSourceOptions.Tags to indicate that an event + /// should be transmitted via the preferred connection based on device policy. + /// + public const EventTags NormalLatency = (EventTags)0x0400000; + + /// + /// Add CriticalPersistence to eventSourceOptions.Tags to indicate that an + /// event should be deleted last when low on spool space. + /// + public const EventTags CriticalPersistence = (EventTags)0x0800000; + + /// + /// Add NormalPersistence to eventSourceOptions.Tags to indicate that an event + /// should be deleted first when low on spool space. + /// + public const EventTags NormalPersistence = (EventTags)0x1000000; + + /// + /// Add DropPii to eventSourceOptions.Tags to indicate that an event contains + /// PII and should be anonymized by the telemetry client. If this tag is + /// present, PartA fields that might allow identification or cross-event + /// correlation will be removed from the event. + /// + public const EventTags DropPii = (EventTags)0x02000000; + + /// + /// Add HashPii to eventSourceOptions.Tags to indicate that an event contains + /// PII and should be anonymized by the telemetry client. If this tag is + /// present, PartA fields that might allow identification or cross-event + /// correlation will be hashed (obfuscated). + /// + public const EventTags HashPii = (EventTags)0x04000000; + + /// + /// Add MarkPii to eventSourceOptions.Tags to indicate that an event contains + /// PII but may be uploaded as-is. If this tag is present, the event will be + /// marked so that it will only appear on the private stream. + /// + public const EventTags MarkPii = (EventTags)0x08000000; + + /// + /// Add DropPiiField to eventFieldAttribute.Tags to indicate that a field + /// contains PII and should be dropped by the telemetry client. + /// + public const EventFieldTags DropPiiField = (EventFieldTags)0x04000000; + + /// + /// Add HashPiiField to eventFieldAttribute.Tags to indicate that a field + /// contains PII and should be hashed (obfuscated) prior to uploading. + /// + public const EventFieldTags HashPiiField = (EventFieldTags)0x08000000; + + /// + /// Constructs a new instance of the TelemetryEventSource class with the + /// specified name. Sets the EtwSelfDescribingEventFormat option. + /// + /// The name of the event source. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + public TelemetryEventSource( + string eventSourceName) + : base( + eventSourceName, + EventSourceSettings.EtwSelfDescribingEventFormat) + { + return; + } + + /// + /// For use by derived classes that set the eventSourceName via EventSourceAttribute. + /// Sets the EtwSelfDescribingEventFormat option. + /// + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + protected TelemetryEventSource() + : base( + EventSourceSettings.EtwSelfDescribingEventFormat) + { + return; + } + + /// + /// Constructs a new instance of the TelemetryEventSource class with the + /// specified name. Sets the EtwSelfDescribingEventFormat option. + /// + /// The name of the event source. + /// The parameter is not used. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "API compatibility")] + public TelemetryEventSource( + string eventSourceName, + TelemetryGroup telemetryGroup) + : base( + eventSourceName, + EventSourceSettings.EtwSelfDescribingEventFormat) + { + return; + } + + /// + /// Returns an instance of EventSourceOptions with the TelemetryKeyword set. + /// + /// Returns an instance of EventSourceOptions with the TelemetryKeyword set. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + public static EventSourceOptions TelemetryOptions() + { + return new EventSourceOptions { Keywords = TelemetryKeyword }; + } + + /// + /// Returns an instance of EventSourceOptions with the MeasuresKeyword set. + /// + /// Returns an instance of EventSourceOptions with the MeasuresKeyword set. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + public static EventSourceOptions MeasuresOptions() + { + return new EventSourceOptions { Keywords = MeasuresKeyword }; + } + } + + /// + /// + /// The PrivTags class defines privacy tags that can be used to specify the privacy + /// category of an event. Add a privacy tag as a field with name "PartA_PrivTags". + /// As a shortcut, you can use _1 as the field name, which will automatically be + /// expanded to "PartA_PrivTags" at runtime. + /// + /// + /// Multiple tags can be OR'ed together if necessary (rarely needed). + /// + /// + /// + /// Typical usage: + /// + /// es.Write("UsageEvent", new + /// { + /// _1 = PrivTags.ProductAndServiceUsage, + /// field1 = fieldValue1, + /// field2 = fieldValue2 + /// }); + /// + /// +#if TELEMETRYEVENTSOURCE_PUBLIC + [CLSCompliant(false)] + public +#else + internal +#endif + static class PrivTags + { + /// + public const Internal.PartA_PrivTags BrowsingHistory = Internal.PartA_PrivTags.BrowsingHistory; + + /// + public const Internal.PartA_PrivTags DeviceConnectivityAndConfiguration = Internal.PartA_PrivTags.DeviceConnectivityAndConfiguration; + + /// + public const Internal.PartA_PrivTags InkingTypingAndSpeechUtterance = Internal.PartA_PrivTags.InkingTypingAndSpeechUtterance; + + /// + public const Internal.PartA_PrivTags ProductAndServicePerformance = Internal.PartA_PrivTags.ProductAndServicePerformance; + + /// + public const Internal.PartA_PrivTags ProductAndServiceUsage = Internal.PartA_PrivTags.ProductAndServiceUsage; + + /// + public const Internal.PartA_PrivTags SoftwareSetupAndInventory = Internal.PartA_PrivTags.SoftwareSetupAndInventory; + } + /// + /// Pass a TelemetryGroup value to the constructor of TelemetryEventSource + /// to control which telemetry group should be joined. + /// Note: has no effect in this version of TelemetryEventSource. + /// +#if TELEMETRYEVENTSOURCE_PUBLIC + public +#else + internal +#endif + enum TelemetryGroup + { + /// + /// The default group. Join this group to log normal, non-critical, non-coredata + /// events. + /// + MicrosoftTelemetry, + + /// + /// Join this group to log CriticalData, CoreData, or other specially approved + /// events. + /// + WindowsCoreTelemetry + } + +#pragma warning disable SA1403 // File may only contain a single namespace + namespace Internal +#pragma warning restore SA1403 // File may only contain a single namespace + { + /// + /// The complete list of privacy tags supported for events. + /// Multiple tags can be OR'ed together if an event belongs in multiple + /// categories. + /// Note that the PartA_PrivTags enum should not be used directly. + /// Instead, use values from the PrivTags class. + /// + [Flags] +#if TELEMETRYEVENTSOURCE_PUBLIC + [CLSCompliant(false)] + public +#else + internal +#endif + enum PartA_PrivTags + : ulong + { + /// + None = 0, + + /// + BrowsingHistory = 0x0000000000000002u, + + /// + DeviceConnectivityAndConfiguration = 0x0000000000000800u, + + /// + InkingTypingAndSpeechUtterance = 0x0000000000020000u, + + /// + ProductAndServicePerformance = 0x0000000001000000u, + + /// + ProductAndServiceUsage = 0x0000000002000000u, + + /// + SoftwareSetupAndInventory = 0x0000000080000000u, + } + } +} diff --git a/extensions/WindowsSandboxExtension/Telemetry/TraceLogging.cs b/extensions/WindowsSandboxExtension/Telemetry/TraceLogging.cs new file mode 100644 index 0000000000..e8a39e8b16 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Telemetry/TraceLogging.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Diagnostics.Telemetry; + +namespace WindowsSandboxExtension.Telemetry; + +internal sealed class TraceLogging +{ + private const string ProviderName = "Microsoft.Windows.Containers.WindowsSandboxExtension"; + private const string StartingEventName = "StartingWindowsSandbox"; + private const string ExceptionThrownEventName = "ExceptionThrown"; + + private static readonly TelemetryEventSource EventSource = new(ProviderName); + + public static void StartingWindowsSandbox() + { + var options = TelemetryEventSource.MeasuresOptions(); + options.Level = EventLevel.Informational; + + EventSource.Write(StartingEventName, options); + } + + public static void ExceptionThrown(Exception exception) + { + var options = TelemetryEventSource.MeasuresOptions(); + options.Level = EventLevel.Error; + + EventSource.Write( + ExceptionThrownEventName, + options, + new + { + name = exception.GetType().Name, + stackTrace = exception.StackTrace, + innerName = exception.InnerException?.GetType().Name, + innerMessage = exception.InnerException?.Message, + innerStackTrace = exception.InnerException?.ToString(), + message = exception.Message, + }); + } +} diff --git a/extensions/WindowsSandboxExtension/WindowsSandboxExtension.cs b/extensions/WindowsSandboxExtension/WindowsSandboxExtension.cs new file mode 100644 index 0000000000..495dec4e7f --- /dev/null +++ b/extensions/WindowsSandboxExtension/WindowsSandboxExtension.cs @@ -0,0 +1,88 @@ +// 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 WindowsSandboxExtension.Helpers; + +namespace WindowsSandboxExtension; + +[ComVisible(true)] +[Guid("6A52115B-083C-4FB1-85F4-BBE23289220E")] +[ComDefaultInterface(typeof(IExtension))] +internal sealed class WindowsSandboxExtension : IExtension, IDisposable +{ + private readonly IHost _host; + private readonly ILogger _logger; + private bool _disposed; + + public WindowsSandboxExtension(IHost host) + { + _host = host; + _logger = Log.ForContext("SourceContext", nameof(WindowsSandboxExtension)); + } + + public ManualResetEvent ExtensionDisposedEvent { get; } = new(false); + + public object? GetProvider(ProviderType providerType) + { + object? provider = null; + + try + { + switch (providerType) + { + case ProviderType.ComputeSystem: + + provider = GetComputeSystemProvider(); + break; + default: + _logger.Information($"Unsupported provider: {providerType}"); + break; + } + } + catch (Exception ex) + { + _logger.Error(ex, $"Failed to get provider for provider type {providerType}"); + } + + return provider; + } + + private object? GetComputeSystemProvider() + { + if (DependencyChecker.IsNewWindowsSandboxExtensionInstalled()) + { + _logger.Information("New Windows Sandbox appx package is installed."); + return null; + } + + if (!DependencyChecker.IsOptionalComponentEnabled()) + { + _logger.Information("Windows Sandbox optional component is not enabled."); + return null; + } + + return _host.Services.GetService(typeof(IComputeSystemProvider)); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + ExtensionDisposedEvent.Set(); + } + + _disposed = true; + } + } +} diff --git a/extensions/WindowsSandboxExtension/WindowsSandboxExtension.csproj b/extensions/WindowsSandboxExtension/WindowsSandboxExtension.csproj new file mode 100644 index 0000000000..4738e476b5 --- /dev/null +++ b/extensions/WindowsSandboxExtension/WindowsSandboxExtension.csproj @@ -0,0 +1,48 @@ + + + + + Exe + + + WinExe + + + + enable + enable + false + false + win-x86;win-x64;win-arm64 + WindowsSandboxExtension.Program + x86;x64;arm64 + $(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + Always + + + diff --git a/extensions/WindowsSandboxExtension/appsettings_WindowsSandbox.json b/extensions/WindowsSandboxExtension/appsettings_WindowsSandbox.json new file mode 100644 index 0000000000..c26d22a19c --- /dev/null +++ b/extensions/WindowsSandboxExtension/appsettings_WindowsSandbox.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": "%DEVHOME_LOGS_ROOT%\\WindowsSandbox.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": "WindowsSandboxExtension" + } + } +} \ No newline at end of file diff --git a/src/DevHome.csproj b/src/DevHome.csproj index 18555a5eb2..ec1465b0f0 100644 --- a/src/DevHome.csproj +++ b/src/DevHome.csproj @@ -83,6 +83,7 @@ + diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest index 66de1e44c5..6054c5030d 100644 --- a/src/Package.appxmanifest +++ b/src/Package.appxmanifest @@ -66,6 +66,13 @@ + + + + + + + @@ -107,7 +114,21 @@ - + + + + + + + + + + + + + + + diff --git a/src/Strings/en-us/Resources.resw b/src/Strings/en-us/Resources.resw index a03ca81571..369cefdcf6 100644 --- a/src/Strings/en-us/Resources.resw +++ b/src/Strings/en-us/Resources.resw @@ -310,4 +310,12 @@ Hyper-V Extension {Locked="Hyper-V"} Extension Display Name + + Windows Sandbox Extension + {Locked="Windows Sandbox"} Extension name + + + Windows Sandbox Extension + {Locked="Windows Sandbox"} Extension description + \ No newline at end of file