diff --git a/.github/policies/moderatorTriggers.yml b/.github/policies/moderatorTriggers.yml index 9b5608c01f..4b92473c66 100644 --- a/.github/policies/moderatorTriggers.yml +++ b/.github/policies/moderatorTriggers.yml @@ -90,8 +90,9 @@ configuration: reply: >- Hi @${issueAuthor}. It would be helpful for us if you could share your Dev Home logs. These logs can be found at - `%LOCALAPPDATA%\Packages\Microsoft.Windows.DevHome_8wekyb3d8bbwe\TempState` and - `%LOCALAPPDATA%\Packages\Microsoft.Windows.DevHomeGitHubExtension_8wekyb3d8bbwe\TempState`. + `%LOCALAPPDATA%\Packages\Microsoft.Windows.DevHome_8wekyb3d8bbwe\TempState`, + `%LOCALAPPDATA%\Packages\Microsoft.Windows.DevHomeGitHubExtension_8wekyb3d8bbwe\TempState`, and + `%LOCALAPPDATA%\Packages\Microsoft.Windows.DevHomeAzureExtension_8wekyb3d8bbwe\TempState`. You can share these folders via a OneDrive link or zip them and attach them to a comment here. If you share this way, you may want to look through the logs in case there are any details included that you would like to remove (for example, private diff --git a/.github/workflows/DevHome-CI.yml b/.github/workflows/DevHome-CI.yml index 8290067b59..f633cff6a2 100644 --- a/.github/workflows/DevHome-CI.yml +++ b/.github/workflows/DevHome-CI.yml @@ -72,7 +72,7 @@ jobs: - name: Compress_DevSetupAgent_x86 if: ${{ matrix.platform != 'arm64' }} shell: pwsh - run: Compress-Archive -Force -Path HyperVExtension\src\DevSetupAgent\bin\x86\${{ matrix.configuration }}\net8.0-windows10.0.22621.0\win10-x86\* -DestinationPath "HyperVExtension\src\DevSetupAgent\bin\x86\${{ matrix.configuration }}\DevSetupAgent_x86.zip" + run: Compress-Archive -Force -Path HyperVExtension\src\DevSetupAgent\bin\x86\${{ matrix.configuration }}\net8.0-windows10.0.22621.0\win-x86\* -DestinationPath "HyperVExtension\src\DevSetupAgent\bin\x86\${{ matrix.configuration }}\DevSetupAgent_x86.zip" - name: Build_DevSetupAgent_arm64 if: ${{ matrix.platform == 'arm64' }} @@ -81,7 +81,7 @@ jobs: - name: Compress_DevSetupAgent_arm64 if: ${{ matrix.platform == 'arm64' }} shell: pwsh - run: Compress-Archive -Force -Path HyperVExtension\src\DevSetupAgent\bin\arm64\${{ matrix.configuration }}\net8.0-windows10.0.22621.0\win10-arm64\* -DestinationPath "HyperVExtension\src\DevSetupAgent\bin\arm64\${{ matrix.configuration }}\DevSetupAgent_arm64.zip" + run: Compress-Archive -Force -Path HyperVExtension\src\DevSetupAgent\bin\arm64\${{ matrix.configuration }}\net8.0-windows10.0.22621.0\win-arm64\* -DestinationPath "HyperVExtension\src\DevSetupAgent\bin\arm64\${{ matrix.configuration }}\DevSetupAgent_arm64.zip" - name: Build_DevHome run: cmd /c "$env:VSDevCmd" "&" msbuild /p:Configuration=${{ matrix.configuration }},Platform=${{ matrix.platform }} DevHome.sln diff --git a/Directory.Build.props b/Directory.Build.props index 2d989711ee..4f951f1b9f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,6 +12,7 @@ true Recommended $(Platform) + false - - - true - - - - + \ No newline at end of file diff --git a/HyperVExtension/BuildDevSetupAgentHelper.ps1 b/HyperVExtension/BuildDevSetupAgentHelper.ps1 index 4d64e35c69..3be5d8836c 100644 --- a/HyperVExtension/BuildDevSetupAgentHelper.ps1 +++ b/HyperVExtension/BuildDevSetupAgentHelper.ps1 @@ -93,7 +93,7 @@ Try { & $msbuildPath $msbuildArgs # SDK version and .NEt version needs to stay in sync with ToolingVersion.props, DevSetupEngineIdl.vcxproj, and DevHome-CL.yaml - $binariesOutputPath = (Join-Path $env:Build_RootDirectory "HyperVExtension\src\DevSetupAgent\bin\$Platform\$Configuration\net8.0-windows10.0.22621.0\win10-$Platform\*") + $binariesOutputPath = (Join-Path $env:Build_RootDirectory "HyperVExtension\src\DevSetupAgent\bin\$Platform\$Configuration\net8.0-windows10.0.22621.0\win-$Platform\*") $zipOutputPath = (Join-Path $env:Build_RootDirectory "HyperVExtension\src\DevSetupAgent\bin\$Platform\$Configuration\DevSetupAgent_$Platform.zip") Compress-Archive -Force -Path $binariesOutputPath $zipOutputPath diff --git a/HyperVExtension/src/DevSetupAgent/DevAgentService.cs b/HyperVExtension/src/DevSetupAgent/DevAgentService.cs index f0b5c958c8..9b5b89a18b 100644 --- a/HyperVExtension/src/DevSetupAgent/DevAgentService.cs +++ b/HyperVExtension/src/DevSetupAgent/DevAgentService.cs @@ -41,13 +41,13 @@ protected async override Task ExecuteAsync(CancellationToken stoppingToken) } catch (Exception ex) { - _log.Error($"Exception in DevAgentService.", ex); + _log.Error(ex, $"Exception in DevAgentService."); } } } catch (Exception ex) { - _log.Error($"Failed to run DevSetupAgent.", ex); + _log.Error(ex, $"Failed to run DevSetupAgent."); throw; } finally diff --git a/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj b/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj index ec94f1721a..b0ce1b6506 100644 --- a/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj +++ b/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj @@ -6,8 +6,8 @@ dotnet-DevSetupAgent-674f51cd-70a6-4b78-8376-66efbf84c412 Dev x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 - Properties\PublishProfiles\win10-$(Platform).pubxml + win-x86;win-x64;win-arm64 + Properties\PublishProfiles\win-$(Platform).pubxml true diff --git a/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs b/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs index 518dc93ed2..11e8f8a076 100644 --- a/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs +++ b/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs @@ -113,7 +113,7 @@ await Task.Run( } catch (Exception ex) { - _log.Error($"Could not write host message. Response ID: {responseMessage.ResponseId}", ex); + _log.Error(ex, $"Could not write host message. Response ID: {responseMessage.ResponseId}"); } }, stoppingToken); @@ -130,7 +130,7 @@ await Task.Run( } catch (Exception ex) { - _log.Error($"Could not delete host message. Response ID: {responseId}", ex); + _log.Error(ex, $"Could not delete host message. Response ID: {responseId}"); } }, stoppingToken); @@ -207,7 +207,7 @@ private RequestMessage TryReadMessage() } catch (Exception ex) { - _log.Error($"Could not read host message {valueName}", ex); + _log.Error(ex, $"Could not read host message {valueName}"); } MessageHelper.DeleteAllMessages(_registryHiveKey, _fromHostRegistryKeyPath, s[0]); @@ -218,7 +218,7 @@ private RequestMessage TryReadMessage() } catch (Exception ex) { - _log.Error("Could not read host message.", ex); + _log.Error(ex, "Could not read host message."); } return requestMessage; diff --git a/HyperVExtension/src/DevSetupAgent/Logging.cs b/HyperVExtension/src/DevSetupAgent/Logging.cs new file mode 100644 index 0000000000..920111d914 --- /dev/null +++ b/HyperVExtension/src/DevSetupAgent/Logging.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Windows.Storage; + +namespace HyperVExtension.DevSetupAgent; + +public class Logging +{ + public static readonly string LogExtension = ".dhlog"; + + public static readonly string LogFolderName = "Logs"; + + public static readonly string AppName = "DevSetupAgent"; + + public static readonly string DefaultLogFileName = "hyperv_setup"; + + private static readonly Lazy _logFolderRoot = new(() => Path.Combine(Path.GetTempPath(), AppName, LogFolderName)); + + public static readonly string LogFolderRoot = _logFolderRoot.Value; +} diff --git a/HyperVExtension/src/DevSetupAgent/NativeMethods.txt b/HyperVExtension/src/DevSetupAgent/NativeMethods.txt index 13b6a24b02..82f030d248 100644 --- a/HyperVExtension/src/DevSetupAgent/NativeMethods.txt +++ b/HyperVExtension/src/DevSetupAgent/NativeMethods.txt @@ -6,8 +6,14 @@ CLSCTX WIN32_ERROR S_OK E_FAIL +E_UNEXPECTED LsaEnumerateLogonSessions LsaGetLogonSessionData Windows.Win32.Security.Authentication.Identity.LsaFreeReturnBuffer SECURITY_LOGON_TYPE STATUS_SUCCESS +MakeAbsoluteSD +ConvertStringSecurityDescriptorToSecurityDescriptor +LocalAlloc +LocalFree +SDDL_REVISION_1 diff --git a/HyperVExtension/src/DevSetupAgent/Program.cs b/HyperVExtension/src/DevSetupAgent/Program.cs index d8dbf882e2..dbc372a24c 100644 --- a/HyperVExtension/src/DevSetupAgent/Program.cs +++ b/HyperVExtension/src/DevSetupAgent/Program.cs @@ -1,34 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.ComponentModel; using System.Runtime.InteropServices; using HyperVExtension.DevSetupAgent; +using HyperVExtension.HostGuestCommunication; using Serilog; using Windows.Win32; +using Windows.Win32.Foundation; using Windows.Win32.Security; using Windows.Win32.System.Com; -unsafe -{ - // TODO: Set real security descriptor to allow access from System+Admns+Interactive Users - var hr = PInvoke.CoInitializeSecurity( - new(null), - -1, - null, - null, - RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_DEFAULT, - RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IDENTIFY, - null, - EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); - - if (hr < 0) - { - Marshal.ThrowExceptionForHR(hr); - } -} - // Set up Logging -Environment.SetEnvironmentVariable("DEVHOME_LOGS_ROOT", Path.Join(HyperVExtension.DevSetupEngine.Logging.LogFolderRoot, "HyperV")); +Environment.SetEnvironmentVariable("DEVHOME_LOGS_ROOT", Path.Join(Logging.LogFolderRoot, "HyperV")); var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings_hypervsetupagent.json") .Build(); @@ -36,6 +20,90 @@ .ReadFrom.Configuration(configuration) .CreateLogger(); +unsafe +{ + PSECURITY_DESCRIPTOR absolutSd = new(null); + PSID ownerSid = new(null); + PSID groupSid = new(null); + ACL* dacl = default; + ACL* sacl = default; + + try + { + // O:PSG:BU Owner Principal Self, Group Built-in Users + // (A;;0x3;;;SY) Allow Local System + // (A;;0x3;;;IU) Allow Interactive User + var accessPermission = "O:PSG:BUD:(A;;0x3;;;SY)(A;;0x3;;;IU)"; + uint securityDescriptorSize; + PInvoke.ConvertStringSecurityDescriptorToSecurityDescriptor(accessPermission, PInvoke.SDDL_REVISION_1, out var securityDescriptor, &securityDescriptorSize); + + uint absoluteSdSize = default; + uint daclSize = default; + uint saclSize = default; + uint ownerSize = default; + uint groupSize = default; + + if (PInvoke.MakeAbsoluteSD(securityDescriptor, absolutSd, ref absoluteSdSize, null, ref daclSize, null, ref saclSize, ownerSid, ref ownerSize, groupSid, ref groupSize)) + { + throw new HResultException(HRESULT.E_UNEXPECTED); + } + + var error = Marshal.GetLastWin32Error(); + if (error != (int)WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) + { + throw new Win32Exception(error); + } + + absolutSd = new(PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, absoluteSdSize)); + dacl = (ACL*)PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, daclSize); + sacl = (ACL*)PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, saclSize); + ownerSid = new(PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, ownerSize)); + groupSid = new(PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, groupSize)); + + if (!PInvoke.MakeAbsoluteSD(securityDescriptor, absolutSd, ref absoluteSdSize, dacl, ref daclSize, sacl, ref saclSize, ownerSid, ref ownerSize, groupSid, ref groupSize)) + { + throw new HResultException(Marshal.GetLastWin32Error()); + } + + var hr = PInvoke.CoInitializeSecurity( + absolutSd, + -1, + null, + null, + RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_DEFAULT, + RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IDENTIFY, + null, + EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); + + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + } + finally + { + if (sacl != default) + { + PInvoke.LocalFree((HLOCAL)sacl); + } + + if (dacl != default) + { + PInvoke.LocalFree((HLOCAL)dacl); + } + + if (groupSid != default) + { + PInvoke.LocalFree((HLOCAL)groupSid.Value); + } + + if (ownerSid != default) + { + PInvoke.LocalFree((HLOCAL)ownerSid.Value); + } + } +} + var host = Host.CreateDefaultBuilder(args) .UseWindowsService(options => { diff --git a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-arm64.pubxml b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-arm64.pubxml similarity index 90% rename from HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-arm64.pubxml rename to HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-arm64.pubxml index 08079c2934..ced5ea3247 100644 --- a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-arm64.pubxml +++ b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-arm64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem arm64 - win10-arm64 + win-arm64 true False False diff --git a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x64.pubxml b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x64.pubxml similarity index 91% rename from HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x64.pubxml rename to HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x64.pubxml index 94861ecd4c..e4ca421fa6 100644 --- a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x64.pubxml +++ b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem x64 - win10-x64 + win-x64 true False False diff --git a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x86.pubxml b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x86.pubxml similarity index 91% rename from HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x86.pubxml rename to HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x86.pubxml index 3a63ea8fb9..69092cd4a2 100644 --- a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x86.pubxml +++ b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x86.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem x86 - win10-x86 + win-x86 true False False diff --git a/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs b/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs index 019407c889..023f46ca27 100644 --- a/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs +++ b/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs @@ -76,14 +76,14 @@ public void Start() } catch (Exception ex) { - _log.Error("RegistryChanged delegate failed.", ex); + _log.Error(ex, "RegistryChanged delegate failed."); } } } } catch (Exception ex) { - _log.Error("Registry Watcher thread failed.", ex); + _log.Error(ex, "Registry Watcher thread failed."); } }); _log.Information("Registry Watcher thread started."); diff --git a/HyperVExtension/src/DevSetupAgent/RequestManager.cs b/HyperVExtension/src/DevSetupAgent/RequestManager.cs index 9c799d77f6..07cde9cc46 100644 --- a/HyperVExtension/src/DevSetupAgent/RequestManager.cs +++ b/HyperVExtension/src/DevSetupAgent/RequestManager.cs @@ -102,7 +102,7 @@ private void ProcessRequestQueue(CancellationToken stoppingToken) } catch (Exception ex) { - _log.Error($"Failed to execute request.", ex); + _log.Error(ex, $"Failed to execute request."); } } } diff --git a/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs b/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs index fabc9a2052..725f3dedf5 100644 --- a/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs +++ b/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs @@ -9,10 +9,11 @@ namespace HyperVExtension.DevSetupAgent; /// internal class ErrorRequest : IHostRequest { - public ErrorRequest(IRequestMessage requestMessage) + public ErrorRequest(IRequestMessage requestMessage, Exception? ex = null) { Timestamp = DateTime.UtcNow; RequestId = requestMessage.RequestId!; + Error = ex; } public virtual uint Version { get; set; } = 1; @@ -27,6 +28,8 @@ public ErrorRequest(IRequestMessage requestMessage) public virtual IHostResponse Execute(ProgressHandler progressHandler, CancellationToken stoppingToken) { - return new ErrorResponse(RequestId); + return new ErrorResponse(RequestId, Error); } + + private Exception? Error { get; } } diff --git a/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs b/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs index 7067634443..bd15df8917 100644 --- a/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs +++ b/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs @@ -42,7 +42,6 @@ public IHostRequest CreateRequest(IRequestContext requestContext) { if (_requestFactories.TryGetValue(requestType, out var createRequest)) { - // TODO: Try/catch error. requestContext.JsonData = requestJson!; return createRequest(requestContext); } @@ -65,7 +64,7 @@ public IHostRequest CreateRequest(IRequestContext requestContext) { var messageId = requestContext.RequestMessage.RequestId ?? ""; var requestData = requestContext.RequestMessage.RequestData ?? ""; - _log.Error($"Error processing message. Message ID: {messageId}. Request data: {requestData}", ex); + _log.Error(ex, $"Error processing message. Message ID: {messageId}. Request data: {requestData}"); return new ErrorRequest(requestContext.RequestMessage); } } diff --git a/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs b/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs index 4428ebf76e..da53faf194 100644 --- a/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs +++ b/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs @@ -9,11 +9,20 @@ namespace HyperVExtension.DevSetupAgent; /// internal sealed class ErrorResponse : ResponseBase { - public ErrorResponse(string requestId) + public ErrorResponse(string requestId, Exception? error) : base(requestId) { - Status = Windows.Win32.Foundation.HRESULT.E_FAIL; - ErrorDescription = "Missing Request data."; + if (error != null) + { + ErrorDescription = error.Message; + Status = (uint)error.HResult; + } + else + { + ErrorDescription = "Missing Request data."; + Status = Windows.Win32.Foundation.HRESULT.E_FAIL; + } + GenerateJsonData(); } } diff --git a/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs b/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs index c0e0331f13..e57dfd4a9a 100644 --- a/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs +++ b/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs @@ -13,7 +13,7 @@ public ErrorUnsupportedRequestResponse(string requestId, string requestType) : base(requestId, requestType) { Status = Windows.Win32.Foundation.HRESULT.E_FAIL; - ErrorDescription = "Missing Request type."; + ErrorDescription = "Unsupported Request type."; GenerateJsonData(); } } diff --git a/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json b/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json index e5932df92c..b66927f704 100644 --- a/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json +++ b/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json @@ -12,7 +12,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -20,7 +20,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\hyperv_setupagent.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs b/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs index c7d7862dd7..c8885f2d4c 100644 --- a/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs +++ b/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs @@ -279,22 +279,21 @@ private void LogConfigurationDiagnostics(WinGet.IDiagnosticInformation diagnosti { _log.Information($"WinGet: {diagnosticInformation.Message}"); - var sourceComponent = nameof(WinGet.ConfigurationProcessor); switch (diagnosticInformation.Level) { case WinGet.DiagnosticLevel.Warning: - _log.Warning(sourceComponent, diagnosticInformation.Message); + _log.Warning(diagnosticInformation.Message); return; case WinGet.DiagnosticLevel.Error: - _log.Error(sourceComponent, diagnosticInformation.Message); + _log.Error(diagnosticInformation.Message); return; case WinGet.DiagnosticLevel.Critical: - _log.Fatal(sourceComponent, diagnosticInformation.Message); + _log.Fatal(diagnosticInformation.Message); return; case WinGet.DiagnosticLevel.Verbose: case WinGet.DiagnosticLevel.Informational: default: - _log.Information(sourceComponent, diagnosticInformation.Message); + _log.Information(diagnosticInformation.Message); return; } } diff --git a/HyperVExtension/src/DevSetupEngine/DevSetupEngine.csproj b/HyperVExtension/src/DevSetupEngine/DevSetupEngine.csproj index a2bfa10cb3..448a52e474 100644 --- a/HyperVExtension/src/DevSetupEngine/DevSetupEngine.csproj +++ b/HyperVExtension/src/DevSetupEngine/DevSetupEngine.csproj @@ -17,8 +17,8 @@ true Dev x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 - Properties\PublishProfiles\win10-$(Platform).pubxml + win-x86;win-x64;win-arm64 + Properties\PublishProfiles\win-$(Platform).pubxml true diff --git a/HyperVExtension/src/DevSetupEngine/Logging.cs b/HyperVExtension/src/DevSetupEngine/Logging.cs index e3e5e40435..a2cdefcdfe 100644 --- a/HyperVExtension/src/DevSetupEngine/Logging.cs +++ b/HyperVExtension/src/DevSetupEngine/Logging.cs @@ -9,11 +9,13 @@ public class Logging { public static readonly string LogExtension = ".dhlog"; - public static readonly string LogFolderName = "Logs"; + public static readonly string LogFolderName = "Logs"; + + public static readonly string AppName = "DevSetupEngine"; public static readonly string DefaultLogFileName = "hyperv_setup"; - - private static readonly Lazy _logFolderRoot = new(() => Path.Combine(ApplicationData.Current.TemporaryFolder.Path, LogFolderName)); + + private static readonly Lazy _logFolderRoot = new(() => Path.Combine(Path.GetTempPath(), AppName, LogFolderName)); public static readonly string LogFolderRoot = _logFolderRoot.Value; } diff --git a/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs b/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs index 9b149bede0..eaca912ed9 100644 --- a/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs +++ b/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs @@ -19,6 +19,6 @@ public PackageOperationException(ErrorCode errorCode, string message) { HResult = (int)errorCode; var log = Log.ForContext("SourceContext", nameof(PackageOperationException)); - log.Error(message, this); + log.Error(this, message); } } diff --git a/HyperVExtension/src/DevSetupEngine/Program.cs b/HyperVExtension/src/DevSetupEngine/Program.cs index 6bff3af2d6..4c1c269ebf 100644 --- a/HyperVExtension/src/DevSetupEngine/Program.cs +++ b/HyperVExtension/src/DevSetupEngine/Program.cs @@ -58,7 +58,7 @@ public static int Main([System.Runtime.InteropServices.WindowsRuntime.ReadOnlyAr } catch (Exception ex) { - Log.Error($"Exception: {ex}", ex); + Log.Error(ex, $"Exception: {ex}"); Log.CloseAndFlush(); return ex.HResult; } diff --git a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-arm64.pubxml b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-arm64.pubxml similarity index 90% rename from HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-arm64.pubxml rename to HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-arm64.pubxml index 08079c2934..ced5ea3247 100644 --- a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-arm64.pubxml +++ b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-arm64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem arm64 - win10-arm64 + win-arm64 true False False diff --git a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x64.pubxml b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x64.pubxml similarity index 91% rename from HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x64.pubxml rename to HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x64.pubxml index 94861ecd4c..e4ca421fa6 100644 --- a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x64.pubxml +++ b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem x64 - win10-x64 + win-x64 true False False diff --git a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x86.pubxml b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x86.pubxml similarity index 91% rename from HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x86.pubxml rename to HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x86.pubxml index 3a63ea8fb9..69092cd4a2 100644 --- a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x86.pubxml +++ b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x86.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem x86 - win10-x86 + win-x86 true False False diff --git a/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json b/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json index 9653f50282..ce815ea0b5 100644 --- a/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json +++ b/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json @@ -6,7 +6,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -14,7 +14,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\hyperv_setup.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/HyperVExtension/src/DevSetupEngineProjection/DevSetupEngineProjection.csproj b/HyperVExtension/src/DevSetupEngineProjection/DevSetupEngineProjection.csproj index b44bff1bfd..b0e491b69f 100644 --- a/HyperVExtension/src/DevSetupEngineProjection/DevSetupEngineProjection.csproj +++ b/HyperVExtension/src/DevSetupEngineProjection/DevSetupEngineProjection.csproj @@ -6,7 +6,7 @@ None x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 diff --git a/HyperVExtension/src/HyperVExtension.Common/HyperVExtension.Common.csproj b/HyperVExtension/src/HyperVExtension.Common/HyperVExtension.Common.csproj index 0efc2965e6..cc17ff4aad 100644 --- a/HyperVExtension/src/HyperVExtension.Common/HyperVExtension.Common.csproj +++ b/HyperVExtension/src/HyperVExtension.Common/HyperVExtension.Common.csproj @@ -3,7 +3,7 @@ HyperVExtension.Common x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 diff --git a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/HResultException.cs b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HResultException.cs similarity index 66% rename from HyperVExtension/src/HyperVExtension/CommunicationWithGuest/HResultException.cs rename to HyperVExtension/src/HyperVExtension.HostGuestCommunication/HResultException.cs index f812115e95..c64c211b30 100644 --- a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/HResultException.cs +++ b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HResultException.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace HyperVExtension.CommunicationWithGuest; +namespace HyperVExtension.HostGuestCommunication; -internal sealed class HResultException : Exception +public sealed class HResultException : Exception { public HResultException(int resultCode, string? description = null) : base(description) diff --git a/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HyperVExtension.HostGuestCommunication.csproj b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HyperVExtension.HostGuestCommunication.csproj index c557df9525..d59ecbc4c1 100644 --- a/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HyperVExtension.HostGuestCommunication.csproj +++ b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HyperVExtension.HostGuestCommunication.csproj @@ -3,7 +3,7 @@ HyperVExtension.HostGuestCommunication x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 enable enable diff --git a/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs index 8ec2c1ee14..6d8401907c 100644 --- a/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs +++ b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs @@ -110,7 +110,7 @@ public static Dictionary MergeMessageParts(Dictionary"; var responseData = message?.ResponseData ?? ""; - _log.Error($"Error processing message. Message ID: {messageId}. Request data: {responseData}", ex); + _log.Error(ex, $"Error processing message. Message ID: {messageId}. Request data: {responseData}"); return new ErrorResponse(message!); } } diff --git a/HyperVExtension/src/HyperVExtension/Constants.cs b/HyperVExtension/src/HyperVExtension/Constants.cs index 8a9971b66a..378dbe17c3 100644 --- a/HyperVExtension/src/HyperVExtension/Constants.cs +++ b/HyperVExtension/src/HyperVExtension/Constants.cs @@ -22,4 +22,6 @@ internal sealed class Constants #else public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome.Dev/Files/HyperVExtension/Assets/hyper-v-provider-icon.png"; #endif + + public const string ExtensionIconInternal = "ms-appx:///HyperVExtension/Assets/hyper-v-provider-icon.png"; } diff --git a/HyperVExtension/src/HyperVExtension/Exceptions/AdaptiveCardInvalidActionException.cs b/HyperVExtension/src/HyperVExtension/Exceptions/AdaptiveCardInvalidActionException.cs new file mode 100644 index 0000000000..38c73aeddc --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Exceptions/AdaptiveCardInvalidActionException.cs @@ -0,0 +1,18 @@ +// 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 HyperVExtension.Exceptions; + +public class AdaptiveCardInvalidActionException : Exception +{ + public AdaptiveCardInvalidActionException(string message) + : base(message) + { + } +} diff --git a/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs b/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs index a1af58afa9..c44703db17 100644 --- a/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs +++ b/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using HyperVExtension.Models; namespace HyperVExtension.Extensions; @@ -19,10 +20,11 @@ public static class StreamExtensions /// The object that progress will be reported to /// The size of the buffer which is used to read data from the source stream and write it to the destination stream /// A cancellation token that will allow the caller to cancel the operation - public static async Task CopyToAsync(this Stream source, Stream destination, IProgress progressProvider, int bufferSize, CancellationToken cancellationToken) + public static async Task CopyToAsync(this Stream source, Stream destination, IProgress progressProvider, int bufferSize, long totalBytesToExtract, CancellationToken cancellationToken) { var buffer = new byte[bufferSize]; long totalRead = 0; + var lastPercentage = 0U; while (true) { @@ -37,8 +39,15 @@ public static async Task CopyToAsync(this Stream source, Stream destination, IPr await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); totalRead += bytesRead; - // Report the progress of the operation. - progressProvider.Report(totalRead); + var progressPercentage = (uint)(totalRead / (double)totalBytesToExtract * 100D); + + // Only update progress when a whole percentage has been completed. + if (progressPercentage != lastPercentage) + { + // Report the progress of the operation. + progressProvider.Report(new ByteTransferProgress(totalRead, totalBytesToExtract)); + lastPercentage = progressPercentage; + } } } } diff --git a/HyperVExtension/src/HyperVExtension/Helpers/AdaptiveCardActionPayload.cs b/HyperVExtension/src/HyperVExtension/Helpers/AdaptiveCardActionPayload.cs index 3b3d4f1b3c..d4bc237b70 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/AdaptiveCardActionPayload.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/AdaptiveCardActionPayload.cs @@ -30,6 +30,11 @@ public string? Type get; set; } + public string? Mode + { + get; set; + } + public bool IsCancelAction() { return Id == "cancelAction"; diff --git a/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs b/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs index cb6ccaeaf6..b5796b7cc1 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs @@ -3,6 +3,7 @@ using System.Management.Automation; using System.Security; +using System.Text; using HyperVExtension.Exceptions; using HyperVExtension.Services; @@ -28,6 +29,7 @@ private enum ProcessorArchitecture : ushort private readonly IPowerShellService _powerShellService; private readonly string _vmId; + private readonly Lazy _script = new(() => LoadScript()); public DevSetupAgentDeploymentHelper(IPowerShellService powerShellService, string vmId) { @@ -43,7 +45,7 @@ public void DeployDevSetupAgent(string userName, SecureString password) var sourcePath = GetSourcePath(architecture); var deployDevSetupAgentStatement = new StatementBuilder() - .AddScript(_script, false) + .AddScript(_script.Value, false) .AddCommand("Install-DevSetupAgent") .AddParameter("VMId", _vmId) .AddParameter("Session", session) @@ -124,203 +126,9 @@ private ushort GetVMArchitechture(PSObject session) return (ushort)psObject.BaseObject; } - private readonly string _script = @" -function Install-DevSetupAgent -{ - Param( - [Parameter(Mandatory = $true)] - [Guid] $VMId, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.Runspaces.PSSession] $Session, - - [Parameter(Mandatory = $true)] - [string] $Path - ) - - $ErrorActionPreference = ""Stop"" - $activity = ""Installing DevSetupAgent to VM $VMId"" - - # Validate input. Only .cab and .zip files are supported - # If $Path is a directory, it will be copied to the VM and installed as is - $isDirectory = $false - $isCab = $false - $inputFileName = $null - if (Test-Path -Path $Path -PathType 'Container') - { - $isDirectory = $true - } - elseif (Test-Path -Path $Path -PathType 'Leaf') - { - if ($Path -match '\.(cab)$') - { - $isCab = $true - } - elseif (-not $Path -match '\.(zip)$') - { - throw ""Only .cab and .zip files are supported"" - } - $inputFileName = Split-Path -Path $Path -Leaf - } - else - { - throw ""$Path does not exist"" - } - - - $DevSetupAgentConst = ""DevSetupAgent"" - $DevSetupEngineConst = ""DevSetupEngine"" - $session = $Session - - $guestTempDirectory = Invoke-Command -Session $session -ScriptBlock { $env:temp } - - [string] $guid = [System.Guid]::NewGuid() - $guestUnpackDirectory = Join-Path -Path $guestTempDirectory -ChildPath $guid - $guestDevSetupAgentTempDirectory = Join-Path -Path $guestUnpackDirectory -ChildPath $DevSetupAgentConst - - Write-Host ""Creating VM temporary folder $guestUnpackDirectory"" - Write-Progress -Activity $activity -Status ""Creating VM temporary folder $guestUnpackDirectory"" -PercentComplete 10 - Invoke-Command -Session $session -ScriptBlock { New-Item -Path ""$using:guestUnpackDirectory"" -ItemType ""directory"" } - - if ($isDirectory) - { - $destinationPath = $guestDevSetupAgentTempDirectory - } - else - { - $destinationPath = $guestUnpackDirectory - } - - Write-Host ""Copying $Path to VM $destinationPath"" - Write-Progress -Activity $activity -Status ""Copying DevSetupAgent to VM $destinationPath"" -PercentComplete 15 - Copy-Item -ToSession $session -Recurse -Path $Path -Destination $destinationPath - - - Invoke-Command -Session $session -ScriptBlock { - $ErrorActionPreference = ""Stop"" - - try - { - $guestDevSetupAgentPath = Join-Path -Path $Env:Programfiles -ChildPath $using:DevSetupAgentConst - - # Stop and remove previous version of DevSetupAgent service if it exists - $service = Get-Service -Name $using:DevSetupAgentConst -ErrorAction SilentlyContinue - if ($service) - { - $serviceWMI = Get-WmiObject -Class Win32_Service -Filter ""Name='$using:DevSetupAgentConst'"" - $existingServicePath = $serviceWMI.Properties[""PathName""].Value - if ($existingServicePath) - { - $guestDevSetupAgentPath = Split-Path $existingServicePath -Parent - } - - try - { - Write-Host ""Stopping DevSetupAgent service"" - Write-Progress -Activity $using:activity -Status ""Stopping DevSetupAgent service $destinationPath"" -PercentComplete 30 - $service.Stop() - } - catch - { - Write-Host ""Ignoring error: $PSItem"" - } - - Remove-Variable -Name service -ErrorAction SilentlyContinue - - # Remove-Service is only available in PowerShell 6.0 and later. Windows doesn't come with it preinstalled. - Write-Host ""Removing DevSetupAgent service"" - Write-Progress -Activity $using:activity -Status ""Removing DevSetupAgent service"" -PercentComplete 35 - $serviceWMI = Get-WmiObject -Class Win32_Service -Filter ""Name='$using:DevSetupAgentConst'"" - $serviceWMI.Delete() - Remove-Variable -Name serviceWMI -ErrorAction SilentlyContinue - } - - # Stop previous version of DevSetupEngine COM server if it exists - $devSetupEngineProcess = Get-Process -Name ""$using:DevSetupEngineConst"" -ErrorAction SilentlyContinue - if ($devSetupEngineProcess -ne $null) - { - Write-Host ""Stopping $using:DevSetupEngineConst process"" - Write-Progress -Activity $using:activity -Status ""Stopping $using:DevSetupEngineConst process"" -PercentComplete 40 - Stop-Process -Force -Name ""$using:DevSetupEngineConst"" - } - - # Unregister DevSetupEngine - $enginePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath ""$using:DevSetupEngineConst.exe"" - if (Test-Path -Path $enginePath) - { - Write-Host ""Unregistering DevSetupEngine ($enginePath)"" - Write-Progress -Activity $using:activity -Status ""Registering DevSetupEngine ($enginePath)"" -PercentComplete 88 - &$enginePath ""-UnregisterComServer"" - } - - # Remove previous version of DevSetupAgent service files - if (Test-Path -Path $guestDevSetupAgentPath) - { - # Sleep a few seconds to make sure all handles released after shutting down previous DevSetupEngine - Start-Sleep -Seconds 7 - Write-Host ""Deleting old DevSetupAgent service files"" - Write-Progress -Activity $using:activity -Status ""Deleting old DevSetupAgent service files"" -PercentComplete 45 - Remove-Item -Recurse -Force -Path $guestDevSetupAgentPath - } - - if ($using:isDirectory) - { - Write-Host ""Copying DevSetupAgent to $guestDevSetupAgentPath"" - Write-Progress -Activity $using:activity -Status ""Deleting old DevSetupAgent service files"" -PercentComplete 50 - Copy-Item -Recurse -Path $using:guestDevSetupAgentTempDirectory -Destination $guestDevSetupAgentPath - } - elseif ($using:isCab) - { - $cabPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName - Write-Host ""Unpacking $cabPath to $guestDevSetupAgentPath"" - Write-Progress -Activity $using:activity -Status ""Unpacking $cabPath to $guestDevSetupAgentPath"" -PercentComplete 60 - $expandOutput=&""$Env:SystemRoot\System32\expand.exe"" $cabPath /F:* $Env:Programfiles - if ($LastExitCode -ne 0) - { - throw ""Error unpacking $cabPath`:`n$LastExitCode`n$($expandOutput|Out-String)"" - } - } - else - { - $zipPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName - Write-Host ""Unpacking $using:inputFileName to $guestDevSetupAgentPath"" - Write-Progress -Activity $using:activity -Status ""Unpacking $using:inputFileName to $guestDevSetupAgentPath"" -PercentComplete 60 - Expand-Archive -Path $zipPath -Destination $guestDevSetupAgentPath - } - - # Register DevSetupAgent service - $servicePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath ""$using:DevSetupAgentConst.exe"" - Write-Host ""Registering DevSetupAgent service ($servicePath)"" - Write-Progress -Activity $using:activity -Status ""Registering DevSetupAgent service ($servicePath)"" -PercentComplete 85 - New-Service -Name $using:DevSetupAgentConst -BinaryPathName $servicePath -StartupType Automatic - - # Register DevSetupEngine - Write-Host ""Registering DevSetupEngine ($enginePath)"" - Write-Progress -Activity $using:activity -Status ""Registering DevSetupEngine ($enginePath)"" -PercentComplete 88 - - # Executing non-console apps using '&' does not set $LastExitCode. Using Start-Process here to get the returned error code. - $process = Start-Process -NoNewWindow -Wait $enginePath -ArgumentList ""-RegisterComServer"" -PassThru - if ($process.ExitCode -ne 0) - { - throw ""Error registering $enginePath`: $process.ExitCode"" - } - - Write-Host ""Starting DevSetupAgent service"" - Write-Progress -Activity $using:activity -Status ""Starting DevSetupAgent service"" -PercentComplete 92 - Start-Service $using:DevSetupAgentConst - } - catch - { - Write-Host ""Error on guest OS: $PSItem"" - } - finally - { - Write-Host ""Removing temporary directory $using:guestUnpackDirectory"" - Remove-Item -Recurse -Force -Path $using:guestUnpackDirectory -ErrorAction SilentlyContinue - } - } - - Remove-PSSession $session -} -"; + private static string LoadScript() + { + var path = Path.Combine(AppContext.BaseDirectory, "HyperVExtension", "Scripts", "DevSetupAgent.ps1"); + return File.ReadAllText(path, Encoding.Default) ?? throw new FileNotFoundException(path); + } } diff --git a/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs b/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs index 3c88fd8388..e3a1713603 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs @@ -71,7 +71,7 @@ public PsObjectHelper(in PSObject pSObject) catch (Exception ex) { var log = Log.ForContext("SourceContext", nameof(PsObjectHelper)); - log.Error($"Failed to get property value with name {propertyName} from object with type {type}.", ex); + log.Error(ex, $"Failed to get property value with name {propertyName} from object with type {type}."); } return default(T); diff --git a/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs b/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs index 570a98ecff..eeb03be0e0 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs @@ -23,7 +23,7 @@ public static string GetResource(string identifier, ILogger? log = null) } catch (Exception ex) { - log?.Error($"Failed loading resource: {identifier}", 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/HyperVExtension/src/HyperVExtension/HyperVExtension.cs b/HyperVExtension/src/HyperVExtension/HyperVExtension.cs index 3bc6f6a097..e71c411242 100644 --- a/HyperVExtension/src/HyperVExtension/HyperVExtension.cs +++ b/HyperVExtension/src/HyperVExtension/HyperVExtension.cs @@ -57,7 +57,7 @@ public HyperVExtension(IHost host) } catch (Exception ex) { - log.Error($"Failed to get provider for provider type {providerType}", ex); + log.Error(ex, $"Failed to get provider for provider type {providerType}"); } return provider; diff --git a/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj b/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj index 446faa6e5c..4e3f564cf0 100644 --- a/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj +++ b/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj @@ -5,13 +5,17 @@ enable enable Dev - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 + + + Always + Always @@ -19,7 +23,14 @@ Always - + + + Always + + + Always + + diff --git a/HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs b/HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs new file mode 100644 index 0000000000..1cb1cf2b51 --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs @@ -0,0 +1,28 @@ +// 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 HyperVExtension.Models; + +/// +/// Represents progress of an operation that require transferring bytes from one place to another. +/// +public class ByteTransferProgress +{ + public long BytesReceived { get; set; } + + public long TotalBytesToReceive { get; set; } + + public uint PercentageComplete => (uint)((BytesReceived / (double)TotalBytesToReceive) * 100); + + public ByteTransferProgress(long bytesReceived, long totalBytesToReceive) + { + BytesReceived = bytesReceived; + TotalBytesToReceive = totalBytesToReceive; + } +} diff --git a/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs b/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs index eccfa1458d..0e190d4bca 100644 --- a/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs +++ b/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs @@ -192,7 +192,7 @@ private ComputeSystemOperationResult Start(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Start), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Start)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } } @@ -253,7 +253,7 @@ public IAsyncOperation TerminateAsync(string optio catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Terminate), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Terminate)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -278,7 +278,7 @@ public IAsyncOperation DeleteAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Delete), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Delete)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -309,7 +309,7 @@ public IAsyncOperation SaveAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Save), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Save)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -340,7 +340,7 @@ public IAsyncOperation PauseAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Pause), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Pause)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -371,7 +371,7 @@ public IAsyncOperation ResumeAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Resume), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Resume)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -393,7 +393,7 @@ public IAsyncOperation CreateSnapshotAsync(string } catch (Exception ex) { - _log.Error(OperationErrorString(ComputeSystemOperations.CreateSnapshot), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.CreateSnapshot)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -416,7 +416,7 @@ public IAsyncOperation RevertSnapshotAsync(string } catch (Exception ex) { - _log.Error(OperationErrorString(ComputeSystemOperations.RevertSnapshot), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.RevertSnapshot)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -439,7 +439,7 @@ public IAsyncOperation DeleteSnapshotAsync(string } catch (Exception ex) { - _log.Error(OperationErrorString(ComputeSystemOperations.DeleteSnapshot), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.DeleteSnapshot)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -457,7 +457,7 @@ public IAsyncOperation ConnectAsync(string options } catch (Exception ex) { - _log.Error($"Failed to launch vmconnect on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to launch vmconnect on {DateTime.Now}: VM details: {this}"); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -487,7 +487,7 @@ public IAsyncOperation RestartAsync(string options catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Restart), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Restart)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -536,7 +536,7 @@ public IAsyncOperation> GetComputeSystemPrope } catch (Exception ex) { - _log.Error($"Failed to GetComputeSystemPropertiesAsync on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to GetComputeSystemPropertiesAsync on {DateTime.Now}: VM details: {this}"); return new List(); } }).AsAsyncOperation(); @@ -708,7 +708,7 @@ public SDK.ApplyConfigurationResult ApplyConfiguration(ApplyConfigurationOperati } catch (Exception ex) { - _log.Error($"Failed to apply configuration on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to apply configuration on {DateTime.Now}: VM details: {this}"); return operation.CompleteOperation(new HostGuestCommunication.ApplyConfigurationResult(ex.HResult, ex.Message)); } } @@ -721,7 +721,7 @@ public SDK.ApplyConfigurationResult ApplyConfiguration(ApplyConfigurationOperati } catch (Exception ex) { - _log.Error($"Failed to apply configuration on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to apply configuration on {DateTime.Now}: VM details: {this}"); return new ApplyConfigurationOperation(this, ex); } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VMGalleryCreationAdaptiveCardSession.cs b/HyperVExtension/src/HyperVExtension/Models/VMGalleryCreationAdaptiveCardSession.cs new file mode 100644 index 0000000000..7136b91b3f --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Models/VMGalleryCreationAdaptiveCardSession.cs @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.InteropServices.WindowsRuntime; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using HyperVExtension.Common; +using HyperVExtension.Exceptions; +using HyperVExtension.Helpers; +using HyperVExtension.Models.VirtualMachineCreation; +using HyperVExtension.Models.VMGalleryJsonToClasses; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Foundation; +using Windows.Storage; +using Windows.Storage.Streams; + +namespace HyperVExtension.Models; + +public enum SessionState +{ + InitialCreationForm, + ReviewForm, +} + +public class VMGalleryCreationAdaptiveCardSession : IExtensionAdaptiveCardSession2 +{ + private readonly Serilog.ILogger _log = Log.ForContext("SourceContext", nameof(HyperVVirtualMachine)); + + private readonly string _pathToInitialCreationFormTemplate = Path.Combine(AppContext.BaseDirectory, @"HyperVExtension\Templates\", "InitialVMGalleryCreationForm.json"); + + private readonly string _pathToReviewFormTemplate = Path.Combine(AppContext.BaseDirectory, @"HyperVExtension\Templates\", "ReviewFormForVMGallery.json"); + + private readonly string _adaptiveCardNextButtonId = "DevHomeMachineConfigurationNextButton"; + + /// + /// The gallery images that will be displayed in the initial creation form. We retrieve these from the VMGallery.json file in the Microsoft servers. + /// + private readonly VMGalleryImageList _vMGalleryImageList; + + private readonly IStringResource _stringResource; + + /// + /// Gets the Json string that represents the user input that was passed to the adaptive card session. We'll keep this so we can pass it back to Dev Home + /// at the end of the session. + /// + public string OriginalUserInputJson { get; private set; } = string.Empty; + + public event TypedEventHandler? Stopped; + + public void Dispose() + { + } + + public VMGalleryCreationAdaptiveCardSession(VMGalleryImageList galleryImages, IStringResource stringResource) + { + _vMGalleryImageList = galleryImages; + _stringResource = stringResource; + } + + private IExtensionAdaptiveCard? _creationAdaptiveCard; + + public bool ShouldEndSession { get; private set; } + + public ProviderOperationResult Initialize(IExtensionAdaptiveCard extensionUI) + { + _creationAdaptiveCard = extensionUI; + + return GetInitialCreationFormAdaptiveCard(); + } + + public IAsyncOperation OnAction(string action, string inputs) + { + return Task.Run(async () => + { + ProviderOperationResult operationResult; + var shouldEndSession = false; + var adaptiveCardStateNotRecognizedError = _stringResource.GetLocalized("AdaptiveCardStateNotRecognizedError"); + + var actionPayload = Helpers.Json.ToObject(action); + if (actionPayload == null) + { + _log.Error($"Actions in Adaptive card action Json not recognized: {action}"); + var creationFormGenerationError = _stringResource.GetLocalized("AdaptiveCardUnRecognizedAction"); + var exception = new AdaptiveCardInvalidActionException(creationFormGenerationError); + return new ProviderOperationResult(ProviderOperationStatus.Failure, exception, creationFormGenerationError, creationFormGenerationError); + } + + switch (_creationAdaptiveCard?.State) + { + case "initialCreationForm": + operationResult = await HandleActionWhenFormInInitialState(actionPayload, inputs); + break; + case "reviewForm": + (operationResult, shouldEndSession) = await HandleActionWhenFormInReviewState(actionPayload); + break; + default: + shouldEndSession = true; + operationResult = new ProviderOperationResult( + ProviderOperationStatus.Failure, + new InvalidOperationException(nameof(action)), + adaptiveCardStateNotRecognizedError, + $"Unexpected state:{_creationAdaptiveCard?.State}"); + 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, OriginalUserInputJson)); + } + + return operationResult; + }).AsAsyncOperation(); + } + + /// + /// Loads the adaptive card template based on the session state. + /// + /// State the adaptive card session + /// A Json string representing the adaptive card + public string LoadTemplate(SessionState state) + { + var pathToTemplate = state switch + { + SessionState.InitialCreationForm => _pathToInitialCreationFormTemplate, + SessionState.ReviewForm => _pathToReviewFormTemplate, + _ => _pathToInitialCreationFormTemplate, + }; + + return File.ReadAllText(pathToTemplate, Encoding.Default); + } + + /// + /// Creates the initial form that will be displayed to the user. It will be a list of Windows community toolkit's settings + /// cards that can be displayed in the Dev Homes UI. + /// + /// Result of the operation + private ProviderOperationResult GetInitialCreationFormAdaptiveCard() + { + try + { + // Create the JSON array for the gallery images and add the data for each image. + // these will be display in the initial creation form. + var jsonArrayOfGalleryImages = new JsonArray(); + var primaryButtonForCreationFlowText = _stringResource.GetLocalized("PrimaryButtonLabelForCreationFlow"); + var secondaryButtonForCreationFlowText = _stringResource.GetLocalized("SecondaryButtonLabelForCreationFlow"); + var secondaryButtonForContentDialogText = _stringResource.GetLocalized("SecondaryButtonForContentDialogText"); + var buttonToLaunchContentDialogLabel = _stringResource.GetLocalized("ButtonToLaunchContentDialogLabel"); + var settingsCardLabel = _stringResource.GetLocalized("SettingsCardLabel"); + var enterNewVMNameLabel = _stringResource.GetLocalized("EnterNewVMNameLabel"); + var enterNewVMNamePlaceHolder = _stringResource.GetLocalized("EnterNewVMNamePlaceHolder"); + + foreach (var image in _vMGalleryImageList.Images) + { + var dataJson = new JsonObject + { + { "ImageDescription", GetMergedDescription(image) }, + { "SubDescription", image.Publisher }, + { "Header", image.Name }, + { "HeaderIcon", image.Symbol.Base64Image }, + { "ActionButtonText", "More info" }, + { "ContentDialogInfo", SetupContentDialogInfo(image) }, + { "ButtonToLaunchContentDialogLabel", buttonToLaunchContentDialogLabel }, + { "SecondaryButtonForContentDialogText", secondaryButtonForContentDialogText }, + }; + + jsonArrayOfGalleryImages.Add(dataJson); + } + + var templateData = + $"{{\"PrimaryButtonLabelForCreationFlow\" : \"{primaryButtonForCreationFlowText}\"," + + $"\"SecondaryButtonLabelForCreationFlow\" : \"{secondaryButtonForCreationFlowText}\"," + + $"\"SettingsCardLabel\": \"{settingsCardLabel}\"," + + $"\"EnterNewVMNameLabel\": \"{enterNewVMNameLabel}\"," + + $"\"EnterNewVMNamePlaceHolder\": \"{enterNewVMNamePlaceHolder}\"," + + $"\"GalleryImages\" : {jsonArrayOfGalleryImages.ToJsonString()}" + + $"}}"; + + var template = LoadTemplate(SessionState.InitialCreationForm); + + return _creationAdaptiveCard!.Update(template, templateData, "initialCreationForm"); + } + catch (Exception ex) + { + var creationFormGenerationError = _stringResource.GetLocalized("InitialCreationFormGenerationFailedError"); + return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, creationFormGenerationError, 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 async Task GetForReviewFormAdaptiveCardAsync(string inputJson) + { + try + { + var deserializedObject = JsonSerializer.Deserialize(inputJson, typeof(VMGalleryCreationUserInput)); + var inputForGalleryOperation = deserializedObject as VMGalleryCreationUserInput ?? throw new InvalidOperationException($"Json deserialization failed for input Json: {inputJson}"); + + if (inputForGalleryOperation.SelectedImageListIndex < 0 || + inputForGalleryOperation.SelectedImageListIndex > _vMGalleryImageList.Images.Count) + { + return new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Failed to get review form", "Selected image index is out of range"); + } + + var galleryImage = _vMGalleryImageList.Images[inputForGalleryOperation.SelectedImageListIndex]; + var newEnvironmentNameLabel = _stringResource.GetLocalized("NameLabelForNewVirtualMachine", ":"); + var primaryButtonForCreationFlowText = _stringResource.GetLocalized("PrimaryButtonLabelForCreationFlow"); + var secondaryButtonForCreationFlowText = _stringResource.GetLocalized("SecondaryButtonLabelForCreationFlow"); + var storageFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(Constants.ExtensionIconInternal)); + 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); + var providerBase64Image = Convert.ToBase64String(bytes); + var reviewFormData = new JsonObject + { + { "ProviderName", HyperVStrings.HyperVProviderDisplayName }, + { "DiskImageSize", BytesHelper.ConvertBytesToString(galleryImage.Disk.SizeInBytes) }, + { "VMGalleryImageName", galleryImage.Name }, + { "Publisher", galleryImage.Publisher }, + { "NameOfNewVM", inputForGalleryOperation.NewEnvironmentName }, + { "NameLabel", newEnvironmentNameLabel }, + { "Base64ImageForProvider", providerBase64Image }, + { "DiskImageUrl", galleryImage.Symbol.Uri }, + { "PrimaryButtonLabelForCreationFlow", primaryButtonForCreationFlowText }, + { "SecondaryButtonLabelForCreationFlow", secondaryButtonForCreationFlowText }, + }; + + var template = LoadTemplate(SessionState.ReviewForm); + + return _creationAdaptiveCard!.Update(LoadTemplate(SessionState.ReviewForm), reviewFormData.ToJsonString(), "reviewForm"); + } + catch (Exception ex) + { + var reviewFormGenerationError = _stringResource.GetLocalized("ReviewFormGenerationFailedError"); + return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, reviewFormGenerationError, ex.Message); + } + } + + /// + /// The description for VM gallery images is stored in a list of strings. This method merges the strings into one string. + /// + /// The c# class that represents the gallery image + /// A string that combines the original list of strings into one + public string GetMergedDescription(VMGalleryImage image) + { + var description = string.Empty; + for (var i = 0; i < image.Description.Count; i++) + { + description += image.Description[i].Replace("\n", string.Empty).Replace("\r", string.Empty); + } + + return string.Join(string.Empty, description); + } + + /// + /// In Dev Homes UI, the user can click a more info button to get more information about the VM gallery image. + /// This method sets up the content dialog's body data that will be displayed when the user clicks the more info button. + /// + /// The c# class that represents the gallery image + /// A Json object that contains the data needed to display an adaptive card within a content dialogs body + private JsonObject SetupContentDialogInfo(VMGalleryImage image) + { + var adaptiveCardImageFacts = new JsonArray(); + foreach (var fact in image.Details) + { + var adaptiveCardfactObj = new JsonObject + { + { "title", fact.Name }, + { "value", fact.Value }, + }; + adaptiveCardImageFacts.Add(adaptiveCardfactObj); + } + + var osVersionForContentDialog = _stringResource.GetLocalized("OsVersionForContentDialog"); + var localeForContentDialog = _stringResource.GetLocalized("LocaleForContentDialog"); + var lastUpdatedForContentDialog = _stringResource.GetLocalized("LastUpdatedForContentDialog"); + var downloadForContentDialog = _stringResource.GetLocalized("DownloadForContentDialog"); + + adaptiveCardImageFacts.Add(new JsonObject() { { "title", osVersionForContentDialog }, { "value", image.Version } }); + adaptiveCardImageFacts.Add(new JsonObject() { { "title", localeForContentDialog }, { "value", image.Locale } }); + adaptiveCardImageFacts.Add(new JsonObject() { { "title", lastUpdatedForContentDialog }, { "value", image.LastUpdated.ToLongDateString() } }); + adaptiveCardImageFacts.Add(new JsonObject() { { "title", downloadForContentDialog }, { "value", BytesHelper.ConvertBytesToString(image.Disk.SizeInBytes) } }); + + return new JsonObject + { + { "GalleryImageFacts", adaptiveCardImageFacts }, + { "ImageDescription", GetMergedDescription(image) }, + }; + } + + private async Task 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. + OriginalUserInputJson = inputs; + operationResult = await GetForReviewFormAdaptiveCardAsync(inputs); + } + else + { + operationResult = GetInitialCreationFormAdaptiveCard(); + } + + return operationResult; + } + + private async Task<(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 = await GetForReviewFormAdaptiveCardAsync(OriginalUserInputJson); + } + else + { + operationResult = GetInitialCreationFormAdaptiveCard(); + } + + return (operationResult, shouldEndSession); + } +} diff --git a/HyperVExtension/src/HyperVExtension/Models/VMGalleryJsonToClasses/VMGalleryDisk.cs b/HyperVExtension/src/HyperVExtension/Models/VMGalleryJsonToClasses/VMGalleryDisk.cs index f8078cfcfb..9bf8594afd 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VMGalleryJsonToClasses/VMGalleryDisk.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VMGalleryJsonToClasses/VMGalleryDisk.cs @@ -10,5 +10,5 @@ public sealed class VMGalleryDisk : VMGalleryItemWithHashBase { public string ArchiveRelativePath { get; set; } = string.Empty; - public long SizeInBytes { get; set; } + public ulong SizeInBytes { get; set; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs index c5c5657e11..e606dacf64 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; + namespace HyperVExtension.Models.VirtualMachineCreation; /// @@ -10,15 +12,12 @@ public sealed class ArchiveExtractionReport : IOperationReport { public ReportKind ReportKind => ReportKind.ArchiveExtraction; - public string LocalizationKey => "ExtractingFile"; - - public ulong BytesReceived { get; private set; } + public string LocalizationKey => "ExtractionInProgress"; - public ulong TotalBytesToReceive { get; private set; } + public ByteTransferProgress ProgressObject { get; private set; } - public ArchiveExtractionReport(ulong bytesReceived, ulong totalBytesToReceive) + public ArchiveExtractionReport(ByteTransferProgress progressObj) { - BytesReceived = bytesReceived; - TotalBytesToReceive = totalBytesToReceive; + ProgressObject = progressObj; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs index 8c76087c49..073a2822ed 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs @@ -28,13 +28,13 @@ public async Task ExtractArchiveAsync(IProgress progressProvid using var outputFileStream = File.OpenWrite(destinationAbsoluteFilePath); using var zipArchiveEntryStream = zipArchiveEntry.Open(); - var fileExtractionProgress = new Progress(bytesCopied => + var fileExtractionProgress = new Progress(progressObj => { - progressProvider.Report(new ArchiveExtractionReport((ulong)bytesCopied, (ulong)totalBytesToExtract)); + progressProvider.Report(new ArchiveExtractionReport(progressObj)); }); outputFileStream.SetLength(totalBytesToExtract); - await zipArchiveEntryStream.CopyToAsync(outputFileStream, fileExtractionProgress, _transferBufferSize, cancellationToken); + await zipArchiveEntryStream.CopyToAsync(outputFileStream, fileExtractionProgress, _transferBufferSize, totalBytesToExtract, cancellationToken); File.SetLastWriteTime(destinationAbsoluteFilePath, zipArchiveEntry.LastWriteTime.DateTime); } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs index abec4f6301..53fe906262 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs @@ -9,13 +9,10 @@ public class DownloadOperationReport : IOperationReport public string LocalizationKey => "DownloadInProgress"; - public ulong BytesReceived { get; private set; } + public ByteTransferProgress ProgressObject { get; private set; } - public ulong TotalBytesToReceive { get; private set; } - - public DownloadOperationReport(ulong bytesReceived, ulong totalBytesToReceive) + public DownloadOperationReport(ByteTransferProgress progressObj) { - BytesReceived = bytesReceived; - TotalBytesToReceive = totalBytesToReceive; + ProgressObject = progressObj; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs index bf6bd3eb17..e418d3266a 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs @@ -15,7 +15,5 @@ public interface IOperationReport public string LocalizationKey { get; } - public ulong BytesReceived { get; } - - public ulong TotalBytesToReceive { get; } + public ByteTransferProgress ProgressObject { get; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs index 51d52cba28..73ecb39d1e 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.Json.Serialization; + namespace HyperVExtension.Models.VirtualMachineCreation; /// @@ -8,7 +10,8 @@ namespace HyperVExtension.Models.VirtualMachineCreation; /// public sealed class VMGalleryCreationUserInput { - public string NewVirtualMachineName { get; set; } = string.Empty; + public string NewEnvironmentName { get; set; } = string.Empty; + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public int SelectedImageListIndex { get; set; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs index 64463af6dc..8bf3fcb3bc 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs @@ -79,14 +79,7 @@ public VMGalleryVMCreationOperation( /// The archive extraction operation returned by the progress handler which extracts the archive file public void Report(IOperationReport value) { - var displayText = Image.Name; - - if (value.ReportKind == ReportKind.ArchiveExtraction) - { - displayText = $"{ArchivedFile!.Name} ({Image.Name})"; - } - - UpdateProgress(value, value.LocalizationKey, displayText); + UpdateProgress(value, value.LocalizationKey, $"({Image.Name})"); } /// @@ -114,6 +107,7 @@ public void Report(IOperationReport value) IsOperationInProgress = true; } + UpdateProgress(_stringResource.GetLocalized("CreationStarting", $"({_userInputParameters.NewEnvironmentName})")); var imageList = await _vmGalleryService.GetGalleryImagesAsync(); if (imageList.Images.Count == 0) { @@ -130,12 +124,12 @@ public void Report(IOperationReport value) var archiveProvider = _archiveProviderFactory.CreateArchiveProvider(ArchivedFile!.FileType); await archiveProvider.ExtractArchiveAsync(this, ArchivedFile!, absoluteFilePathForVhd, CancellationTokenSource.Token); - var virtualMachineName = MakeFileNameValid(_userInputParameters.NewVirtualMachineName); + var virtualMachineName = MakeFileNameValid(_userInputParameters.NewEnvironmentName); // Use the Hyper-V manager to create the VM. UpdateProgress(_stringResource.GetLocalized("CreationInProgress", virtualMachineName)); var creationParameters = new VirtualMachineCreationParameters( - _userInputParameters.NewVirtualMachineName, + _userInputParameters.NewEnvironmentName, GetVirtualMachineProcessorCount(), absoluteFilePathForVhd, Image.Config.SecureBoot, @@ -145,7 +139,7 @@ public void Report(IOperationReport value) } catch (Exception ex) { - _log.Error("Operation to create compute system failed", ex); + _log.Error(ex, "Operation to create compute system failed"); ComputeSystemResult = new CreateComputeSystemResult(ex, ex.Message, ex.Message); } @@ -158,16 +152,29 @@ public void Report(IOperationReport value) private void UpdateProgress(IOperationReport report, string localizedKey, string fileName) { - var bytesReceivedSoFar = BytesHelper.ConvertBytesToString(report.BytesReceived); - var totalBytesToReceive = BytesHelper.ConvertBytesToString(report.TotalBytesToReceive); - var progressPercentage = (uint)((report.BytesReceived / (double)report.TotalBytesToReceive) * 100D); - var displayString = _stringResource.GetLocalized(localizedKey, fileName, $"{bytesReceivedSoFar}/{totalBytesToReceive}"); - Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(displayString, progressPercentage)); + var bytesReceivedSoFar = BytesHelper.ConvertBytesToString((ulong)report.ProgressObject.BytesReceived); + var totalBytesToReceive = BytesHelper.ConvertBytesToString((ulong)report.ProgressObject.TotalBytesToReceive); + var displayString = _stringResource.GetLocalized(localizedKey, fileName, $"{bytesReceivedSoFar} / {totalBytesToReceive}"); + try + { + Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(displayString, report.ProgressObject.PercentageComplete)); + } + catch (Exception ex) + { + _log.Error(ex, "Failed to update progress"); + } } private void UpdateProgress(string localizedString, uint percentage = 0u) { - Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(localizedString, percentage)); + try + { + Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(localizedString, percentage)); + } + catch (Exception ex) + { + _log.Error(ex, "Failed to update progress"); + } } /// @@ -214,7 +221,7 @@ private async Task DeleteFileIfExists(StorageFile file) } catch (Exception ex) { - _log.Error($"Failed to delete file {file.Path}", ex); + _log.Error(ex, $"Failed to delete file {file.Path}"); } } @@ -227,7 +234,7 @@ private string MakeFileNameValid(string originalName) private string GetUniqueAbsoluteFilePath(string defaultVirtualDiskPath) { var extension = Path.GetExtension(Image.Disk.ArchiveRelativePath); - var expectedExtractedFileLocation = Path.Combine(defaultVirtualDiskPath, $"{_userInputParameters.NewVirtualMachineName}{extension}"); + var expectedExtractedFileLocation = Path.Combine(defaultVirtualDiskPath, $"{_userInputParameters.NewEnvironmentName}{extension}"); var appendedNumber = 1u; var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(expectedExtractedFileLocation); diff --git a/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs b/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs index 009dc78c95..b7be3a7ef1 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs @@ -163,7 +163,7 @@ public IAsyncOperation OnAction(string action, string i } catch (Exception ex) { - _log.Error($"Exception in OnAction: {ex}"); + _log.Error(ex, $"Exception in OnAction: {ex}"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Something went wrong", ex.Message); } diff --git a/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs b/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs index 677aa14d11..c5baabb0f5 100644 --- a/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs +++ b/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs @@ -144,7 +144,7 @@ public IAsyncOperation OnAction(string action, string i } catch (Exception ex) { - _log.Error($"Exception in OnAction: {ex}"); + _log.Error(ex, $"Exception in OnAction: {ex}"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Something went wrong", ex.Message); } diff --git a/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs b/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs index c625cd657e..dd00cd5376 100644 --- a/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs +++ b/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs @@ -4,6 +4,7 @@ using System.Text.Json; using HyperVExtension.Common; using HyperVExtension.Helpers; +using HyperVExtension.Models; using HyperVExtension.Models.VirtualMachineCreation; using HyperVExtension.Services; using Microsoft.Windows.DevHome.SDK; @@ -25,14 +26,21 @@ public class HyperVProvider : IComputeSystemProvider private readonly VmGalleryCreationOperationFactory _vmGalleryCreationOperationFactory; + private readonly IVMGalleryService _vmGalleryService; + // Temporary will need to add more error strings for different operations. public string OperationErrorString => _stringResource.GetLocalized(errorResourceKey); - public HyperVProvider(IHyperVManager hyperVManager, IStringResource stringResource, VmGalleryCreationOperationFactory vmGalleryCreationOperationFactory) + public HyperVProvider( + IHyperVManager hyperVManager, + IStringResource stringResource, + VmGalleryCreationOperationFactory vmGalleryCreationOperationFactory, + IVMGalleryService vmGalleryService) { _hyperVManager = hyperVManager; _stringResource = stringResource; _vmGalleryCreationOperationFactory = vmGalleryCreationOperationFactory; + _vmGalleryService = vmGalleryService; } /// Gets or sets the default compute system properties. @@ -67,7 +75,7 @@ public IAsyncOperation GetComputeSystemsAsync(IDeveloperId } catch (Exception ex) { - _log.Error($"Failed to retrieved all virtual machines on: {DateTime.Now}", ex); + _log.Error(ex, $"Failed to retrieved all virtual machines on: {DateTime.Now}"); return new ComputeSystemsResult(ex, OperationErrorString, ex.Message); } }).AsAsyncOperation(); @@ -75,9 +83,8 @@ public IAsyncOperation GetComputeSystemsAsync(IDeveloperId public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForDeveloperId(IDeveloperId developerId, ComputeSystemAdaptiveCardKind sessionKind) { - // This won't be supported until creation is supported. - var notImplementedException = new NotImplementedException($"Method not implemented by Hyper-V Compute System Provider"); - return new ComputeSystemAdaptiveCardResult(notImplementedException, OperationErrorString, notImplementedException.Message); + var imageList = _vmGalleryService.GetGalleryImagesAsync().GetAwaiter().GetResult(); + return new ComputeSystemAdaptiveCardResult(new VMGalleryCreationAdaptiveCardSession(imageList, _stringResource)); } public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForComputeSystem(IComputeSystem computeSystem, ComputeSystemAdaptiveCardKind sessionKind) @@ -98,7 +105,7 @@ public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForComputeSystem } catch (Exception ex) { - _log.Error($"Failed to create a new virtual machine on: {DateTime.Now}", ex); + _log.Error(ex, $"Failed to create a new virtual machine on: {DateTime.Now}"); // 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. diff --git a/HyperVExtension/src/HyperVExtension/Scripts/DevSetupAgent.ps1 b/HyperVExtension/src/HyperVExtension/Scripts/DevSetupAgent.ps1 new file mode 100644 index 0000000000..f81bce1a5d --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Scripts/DevSetupAgent.ps1 @@ -0,0 +1,208 @@ +<# +.SYNOPSIS + +Install DevSetupAgent Windows service and DevSetupEngine COM server on a VM + +.DESCRIPTION + +Install DevSetupAgent Windows service and DevSetupEngine COM server on a VM +through the provided PSSession. +#> + +function Install-DevSetupAgent +{ + Param( + [Parameter(Mandatory = $true)] + [Guid] $VMId, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.Runspaces.PSSession] $Session, + + [Parameter(Mandatory = $true)] + [string] $Path + ) + + $ErrorActionPreference = "Stop" + $activity = "Installing DevSetupAgent to VM $VMId" + + # Validate input. Only .cab and .zip files are supported + # If $Path is a directory, it will be copied to the VM and installed as is + $isDirectory = $false + $isCab = $false + $inputFileName = $null + if (Test-Path -Path $Path -PathType 'Container') + { + $isDirectory = $true + } + elseif (Test-Path -Path $Path -PathType 'Leaf') + { + if ($Path -match '\.(cab)$') + { + $isCab = $true + } + elseif (-not $Path -match '\.(zip)$') + { + throw "Only .cab and .zip files are supported" + } + $inputFileName = Split-Path -Path $Path -Leaf + } + else + { + throw "$Path does not exist" + } + + + $DevSetupAgentConst = "DevSetupAgent" + $DevSetupEngineConst = "DevSetupEngine" + $session = $Session + + $guestTempDirectory = Invoke-Command -Session $session -ScriptBlock { $env:temp } + + [string] $guid = [System.Guid]::NewGuid() + $guestUnpackDirectory = Join-Path -Path $guestTempDirectory -ChildPath $guid + $guestDevSetupAgentTempDirectory = Join-Path -Path $guestUnpackDirectory -ChildPath $DevSetupAgentConst + + Write-Host "Creating VM temporary folder $guestUnpackDirectory" + Write-Progress -Activity $activity -Status "Creating VM temporary folder $guestUnpackDirectory" -PercentComplete 10 + Invoke-Command -Session $session -ScriptBlock { New-Item -Path "$using:guestUnpackDirectory" -ItemType "directory" } + + if ($isDirectory) + { + $destinationPath = $guestDevSetupAgentTempDirectory + } + else + { + $destinationPath = $guestUnpackDirectory + } + + Write-Host "Copying $Path to VM $destinationPath" + Write-Progress -Activity $activity -Status "Copying DevSetupAgent to VM $destinationPath" -PercentComplete 15 + Copy-Item -ToSession $session -Recurse -Path $Path -Destination $destinationPath + + + Invoke-Command -Session $session -ScriptBlock { + $ErrorActionPreference = "Stop" + + try + { + $guestDevSetupAgentPath = Join-Path -Path $Env:Programfiles -ChildPath $using:DevSetupAgentConst + + # Stop and remove previous version of DevSetupAgent service if it exists + $service = Get-Service -Name $using:DevSetupAgentConst -ErrorAction SilentlyContinue + if ($service) + { + $serviceWMI = Get-WmiObject -Class Win32_Service -Filter "Name='$using:DevSetupAgentConst'" + $existingServicePath = $serviceWMI.Properties["PathName"].Value + if ($existingServicePath) + { + $guestDevSetupAgentPath = Split-Path $existingServicePath -Parent + } + + try + { + Write-Host "Stopping DevSetupAgent service" + Write-Progress -Activity $using:activity -Status "Stopping DevSetupAgent service $destinationPath" -PercentComplete 30 + $service.Stop() + } + catch + { + Write-Host "Ignoring error: $PSItem" + } + + Remove-Variable -Name service -ErrorAction SilentlyContinue + + # Remove-Service is only available in PowerShell 6.0 and later. Windows doesn't come with it preinstalled. + Write-Host "Removing DevSetupAgent service" + Write-Progress -Activity $using:activity -Status "Removing DevSetupAgent service" -PercentComplete 35 + $serviceWMI = Get-WmiObject -Class Win32_Service -Filter "Name='$using:DevSetupAgentConst'" + $serviceWMI.Delete() + Remove-Variable -Name serviceWMI -ErrorAction SilentlyContinue + } + + # Stop previous version of DevSetupEngine COM server if it exists + $devSetupEngineProcess = Get-Process -Name "$using:DevSetupEngineConst" -ErrorAction SilentlyContinue + if ($devSetupEngineProcess -ne $null) + { + Write-Host "Stopping $using:DevSetupEngineConst process" + Write-Progress -Activity $using:activity -Status "Stopping $using:DevSetupEngineConst process" -PercentComplete 40 + Stop-Process -Force -Name "$using:DevSetupEngineConst" + } + + # Unregister DevSetupEngine + $enginePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath "$using:DevSetupEngineConst.exe" + if (Test-Path -Path $enginePath) + { + Write-Host "Unregistering DevSetupEngine ($enginePath)" + Write-Progress -Activity $using:activity -Status "Registering DevSetupEngine ($enginePath)" -PercentComplete 88 + &$enginePath "-UnregisterComServer" + } + + # Remove previous version of DevSetupAgent service files + if (Test-Path -Path $guestDevSetupAgentPath) + { + # Sleep a few seconds to make sure all handles released after shutting down previous DevSetupEngine + Start-Sleep -Seconds 7 + Write-Host "Deleting old DevSetupAgent service files" + Write-Progress -Activity $using:activity -Status "Deleting old DevSetupAgent service files" -PercentComplete 45 + Remove-Item -Recurse -Force -Path $guestDevSetupAgentPath + } + + if ($using:isDirectory) + { + Write-Host "Copying DevSetupAgent to $guestDevSetupAgentPath" + Write-Progress -Activity $using:activity -Status "Deleting old DevSetupAgent service files" -PercentComplete 50 + Copy-Item -Recurse -Path $using:guestDevSetupAgentTempDirectory -Destination $guestDevSetupAgentPath + } + elseif ($using:isCab) + { + $cabPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName + Write-Host "Unpacking $cabPath to $guestDevSetupAgentPath" + Write-Progress -Activity $using:activity -Status "Unpacking $cabPath to $guestDevSetupAgentPath" -PercentComplete 60 + $expandOutput=&"$Env:SystemRoot\System32\expand.exe" $cabPath /F:* $Env:Programfiles + if ($LastExitCode -ne 0) + { + throw "Error unpacking $cabPath`:`n$LastExitCode`n$($expandOutput|Out-String)" + } + } + else + { + $zipPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName + Write-Host "Unpacking $using:inputFileName to $guestDevSetupAgentPath" + Write-Progress -Activity $using:activity -Status "Unpacking $using:inputFileName to $guestDevSetupAgentPath" -PercentComplete 60 + Expand-Archive -Path $zipPath -Destination $guestDevSetupAgentPath + } + + # Register DevSetupAgent service + $servicePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath "$using:DevSetupAgentConst.exe" + Write-Host "Registering DevSetupAgent service ($servicePath)" + Write-Progress -Activity $using:activity -Status "Registering DevSetupAgent service ($servicePath)" -PercentComplete 85 + New-Service -Name $using:DevSetupAgentConst -BinaryPathName $servicePath -StartupType Automatic + + # Register DevSetupEngine + Write-Host "Registering DevSetupEngine ($enginePath)" + Write-Progress -Activity $using:activity -Status "Registering DevSetupEngine ($enginePath)" -PercentComplete 88 + + # Executing non-console apps using '&' does not set $LastExitCode. Using Start-Process here to get the returned error code. + $process = Start-Process -NoNewWindow -Wait $enginePath -ArgumentList "-RegisterComServer" -PassThru + if ($process.ExitCode -ne 0) + { + throw "Error registering $enginePath`: $process.ExitCode" + } + + Write-Host "Starting DevSetupAgent service" + Write-Progress -Activity $using:activity -Status "Starting DevSetupAgent service" -PercentComplete 92 + Start-Service $using:DevSetupAgentConst + } + catch + { + Write-Host "Error on guest OS: $PSItem" + } + finally + { + Write-Host "Removing temporary directory $using:guestUnpackDirectory" + Remove-Item -Recurse -Force -Path $using:guestUnpackDirectory -ErrorAction SilentlyContinue + } + } + + Remove-PSSession $session +} diff --git a/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs b/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs index 9a86fec523..dda443863d 100644 --- a/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using HyperVExtension.Extensions; +using HyperVExtension.Models; using HyperVExtension.Models.VirtualMachineCreation; namespace HyperVExtension.Services; @@ -32,13 +33,12 @@ public async Task StartDownloadAsync(IProgress progressProvide using var outputFileStream = File.OpenWrite(destinationFile); outputFileStream.SetLength(totalBytesToReceive); - var downloadProgress = new Progress(bytesCopied => + var downloadProgress = new Progress(progressObj => { - var percentage = (uint)(bytesCopied / (double)totalBytesToReceive * 100D); - progressProvider.Report(new DownloadOperationReport((ulong)bytesCopied, (ulong)totalBytesToReceive)); + progressProvider.Report(new DownloadOperationReport(progressObj)); }); - await webFileStream.CopyToAsync(outputFileStream, downloadProgress, _transferBufferSize, cancellationToken); + await webFileStream.CopyToAsync(outputFileStream, downloadProgress, _transferBufferSize, totalBytesToReceive, cancellationToken); } /// @@ -55,6 +55,12 @@ public async Task DownloadByteArrayAsync(string sourceWebUri, Cancellati return await httpClient.GetByteArrayAsync(sourceWebUri, cancellationToken); } + public async Task GetHeaderContentLength(Uri sourceWebUri, CancellationToken cancellationToken) + { + var httpClient = _httpClientFactory.CreateClient(); + return GetTotalBytesToReceive(await httpClient.GetAsync(sourceWebUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)); + } + private long GetTotalBytesToReceive(HttpResponseMessage response) { if (response.Content.Headers.ContentLength.HasValue) diff --git a/HyperVExtension/src/HyperVExtension/Services/IDownloaderService.cs b/HyperVExtension/src/HyperVExtension/Services/IDownloaderService.cs index 55704fe976..2d4e98226d 100644 --- a/HyperVExtension/src/HyperVExtension/Services/IDownloaderService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/IDownloaderService.cs @@ -35,4 +35,6 @@ public interface IDownloaderService /// A token that can allow the operation to be cancelled while it is running /// Content returned by web server represented as an array of bytes public Task DownloadByteArrayAsync(string sourceWebUri, CancellationToken cancellationToken); + + public Task GetHeaderContentLength(Uri sourceWebUri, CancellationToken cancellationToken); } diff --git a/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs b/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs index dbfb896090..b665df70a7 100644 --- a/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs @@ -60,7 +60,7 @@ public PowerShellResult Execute(IEnumerable comm catch (Exception ex) { var commandStrings = string.Join(Environment.NewLine, commandLineStatements.Select(cmd => cmd.ToString())); - _log.Error($"Error running PowerShell commands: {commandStrings}", ex); + _log.Error(ex, $"Error running PowerShell commands: {commandStrings}"); throw; } } diff --git a/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs b/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs index 2c87c051f9..e09837ce43 100644 --- a/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Globalization; using System.Security.Cryptography; using System.Text.Json; using HyperVExtension.Models.VMGalleryJsonToClasses; @@ -73,11 +74,20 @@ public async Task GetGalleryImagesAsync() image.Symbol.Base64Image = Convert.ToBase64String(byteArray); } + + if (!string.IsNullOrEmpty(image.Disk.Uri)) + { + var totalSizeOfDisk = await _downloaderService.GetHeaderContentLength(new Uri(image.Disk.Uri), cancellationTokenSource.Token); + if (ulong.TryParse(image.Requirements.DiskSpace, CultureInfo.InvariantCulture, out var requiredDiskSpace)) + { + image.Disk.SizeInBytes = (ulong)totalSizeOfDisk; + } + } } } catch (Exception ex) { - _log.Error($"Unable to retrieve VM gallery images", ex); + _log.Error(ex, $"Unable to retrieve VM gallery images"); } return _imageList; diff --git a/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw b/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw index 5586a3455e..7f7aa770ac 100644 --- a/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw +++ b/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw @@ -118,8 +118,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Creating: {0} - Locked="{0}" text to tell the user that we're currently creating the virtual machine. {0} is the name of the virtual machine + Adding network switch, secure boot and enhanced session configuration for {0} + Locked="{0}" Text to tell the user that we're performing post creation actions like adding a network switch to the virtual machine. {0} is the name of the virtual machine + + + Starting the creation process for {0} + Locked="{0}" Text to tell the user that we're starting the process to create the virtual machine. {0} is the name of the virtual machine Current Checkpoint @@ -130,7 +134,7 @@ Locked="{0}" text to tell the user that a file exists and we do not need to download it again. {0} is a previously download file. We show the file name in {0}. - Downloading {0}. {1} + Downloading {0} {1} Locked="{0}" text to tell the user that we are downloading a file from the web. {0} is the file we're downloading. {1} the progress in the form of "bytes received / total bytes needed". E.g "10 Mb / 400 Mb" @@ -158,7 +162,7 @@ Attempt counter text for the dialog to enter Hyper-V VM admin credential ({CurrentAttempt}/{MaxAttempts}). - Extracting file {0}. {1} + Extracting file {0} {1} Locked="{0}" text to tell the user that we're extracting a zip file into a location on their computer. {0} is the zip file we're extracting. {1} the progress in the form of "bytes extracted / total bytes needed". E.g "10 Mb / 400 Mb" @@ -257,4 +261,72 @@ Please log on to your Hyper-V Title text of the dialog asking to log in to Hyper-V VM. + + Adaptive card state not recognized + Error text to show when we don't recognize the state of the adaptive card that was given to us + + + 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 + + + More Info + Text for a button that will launch a content dialog that displays more information to the user about a disk image + + + Download + label text for the download size of the disk image + + + New virtual machine name + Label text for textbox where users will enter the name for their new virtual machine + + + Enter the name of your new virtual machine + place holder text that will appear within a text box + + + Failed to generate the initial creation form + Error text to show the user when the was an error getting the initial disk image selection page in the creation wizard flow + + + Last updated + label text for when the disk image was last updated + + + Locale + label text for locale of operation system that is installed on the disk image + + + Name{0} + Locked="{0}" text label that will be on top of the name the user provides in a textbox. {0} is the colon e.g ":" special character + + + Version + label text for version of operating system that is installed on the disk image + + + Ok + Text for primary button of the content dialog + + + Next + Text to display to the user about what the primary button does in the UI + + + Failed to generate the review form + Error text to show the user when the was an error getting the review page in the creation wizard flow + + + Cancel + Text for the secondary button of the content dialog + + + Previous + Text to display to the user about what the secondary button does in the UI. + + + Choose an image to use + Label text for a list of cards that appear in the UI + \ No newline at end of file diff --git a/HyperVExtension/src/HyperVExtension/Templates/InitialVMGalleryCreationForm.json b/HyperVExtension/src/HyperVExtension/Templates/InitialVMGalleryCreationForm.json new file mode 100644 index 0000000000..31a9d10b4a --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Templates/InitialVMGalleryCreationForm.json @@ -0,0 +1,85 @@ +{ + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5", + "body": [ + { + "type": "Input.Text", + "id": "NewEnvironmentName", + "label": "${EnterNewVMNameLabel}", + "placeholder": "${EnterNewVMNamePlaceHolder}", + "maxLength": 100, + "isRequired": true, + "Spacing": "Large" + }, + { + "type": "DevHome.SettingsCardChoiceSet", + "id": "SelectedImageListIndex", + "label": "${SettingsCardLabel}", + "isRequired": true, + "devHomeSettingsCards": [ + { + "type": "DevHome.SettingsCard", + "$data": "${GalleryImages}", + "devHomeSettingsCardDescription": "${SubDescription}", + "devHomeSettingsCardHeader": "${Header}", + "devHomeSettingsCardHeaderIcon": "${HeaderIcon}", + "devHomeSettingsCardActionElement": { + "type": "DevHome.LaunchContentDialogButton", + "devHomeActionText": "${ButtonToLaunchContentDialogLabel}", + "devHomeContentDialogContent": { + "devHomeContentDialogTitle": "${Header}", + "devHomeContentDialogBodyAdaptiveCard": { + "type": "AdaptiveCard", + "version": "1.5", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": [ + { + "type": "Container", + "$data": "${ContentDialogInfo}", + "items": [ + { + "type": "TextBlock", + "text": "${ImageDescription}", + "isMultiline": true, + "Spacing": "Medium", + "size": "Medium", + "wrap": true + }, + { + "$data": "${GalleryImageFacts}", + "type": "FactSet", + "facts": [ + { + "title": "${title}", + "value": "${value}" + } + ] + } + ] + } + ] + }, + "devHomeContentDialogSecondaryButtonText": "${SecondaryButtonForContentDialogText}" + } + } + } + ] + }, + { + "type": "ActionSet", + "actions": [ + { + "id": "DevHomeMachineConfigurationNextButton", + "type": "Action.Submit", + "title": "${PrimaryButtonLabelForCreationFlow}" + }, + { + "id": "DevHomeMachineConfigurationPreviousButton", + "type": "Action.Submit", + "title": "${SecondaryButtonLabelForCreationFlow}" + } + ] + } + ] +} \ No newline at end of file diff --git a/HyperVExtension/src/HyperVExtension/Templates/ReviewFormForVMGallery.json b/HyperVExtension/src/HyperVExtension/Templates/ReviewFormForVMGallery.json new file mode 100644 index 0000000000..20335479bd --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Templates/ReviewFormForVMGallery.json @@ -0,0 +1,98 @@ +{ + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.6", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "spacing": "medium", + "items": [ + { + "type": "TextBlock", + "text": "${ProviderName}", + "wrap": true, + "size": "medium" + }, + { + "type": "TextBlock", + "text": "${DiskImageSize}", + "wrap": true, + "size": "medium" + } + ] + }, + { + "type": "Column", + "width": "auto", + "spacing": "extraLarge", + "items": [ + { + "type": "TextBlock", + "text": "${NameLabel}", + "wrap": true, + "size": "medium" + }, + { + "type": "TextBlock", + "text": "${NameOfNewVM}", + "wrap": true, + "size": "medium" + } + ] + }, + { + "type": "Column", + "width": "auto", + "spacing": "extraLarge", + "verticalContentAlignment": "center", + "items": [ + { + "type": "Image", + "url": "${DiskImageUrl}", + "height": "32px" + } + ] + }, + { + "type": "Column", + "width": "auto", + "spacing": "medium", + "items": [ + { + "type": "TextBlock", + "text": "${VMGalleryImageName}", + "wrap": true, + "size": "medium" + }, + { + "type": "TextBlock", + "text": "${Publisher}", + "wrap": true, + "isSubtle": true, + "size": "medium" + } + ] + } + ] + }, + { + "type": "ActionSet", + "actions": [ + { + "id": "DevHomeMachineConfigurationNextButton", + "type": "Action.Submit", + "title": "${PrimaryButtonLabelForCreationFlow}" + }, + { + "id": "DevHomeMachineConfigurationPreviousButton", + "type": "Action.Submit", + "title": "${SecondaryButtonLabelForCreationFlow}" + } + ] + } + ] +} \ No newline at end of file diff --git a/HyperVExtension/src/HyperVExtensionServer/HyperVExtensionServer.csproj b/HyperVExtension/src/HyperVExtensionServer/HyperVExtensionServer.csproj index 8e82ab2712..3b3bf63159 100644 --- a/HyperVExtension/src/HyperVExtensionServer/HyperVExtensionServer.csproj +++ b/HyperVExtension/src/HyperVExtensionServer/HyperVExtensionServer.csproj @@ -14,9 +14,9 @@ false false HyperVExtension.Program - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 x86;x64;arm64 - $(SolutionDir)\src\Properties\PublishProfiles\win10-$(Platform).pubxml + $(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml diff --git a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-arm64.pubxml b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-arm64.pubxml similarity index 91% rename from HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-arm64.pubxml rename to HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-arm64.pubxml index 2593bad1c5..14e602a691 100644 --- a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-arm64.pubxml +++ b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-arm64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem arm64 - win10-arm64 + win-arm64 true False False diff --git a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x64.pubxml b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x64.pubxml similarity index 91% rename from HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x64.pubxml rename to HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x64.pubxml index 8b6ea06a13..afc8a98a2f 100644 --- a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x64.pubxml +++ b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem x64 - win10-x64 + win-x64 true False False diff --git a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x86.pubxml b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x86.pubxml similarity index 91% rename from HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x86.pubxml rename to HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x86.pubxml index 99985acadf..5408399634 100644 --- a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x86.pubxml +++ b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x86.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem x86 - win10-x86 + win-x86 true False False diff --git a/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json b/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json index 15a4f5fade..f79a3cf6d0 100644 --- a/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json +++ b/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json @@ -6,7 +6,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -14,7 +14,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\hyperv.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/HyperVExtension/src/Telemetry/HyperVExtension.Telemetry.csproj b/HyperVExtension/src/Telemetry/HyperVExtension.Telemetry.csproj index 467a65659e..d096236e4e 100644 --- a/HyperVExtension/src/Telemetry/HyperVExtension.Telemetry.csproj +++ b/HyperVExtension/src/Telemetry/HyperVExtension.Telemetry.csproj @@ -3,7 +3,7 @@ HyperVExtension.Telemetry x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 true diff --git a/HyperVExtension/test/DevSetupAgent.Test/DevSetupAgent.Test.csproj b/HyperVExtension/test/DevSetupAgent.Test/DevSetupAgent.Test.csproj index 372bd9bd6d..d0d2d2c48b 100644 --- a/HyperVExtension/test/DevSetupAgent.Test/DevSetupAgent.Test.csproj +++ b/HyperVExtension/test/DevSetupAgent.Test/DevSetupAgent.Test.csproj @@ -3,7 +3,7 @@ enable enable - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 x86;x64;arm64 false true diff --git a/HyperVExtension/test/DevSetupEngine.Test/DevSetupEngine.Test.csproj b/HyperVExtension/test/DevSetupEngine.Test/DevSetupEngine.Test.csproj index 0378daeb3b..768c60e6a7 100644 --- a/HyperVExtension/test/DevSetupEngine.Test/DevSetupEngine.Test.csproj +++ b/HyperVExtension/test/DevSetupEngine.Test/DevSetupEngine.Test.csproj @@ -3,7 +3,7 @@ enable enable - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 x86;x64;arm64 false true diff --git a/HyperVExtension/test/HyperVExtension/HyperVExtension.UnitTest.csproj b/HyperVExtension/test/HyperVExtension/HyperVExtension.UnitTest.csproj index 898887e9cc..ab0eddaa15 100644 --- a/HyperVExtension/test/HyperVExtension/HyperVExtension.UnitTest.csproj +++ b/HyperVExtension/test/HyperVExtension/HyperVExtension.UnitTest.csproj @@ -2,7 +2,7 @@ HyperVExtension.UnitTest - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 x86;x64;arm64 false enable diff --git a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs index 97a0aaf1ce..8de473bab9 100644 --- a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs +++ b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs @@ -393,7 +393,7 @@ public async Task TestVirtualMachineCreationFromVmGallery() var smallestImageIndex = await GetIndexOfImageWithSmallestRequiredSpace(imageList); var inputJson = JsonSerializer.Serialize(new VMGalleryCreationUserInput() { - NewVirtualMachineName = expectedVMName, + NewEnvironmentName = expectedVMName, // Get Image with the smallest size from gallery, we'll use it to create a VM. SelectedImageListIndex = smallestImageIndex, diff --git a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs index 68ca35480d..68a62af01c 100644 --- a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs +++ b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs @@ -77,7 +77,7 @@ public async Task HyperVProvider_Can_Create_VirtualMachine() var hyperVProvider = TestHost!.GetService(); var inputJson = JsonSerializer.Serialize(new VMGalleryCreationUserInput() { - NewVirtualMachineName = _expectedVmName, + NewEnvironmentName = _expectedVmName, SelectedImageListIndex = 0, // Our test gallery image list Json only has one image }); diff --git a/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs b/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs index 06dc15a7ac..52c8134a33 100644 --- a/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs +++ b/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using HyperVExtension.Models; using HyperVExtension.Models.VirtualMachineCreation; using HyperVExtension.Services; using Windows.Storage; @@ -11,9 +12,9 @@ public class DownloaderServiceMock : IDownloaderService { private readonly int _totalIterations = 4; - private readonly ulong _totalBytesToReceive = 1000; + private readonly long _totalBytesToReceive = 1000; - private readonly ulong _bytesReceivedEachIteration = 250; + private readonly long _bytesReceivedEachIteration = 250; private readonly IHttpClientFactory _httpClientFactory; @@ -24,12 +25,12 @@ public DownloaderServiceMock(IHttpClientFactory httpClientFactory) public async Task StartDownloadAsync(IProgress progressProvider, Uri sourceWebUri, string destinationFile, CancellationToken cancellationToken) { - var bytesReceivedSoFar = 0UL; + var bytesReceivedSoFar = 0L; for (var i = 0; i < _totalIterations; i++) { await Task.Delay(100, cancellationToken); bytesReceivedSoFar += _bytesReceivedEachIteration; - progressProvider.Report(new DownloadOperationReport(bytesReceivedSoFar, _totalBytesToReceive)); + progressProvider.Report(new DownloadOperationReport(new ByteTransferProgress(bytesReceivedSoFar, _totalBytesToReceive))); } var zipFile = await GetTestZipFileInPackage(); @@ -56,4 +57,10 @@ public async Task DownloadByteArrayAsync(string sourceWebUri, Cancellati var httpClient = _httpClientFactory.CreateClient(); return await httpClient.GetByteArrayAsync(sourceWebUri, cancellationToken); } + + public async Task GetHeaderContentLength(Uri sourceWebUri, CancellationToken cancellationToken) + { + await Task.Delay(1, cancellationToken); + return 100L; + } } diff --git a/common/DevHome.Common.csproj b/common/DevHome.Common.csproj index f9b8d6bf65..6fa639c267 100644 --- a/common/DevHome.Common.csproj +++ b/common/DevHome.Common.csproj @@ -3,7 +3,7 @@ DevHome.Common x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 enable true $(DevHomeSDKVersion) diff --git a/common/Environments/Converters/CardStateColorToBrushConverter.cs b/common/Environments/Converters/CardStateColorToBrushConverter.cs index 0ac1303b9b..ca01704c09 100644 --- a/common/Environments/Converters/CardStateColorToBrushConverter.cs +++ b/common/Environments/Converters/CardStateColorToBrushConverter.cs @@ -25,6 +25,7 @@ public object Convert(object value, Type targetType, object parameter, string la CardStateColor.Success => (SolidColorBrush)Application.Current.Resources["SystemFillColorSuccessBrush"], CardStateColor.Neutral => (SolidColorBrush)Application.Current.Resources["SystemFillColorSolidNeutralBrush"], CardStateColor.Caution => (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"], + CardStateColor.Failure => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"], _ => (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"], }; } diff --git a/common/Environments/CustomControls/CardBody.xaml b/common/Environments/CustomControls/CardBody.xaml index 5fa356c1ff..5c72231e56 100644 --- a/common/Environments/CustomControls/CardBody.xaml +++ b/common/Environments/CustomControls/CardBody.xaml @@ -7,10 +7,14 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:devEnvConverters="using:DevHome.Common.Environments.Converters" xmlns:controls="using:CommunityToolkit.WinUI.Controls" + xmlns:converters="using:CommunityToolkit.WinUI.Converters" mc:Ignorable="d"> + + + - + - - - - + + + + - + - + + @@ -81,6 +95,10 @@ HorizontalSpacing="15"/> + + + @@ -142,4 +145,14 @@ $(DefineConstants);STABLE_BUILD + + + + + + + + + + diff --git a/src/Models/ExtensionWrapper.cs b/src/Models/ExtensionWrapper.cs index 2b6552c633..7f5743aff4 100644 --- a/src/Models/ExtensionWrapper.cs +++ b/src/Models/ExtensionWrapper.cs @@ -32,7 +32,8 @@ public class ExtensionWrapper : IExtensionWrapper public ExtensionWrapper(AppExtension appExtension, string classId) { - Name = appExtension.DisplayName; + PackageDisplayName = appExtension.Package.DisplayName; + ExtensionDisplayName = appExtension.DisplayName; PackageFullName = appExtension.Package.Id.FullName; PackageFamilyName = appExtension.Package.Id.FamilyName; ExtensionClassId = classId ?? throw new ArgumentNullException(nameof(classId)); @@ -42,40 +43,21 @@ public ExtensionWrapper(AppExtension appExtension, string classId) ExtensionUniqueId = appExtension.AppInfo.AppUserModelId + "!" + appExtension.Id; } - public string Name - { - get; - } + public string PackageDisplayName { get; } - public string PackageFullName - { - get; - } + public string ExtensionDisplayName { get; } - public string PackageFamilyName - { - get; - } + public string PackageFullName { get; } - public string ExtensionClassId - { - get; - } + public string PackageFamilyName { get; } - public string Publisher - { - get; - } + public string ExtensionClassId { get; } - public DateTimeOffset InstalledDate - { - get; - } + public string Publisher { get; } - public PackageVersion Version - { - get; - } + public DateTimeOffset InstalledDate { get; } + + public PackageVersion Version { get; } /// /// Gets the unique id for this Dev Home extension. The unique id is a concatenation of: @@ -86,10 +68,7 @@ public PackageVersion Version /// The Extension Id. This is the unique identifier of the extension within the application. /// /// - public string ExtensionUniqueId - { - get; - } + public string ExtensionUniqueId { get; } public bool IsRunning() { diff --git a/src/NavConfig.jsonc b/src/NavConfig.jsonc index d134ce80be..c65878b80c 100644 --- a/src/NavConfig.jsonc +++ b/src/NavConfig.jsonc @@ -45,7 +45,7 @@ { "buildType": "dev", "enabledByDefault": true, - "visible": true + "visible": false }, { "buildType": "canary", @@ -53,7 +53,7 @@ "visible": true }, { - "buildType": "release", + "buildType": "stable", "enabledByDefault": false, "visible": false } @@ -74,7 +74,7 @@ "visible": false }, { - "buildType": "release", + "buildType": "stable", "enabledByDefault": false, "visible": false } @@ -95,7 +95,28 @@ "visible": false }, { - "buildType": "release", + "buildType": "stable", + "enabledByDefault": false, + "visible": false + } + ] + }, + { + "identity": "EnvironmentsCreationFlow", + "enabledByDefault": false, + "buildTypeOverrides": [ + { + "buildType": "dev", + "enabledByDefault": true, + "visible": true + }, + { + "buildType": "canary", + "enabledByDefault": true, + "visible": true + }, + { + "buildType": "stable", "enabledByDefault": false, "visible": false } diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest index 27b4f1e112..3e44bb0bc3 100644 --- a/src/Package.appxmanifest +++ b/src/Package.appxmanifest @@ -84,8 +84,10 @@ - + + + @@ -95,7 +97,7 @@ - + @@ -296,4 +298,4 @@ - \ No newline at end of file + diff --git a/src/Properties/PublishProfiles/win10-arm64.pubxml b/src/Properties/PublishProfiles/win-arm64.pubxml similarity index 92% rename from src/Properties/PublishProfiles/win10-arm64.pubxml rename to src/Properties/PublishProfiles/win-arm64.pubxml index 87058c3fd3..227cf87736 100644 --- a/src/Properties/PublishProfiles/win10-arm64.pubxml +++ b/src/Properties/PublishProfiles/win-arm64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem arm64 - win10-arm64 + win-arm64 true False False diff --git a/src/Properties/PublishProfiles/win10-x64.pubxml b/src/Properties/PublishProfiles/win-x64.pubxml similarity index 92% rename from src/Properties/PublishProfiles/win10-x64.pubxml rename to src/Properties/PublishProfiles/win-x64.pubxml index ab80eaaf14..19ae2a6b9c 100644 --- a/src/Properties/PublishProfiles/win10-x64.pubxml +++ b/src/Properties/PublishProfiles/win-x64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem x64 - win10-x64 + win-x64 true False False diff --git a/src/Properties/PublishProfiles/win10-x86.pubxml b/src/Properties/PublishProfiles/win-x86.pubxml similarity index 92% rename from src/Properties/PublishProfiles/win10-x86.pubxml rename to src/Properties/PublishProfiles/win-x86.pubxml index 5b0b0359a6..dace1fa912 100644 --- a/src/Properties/PublishProfiles/win10-x86.pubxml +++ b/src/Properties/PublishProfiles/win-x86.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem x86 - win10-x86 + win-x86 true False False diff --git a/src/Services/AccountsService.cs b/src/Services/AccountsService.cs index 7127352bf4..9574232fbb 100644 --- a/src/Services/AccountsService.cs +++ b/src/Services/AccountsService.cs @@ -59,7 +59,7 @@ public async Task> GetDevIdProviders() } catch (Exception ex) { - _log.Error($"Failed to get {nameof(IDeveloperIdProvider)} provider from '{extension.Name}'", ex); + _log.Error(ex, $"Failed to get {nameof(IDeveloperIdProvider)} provider from '{extension.PackageFamilyName}/{extension.ExtensionDisplayName}'"); } } diff --git a/src/Services/DSCFileActivationHandler.cs b/src/Services/DSCFileActivationHandler.cs index 2c366c7ce6..1731684323 100644 --- a/src/Services/DSCFileActivationHandler.cs +++ b/src/Services/DSCFileActivationHandler.cs @@ -98,7 +98,7 @@ await _mainWindow.ShowErrorMessageDialogAsync( } catch (Exception ex) { - _log.Error("Error executing the DSC activation flow", ex); + _log.Error(ex, "Error executing the DSC activation flow"); } } } diff --git a/src/Services/ExtensionService.cs b/src/Services/ExtensionService.cs index 0566a581bf..032d45c2cf 100644 --- a/src/Services/ExtensionService.cs +++ b/src/Services/ExtensionService.cs @@ -313,12 +313,12 @@ protected virtual void Dispose(bool disposing) private IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) { - return propSet[name] as IPropertySet; + return propSet.TryGetValue(name, out var value) ? value as IPropertySet : null; } private object[]? GetSubPropertySetArray(IPropertySet propSet, string name) { - return propSet[name] as object[]; + return propSet.TryGetValue(name, out var value) ? value as object[] : null; } /// @@ -330,7 +330,6 @@ private List GetCreateInstanceList(IPropertySet activationPropSet) { var propSetList = new List(); var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty); - var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty); if (singlePropertySet != null) { var classId = GetProperty(singlePropertySet, ClassIdProperty); @@ -341,19 +340,23 @@ private List GetCreateInstanceList(IPropertySet activationPropSet) propSetList.Add(classId); } } - else if (propertySetArray != null) + else { - foreach (var prop in propertySetArray) + var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty); + if (propertySetArray != null) { - if (prop is not IPropertySet propertySet) + foreach (var prop in propertySetArray) { - continue; - } + if (prop is not IPropertySet propertySet) + { + continue; + } - var classId = GetProperty(propertySet, ClassIdProperty); - if (classId != null) - { - propSetList.Add(classId); + var classId = GetProperty(propertySet, ClassIdProperty); + if (classId != null) + { + propSetList.Add(classId); + } } } } diff --git a/src/ViewModels/InitializationViewModel.cs b/src/ViewModels/InitializationViewModel.cs index 025c6c616c..7c0b33a0cd 100644 --- a/src/ViewModels/InitializationViewModel.cs +++ b/src/ViewModels/InitializationViewModel.cs @@ -65,7 +65,7 @@ public async void OnPageLoaded() } catch (Exception ex) { - _log.Information("Installing WidgetService failed: ", ex); + _log.Information(ex, "Installing WidgetService failed: "); } // Install the DevHomeGitHubExtension, unless it's already installed or a dev build is running. @@ -82,7 +82,7 @@ public async void OnPageLoaded() } catch (Exception ex) { - _log.Information("Installing DevHomeGitHubExtension failed: ", ex); + _log.Information(ex, "Installing DevHomeGitHubExtension failed: "); } } diff --git a/src/Views/ShellPage.xaml b/src/Views/ShellPage.xaml index 37b0b4086f..2b58a85590 100644 --- a/src/Views/ShellPage.xaml +++ b/src/Views/ShellPage.xaml @@ -126,7 +126,7 @@ from reading the control value. --> - + diff --git a/src/appsettings.json b/src/appsettings.json index d118f3fcc9..b16cd33ca4 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -15,7 +15,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -23,7 +23,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\devhome.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/telemetry/DevHome.Telemetry/DevHome.Telemetry.csproj b/telemetry/DevHome.Telemetry/DevHome.Telemetry.csproj index b56449c534..bd827b8dcd 100644 --- a/telemetry/DevHome.Telemetry/DevHome.Telemetry.csproj +++ b/telemetry/DevHome.Telemetry/DevHome.Telemetry.csproj @@ -1,19 +1,19 @@ - - - - DevHome.Telemetry - x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 - true - - TELEMETRYEVENTSOURCE_PUBLIC - - - - - - - - - + + + + DevHome.Telemetry + x86;x64;arm64 + win-x86;win-x64;win-arm64 + true + + TELEMETRYEVENTSOURCE_PUBLIC + + + + + + + + + \ No newline at end of file diff --git a/test/DevHome.Test.csproj b/test/DevHome.Test.csproj index 05dc1101f1..5027457d77 100644 --- a/test/DevHome.Test.csproj +++ b/test/DevHome.Test.csproj @@ -3,7 +3,7 @@ DevHome.Test x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 false enable enable diff --git a/tools/Customization/DevHome.Customization/DevHome.Customization.csproj b/tools/Customization/DevHome.Customization/DevHome.Customization.csproj index d0dd34f2d4..e99af82ccd 100644 --- a/tools/Customization/DevHome.Customization/DevHome.Customization.csproj +++ b/tools/Customization/DevHome.Customization/DevHome.Customization.csproj @@ -3,7 +3,7 @@ DevHome.Customization x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 enable true diff --git a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs index a309f9182e..b06769e22b 100644 --- a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs +++ b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs @@ -19,7 +19,7 @@ public static IServiceCollection AddWindowsCustomization(this IServiceCollection services.AddSingleton(); services.AddTransient(); - services.AddSingleton(sp => (cacheLocation, environmentVariable) => ActivatorUtilities.CreateInstance(sp, cacheLocation, environmentVariable)); + services.AddSingleton(sp => (cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters) => ActivatorUtilities.CreateInstance(sp, cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters)); services.AddSingleton(); services.AddTransient(); diff --git a/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs b/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs index 8fb1762a18..8b9223c3cc 100644 --- a/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs +++ b/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs @@ -17,5 +17,5 @@ public partial class DevDriveCacheData public List? CacheDirectory { get; set; } - public string? ExampleDirectory { get; set; } + public string? ExampleSubDirectory { get; set; } } diff --git a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw index d6a6c7a6f7..3914c8eb00 100644 --- a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw +++ b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw @@ -126,7 +126,7 @@ Dev drive size free - All things, dev drives, optimizations, etc. + All things, Dev Drives, optimizations, etc. The description for the Dev Drive Insights settings card @@ -161,9 +161,9 @@ Enable end task in taskbar by right click The description for the end task on task bar settings card - - Example: E:\packages\pip - Example dev drive location + + Example: + Example string, will be followed by a sample location to move the cache to a dev drive location End Task diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs index aa64ba3f0b..e0175b81de 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -18,6 +19,8 @@ public partial class DevDriveOptimizerCardViewModel : ObservableObject { public OptimizeDevDriveDialogViewModelFactory OptimizeDevDriveDialogViewModelFactory { get; set; } + public List ExistingDevDriveLetters { get; set; } + public string CacheToBeMoved { get; set; } public string DevDriveOptimizationSuggestion { get; set; } @@ -41,7 +44,11 @@ private async Task OptimizeDevDriveAsync(object sender) var settingsCard = sender as Button; if (settingsCard != null) { - var optimizeDevDriveViewModel = OptimizeDevDriveDialogViewModelFactory(ExistingCacheLocation, EnvironmentVariableToBeSet); + var optimizeDevDriveViewModel = OptimizeDevDriveDialogViewModelFactory( + ExistingCacheLocation, + EnvironmentVariableToBeSet, + ExampleLocationOnDevDrive, + ExistingDevDriveLetters); var optimizeDevDriveDialog = new OptimizeDevDriveDialog(optimizeDevDriveViewModel); optimizeDevDriveDialog.XamlRoot = settingsCard.XamlRoot; optimizeDevDriveDialog.RequestedTheme = settingsCard.ActualTheme; @@ -49,9 +56,16 @@ private async Task OptimizeDevDriveAsync(object sender) } } - public DevDriveOptimizerCardViewModel(OptimizeDevDriveDialogViewModelFactory optimizeDevDriveDialogViewModelFactory, string cacheToBeMoved, string existingCacheLocation, string exampleLocationOnDevDrive, string environmentVariableToBeSet) + public DevDriveOptimizerCardViewModel( + OptimizeDevDriveDialogViewModelFactory optimizeDevDriveDialogViewModelFactory, + string cacheToBeMoved, + string existingCacheLocation, + string exampleLocationOnDevDrive, + string environmentVariableToBeSet, + List existingDevDriveLetters) { OptimizeDevDriveDialogViewModelFactory = optimizeDevDriveDialogViewModelFactory; + ExistingDevDriveLetters = existingDevDriveLetters; CacheToBeMoved = cacheToBeMoved; ExistingCacheLocation = existingCacheLocation; ExampleLocationOnDevDrive = exampleLocationOnDevDrive; diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs index 2d0cedc5c4..340da507e9 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs @@ -2,15 +2,22 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Data.SqlTypes; using System.IO; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; using DevHome.Common.Services; +using DevHome.Common.TelemetryEvents; +using DevHome.Telemetry; +using Microsoft.UI.Xaml.Controls; using Serilog; +using Windows.Media.Protection; using Windows.Storage.Pickers; using WinUIEx; +using static Microsoft.Extensions.Logging.EventSource.LoggingEventSource; namespace DevHome.Customization.ViewModels.DevDriveInsights; @@ -19,6 +26,9 @@ namespace DevHome.Customization.ViewModels.DevDriveInsights; /// public partial class OptimizeDevDriveDialogViewModel : ObservableObject { + [ObservableProperty] + private List _existingDevDriveLetters; + [ObservableProperty] private string _exampleDevDriveLocation; @@ -40,11 +50,16 @@ public partial class OptimizeDevDriveDialogViewModel : ObservableObject [ObservableProperty] private string _directoryPathTextBox; - public OptimizeDevDriveDialogViewModel(string existingCacheLocation, string environmentVariableToBeSet) + public OptimizeDevDriveDialogViewModel( + string existingCacheLocation, + string environmentVariableToBeSet, + string exampleDevDriveLocation, + List existingDevDriveLetters) { DirectoryPathTextBox = string.Empty; var stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); - ExampleDevDriveLocation = stringResource.GetLocalized("ExampleDevDriveLocation"); + ExistingDevDriveLetters = existingDevDriveLetters; + ExampleDevDriveLocation = stringResource.GetLocalized("ExampleText") + exampleDevDriveLocation; ChooseDirectoryPromptText = stringResource.GetLocalized("ChooseDirectoryPromptText"); MakeChangesText = stringResource.GetLocalized("MakeChangesText"); ExistingCacheLocation = existingCacheLocation; @@ -78,7 +93,20 @@ private async Task BrowseButtonClick(object sender) } } - private void MoveDirectory(string sourceDirectory, string targetDirectory) + private string RemovePrivacyInfo(string input) + { + var output = input; + var userProfilePath = Environment.ExpandEnvironmentVariables("%userprofile%"); + if (input.StartsWith(userProfilePath, StringComparison.OrdinalIgnoreCase)) + { + var index = input.LastIndexOf(userProfilePath, StringComparison.OrdinalIgnoreCase) + userProfilePath.Length; + output = Path.Join("%userprofile%", input.Substring(index)); + } + + return output; + } + + private bool MoveDirectory(string sourceDirectory, string targetDirectory) { try { @@ -110,10 +138,13 @@ private void MoveDirectory(string sourceDirectory, string targetDirectory) // Delete the source directory Directory.Delete(sourceDirectory, true); + return true; } catch (Exception ex) { Log.Error($"Error in MoveDirectory. Error: {ex}"); + TelemetryFactory.Get().LogError("DevDriveInsights_PackageCacheMoveDirectory_Error", LogLevel.Critical, new ExceptionEvent(ex.HResult, RemovePrivacyInfo(sourceDirectory))); + return false; } } @@ -125,8 +156,21 @@ private void SetEnvironmentVariable(string variableName, string value) } catch (Exception ex) { - Log.Error($"Error in SetEnvironmentVariable. Error: {ex}"); + Log.Error(ex, $"Error in SetEnvironmentVariable. Error: {ex}"); + } + } + + private bool ChosenDirectoryInDevDrive(string directoryPath) + { + foreach (var devDriveLetter in ExistingDevDriveLetters) + { + if (directoryPath.StartsWith(devDriveLetter + ":", StringComparison.OrdinalIgnoreCase)) + { + return true; + } } + + return false; } [RelayCommand] @@ -137,9 +181,21 @@ private void DirectoryInputConfirmed() if (!string.IsNullOrEmpty(directoryPath)) { // Handle the selected folder - // TODO: If chosen folder not a dev drive location, currently we no-op. Instead we should display the error. - MoveDirectory(ExistingCacheLocation, directoryPath); - SetEnvironmentVariable(EnvironmentVariableToBeSet, directoryPath); + // TODO: If chosen folder not a dev drive location, currently we no-op and log the error. Instead we should display the error. + if (ChosenDirectoryInDevDrive(directoryPath)) + { + if (MoveDirectory(ExistingCacheLocation, directoryPath)) + { + SetEnvironmentVariable(EnvironmentVariableToBeSet, directoryPath); + var existingCacheLocationVetted = RemovePrivacyInfo(ExistingCacheLocation); + Log.Debug($"Moved cache from {existingCacheLocationVetted} to {directoryPath}"); + TelemetryFactory.Get().Log("DevDriveInsights_PackageCacheMovedSuccessfully_Event", LogLevel.Critical, new ExceptionEvent(0, existingCacheLocationVetted)); + } + } + else + { + Log.Error($"Chosen directory {directoryPath} not on a dev drive."); + } } } } diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs index 5e3fa54bd4..15000d4f2c 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs @@ -12,12 +12,17 @@ using DevHome.Customization.Helpers; using DevHome.Customization.ViewModels.DevDriveInsights; using DevHome.Customization.Views; +using Microsoft.Internal.Windows.DevHome.Helpers; using Serilog; namespace DevHome.Customization.ViewModels; public partial class DevDriveInsightsViewModel : ObservableObject { + private readonly ShellSettings _shellSettings; + + public ObservableCollection Breadcrumbs { get; } + public ObservableCollection DevDriveCardCollection { get; private set; } = new(); public ObservableCollection DevDriveOptimizerCardCollection { get; private set; } = new(); @@ -48,12 +53,29 @@ public partial class DevDriveInsightsViewModel : ObservableObject private IEnumerable ExistingDevDrives { get; set; } = Enumerable.Empty(); + private static readonly string _appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + private static readonly string _localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); private static readonly string _userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + private const string PackagesStr = "packages"; + + private const string CacheStr = "cache"; + + private const string ArchivesStr = "archives"; + public DevDriveInsightsViewModel(IDevDriveManager devDriveManager, OptimizeDevDriveDialogViewModelFactory optimizeDevDriveDialogViewModelFactory) { + _shellSettings = new ShellSettings(); + + var stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); + Breadcrumbs = + [ + new(stringResource.GetLocalized("MainPage_Header"), typeof(MainPageViewModel).FullName!), + new(stringResource.GetLocalized("DevDriveInsights_Header"), typeof(DevDriveInsightsViewModel).FullName!) + ]; + _optimizeDevDriveDialogViewModelFactory = optimizeDevDriveDialogViewModelFactory; DevDriveManagerObj = devDriveManager; } @@ -175,7 +197,7 @@ public void LoadAllDevDrivesInTheUI() } catch (Exception ex) { - Log.Error($"Error loading Dev Drives data. Error: {ex}"); + Log.Error(ex, $"Error loading Dev Drives data. Error: {ex}"); } } @@ -195,7 +217,7 @@ public void LoadAllDevDriveOptimizersInTheUI() } catch (Exception ex) { - Log.Error($"Error loading Dev Drive Optimizers data. Error: {ex}"); + Log.Error(ex, $"Error loading Dev Drive Optimizers data. Error: {ex}"); } } @@ -215,7 +237,7 @@ public void LoadAllDevDriveOptimizedsInTheUI() } catch (Exception ex) { - Log.Error($"Error loading Dev Drive Optimized data. Error: {ex}"); + Log.Error(ex, $"Error loading Dev Drive Optimized data. Error: {ex}"); } } @@ -237,17 +259,60 @@ public void UpdateListViewModelList() EnvironmentVariable = "PIP_CACHE_DIR", CacheDirectory = new List { - Path.Join(_localAppDataPath, "pip", "cache"), - Path.Join(_localAppDataPath, "packages", "PythonSoftwareFoundation.Python"), + Path.Join(_localAppDataPath, "pip", CacheStr), + Path.Join(_localAppDataPath, PackagesStr, "PythonSoftwareFoundation.Python"), }, - ExampleDirectory = Path.Join("D:", "packages", "pip", "cache"), + ExampleSubDirectory = Path.Join(PackagesStr, "pip", CacheStr), }, new DevDriveCacheData { CacheName = "NuGet cache (dotnet)", EnvironmentVariable = "NUGET_PACKAGES", - CacheDirectory = new List { Path.Join(_userProfilePath, ".nuget", "packages") }, - ExampleDirectory = Path.Join("D:", "packages", "NuGet", "Cache"), + CacheDirectory = new List { Path.Join(_userProfilePath, ".nuget", PackagesStr) }, + ExampleSubDirectory = Path.Join(PackagesStr, "NuGet", CacheStr), + }, + new DevDriveCacheData + { + CacheName = "Npm cache (NodeJS)", + EnvironmentVariable = "NPM_CONFIG_CACHE", + CacheDirectory = new List + { + Path.Join(_appDataPath, "npm-cache"), + Path.Join(_localAppDataPath, "npm-cache"), + }, + ExampleSubDirectory = Path.Join(PackagesStr, "npm"), + }, + new DevDriveCacheData + { + CacheName = "Vcpkg cache", + EnvironmentVariable = "VCPKG_DEFAULT_BINARY_CACHE", + CacheDirectory = new List + { + Path.Join(_appDataPath, "vcpkg", ArchivesStr), + Path.Join(_localAppDataPath, "vcpkg", ArchivesStr), + }, + ExampleSubDirectory = Path.Join(PackagesStr, "vcpkg"), + }, + new DevDriveCacheData + { + CacheName = "Cargo cache (Rust)", + EnvironmentVariable = "CARGO_HOME", + CacheDirectory = new List { Path.Join(_userProfilePath, ".cargo") }, + ExampleSubDirectory = Path.Join(PackagesStr, "cargo"), + }, + new DevDriveCacheData + { + CacheName = "Maven cache (Java)", + EnvironmentVariable = "MAVEN_OPTS", + CacheDirectory = new List { Path.Join(_userProfilePath, ".m2") }, + ExampleSubDirectory = Path.Join(PackagesStr, "m2"), + }, + new DevDriveCacheData + { + CacheName = "Gradle cache (Java)", + EnvironmentVariable = "GRADLE_USER_HOME", + CacheDirectory = new List { Path.Join(_userProfilePath, ".gradle") }, + ExampleSubDirectory = Path.Join(PackagesStr, "gradle"), } ]; @@ -261,13 +326,13 @@ public void UpdateListViewModelList() } else { - var subDirectories = Directory.GetDirectories(_localAppDataPath + "\\Packages", "*", SearchOption.TopDirectoryOnly); + var subDirectories = Directory.GetDirectories(Path.Join(_localAppDataPath, PackagesStr), "*", SearchOption.TopDirectoryOnly); var matchingSubdirectory = subDirectories.FirstOrDefault(subdir => subdir.StartsWith(cacheDirectory, StringComparison.OrdinalIgnoreCase)); if (Directory.Exists(matchingSubdirectory)) { if (matchingSubdirectory.Contains("PythonSoftwareFoundation")) { - return Path.Join(matchingSubdirectory, "LocalCache", "Local", "pip", "cache"); + return Path.Join(matchingSubdirectory, "LocalCache", "Local", "pip", CacheStr); } return matchingSubdirectory; @@ -307,12 +372,16 @@ public void UpdateOptimizerListViewModelList() continue; } + List existingDevDriveLetters = ExistingDevDrives.Select(x => x.DriveLetter.ToString()).ToList(); + + var exampleDirectory = Path.Join(existingDevDriveLetters[0] + ":", cache.ExampleSubDirectory); var card = new DevDriveOptimizerCardViewModel( _optimizeDevDriveDialogViewModelFactory, cache.CacheName!, existingCacheLocation, - cache.ExampleDirectory!, // example location on dev drive to move cache to - cache.EnvironmentVariable!); // environmentVariableToBeSet + exampleDirectory!, // example location on dev drive to move cache to + cache.EnvironmentVariable!, // environmentVariableToBeSet + existingDevDriveLetters); DevDriveOptimizerCardCollection.Add(card); } diff --git a/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs index cff6dc052c..b54ea197af 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs @@ -3,12 +3,14 @@ using System; using System.Collections.ObjectModel; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; using DevHome.Common.Models; using DevHome.Common.Services; +using Microsoft.UI.Xaml; using Windows.System; namespace DevHome.Customization.ViewModels; @@ -45,4 +47,6 @@ private void NavigateToDevDriveInsightsPage() { NavigationService.NavigateTo(typeof(DevDriveInsightsViewModel).FullName!); } + + public bool AnyDevDrivesPresent => Application.Current.GetService().GetAllDevDrivesThatExistOnSystem().Any(); } diff --git a/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs index 11a9ac4b8e..c7d360562a 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs @@ -110,7 +110,7 @@ public void QuietButtonClicked() catch (Exception ex) { SessionStateText = GetStatusString("SessionError"); - _log.Error("QuietBackgroundProcessesSession::Start failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::Start failed"); } } else @@ -124,7 +124,7 @@ public void QuietButtonClicked() catch (Exception ex) { SessionStateText = GetStatusString("UnableToCancelSession"); - _log.Error("QuietBackgroundProcessesSession::Stop failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::Stop failed"); } } } @@ -142,7 +142,7 @@ private bool GetIsActive() catch (Exception ex) { SessionStateText = GetStatusString("SessionError"); - _log.Error("QuietBackgroundProcessesSession::IsActive failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::IsActive failed"); } return false; @@ -157,7 +157,7 @@ private int GetTimeRemaining() catch (Exception ex) { SessionStateText = GetStatusString("SessionError"); - _log.Error("QuietBackgroundProcessesSession::TimeLeftInSeconds failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::TimeLeftInSeconds failed"); return 0; } } diff --git a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml index abce444f88..e9ad085133 100644 --- a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml +++ b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml @@ -1,22 +1,15 @@ - - - - - - - - - - - - - + + + + + + + + diff --git a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml index 64ad93ec49..c7b8080aae 100644 --- a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml +++ b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml @@ -22,6 +22,7 @@ AutomationProperties.AccessibilityView="Control" AutomationProperties.AutomationId="NavigateDevDriveInsightsCardButton" Command="{x:Bind ViewModel.NavigateToDevDriveInsightsPageCommand}" + Visibility="{x:Bind ViewModel.AnyDevDrivesPresent}" IsClickEnabled="True" > diff --git a/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs b/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs index 8191dc757f..b6cde57e4b 100644 --- a/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs +++ b/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs @@ -1,12 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; using DevHome.Customization.ViewModels.DevDriveInsights; using Microsoft.UI.Xaml.Controls; namespace DevHome.Customization.Views; -public delegate OptimizeDevDriveDialogViewModel OptimizeDevDriveDialogViewModelFactory(string existingCacheLocation, string environmentVariableToBeSet); +public delegate OptimizeDevDriveDialogViewModel OptimizeDevDriveDialogViewModelFactory( + string existingCacheLocation, + string environmentVariableToBeSet, + string exampleDevDriveLocation, + List existingDevDriveLetters); public sealed partial class OptimizeDevDriveDialog : ContentDialog { diff --git a/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj b/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj index fb618c002f..32d898a429 100644 --- a/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj +++ b/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj @@ -3,7 +3,7 @@ Dashboard.Test x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 false enable enable diff --git a/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs b/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs index 10237c3949..9af21e4ed2 100644 --- a/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs +++ b/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs @@ -47,7 +47,7 @@ public void RemoveFromSelection() public void Select() { - IsSelected = true; + Invoke(); } protected override string GetClassNameCore() diff --git a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs index d3b607beaa..b4a2471d87 100644 --- a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs @@ -105,6 +105,8 @@ private async void OnRemoveWidgetClick(object sender, RoutedEventArgs e) var widgetIdToDelete = widgetViewModel.Widget.Id; var widgetToDelete = widgetViewModel.Widget; _log.Debug($"User removed widget, delete widget {widgetIdToDelete}"); + var stringResource = new StringResource("DevHome.Dashboard.pri", "DevHome.Dashboard/Resources"); + Application.Current.GetService().Announce(stringResource.GetLocalized("WidgetRemoved")); DashboardView.PinnedWidgets.Remove(widgetViewModel); try { @@ -113,7 +115,7 @@ private async void OnRemoveWidgetClick(object sender, RoutedEventArgs e) } catch (Exception ex) { - _log.Error($"Didn't delete Widget {widgetIdToDelete}", ex); + _log.Error(ex, $"Didn't delete Widget {widgetIdToDelete}"); } } } @@ -202,7 +204,7 @@ private void MarkSize(SelectableMenuFlyoutItem menuSizeItem) }; menuSizeItem.Icon = fontIcon; var peer = FrameworkElementAutomationPeer.FromElement(menuSizeItem) as SelectableMenuFlyoutItemAutomationPeer; - peer.Select(); + peer.AddToSelection(); } private void AddCustomizeToWidgetMenu(MenuFlyout widgetMenuFlyout, WidgetViewModel widgetViewModel) diff --git a/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj b/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj index 4c193d6e61..c3c1abe0b7 100644 --- a/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj +++ b/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj @@ -3,7 +3,7 @@ DevHome.Dashboard x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 true Microsoft.Windows.Widgets.Hosts diff --git a/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs index 9295b65842..30d5e77417 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs @@ -115,7 +115,7 @@ private async Task UpdateHostConfig() } catch (Exception ex) { - _log.Error("Error retrieving HostConfig", ex); + _log.Error(ex, "Error retrieving HostConfig"); } _windowEx.DispatcherQueue.TryEnqueue(() => diff --git a/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs index 175e47f5a7..2099ed7bef 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs @@ -119,7 +119,7 @@ public async Task GetWidgetHostAsync() } catch (Exception ex) { - _log.Error("Exception in WidgetHost.Register:", ex); + _log.Error(ex, "Exception in WidgetHost.Register:"); } } @@ -136,7 +136,7 @@ public async Task GetWidgetCatalogAsync() } catch (Exception ex) { - _log.Error("Exception in WidgetCatalog.GetDefault:", ex); + _log.Error(ex, "Exception in WidgetCatalog.GetDefault:"); } } diff --git a/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw b/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw index b1231169a4..5ca6692709 100644 --- a/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw +++ b/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw @@ -1,5 +1,64 @@  + @@ -190,4 +249,12 @@ Close Text for a button to close a content dialog. + + Dashboard + Dashboard accessible name to be narrated + + + Widget removed + This is said by narrator whenever a widget is removed + diff --git a/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs b/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs index 5d48d0e1f9..a5197d6f9b 100644 --- a/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs +++ b/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs @@ -155,7 +155,7 @@ await Task.Run(async () => } catch (Exception ex) { - _log.Warning("There was an error expanding the Widget template with data: ", ex); + _log.Warning(ex, "There was an error expanding the Widget template with data: "); ShowErrorCard("WidgetErrorCardDisplayText"); return; } @@ -192,7 +192,7 @@ await Task.Run(async () => } catch (Exception ex) { - _log.Error("Error rendering widget card: ", ex); + _log.Error(ex, "Error rendering widget card: "); WidgetFrameworkElement = GetErrorCard("WidgetErrorCardDisplayText"); } }); diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml index 22512232aa..c2f82f92e8 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml @@ -3,6 +3,7 @@ SubscribeToWidgetCatalogEventsAsync() } catch (Exception ex) { - _log.Error("Exception in SubscribeToWidgetCatalogEvents:", ex); + _log.Error(ex, "Exception in SubscribeToWidgetCatalogEvents:"); return false; } @@ -287,7 +287,7 @@ private async Task RestorePinnedWidgetsAsync(Widget[] hostWidgets) } catch (Exception ex) { - _log.Error($"RestorePinnedWidgets(): ", ex); + _log.Error(ex, $"RestorePinnedWidgets(): "); } } @@ -374,7 +374,7 @@ private async Task PinDefaultWidgetAsync(WidgetDefinition defaultWidgetDefinitio } catch (Exception ex) { - _log.Error($"PinDefaultWidget failed: ", ex); + _log.Error(ex, $"PinDefaultWidget failed: "); } } @@ -424,7 +424,7 @@ public async Task AddWidgetClickAsync() } catch (Exception ex) { - _log.Warning($"Creating widget failed: ", ex); + _log.Warning(ex, $"Creating widget failed: "); var mainWindow = Application.Current.GetService(); var stringResource = new StringResource("DevHome.Dashboard.pri", "DevHome.Dashboard/Resources"); await mainWindow.ShowErrorMessageDialogAsync( @@ -464,7 +464,7 @@ await Task.Run(async () => { // TODO Support concurrency in dashboard. Today concurrent async execution can cause insertion errors. // https://github.com/microsoft/devhome/issues/1215 - _log.Warning($"Couldn't insert pinned widget", ex); + _log.Warning(ex, $"Couldn't insert pinned widget"); } }); } @@ -479,7 +479,7 @@ await Task.Run(async () => } catch (Exception ex) { - _log.Information($"Error deleting widget", ex); + _log.Information(ex, $"Error deleting widget"); } } }); diff --git a/tools/Environments/DevHome.Environments/DevHome.Environments.csproj b/tools/Environments/DevHome.Environments/DevHome.Environments.csproj index 3bfdf0d8a1..489146ca5d 100644 --- a/tools/Environments/DevHome.Environments/DevHome.Environments.csproj +++ b/tools/Environments/DevHome.Environments/DevHome.Environments.csproj @@ -3,7 +3,7 @@ DevHome.Environments x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 enable true @@ -28,7 +28,6 @@ - diff --git a/tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs b/tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs new file mode 100644 index 0000000000..c87dc219a9 --- /dev/null +++ b/tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Environments.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.DevHome.SDK; + +namespace DevHome.Environments.Selectors; + +public class CardItemTemplateSelector : DataTemplateSelector +{ + public DataTemplate? CreateComputeSystemOperationTemplate { get; set; } + + public DataTemplate? ComputeSystemTemplate { get; set; } + + protected override DataTemplate? SelectTemplateCore(object item) + { + return ResolveDataTemplate(item); + } + + protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container) + { + return ResolveDataTemplate(item); + } + + /// + /// Resolves the data template based on the if the ComputeSystemsListViewModel currently containers any ComputeSystemWrappers. + /// + /// The ComputeSystemsListViewModel object + private DataTemplate? ResolveDataTemplate(object item) + { + if (item is CreateComputeSystemOperationViewModel) + { + return CreateComputeSystemOperationTemplate; + } + else + { + return ComputeSystemTemplate; + } + } +} diff --git a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw index b7f534243f..67bfbae855 100644 --- a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw +++ b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw @@ -126,6 +126,10 @@ Sync Text for the sync button on the top right side + + Create Environment + Text for button that will redirect the user to the Create Environment page in Dev Home + Environments Title text for the main landing page @@ -190,4 +194,28 @@ All Text for the default value of all providers for filtering + + Remove + Text for the remove button when the user wants to remove an environment being created from the UI + + + Failed to create environment due to error: {0} + Locked="{0}" Text to show the the user when Dev Home receives the notification that the creation of the new environment has failed. {0} is the error that is displayed to the user + + + The {0} provider has completed its creation steps. Re-sync the page to manage you're new environment + Locked="{0}" Text to show the the user when Dev Home receives the notification that the users new environment has been created. {0} is the of the provider who sent the notification + + + Status: {0} + Locked="{0}" provides the user with status message updates about a currently running operation. {0} is the status message that will appear in the UI + + + Environments being created: + header text for the cards in the UI that will be displayed when they are being created + + + Manage environments: + Header text for the cards that in the UI that are not being created. + \ No newline at end of file diff --git a/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs b/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs index aba63a03af..4a066c0e05 100644 --- a/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs +++ b/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs @@ -12,7 +12,9 @@ namespace DevHome.Environments.TestModels; public class TestExtensionWrapper : IExtensionWrapper { - public string Name => throw new NotImplementedException(); + public string PackageDisplayName => throw new NotImplementedException(); + + public string ExtensionDisplayName => throw new NotImplementedException(); public string PackageFullName => "Microsoft.Windows.DevHome.Dev_0.0.0.0_x64__8wekyb3d8bbwe"; diff --git a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs new file mode 100644 index 0000000000..7be98f72e5 --- /dev/null +++ b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Environments.Models; +using Microsoft.UI.Xaml.Media.Imaging; +using Microsoft.Windows.DevHome.SDK; + +namespace DevHome.Environments.ViewModels; + +/// +/// Base class for all compute system cards that will appear in the UI. +/// +public abstract partial class ComputeSystemCardBase : ObservableObject +{ + public string Name { get; protected set; } = string.Empty; + + public string AlternativeName { get; protected set; } = string.Empty; + + public DateTime LastConnected { get; protected set; } = DateTime.Now; + + public bool IsCreateComputeSystemOperation { get; protected set; } + + // Will hold the supported actions that the user can perform on in the UI. E.g Remove button + public ObservableCollection? DotOperations { get; protected set; } + + [ObservableProperty] + private ComputeSystemState _state; + + [ObservableProperty] + private bool _isCreationInProgress; + + [ObservableProperty] + private CardStateColor _stateColor; + + public BitmapImage? HeaderImage { get; protected set; } = new(); + + public BitmapImage? BodyImage { get; protected set; } = new(); + + public ComputeSystem? ComputeSystem { get; protected set; } + + public string ProviderDisplayName { get; protected set; } = string.Empty; + + [ObservableProperty] + private string _uiMessageToDisplay = string.Empty; +} diff --git a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs index a411effee6..1c23259c4a 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs @@ -23,44 +23,21 @@ namespace DevHome.Environments.ViewModels; /// View model for a compute system. Each 'card' in the UI represents a compute system. /// Contains an instance of the compute system object as well. /// -public partial class ComputeSystemViewModel : ObservableObject +public partial class ComputeSystemViewModel : ComputeSystemCardBase { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ComputeSystemViewModel)); private readonly WindowEx _windowEx; - public string Name => ComputeSystem.DisplayName; - private readonly IComputeSystemManager _computeSystemManager; - public ComputeSystem ComputeSystem { get; } - - public string AlternativeName { get; } = string.Empty; - - public DateTime LastConnected { get; set; } = DateTime.Now; - - public string Type { get; } - public bool IsOperationInProgress { get; set; } // Launch button operations public ObservableCollection LaunchOperations { get; set; } - // Dot button operations - public ObservableCollection DotOperations { get; set; } - public ObservableCollection Properties { get; set; } = new(); - [ObservableProperty] - private ComputeSystemState _state; - - [ObservableProperty] - private CardStateColor _stateColor; - - public BitmapImage? HeaderImage { get; set; } = new(); - - public BitmapImage? BodyImage { get; set; } = new(); - public string PackageFullName { get; set; } public ComputeSystemViewModel( @@ -74,8 +51,9 @@ public ComputeSystemViewModel( _computeSystemManager = manager; ComputeSystem = new(system); - Type = provider.DisplayName; + ProviderDisplayName = provider.DisplayName; PackageFullName = packageFullName; + Name = ComputeSystem.DisplayName; if (!string.IsNullOrEmpty(ComputeSystem.SupplementalDisplayName)) { @@ -98,7 +76,7 @@ public async Task InitializeCardDataAsync() private async Task InitializeStateAsync() { - var result = await ComputeSystem.GetStateAsync(); + var result = await ComputeSystem!.GetStateAsync(); if (result.Result.Status == ProviderOperationStatus.Failure) { _log.Error($"Failed to get state for {ComputeSystem.DisplayName} due to {result.Result.DiagnosticText}"); @@ -110,12 +88,12 @@ private async Task InitializeStateAsync() private async Task SetBodyImageAsync() { - BodyImage = await ComputeSystemHelpers.GetBitmapImageAsync(ComputeSystem); + BodyImage = await ComputeSystemHelpers.GetBitmapImageAsync(ComputeSystem!); } private async Task SetPropertiesAsync() { - foreach (var property in await ComputeSystemHelpers.GetComputeSystemPropertiesAsync(ComputeSystem, PackageFullName)) + foreach (var property in await ComputeSystemHelpers.GetComputeSystemPropertiesAsync(ComputeSystem!, PackageFullName)) { Properties.Add(property); } @@ -125,7 +103,7 @@ public void OnComputeSystemStateChanged(ComputeSystem sender, ComputeSystemState { _windowEx.DispatcherQueue.TryEnqueue(() => { - if (sender.Id == ComputeSystem.Id) + if (sender.Id == ComputeSystem!.Id) { State = state; StateColor = ComputeSystemHelpers.GetColorBasedOnState(state); @@ -135,7 +113,7 @@ public void OnComputeSystemStateChanged(ComputeSystem sender, ComputeSystemState public void RemoveStateChangedHandler() { - ComputeSystem.StateChanged -= _computeSystemManager.OnComputeSystemStateChanged; + ComputeSystem!.StateChanged -= _computeSystemManager.OnComputeSystemStateChanged; _computeSystemManager.ComputeSystemStateChanged -= OnComputeSystemStateChanged; } @@ -148,7 +126,7 @@ public void LaunchAction() Task.Run(async () => { IsOperationInProgress = true; - await ComputeSystem.ConnectAsync(string.Empty); + await ComputeSystem!.ConnectAsync(string.Empty); IsOperationInProgress = false; }); } diff --git a/tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs new file mode 100644 index 0000000000..d8bbf389c5 --- /dev/null +++ b/tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Environments.Models; +using DevHome.Common.Environments.Services; +using DevHome.Common.Services; +using Microsoft.UI.Xaml.Media.Imaging; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using WinUIEx; + +namespace DevHome.Environments.ViewModels; + +/// +/// Represents a view model for the create compute system operation that will appear in the UI +/// +public partial class CreateComputeSystemOperationViewModel : ComputeSystemCardBase +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ComputeSystemViewModel)); + + private readonly IComputeSystemManager _computeSystemManager; + + private readonly WindowEx _windowEx; + + private readonly StringResource _stringResource; + + /// + /// This glyph can be found in the Fluent UI MDL2 Assets font that ships with Windows. + /// Check the Win UI 3 gallery for the visual representation of the glyph. It is + /// the trash can icon. + /// + private readonly string _cancelationUniCodeForGlyph = "\uE74D"; + + public string EnvironmentName => Operation.EnvironmentName; + + /// + /// Callback action to remove the view model from the view. + /// + private readonly Func _removalAction; + + public CreateComputeSystemOperation Operation { get; } + + public CreateComputeSystemOperationViewModel( + IComputeSystemManager computeSystemManager, + StringResource stringResource, + WindowEx windowsEx, + Func removalAction, + CreateComputeSystemOperation operation) + { + IsCreationInProgress = true; + _windowEx = windowsEx; + _removalAction = removalAction; + _stringResource = stringResource; + _computeSystemManager = computeSystemManager; + Operation = operation; + + var providerDetails = Operation.ProviderDetails; + ProviderDisplayName = providerDetails.ComputeSystemProvider.DisplayName; + IsCreateComputeSystemOperation = true; + + // Hook up event handlers to the operation + Operation.Completed += OnOperationCompleted; + Operation.Progress += OnOperationProgressChanged; + + // make sure the last update appears in the UI if the operation is already completed at this point + UpdateUiMessage(Operation.LastProgressMessage, Operation.LastProgressPercentage); + + // Update the state of the card + State = ComputeSystemState.Creating; + StateColor = CardStateColor.Caution; + + // Setup the button to remove the the view model from the UI and the header Image + DotOperations = new ObservableCollection() { new(_stringResource.GetLocalized("RemoveButtonTextForCreateComputeSystem"), _cancelationUniCodeForGlyph, RemoveViewModelFromUI) }; + HeaderImage = CardProperty.ConvertMsResourceToIcon(providerDetails.ComputeSystemProvider.Icon, providerDetails.ExtensionWrapper.PackageFullName); + + // If the operation is already completed update the status + if (operation.CreateComputeSystemResult != null) + { + UpdateStatusIfCompleted(operation.CreateComputeSystemResult); + } + } + + private void OnOperationCompleted(object sender, CreateComputeSystemResult createComputeSystemResult) + { + UpdateStatusIfCompleted(createComputeSystemResult); + } + + private void UpdateStatusIfCompleted(CreateComputeSystemResult createComputeSystemResult) + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + // Update the creation status + IsCreationInProgress = false; + if (createComputeSystemResult.Result.Status == ProviderOperationStatus.Success) + { + UpdateUiMessage(_stringResource.GetLocalized("SuccessMessageForCreateComputeSystem", ProviderDisplayName)); + ComputeSystem = new(createComputeSystemResult.ComputeSystem); + State = ComputeSystemState.Created; + StateColor = CardStateColor.Success; + } + else + { + UpdateUiMessage(_stringResource.GetLocalized("FailureMessageForCreateComputeSystem", createComputeSystemResult.Result.DisplayMessage)); + State = ComputeSystemState.Unknown; + StateColor = CardStateColor.Failure; + } + + RemoveEventHandlers(); + }); + } + + private void OnOperationProgressChanged(object sender, CreateComputeSystemProgressEventArgs args) + { + UpdateUiMessage(args.Status, args.PercentageCompleted); + } + + public void RemoveEventHandlers() + { + Operation.Completed -= OnOperationCompleted; + Operation.Progress -= OnOperationProgressChanged; + } + + private void UpdateUiMessage(string operationStatus, uint percentage = 0) + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + if (operationStatus == null) + { + return; + } + + var percentageString = percentage == 0 ? string.Empty : $"({percentage}%)"; + UiMessageToDisplay = _stringResource.GetLocalized("CreationStatusTextForCreateEnvironmentFlow", $"{operationStatus} {percentageString}"); + }); + } + + private void RemoveViewModelFromUI() + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + _removalAction(this); + RemoveEventHandlers(); + Operation.CancelOperation(); + _computeSystemManager.RemoveOperation(Operation); + }); + } +} diff --git a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs index ce8a84fadb..3b0d268cfe 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; @@ -36,15 +37,19 @@ public partial class LandingPageViewModel : ObservableObject, IDisposable private readonly IComputeSystemManager _computeSystemManager; + private readonly INavigationService _navigationService; + private readonly StringResource _stringResource; private readonly object _lock = new(); + private bool _wasSyncButtonClicked; + public bool IsLoading { get; set; } - public ObservableCollection ComputeSystems { get; set; } = new(); + public ObservableCollection ComputeSystemCards { get; set; } = new(); - public AdvancedCollectionView ComputeSystemsView { get; set; } + public AdvancedCollectionView ComputeSystemCardsView { get; set; } public bool HasPageLoadedForTheFirstTime { get; set; } @@ -60,11 +65,15 @@ public partial class LandingPageViewModel : ObservableObject, IDisposable [ObservableProperty] private string _lastSyncTime; + [ObservableProperty] + private bool _shouldShowCreationHeader; + public ObservableCollection Providers { get; set; } private CancellationTokenSource _cancellationTokenSource = new(); public LandingPageViewModel( + INavigationService navigationService, IComputeSystemManager manager, EnvironmentsExtensionsService extensionsService, NotificationService notificationService, @@ -74,6 +83,7 @@ public LandingPageViewModel( _extensionsService = extensionsService; _notificationService = notificationService; _windowEx = windowEx; + _navigationService = navigationService; _stringResource = new StringResource("DevHome.Environments.pri", "DevHome.Environments/Resources"); @@ -81,7 +91,7 @@ public LandingPageViewModel( Providers = new() { _stringResource.GetLocalized("AllProviders") }; _lastSyncTime = _stringResource.GetLocalized("MomentsAgo"); - ComputeSystemsView = new AdvancedCollectionView(ComputeSystems); + ComputeSystemCardsView = new AdvancedCollectionView(ComputeSystemCards); } public void Initialize(StackedNotificationsBehavior notificationQueue) @@ -99,18 +109,28 @@ public async Task SyncButton() SelectedSortIndex = -1; Providers = new ObservableCollection { _stringResource.GetLocalized("AllProviders") }; SelectedProviderIndex = 0; + _wasSyncButtonClicked = true; // Reset the old sync timer _cancellationTokenSource.Cancel(); await _windowEx.DispatcherQueue.EnqueueAsync(() => LastSyncTime = _stringResource.GetLocalized("MomentsAgo")); + // We need to signal to the compute system manager that it can remove all the completed operations now that + // we're done showing them in the view. + _computeSystemManager.RemoveAllCompletedOperations(); await LoadModelAsync(); + _wasSyncButtonClicked = false; + } - // Start a new sync timer - _ = Task.Run(async () => - { - await RunSyncTimmer(); - }); + /// + /// Navigates the user to the select environments page in the setup flow. This is the first page in the create environment + /// process. + /// + [RelayCommand] + public void CreateEnvironmentButton() + { + _log.Information("User clicked on the create environment button. Navigating to Select environment page in Setup flow"); + _navigationService.NavigateTo(KnownPageKeys.SetupFlow, "startCreationFlow"); } // Updates the last sync time on the UI thread after set delay @@ -171,20 +191,25 @@ public async Task LoadModelAsync(bool useDebugValues = false) return; } - HasPageLoadedForTheFirstTime = true; + // If the page has already loaded once, then we don't need to re-load the compute systems as that can take a while. + // The user can click the sync button to refresh the compute systems. However, there may be new operations that have started + // since the last time the page was loaded. So we need to add those to the view model quickly. + SetupCreateComputeSystemOperationForUI(); + if (HasPageLoadedForTheFirstTime && !_wasSyncButtonClicked) + { + return; + } + IsLoading = true; } - // Start a new sync timer - _ = Task.Run(async () => + for (var i = ComputeSystemCards.Count - 1; i >= 0; i--) { - await RunSyncTimmer(); - }); - - for (var i = ComputeSystems.Count - 1; i >= 0; i--) - { - ComputeSystems[i].RemoveStateChangedHandler(); - ComputeSystems.RemoveAt(i); + if (ComputeSystemCards[i] is ComputeSystemViewModel computeSystemViewModel) + { + computeSystemViewModel.RemoveStateChangedHandler(); + ComputeSystemCards.RemoveAt(i); + } } ShowLoadingShimmer = true; @@ -194,6 +219,34 @@ public async Task LoadModelAsync(bool useDebugValues = false) lock (_lock) { IsLoading = false; + HasPageLoadedForTheFirstTime = true; + } + } + + /// + /// Sets up the view model to show the create compute system operations that the compute system manager contains. + /// + private void SetupCreateComputeSystemOperationForUI() + { + // Remove all the operations from view and then add the ones the manager has. + _log.Information($"Adding any new create compute system operations to ComputeSystemCards list"); + var curOperations = _computeSystemManager.GetRunningOperationsForCreation(); + for (var i = ComputeSystemCards.Count - 1; i >= 0; i--) + { + if (ComputeSystemCards[i].IsCreateComputeSystemOperation) + { + var operationViewModel = ComputeSystemCards[i] as CreateComputeSystemOperationViewModel; + operationViewModel!.RemoveEventHandlers(); + ComputeSystemCards.RemoveAt(i); + } + } + + // Add new operations to the list + foreach (var operation in curOperations) + { + // this is a new operation so we need to create a view model for it. + ComputeSystemCards.Add(new CreateComputeSystemOperationViewModel(_computeSystemManager, _stringResource, _windowEx, ComputeSystemCards.Remove, operation)); + _log.Information($"Found new create compute system operation for provider {operation.ProviderDetails.ComputeSystemProvider}, with name {operation.EnvironmentName}"); } } @@ -208,7 +261,7 @@ private async Task AddAllComputeSystemsFromAProvider(ComputeSystemsLoadedData da var result = mapping.Value.Result; await _notificationService.ShowNotificationAsync(provider.DisplayName, result.DisplayMessage, InfoBarSeverity.Error); - _log.Error($"Error occurred while adding Compute systems to environments page for provider: {provider.Id}", result.DiagnosticText, result.ExtendedError); + _log.Error($"Error occurred while adding Compute systems to environments page for provider: {provider.Id}. {result.DiagnosticText}, {result.ExtendedError}"); data.DevIdToComputeSystemMap.Remove(mapping.Key); } @@ -237,12 +290,13 @@ await _windowEx.DispatcherQueue.EnqueueAsync(async () => packageFullName, _windowEx); await computeSystemViewModel.InitializeCardDataAsync(); - ComputeSystems.Add(computeSystemViewModel); + + ComputeSystemCards.Add(computeSystemViewModel); } } catch (Exception ex) { - _log.Error($"Exception occurred while adding Compute systems to environments page for provider: {provider.Id}", ex); + _log.Error(ex, $"Exception occurred while adding Compute systems to environments page for provider: {provider.Id}"); } }); } @@ -253,11 +307,16 @@ await _windowEx.DispatcherQueue.EnqueueAsync(async () => [RelayCommand] public void SearchHandler(string query) { - ComputeSystemsView.Filter = system => + ComputeSystemCardsView.Filter = system => { + if (system is CreateComputeSystemOperationViewModel createComputeSystemOperationViewModel) + { + return createComputeSystemOperationViewModel.EnvironmentName.Contains(query, StringComparison.OrdinalIgnoreCase); + } + if (system is ComputeSystemViewModel computeSystemViewModel) { - var systemName = computeSystemViewModel.ComputeSystem.DisplayName; + var systemName = computeSystemViewModel.ComputeSystem!.DisplayName; var systemAltName = computeSystemViewModel.ComputeSystem.SupplementalDisplayName; return systemName.Contains(query, StringComparison.OrdinalIgnoreCase) || systemAltName.Contains(query, StringComparison.OrdinalIgnoreCase); } @@ -270,20 +329,25 @@ public void SearchHandler(string query) /// Updates the view model to filter the compute systems according to the provider. /// [RelayCommand] - public void ProviderHandler() + public void ProviderHandler(int selectedIndex) { + SelectedProviderIndex = selectedIndex; var currentProvider = Providers[SelectedProviderIndex]; - ComputeSystemsView.Filter = system => + ComputeSystemCardsView.Filter = system => { if (currentProvider.Equals(_stringResource.GetLocalized("AllProviders"), StringComparison.OrdinalIgnoreCase)) { return true; } + if (system is CreateComputeSystemOperationViewModel createComputeSystemOperationViewModel) + { + return createComputeSystemOperationViewModel.ProviderDisplayName.Equals(currentProvider, StringComparison.OrdinalIgnoreCase); + } + if (system is ComputeSystemViewModel computeSystemViewModel) { - var type = computeSystemViewModel.Type; - return type.Equals(currentProvider, StringComparison.OrdinalIgnoreCase); + return computeSystemViewModel.ProviderDisplayName.Equals(currentProvider, StringComparison.OrdinalIgnoreCase); } return false; @@ -293,27 +357,30 @@ public void ProviderHandler() /// /// Updates the view model to sort the compute systems according to the sort criteria. /// + /// + /// New SortDescription property names should be added as new properties to + /// [RelayCommand] public void SortHandler() { - ComputeSystemsView.SortDescriptions.Clear(); + ComputeSystemCardsView.SortDescriptions.Clear(); switch (SelectedSortIndex) { case 0: - ComputeSystemsView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Ascending)); + ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Ascending)); break; case 1: - ComputeSystemsView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Descending)); + ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Descending)); break; case 2: - ComputeSystemsView.SortDescriptions.Add(new SortDescription("AlternativeName", SortDirection.Ascending)); + ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("AlternativeName", SortDirection.Ascending)); break; case 3: - ComputeSystemsView.SortDescriptions.Add(new SortDescription("AlternativeName", SortDirection.Descending)); + ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("AlternativeName", SortDirection.Descending)); break; case 4: - ComputeSystemsView.SortDescriptions.Add(new SortDescription("LastConnected", SortDirection.Ascending)); + ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("LastConnected", SortDirection.Ascending)); break; } } diff --git a/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs index 4b87fa35f9..32d002d684 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs @@ -8,25 +8,44 @@ namespace DevHome.Environments.ViewModels; +public enum OperationKind +{ + ExtensionTask, + DevHomeAction, +} + /// /// Represents an operation that can be performed on a compute system. /// This is used to populate the launch and dot buttons on the compute system card. /// public partial class OperationsViewModel { + private readonly OperationKind _operationKind; + public string Name { get; } public string? Description { get; set; } public string IconGlyph { get; } - private Func> Command { get; } + private Func>? ExtensionTask { get; } + + private Action? DevHomeAction { get; } public OperationsViewModel(string name, string icon, Func> command) { + _operationKind = OperationKind.ExtensionTask; Name = name; IconGlyph = icon; - Command = command; + ExtensionTask = command; + } + + public OperationsViewModel(string name, string icon, Action command) + { + _operationKind = OperationKind.DevHomeAction; + Name = name; + IconGlyph = icon; + DevHomeAction = command; } [RelayCommand] @@ -35,7 +54,14 @@ public void InvokeAction() // We'll need to disable the card UI while the operation is in progress and handle failures. Task.Run(async () => { - await Command(string.Empty); + if (_operationKind == OperationKind.DevHomeAction) + { + DevHomeAction!(); + return; + } + + // We'll need to handle the case where the DevHome service is not available. + await ExtensionTask!(string.Empty); }); } } diff --git a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml index 4b545bab92..910e6d6cb8 100644 --- a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml +++ b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml @@ -8,13 +8,14 @@ xmlns:customControls="using:DevHome.Environments.CustomControls" xmlns:commonCustomControls="using:DevHome.Common.Environments.CustomControls" xmlns:commonModels="using:DevHome.Common.Environments.Models" + xmlns:selectors="using:DevHome.Environments.Selectors" xmlns:i="using:Microsoft.Xaml.Interactivity" xmlns:ic="using:Microsoft.Xaml.Interactions.Core" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:winUIBehaviors="using:CommunityToolkit.WinUI.Behaviors" + xmlns:converters="using:CommunityToolkit.WinUI.Converters" behaviors:NavigationViewHeaderBehavior.HeaderMode="Never" Loaded="OnLoaded"> - @@ -30,6 +31,8 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -118,8 +190,23 @@ + + @@ -142,11 +229,14 @@ + ItemsSource="{x:Bind ViewModel.Providers, Mode=OneWay}" + SelectedIndex="{x:Bind ViewModel.SelectedProviderIndex, Mode=TwoWay}"> - - + + @@ -191,44 +281,15 @@ + MaxWidth="{ThemeResource MaxPageContentWidth}" + ItemsSource="{x:Bind ViewModel.ComputeSystemCardsView}" SelectionMode="None" + ItemTemplateSelector="{StaticResource CardItemTemplateSelector}" + ItemContainerStyle="{StaticResource HorizontalCardListViewItemContainerForManagementPageStyle}"> - - - - - - - - - - - - - - - - - - - DevHome.Experiments x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 true diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj index b1c4b5baef..79a64f896f 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj @@ -3,7 +3,7 @@ DevHome.ExtensionLibrary x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 enable true diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs index 53b24f8638..f9a50252c7 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs @@ -79,7 +79,7 @@ private async Task GetInstalledExtensionsAsync() InstalledPackagesList.Clear(); - extensionWrappers = extensionWrappers.OrderBy(extensionWrapper => extensionWrapper.Name); + extensionWrappers = extensionWrappers.OrderBy(extensionWrapper => extensionWrapper.PackageDisplayName); foreach (var extensionWrapper in extensionWrappers) { @@ -90,7 +90,7 @@ private async Task GetInstalledExtensionsAsync() } var hasSettingsProvider = extensionWrapper.HasProviderType(ProviderType.Settings); - var extension = new InstalledExtensionViewModel(extensionWrapper.Name, extensionWrapper.ExtensionUniqueId, hasSettingsProvider); + var extension = new InstalledExtensionViewModel(extensionWrapper.ExtensionDisplayName, extensionWrapper.ExtensionUniqueId, hasSettingsProvider); // Each extension is shown under the package that contains it. Check if we have the package in the list // already and if not, create it and add it to the list of packages. Then add the extension to that @@ -99,7 +99,7 @@ private async Task GetInstalledExtensionsAsync() if (package == null) { package = new InstalledPackageViewModel( - extensionWrapper.Name, + extensionWrapper.PackageDisplayName, extensionWrapper.Publisher, extensionWrapper.PackageFamilyName, extensionWrapper.InstalledDate, @@ -124,7 +124,7 @@ private async Task GetStoreData() } catch (Exception ex) { - _log.Error("Error retrieving packages", ex); + _log.Error(ex, "Error retrieving packages"); ShouldShowStoreError = true; } diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs index cb1aac5ad3..3a4cb47105 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs @@ -46,7 +46,7 @@ private async Task OnSettingsContentLoadedAsync(ExtensionAdaptiveCardPanel exten if ((_navigationService.LastParameterUsed != null) && ((string)_navigationService.LastParameterUsed == extensionWrapper.ExtensionUniqueId)) { - FillBreadcrumbBar(extensionWrapper.Name); + FillBreadcrumbBar(extensionWrapper.ExtensionDisplayName); var settingsProvider = Task.Run(() => extensionWrapper.GetProviderAsync()).Result; if (settingsProvider != null) diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs index 3232666dad..7593a2ac7a 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs @@ -87,7 +87,7 @@ private void NavigateSettings() public partial class InstalledPackageViewModel : ObservableObject { [ObservableProperty] - private string _title; + private string _displayName; [ObservableProperty] private string _publisher; @@ -103,9 +103,9 @@ public partial class InstalledPackageViewModel : ObservableObject public ObservableCollection InstalledExtensionsList { get; set; } - public InstalledPackageViewModel(string title, string publisher, string packageFamilyName, DateTimeOffset installedDate, PackageVersion version) + public InstalledPackageViewModel(string displayName, string publisher, string packageFamilyName, DateTimeOffset installedDate, PackageVersion version) { - _title = title; + _displayName = displayName; _publisher = publisher; _packageFamilyName = packageFamilyName; _installedDate = installedDate; diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml index c14d73765c..5f56915dde 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml @@ -62,7 +62,7 @@ - - + + + + - @@ -116,6 +119,7 @@ Description="{x:Bind Publisher}" Margin="{ThemeResource SettingsCardMargin}" CornerRadius="3" + AutomationProperties.Name="{x:Bind Title}" IsClickEnabled="False"> diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj index 4438bed669..a66c040a14 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj @@ -6,6 +6,8 @@ enable enable + x86;x64;arm64 + win-x86;win-x64;win-arm64 $(CppBaseOutDir)\DevHome.QuietBackgroundProcesses.Common\ diff --git a/tools/SampleTool/src/SampleTool.csproj b/tools/SampleTool/src/SampleTool.csproj index a966bc9cf7..dab48d8ee9 100644 --- a/tools/SampleTool/src/SampleTool.csproj +++ b/tools/SampleTool/src/SampleTool.csproj @@ -3,7 +3,7 @@ SampleTool x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 true diff --git a/tools/SampleTool/unittest/SampleTool.UnitTest.csproj b/tools/SampleTool/unittest/SampleTool.UnitTest.csproj index 11cc24d0d9..f71eb66bfe 100644 --- a/tools/SampleTool/unittest/SampleTool.UnitTest.csproj +++ b/tools/SampleTool/unittest/SampleTool.UnitTest.csproj @@ -3,7 +3,7 @@ SampleTool.Test x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 false enable enable diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs index e8f8027d7e..f5d356cfb6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs @@ -88,7 +88,7 @@ public int FormatPartitionAsDevDrive(char curDriveLetter, string driveLabel) } catch (CimException e) { - _log.Error($"A CimException occurred while formatting Dev Drive Error.", e); + _log.Error(e, $"A CimException occurred while formatting Dev Drive Error."); return e.HResult; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj b/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj index 21153fbde9..33ab51ab0b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj @@ -3,8 +3,9 @@ DevHome.SetupFlow.Common x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 disable + false diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/DevHome.SetupFlow.ElevatedComponent.csproj b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/DevHome.SetupFlow.ElevatedComponent.csproj index f5b24448fc..fa4f3a6ebc 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/DevHome.SetupFlow.ElevatedComponent.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/DevHome.SetupFlow.ElevatedComponent.csproj @@ -6,6 +6,8 @@ enable enable + x86;x64;arm64 + win-x86;win-x64;win-arm64 @@ -29,11 +31,11 @@ - diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs index ff4a8fcd59..9e848df2dd 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs @@ -43,7 +43,7 @@ public ElevatedComponentOperation(IList tasksArgumentList) } catch (Exception e) { - _log.Error($"Failed to parse tasks arguments", e); + _log.Error(e, $"Failed to parse tasks arguments"); throw; } } @@ -182,7 +182,7 @@ private async Task ValidateAndExecuteAsync( } catch (Exception e) { - _log.Error($"Failed to validate or execute operation", e); + _log.Error(e, $"Failed to validate or execute operation"); EndOperation(taskArguments, false); throw; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs index 4238de1c5e..bad73c3373 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs @@ -56,7 +56,7 @@ public IAsyncOperation ApplyConfiguration(string fi } catch (Exception e) { - log.Error($"Failed to apply configuration.", e); + log.Error(e, $"Failed to apply configuration."); taskResult.TaskSucceeded = false; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs index 588cdab355..a90e148f7e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs @@ -100,7 +100,7 @@ public IAsyncOperation InstallPackage(string packageI } catch (Exception e) { - _log.Error("Elevated app install failed.", e); + _log.Error(e, "Elevated app install failed."); result.TaskSucceeded = false; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj b/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj index 2edbc2043b..e123a8eba7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj @@ -10,8 +10,8 @@ $(SolutionDir)\src\Assets\Canary\DevHome_Canary.ico $(SolutionDir)\src\Assets\Preview\DevHome_Preview.ico x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 - $(SolutionDir)\src\Properties\PublishProfiles\win10-$(Platform).pubxml + win-x86;win-x64;win-arm64 + $(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml enable enable DevHome.SetupFlow.ElevatedServer.Program @@ -22,7 +22,7 @@ - diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj index 8fa0ddcfbe..006b7ed1c3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj @@ -3,7 +3,7 @@ DevHome.SetupFlow.UnitTest x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 false enable enable diff --git a/tools/SetupFlow/DevHome.SetupFlow/Assets/CreateVirtualEnvironment.png b/tools/SetupFlow/DevHome.SetupFlow/Assets/CreateVirtualEnvironment.png new file mode 100644 index 0000000000..a8cb72c3a2 Binary files /dev/null and b/tools/SetupFlow/DevHome.SetupFlow/Assets/CreateVirtualEnvironment.png differ diff --git a/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml b/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml index 43ba6925a8..dc94e8e98b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml @@ -92,7 +92,7 @@ - + (Visibility)GetValue(HeaderVisibilityProperty); set => SetValue(HeaderVisibilityProperty, value); } + + public Visibility ContentVisibility + { + get => (Visibility)GetValue(ContentVisibilityProperty); + set => SetValue(ContentVisibilityProperty, value); + } public SetupShell() { @@ -66,4 +72,5 @@ public SetupShell() public static readonly DependencyProperty HeaderProperty = DependencyProperty.RegisterAttached(nameof(Header), typeof(object), typeof(SetupShell), new PropertyMetadata(null)); public static readonly DependencyProperty OrchestratorProperty = DependencyProperty.RegisterAttached(nameof(Orchestrator), typeof(SetupFlowOrchestrator), typeof(SetupShell), new PropertyMetadata(null)); public static readonly DependencyProperty HeaderVisibilityProperty = DependencyProperty.RegisterAttached(nameof(HeaderVisibility), typeof(Visibility), typeof(SetupShell), new PropertyMetadata(Visibility.Visible)); + public static readonly DependencyProperty ContentVisibilityProperty = DependencyProperty.RegisterAttached(nameof(ContentVisibility), typeof(Visibility), typeof(SetupShell), new PropertyMetadata(Visibility.Visible)); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj index 29cc40326e..2cb6173169 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj @@ -3,7 +3,7 @@ DevHome.SetupFlow x86;x64;arm64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 true diff --git a/tools/SetupFlow/DevHome.SetupFlow/Exceptions/AdaptiveCardNotRetrievedException.cs b/tools/SetupFlow/DevHome.SetupFlow/Exceptions/AdaptiveCardNotRetrievedException.cs new file mode 100644 index 0000000000..d914e7bcc1 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Exceptions/AdaptiveCardNotRetrievedException.cs @@ -0,0 +1,18 @@ +// 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 DevHome.SetupFlow.Exceptions; + +public class AdaptiveCardNotRetrievedException : Exception +{ + public AdaptiveCardNotRetrievedException(string message) + : base(message) + { + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs b/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs index d59e104b7d..d51f380473 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs @@ -10,6 +10,7 @@ using DevHome.SetupFlow.Services.WinGet.Operations; using DevHome.SetupFlow.TaskGroups; using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; @@ -33,6 +34,7 @@ public static IServiceCollection AddSetupFlow(this IServiceCollection services, services.AddReview(); services.AddSummary(); services.AddSummaryInformation(); + services.AddCreateEnvironment(); // View-models services.AddSingleton(); @@ -194,4 +196,18 @@ private static IServiceCollection AddSetupTarget(this IServiceCollection service return services; } + + private static IServiceCollection AddCreateEnvironment(this IServiceCollection services) + { + // Task groups + services.AddTransient(); + services.AddTransient(); + + // View models + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + return services; + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs index 0a9e2f0853..440dd217f7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs @@ -232,7 +232,7 @@ IAsyncOperation ISetupTask.Execute() if (result.Status == ProviderOperationStatus.Failure) { - _log.Error($"Could not clone {RepositoryToClone.DisplayName} because {result.DisplayMessage}", result.ExtendedError); + _log.Error(result.ExtendedError, $"Could not clone {RepositoryToClone.DisplayName} because {result.DisplayMessage}"); TelemetryFactory.Get().LogError("CloneTask_CouldNotClone_Event", LogLevel.Critical, new ExceptionEvent(result.ExtendedError.HResult, result.DisplayMessage)); _actionCenterErrorMessage.PrimaryMessage = _stringResource.GetLocalized(StringResourceKey.CloneRepoErrorForActionCenter, RepositoryToClone.DisplayName, result.DisplayMessage); @@ -242,7 +242,7 @@ IAsyncOperation ISetupTask.Execute() } catch (Exception e) { - _log.Error($"Could not clone {RepositoryToClone.DisplayName}", e); + _log.Error(e, $"Could not clone {RepositoryToClone.DisplayName}"); _actionCenterErrorMessage.PrimaryMessage = _stringResource.GetLocalized(StringResourceKey.CloneRepoErrorForActionCenter, RepositoryToClone.DisplayName, e.HResult.ToString("X", CultureInfo.CurrentCulture)); TelemetryFactory.Get().LogError("CloneTask_CouldNotClone_Event", LogLevel.Critical, new ExceptionEvent(e.HResult)); return TaskFinishedState.Failure; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs index eebbf5f94c..a88c3c2d62 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs @@ -124,7 +124,7 @@ public void SetIcon(ElementTheme theme) } catch (Exception e) { - _log.Error(e.Message, e); + _log.Error(e, e.Message); RepositoryTypeIcon = GetGitIcon(theme); return; } @@ -259,7 +259,7 @@ public string RepositoryProviderDisplayName } catch (Exception e) { - _log.Error(e.Message, e); + _log.Error(e, e.Message); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs index a7fe05d628..c97ca72986 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs @@ -214,7 +214,7 @@ public void OnApplyConfigurationOperationChanged(object sender, SDK.Configuratio } catch (Exception ex) { - _log.Error($"Failed to process configuration progress data on target machine.'{ComputeSystemName}'", ex); + _log.Error(ex, $"Failed to process configuration progress data on target machine.'{ComputeSystemName}'"); } } @@ -254,7 +254,7 @@ public void HandleCompletedOperation(SDK.ApplyConfigurationResult applyConfigura if (resultStatus == ProviderOperationStatus.Failure) { - _log.Error($"Extension failed to configure config file with exception. Diagnostic text: {result.DiagnosticText}", result.ExtendedError); + _log.Error(result.ExtendedError, $"Extension failed to configure config file with exception. Diagnostic text: {result.DiagnosticText}"); throw new SDKApplyConfigurationSetResultException(applyConfigurationResult.Result.DiagnosticText); } @@ -288,7 +288,7 @@ public void HandleCompletedOperation(SDK.ApplyConfigurationResult applyConfigura } catch (Exception ex) { - _log.Error($"Failed to apply configuration on target machine. '{ComputeSystemName}'", ex); + _log.Error(ex, $"Failed to apply configuration on target machine. '{ComputeSystemName}'"); } var tempResultInfo = !string.IsNullOrEmpty(resultInformation) ? resultInformation : string.Empty; @@ -371,7 +371,7 @@ public IAsyncOperation Execute() } catch (Exception e) { - _log.Error($"Failed to apply configuration on target machine.", e); + _log.Error(e, $"Failed to apply configuration on target machine."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs index 2235ebeeac..353e74ba90 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs @@ -107,7 +107,7 @@ IAsyncOperation ISetupTask.Execute() } catch (Exception e) { - _log.Error($"Failed to apply configuration.", e); + _log.Error(e, $"Failed to apply configuration."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs index 19e6869af9..bd43532b02 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs @@ -129,7 +129,7 @@ IAsyncOperation ISetupTask.ExecuteAsAdmin(IElevatedComponentO catch (Exception ex) { result = ex.HResult; - _log.Error($"Failed to create Dev Drive.", ex); + _log.Error(ex, $"Failed to create Dev Drive."); _actionCenterMessages.PrimaryMessage = _stringResource.GetLocalized(StringResourceKey.DevDriveErrorWithReason, _stringResource.GetLocalizedErrorMsg(ex.HResult, Identity.Component.DevDrive)); TelemetryFactory.Get().LogException("CreatingDevDriveException", ex); return TaskFinishedState.Failure; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs new file mode 100644 index 0000000000..41d541f11e --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +extern alias Projection; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.Common.Environments.Models; +using DevHome.Common.Environments.Services; +using DevHome.Common.Models; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; +using Projection::DevHome.SetupFlow.ElevatedComponent; +using Serilog; +using Windows.Foundation; +using DevHomeSDK = Microsoft.Windows.DevHome.SDK; + +namespace DevHome.SetupFlow.Models; + +/// +/// Task that creates an environment using the user input from an adaptive card session. +/// +public sealed class CreateEnvironmentTask : ISetupTask, IDisposable, IRecipient +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(CreateEnvironmentTask)); + + private readonly IComputeSystemManager _computeSystemManager; + + private readonly TaskMessages _taskMessages; + + private readonly ActionCenterMessages _actionCenterMessages = new(); + + private readonly ISetupFlowStringResource _stringResource; + + // Used to signal the task to start the creation operation. This is used when the adaptive card session ends. + private readonly AutoResetEvent _autoResetEventToStartCreationOperation = new(false); + + private readonly SetupFlowViewModel _setupFlowViewModel; + + private bool _disposedValue; + + public event ISetupTask.ChangeMessageHandler AddMessage; + + public string UserJsonInput { get; set; } + + public ComputeSystemProviderDetails ProviderDetails { get; set; } + + public DeveloperIdWrapper DeveloperIdWrapper { get; set; } + + // The "#pragma warning disable 67" directive suppresses the CS0067 warning. + // CS0067 is a warning that occurs when a public event is declared but never used. +#pragma warning disable 67 + public event ISetupTask.ChangeActionCenterMessageHandler UpdateActionCenterMessage; +#pragma warning restore 67 + + public bool RequiresAdmin => false; + + public bool RequiresReboot => false; + + public bool DependsOnDevDriveToBeInstalled => false; + + public bool CreationOperationStarted { get; private set; } + + public ISummaryInformationViewModel SummaryScreenInformation { get; } + + public CreateEnvironmentTask(IComputeSystemManager computeSystemManager, ISetupFlowStringResource stringResource, SetupFlowViewModel setupFlowViewModel) + { + _computeSystemManager = computeSystemManager; + _stringResource = stringResource; + _taskMessages = new TaskMessages + { + Executing = _stringResource.GetLocalized(StringResourceKey.StartingEnvironmentCreation), + Finished = _stringResource.GetLocalized(StringResourceKey.EnvironmentCreationOperationInitializationFinished), + Error = _stringResource.GetLocalized(StringResourceKey.EnvironmentCreationError), + }; + _setupFlowViewModel = setupFlowViewModel; + _setupFlowViewModel.EndSetupFlow += OnEndSetupFlow; + + // Register for the adaptive card session ended message so we can use the session data to create the environment + WeakReferenceMessenger.Default.Register(this); + } + + public ActionCenterMessages GetErrorMessages() => _actionCenterMessages; + + public TaskMessages GetLoadingMessages() => _taskMessages; + + public ActionCenterMessages GetRebootMessage() => new(); + + /// + /// Receives the adaptive card session ended message from the he + /// once the extension sends the session ended event. + /// + /// + /// The message payload that contains the provider and the user input json that will be used to invoke the + /// + /// + public void Receive(CreationAdaptiveCardSessionEndedMessage message) + { + _log.Information("The extension sent the session ended event"); + ProviderDetails = message.Value.ProviderDetails; + + // Json input that the user entered in the adaptive card session + UserJsonInput = message.Value.UserInputResultJson; + + // In the future we'll add the specific developer ID to the task, but for now since we haven't + // add support for switching between developer Id's in the environments pages, we'll use the first one + // in the provider details list of developer IDs. If we get here, then there should be at least one. + DeveloperIdWrapper = message.Value.ProviderDetails.DeveloperIds.First(); + + _log.Information("Signaling to the waiting event handle to Continue the 'Execute' operation"); + _autoResetEventToStartCreationOperation.Set(); + } + + private void OnEndSetupFlow(object sender, EventArgs e) + { + WeakReferenceMessenger.Default.Unregister(this); + _setupFlowViewModel.EndSetupFlow -= OnEndSetupFlow; + } + + IAsyncOperation ISetupTask.Execute() + { + return Task.Run(() => + { + _log.Information("Executing the operation. Waiting to be signalled that the adaptive card session has ended"); + + // Either wait until we're signaled to continue execution or we times out after 1 minute. If this task is initiated + // then that means the user went past the review page. At this point the extension should be firing a session ended + // event. Since the call flow is disjointed an extension may not have sent the session ended event when this method is called. + _autoResetEventToStartCreationOperation.WaitOne(TimeSpan.FromMinutes(1)); + + if (string.IsNullOrWhiteSpace(UserJsonInput)) + { + // The extension's creation adaptive card may not need user input. In that case, the user input will be null or empty. + _log.Information("UserJsonInput is null or empty."); + } + + // If the provider details are null, then we can't proceed with the operation. This happens if the auto event times out. + if (ProviderDetails == null) + { + _log.Error("ProviderDetails is null so we cannot proceed with executing the task"); + AddMessage(_stringResource.GetLocalized(StringResourceKey.EnvironmentCreationFailedToGetProviderInformation), MessageSeverityKind.Error); + return TaskFinishedState.Failure; + } + + var sdkCreateEnvironmentOperation = ProviderDetails.ComputeSystemProvider.CreateCreateComputeSystemOperation(DeveloperIdWrapper.DeveloperId, UserJsonInput); + var createComputeSystemOperationWrapper = new CreateComputeSystemOperation(sdkCreateEnvironmentOperation, ProviderDetails, UserJsonInput); + + // Start the operation, which returns immediately and runs in the background. + createComputeSystemOperationWrapper.StartOperation(); + AddMessage(_stringResource.GetLocalized(StringResourceKey.EnvironmentCreationForProviderStarted), MessageSeverityKind.Info); + + _computeSystemManager.AddRunningOperationForCreation(createComputeSystemOperationWrapper); + CreationOperationStarted = true; + + _log.Information("Successfully started the creation operation"); + return TaskFinishedState.Success; + }).AsAsyncOperation(); + } + + IAsyncOperation ISetupTask.ExecuteAsAdmin(IElevatedComponentOperation elevatedComponentOperation) + { + return Task.Run(() => + { + // No admin rights required for this task. This shouldn't ever be invoked since the RequiresAdmin property is always false. + _log.Error("Admin execution is not required for the create environment task"); + return TaskFinishedState.Failure; + }).AsAsyncOperation(); + } + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _autoResetEventToStartCreationOperation.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedData.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedData.cs new file mode 100644 index 0000000000..30214eb835 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedData.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Environments.Models; + +namespace DevHome.SetupFlow.Models.Environments; + +/// +/// Data payload for when the +/// session Ends. This data is used to send the user input from an adaptive card session back to an object +/// that subscribes to the +/// event. +/// +public class CreationAdaptiveCardSessionEndedData +{ + /// + /// Gets the JSON string of the user input from the adaptive card session + /// + public string UserInputResultJson { get; private set; } + + /// + /// Gets the provider details for the compute system provider. + /// + public ComputeSystemProviderDetails ProviderDetails { get; private set; } + + public CreationAdaptiveCardSessionEndedData(string userInputResultJson, ComputeSystemProviderDetails providerDetails) + { + UserInputResultJson = userInputResultJson; + ProviderDetails = providerDetails; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedMessage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedMessage.cs new file mode 100644 index 0000000000..b6b8439913 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedMessage.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace DevHome.SetupFlow.Models.Environments; + +/// +/// Message for sending the data payload for the +/// object's session back to a subscriber when the session ends. +/// +public class CreationAdaptiveCardSessionEndedMessage : ValueChangedMessage +{ + public CreationAdaptiveCardSessionEndedMessage(CreationAdaptiveCardSessionEndedData value) + : base(value) + { + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsReviewPageDataRequestMessage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsReviewPageDataRequestMessage.cs new file mode 100644 index 0000000000..52a595de06 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsReviewPageDataRequestMessage.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace DevHome.SetupFlow.Models.Environments; + +/// +/// Message for requesting a rendered adaptive card that was created from a +/// object in one view model to a view. +/// +/// +/// This is used when a view that displays an adaptive card needs to request the rendered adaptive card from the view model. +/// The view in this case would not want to using Binding to bind to the adaptive card, but instead request it and then manually +/// add it to its UI. This prevents xaml binding crashes with "Element is already the child of another element" exceptions. +/// +public sealed class CreationOptionsReviewPageDataRequestMessage : RequestMessage +{ +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsViewPageRequestMessage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsViewPageRequestMessage.cs new file mode 100644 index 0000000000..ed4d2796cf --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsViewPageRequestMessage.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace DevHome.SetupFlow.Models.Environments; + +/// +/// Message for requesting a rendered adaptive card that was created from a +/// object in one view model to a view. +/// +/// +/// This is used when a view that displays an adaptive card needs to request the rendered adaptive card from the view model. +/// The view in this case would not want to using Binding to bind to the adaptive card, but instead request it and then manually +/// add it to its UI. This prevents xaml binding crashes with "Element is already the child of another element" exceptions. +/// +public sealed class CreationOptionsViewPageRequestMessage : RequestMessage +{ +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationProviderChangedMessage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationProviderChangedMessage.cs new file mode 100644 index 0000000000..8ff238af50 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationProviderChangedMessage.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.Messaging.Messages; +using DevHome.Common.Environments.Models; + +namespace DevHome.SetupFlow.Models.Environments; + +/// +/// Message for sending the from one view model to +/// another view model when the provider changes. +/// +public class CreationProviderChangedMessage : ValueChangedMessage +{ + public CreationProviderChangedMessage(ComputeSystemProviderDetails value) + : base(value) + { + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/NewAdaptiveCardAvailableMessage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/NewAdaptiveCardAvailableMessage.cs new file mode 100644 index 0000000000..2f5493b924 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/NewAdaptiveCardAvailableMessage.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace DevHome.SetupFlow.Models.Environments; + +/// +/// Message for sending a rendered adaptive card that was created from a +/// object in one view model to a view. +/// +/// +/// Since multiple view models can listen for this message in the setup flow, this object is used to to send the rendered adaptive card +/// as well as the current view model that is being displayed in the setup flow. Listeners can use the current view model in use +/// by the to determine if they should display the adaptive card or not. +/// +public class NewAdaptiveCardAvailableMessage : ValueChangedMessage +{ + public NewAdaptiveCardAvailableMessage(RenderedAdaptiveCardData value) + : base(value) + { + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/RenderedAdaptiveCardData.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/RenderedAdaptiveCardData.cs new file mode 100644 index 0000000000..d3c52776bd --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/RenderedAdaptiveCardData.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AdaptiveCards.Rendering.WinUI3; + +namespace DevHome.SetupFlow.Models.Environments; + +/// +/// Data object that contains the rendered adaptive card and the current view model being used in +/// the setup flow by the . +/// +public class RenderedAdaptiveCardData +{ + public object CurrentSetupFlowViewModel { get; private set; } + + public RenderedAdaptiveCard RenderedAdaptiveCard { get; set; } + + public RenderedAdaptiveCardData(object currentSetupFlowViewModel, RenderedAdaptiveCard renderedAdaptiveCard) + { + CurrentSetupFlowViewModel = currentSetupFlowViewModel; + RenderedAdaptiveCard = renderedAdaptiveCard; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs index d37f37fe95..cfb9c611f6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs @@ -51,22 +51,22 @@ public IAsyncAction CloneRepositoryAsync(string cloneDestination, IDeveloperId d } catch (RecurseSubmodulesException recurseException) { - _log.Error("Could not clone all sub modules", recurseException); + _log.Error(recurseException, "Could not clone all sub modules"); throw; } catch (UserCancelledException userCancelledException) { - _log.Error("The user stoped the clone operation", userCancelledException); + _log.Error(userCancelledException, "The user stoped the clone operation"); throw; } catch (NameConflictException nameConflictException) { - _log.Error(string.Empty, nameConflictException); + _log.Error(nameConflictException, nameConflictException.ToString()); throw; } catch (Exception e) { - _log.Error("Could not clone the repository", e); + _log.Error(e, "Could not clone the repository"); throw; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs index 96052be67f..718ee3802b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs @@ -153,7 +153,7 @@ IAsyncOperation ISetupTask.Execute() catch (Exception e) { ReportAppInstallFailedEvent(); - _log.Error($"Exception thrown while installing package.", e); + _log.Error(e, $"Exception thrown while installing package."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); @@ -189,7 +189,7 @@ IAsyncOperation ISetupTask.ExecuteAsAdmin(IElevatedComponentO catch (Exception e) { ReportAppInstallFailedEvent(); - _log.Error($"Exception thrown while installing package.", e); + _log.Error(e, $"Exception thrown while installing package."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs index 11610776ae..71a414dae1 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs @@ -60,7 +60,7 @@ public RepositoryProvider(IExtensionWrapper extensionWrapper) public string DisplayName => _repositoryProvider.DisplayName; - public string ExtensionDisplayName => _extensionWrapper.Name; + public string ExtensionDisplayName => _extensionWrapper.ExtensionDisplayName; /// /// Starts the extension if it isn't running. @@ -77,7 +77,7 @@ public void StartIfNotRunning() } catch (Exception ex) { - _log.Error($"Could not get repository provider from extension.", ex); + _log.Error(ex, $"Could not get repository provider from extension."); } } @@ -199,7 +199,7 @@ public async Task GetLoginUiAsync() } catch (Exception ex) { - _log.Error($"ShowLoginUIAsync(): loginUIContentDialog failed.", ex); + _log.Error(ex, $"ShowLoginUIAsync(): loginUIContentDialog failed."); } return null; @@ -224,7 +224,7 @@ public IEnumerable GetAllLoggedInAccounts() var developerIdsResult = _devIdProvider.GetLoggedInDeveloperIds(); if (developerIdsResult.Result.Status != ProviderOperationStatus.Success) { - _log.Error($"Could not get logged in accounts. Message: {developerIdsResult.Result.DisplayMessage}", developerIdsResult.Result.ExtendedError); + _log.Error(developerIdsResult.Result.ExtendedError, $"Could not get logged in accounts. Message: {developerIdsResult.Result.DisplayMessage}"); return new List(); } @@ -251,7 +251,7 @@ public RepositorySearchInformation SearchForRepositories(IDeveloperId developerI } else { - _log.Error($"Could not get repositories. Message: {result.Result.DisplayMessage}", result.Result.ExtendedError); + _log.Error(result.Result.ExtendedError, $"Could not get repositories. Message: {result.Result.DisplayMessage}"); } } else @@ -264,7 +264,7 @@ public RepositorySearchInformation SearchForRepositories(IDeveloperId developerI } else { - _log.Error($"Could not get repositories. Message: {result.Result.DisplayMessage}", result.Result.ExtendedError); + _log.Error(result.Result.ExtendedError, $"Could not get repositories. Message: {result.Result.DisplayMessage}"); } } } @@ -282,7 +282,7 @@ public RepositorySearchInformation SearchForRepositories(IDeveloperId developerI } catch (Exception ex) { - _log.Error($"Could not get repositories. Message: {ex}"); + _log.Error(ex, $"Could not get repositories. Message: {ex}"); } _repositories[developerId] = repoSearchInformation.Repositories; @@ -304,7 +304,7 @@ public RepositorySearchInformation GetAllRepositories(IDeveloperId developerId) } else { - _log.Error($"Could not get repositories. Message: {result.Result.DisplayMessage}", result.Result.ExtendedError); + _log.Error(result.Result.ExtendedError, $"Could not get repositories. Message: {result.Result.DisplayMessage}"); } } catch (AggregateException aggregateException) @@ -316,12 +316,12 @@ public RepositorySearchInformation GetAllRepositories(IDeveloperId developerId) } else { - _log.Error(aggregateException.Message, aggregateException); + _log.Error(aggregateException, aggregateException.Message); } } catch (Exception ex) { - _log.Error($"Could not get repositories. Message: {ex}", ex); + _log.Error(ex, $"Could not get repositories. Message: {ex}"); } _repositories[developerId] = repoSearchInformation.Repositories; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs index 7fcdb85427..346ddb20ea 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs @@ -37,7 +37,7 @@ public string DisplayName(string providerName) public RepositoryProviders(IEnumerable extensionWrappers) { - _providers = extensionWrappers.ToDictionary(extensionWrapper => extensionWrapper.Name, extensionWrapper => new RepositoryProvider(extensionWrapper)); + _providers = extensionWrappers.ToDictionary(extensionWrapper => extensionWrapper.ExtensionDisplayName, extensionWrapper => new RepositoryProvider(extensionWrapper)); } public void StartAllExtensions() diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs index 9da80343a6..f74b337de5 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs @@ -133,7 +133,7 @@ private string FindVersion(IReadOnlyList availableVersions, Pa } catch (Exception e) { - _log.Error($"Unable to validate if the version {versionInfo.Version} is in the list of available versions", e); + _log.Error(e, $"Unable to validate if the version {versionInfo.Version} is in the list of available versions"); } return versionInfo.Version; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs index 3b2de8ab81..cabbf87556 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs @@ -49,7 +49,7 @@ public SDKOpenConfigurationSetResult(SDK.OpenConfigurationSetResult result, ISet public string GetErrorMessage() { var log = Log.ForContext("SourceContext", nameof(SDKOpenConfigurationSetResult)); - log.Error($"Extension failed to open the configuration file provided by Dev Home: Field: {Field}, Value: {Value}, Line: {Line}, Column: {Column}", ResultCode); + log.Error(ResultCode, $"Extension failed to open the configuration file provided by Dev Home: Field: {Field}, Value: {Value}, Line: {Line}, Column: {Column}"); return _setupFlowStringResource.GetLocalized(StringResourceKey.SetupTargetConfigurationOpenConfigFailed); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Selectors/ReviewTabViewSelector.cs b/tools/SetupFlow/DevHome.SetupFlow/Selectors/ReviewTabViewSelector.cs index 38d8d86558..db467aeaf2 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Selectors/ReviewTabViewSelector.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Selectors/ReviewTabViewSelector.cs @@ -3,6 +3,7 @@ using System; using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -35,6 +36,11 @@ public DataTemplate SetupTargetTabTemplate get; set; } + public DataTemplate CreateEnvironmentTabTemplate + { + get; set; + } + protected override DataTemplate SelectTemplateCore(object item) { return ResolveDataTemplate(item, () => base.SelectTemplateCore(item)); @@ -59,6 +65,7 @@ private DataTemplate ResolveDataTemplate(object item, Func default RepoConfigReviewViewModel => RepoConfigTabTemplate, AppManagementReviewViewModel => AppManagementTabTemplate, SetupTargetReviewViewModel => SetupTargetTabTemplate, + CreateEnvironmentReviewViewModel => CreateEnvironmentTabTemplate, _ => defaultDataTemplate(), }; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs b/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs index 399b5a2f6a..9aadd5a359 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs @@ -3,6 +3,7 @@ using System; using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -29,7 +30,11 @@ public class SetupFlowViewSelector : DataTemplateSelector public DataTemplate SummaryTemplate { get; set; } - public DataTemplate ConfigurationFileTemplate { get; set; } + public DataTemplate ConfigurationFileTemplate { get; set; } + + public DataTemplate SelectEnvironmentsProviderTemplate { get; set; } + + public DataTemplate EnvironmentCreationOptionsTemplate { get; set; } protected override DataTemplate SelectTemplateCore(object item) { @@ -58,7 +63,9 @@ private DataTemplate ResolveDataTemplate(object item, Func default LoadingViewModel => LoadingTemplate, SummaryViewModel => SummaryTemplate, ConfigurationFileViewModel => ConfigurationFileTemplate, - SetupTargetViewModel => SetupTargetTemplate, + SetupTargetViewModel => SetupTargetTemplate, + SelectEnvironmentProviderViewModel => SelectEnvironmentsProviderTemplate, + EnvironmentCreationOptionsViewModel => EnvironmentCreationOptionsTemplate, _ => defaultDataTemplate(), }; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs index ca3c3f70b3..690bfb7f3c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs @@ -74,7 +74,7 @@ private async Task InitializeWindowsPackageManagerAsync() } catch (Exception e) { - _log.Error($"Unable to correctly initialize app management at the moment. Further attempts will be performed later.", e); + _log.Error(e, $"Unable to correctly initialize app management at the moment. Further attempts will be performed later."); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs index 6ab0f24ab8..26664c6fc2 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs @@ -72,7 +72,7 @@ private async Task InitializeDataSourceAsync(WinGetPackageDataSource dataSource) } catch (Exception e) { - _log.Error($"Exception thrown while initializing data source of type {dataSource.GetType().Name}", e); + _log.Error(e, $"Exception thrown while initializing data source of type {dataSource.GetType().Name}"); } } @@ -89,7 +89,7 @@ private async Task> LoadCatalogsFromDataSourceAsync(WinGet } catch (Exception e) { - _log.Error($"Exception thrown while loading data source of type {dataSource.GetType().Name}", e); + _log.Error(e, $"Exception thrown while loading data source of type {dataSource.GetType().Name}"); } return null; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs index 9efdd3a9e7..ab4936a111 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs @@ -37,7 +37,7 @@ public async Task CreateCardViewModelAsync( catch (Exception ex) { var log = Log.ForContext("SourceContext", nameof(ComputeSystemViewModelFactory)); - log.Error($"Failed to get initial properties for compute system {computeSystem}. Error: {ex.Message}"); + log.Error(ex, $"Failed to get initial properties for compute system {computeSystem}. Error: {ex.Message}"); } return cardViewModel; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs index 21c498c6d4..4059509583 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs @@ -138,7 +138,7 @@ private List GetResourcesForCloneTaskGroup(RepoConfigTaskG } catch (Exception e) { - _log.Error($"Error creating a repository resource entry", e); + _log.Error(e, $"Error creating a repository resource entry"); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs index 1663c249bc..d4b0dbeed3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs @@ -251,7 +251,7 @@ volumeLetter is char newLetter && volumeSize is ulong newSize && catch (Exception ex) { // Log then return empty list, don't show the user their existing dev drive. Not catastrophic failure. - _log.Error($"Failed to get existing Dev Drives.", ex); + _log.Error(ex, $"Failed to get existing Dev Drives."); return new List(); } } @@ -280,7 +280,7 @@ private DevDrive GetDevDriveWithDefaultInfo() } catch (Exception ex) { - _log.Error($"Unable to get available Free Space for {root}.", ex); + _log.Error(ex, $"Unable to get available Free Space for {root}."); validationSuccessful = false; } @@ -372,7 +372,7 @@ public ISet GetDevDriveValidationResults(IDevDrive dev } catch (Exception ex) { - _log.Error($"Failed to validate selected Drive letter ({devDrive.DriveLocation.FirstOrDefault()}).", ex); + _log.Error(ex, $"Failed to validate selected Drive letter ({devDrive.DriveLocation.FirstOrDefault()})."); returnSet.Add(DevDriveValidationResult.DriveLetterNotAvailable); } @@ -405,7 +405,7 @@ public IList GetAvailableDriveLetters(char? usedLetterToKeepInList = null) } catch (Exception ex) { - _log.Error($"Failed to get Available Drive letters.", ex); + _log.Error(ex, $"Failed to get Available Drive letters."); } return driveLetterSet.ToList(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs index 91c5820b27..93469e207a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs @@ -7,10 +7,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AdaptiveCards.ObjectModel.WinUI3; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.DevHomeAdaptiveCards.Parsers; +using DevHome.Common.Renderers; using DevHome.SetupFlow.Common.Contracts; using DevHome.SetupFlow.Common.Elevation; +using DevHome.SetupFlow.Common.Helpers; using DevHome.SetupFlow.Models; using DevHome.SetupFlow.ViewModels; using Projection::DevHome.SetupFlow.ElevatedComponent; @@ -22,6 +27,7 @@ public enum SetupFlowKind { LocalMachine, SetupTarget, + CreateEnvironment, } /// @@ -31,6 +37,10 @@ public partial class SetupFlowOrchestrator : ObservableObject { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(SetupFlowOrchestrator)); + private readonly string _adaptiveCardNextButtonId = "DevHomeMachineConfigurationNextButton"; + + private readonly string _adaptiveCardPreviousButtonId = "DevHomeMachineConfigurationPreviousButton"; + private readonly List _flowPages = new(); /// @@ -120,6 +130,13 @@ public IReadOnlyList FlowPages public bool IsMachineConfigurationInProgress => FlowPages.Count > 1; + /// + /// Gets the renderer for the Dev Home action set. This is used to invoke the the buttons within the top level + /// of the adaptive card. This stitches up the setup flow's next and previous buttons to two buttons within an + /// extensions adaptive card. + /// + public DevHomeActionSet DevHomeActionSetRenderer { get; private set; } = new(TopLevelCardActionSetVisibility.Hidden); + /// /// Gets or sets a value indicating whether the done button should be shown. When false, the cancel /// hyperlink button will be shown in the UI. @@ -179,6 +196,13 @@ public void ReleaseRemoteOperationObject() [RelayCommand(CanExecute = nameof(CanGoToPreviousPage))] public async Task GoToPreviousPage() { + // If an adaptive card is being shown in the setup flow, we need to invoke the action + // of the previous button in the action set to move the flow to the previous page in the adaptive card. + if (DevHomeActionSetRenderer?.ActionButtonInvoker != null) + { + DevHomeActionSetRenderer.InitiateAction(_adaptiveCardPreviousButtonId); + } + await SetCurrentPageIndex(_currentPageIndex - 1); } @@ -190,6 +214,17 @@ private bool CanGoToPreviousPage() [RelayCommand(CanExecute = nameof(CanGoToNextPage))] public async Task GoToNextPage() { + // If an adaptive card is being shown in the setup flow, we need to invoke the action + // of the primary button in the action set to move the flow to the next page in the adaptive card. + if (DevHomeActionSetRenderer?.ActionButtonInvoker != null) + { + if (!TryNavigateToNextAdaptiveCardPage(_adaptiveCardNextButtonId)) + { + // Don't navigate if there were validation errors. + return; + } + } + await SetCurrentPageIndex(_currentPageIndex + 1); } @@ -243,4 +278,25 @@ private async Task SetCurrentPageIndex(int index) await CurrentPageViewModel?.OnNavigateToAsync(); } + + /// + /// Performs the work needed to navigate to the next page in an adaptive card. This is used when the setup flow is + /// rendering a flow that includes an adaptive card style wizard flow. + /// + /// + /// Only adaptive cards that have input controls with the 'isRequired' property set to true will be validated. + /// All other elements within the adaptive card will be ignored. + /// + /// The string Id of the button + /// True when the user inputs have been validated and false otherwise. + private bool TryNavigateToNextAdaptiveCardPage(string buttonId) + { + if (DevHomeActionSetRenderer.TryValidateAndInitiateAction(buttonId, CurrentPageViewModel?.GetAdaptiveCardUserInputsForNavigationValidation())) + { + return true; + } + + _log.Warning($"Failed to invoke adaptive card action with Id: {buttonId} due to input validation failure"); + return false; + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs index a1d3cb4f06..9535a5eaaa 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs @@ -261,4 +261,28 @@ public static class StringResourceKey public static readonly string SetupTargetConfigurationProgressUpdate = nameof(SetupTargetConfigurationProgressUpdate); public static readonly string SetupTargetConfigurationUnitProgressErrorWithMsg = nameof(SetupTargetConfigurationUnitProgressErrorWithMsg); public static readonly string SetupTargetConfigurationUnitUnknown = nameof(SetupTargetConfigurationUnitUnknown); + + // Create Environment flow + public static readonly string SelectEnvironmentPageTitle = nameof(SelectEnvironmentPageTitle); + public static readonly string ConfigureEnvironmentPageTitle = nameof(ConfigureEnvironmentPageTitle); + public static readonly string EnvironmentCreationReviewPageTitle = nameof(EnvironmentCreationReviewPageTitle); + public static readonly string EnvironmentCreationReviewTabTitle = nameof(EnvironmentCreationReviewTabTitle); + public static readonly string EnvironmentCreationError = nameof(EnvironmentCreationError); + public static readonly string StartingEnvironmentCreation = nameof(StartingEnvironmentCreation); + public static readonly string EnvironmentCreationOperationInitializationFinished = nameof(EnvironmentCreationOperationInitializationFinished); + public static readonly string EnvironmentCreationForProviderStarted = nameof(EnvironmentCreationForProviderStarted); + public static readonly string EnvironmentCreationFailedToGetProviderInformation = nameof(EnvironmentCreationFailedToGetProviderInformation); + public static readonly string EnvironmentCreationReviewExpanderDescription = nameof(EnvironmentCreationReviewExpanderDescription); + public static readonly string CreateEnvironmentButtonText = nameof(CreateEnvironmentButtonText); + public static readonly string SetupShellReviewPageDescriptionForEnvironmentCreation = nameof(SetupShellReviewPageDescriptionForEnvironmentCreation); + + // Summary page + public static readonly string SummaryPageOpenDashboard = nameof(SummaryPageOpenDashboard); + public static readonly string SummaryPageRedirectToEnvironmentPageButton = nameof(SummaryPageRedirectToEnvironmentPageButton); + public static readonly string SummaryPageHeader = nameof(SummaryPageHeader); + public static readonly string SummaryPageHeaderForEnvironmentCreationText = nameof(SummaryPageHeaderForEnvironmentCreationText); + + // Review page + public static readonly string ReviewExpanderDescription = nameof(ReviewExpanderDescription); + public static readonly string SetupShellReviewPageDescription = nameof(SetupShellReviewPageDescription); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs index f195240367..d70f08539d 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs @@ -223,7 +223,7 @@ private async Task CreateAndConnectSearchCatalogAsync() } catch (Exception e) { - _log.Error($"Failed to create or connect to search catalog.", e); + _log.Error(e, $"Failed to create or connect to search catalog."); } } @@ -241,7 +241,7 @@ private async Task CreateAndConnectWinGetCatalogAsync() } catch (Exception e) { - _log.Error($"Failed to create or connect to 'winget' catalog source.", e); + _log.Error(e, $"Failed to create or connect to 'winget' catalog source."); } } @@ -259,7 +259,7 @@ private async Task CreateAndConnectMsStoreCatalogAsync() } catch (Exception e) { - _log.Error($"Failed to create or connect to 'msstore' catalog source.", e); + _log.Error(e, $"Failed to create or connect to 'msstore' catalog source."); } } @@ -278,7 +278,7 @@ private async Task CreateAndConnectCustomCatalogAsync(string cata } catch (Exception e) { - _log.Error($"Failed to create or connect to custom catalog with name {catalogName}", e); + _log.Error(e, $"Failed to create or connect to custom catalog with name {catalogName}"); return null; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs index ca212e18b2..8511c9fa81 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs @@ -53,7 +53,7 @@ await Task.Run(() => } catch (Exception e) { - _log.Error($"Failed to create dummy {nameof(PackageManager)} COM object. WinGet COM Server is not available.", e); + _log.Error(e, $"Failed to create dummy {nameof(PackageManager)} COM object. WinGet COM Server is not available."); return false; } } @@ -70,7 +70,7 @@ public async Task IsUpdateAvailableAsync() } catch (Exception e) { - _log.Error("Failed to check if AppInstaller has an update, defaulting to false", e); + _log.Error(e, "Failed to check if AppInstaller has an update, defaulting to false"); return false; } } @@ -87,12 +87,12 @@ public async Task RegisterAppInstallerAsync() } catch (RegisterPackageException e) { - _log.Error($"Failed to register AppInstaller", e); + _log.Error(e, $"Failed to register AppInstaller"); return false; } catch (Exception e) { - _log.Error("An unexpected error occurred when registering AppInstaller", e); + _log.Error(e, "An unexpected error occurred when registering AppInstaller"); return false; } } @@ -106,7 +106,7 @@ public async Task IsConfigurationUnstubbedAsync() } catch (Exception e) { - _log.Error("An unexpected error occurred when checking if configuration is unstubbed", e); + _log.Error(e, "An unexpected error occurred when checking if configuration is unstubbed"); return false; } } @@ -123,7 +123,7 @@ public async Task UnstubConfigurationAsync() } catch (Exception e) { - _log.Error("An unexpected error occurred when unstubbing configuration", e); + _log.Error(e, "An unexpected error occurred when unstubbing configuration"); return false; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs index c4a1006d6c..1212b1d7ec 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs @@ -45,12 +45,12 @@ public async Task DoWithRecoveryAsync(Func> actionFunc) } catch (CatalogNotInitializedException e) { - _log.Error($"Catalog used by the action is not initialized", e); + _log.Error(e, $"Catalog used by the action is not initialized"); await RecoveryAsync(attempt); } catch (COMException e) when (e.HResult == RpcServerUnavailable || e.HResult == RpcCallFailed || e.HResult == PackageUpdating) { - _log.Error($"Failed to operate on out-of-proc object with error code: 0x{e.HResult:x}", e); + _log.Error(e, $"Failed to operate on out-of-proc object with error code: 0x{e.HResult:x}"); await RecoveryAsync(attempt); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs index b58908efbc..9209e267af 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs @@ -90,7 +90,7 @@ await ForEachEnabledExtensionAsync(async (extensionGroups) => } catch (Exception e) { - _log.Error($"Error loading packages from featured applications group.", e); + _log.Error(e, $"Error loading packages from featured applications group."); } } }); @@ -171,7 +171,7 @@ private async Task ForEachEnabledExtensionAsync(Func LoadCatalogAsync(JsonWinGetPackageCatalog jso } catch (Exception e) { - _log.Error($"Error loading packages from winget catalog.", e); + _log.Error(e, $"Error loading packages from winget catalog."); } return null; @@ -184,7 +184,7 @@ private async Task GetJsonApplicationIconAsync(JsonWinGetPa } catch (Exception e) { - _log.Error($"Failed to get icon for JSON package {package.Uri}.", e); + _log.Error(e, $"Failed to get icon for JSON package {package.Uri}."); } _log.Warning($"No icon found for JSON package {package.Uri}. A default one will be provided."); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs index d87fe3126a..fa9bb0b060 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs @@ -97,7 +97,7 @@ public async override Task> LoadCatalogsAsync() } catch (Exception e) { - _log.Error($"Error loading packages from winget restore catalog.", e); + _log.Error(e, $"Error loading packages from winget restore catalog."); } return result; @@ -130,7 +130,7 @@ private async Task GetRestoreApplicationIconAsync(IRestoreA } catch (Exception e) { - _log.Error($"Failed to get icon for restore package {appInfo.Id}", e); + _log.Error(e, $"Failed to get icon for restore package {appInfo.Id}"); } _log.Warning($"No {theme} icon found for restore package {appInfo.Id}. A default one will be provided."); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 2f00a48b7b..6bee36d1ba 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -649,7 +649,7 @@ Generate a WinGet Configuration file (.winget) to repeat this set up in the future or share it with others. {Locked="WinGet",".winget"}Tooltip text about the generated configuration file - + Set up details Header for a section detailing the set up steps to be performed. "Set up" is the noun @@ -713,10 +713,14 @@ Select a target machine to set up. Description for the setup target page - + Review the terms and setup details below before applying these changes to your computer. Description for the review page + + Review the details for your new environment + Description for the review page + Applications Header for a section showing a summary of applications to be installed @@ -1044,7 +1048,7 @@ Installed applications Header for the section that shows all the downloaded apps - + Here's what we set up for you Header text for the summary page @@ -1056,7 +1060,7 @@ Next steps Text for the "Next steps" section of the summary page - + Open Dashboard Button content to let user go to the dashboard @@ -1732,4 +1736,91 @@ View file Button content for viewing a file. + + Select your environment + Title of the 'Select your environment' page in the create environment flow where users can select the provider they will use to create the environment, like Microsoft Hyper-V and Microsoft Dev Box + + + Configure your environment + Title of the 'Configure your environment' page in the environment creation flow where users can choose options that they'd like their environment to be created with + + + Begin by selecting an environment provider below + subtitle text advise the user that they can select an environment option in a list below the text + + + Create virtual environment + Header for a card that when clicked takes the user to a multi-step flow for creating an environment + + + Create a local or cloud environment + Body text description for a card than when clicked takes the user to a multi-step flow for creating an environment + + + Choose an environment provider to create a new dev environment + Description for the setup target page + + + Add options to create your environment + Description for the setup target page + + + There was an error retreiving the adaptive card session from the extension + Error text display when we are unable to retrieve the adaptive card information from an extension + + + Environment + Title for create environment review tab + + + Your environment's details + Title for create environment review tab + + + Review your environment + Title for create environment review page + + + Create Environment + + + There was an error starting the creation operation + Text to tell the user that we couldn't start the operation to create their local or cloud virtual environment + + + The operation to create your environment has started successfully + Text to tell the user that we were able to start the operation that create their local or cloud virtual machine successfully + + + Starting the create environment operation + Text to tell the user that the operation to create their local or cloud virtual environment has started + + + We timed out waiting for the extension to provide us with information to create your environment + Error text to show the user that we timed out while waiting for a response from a Dev Home extension + + + The {0} provider is now creating your environment + {Locked="{0}"} Text that tells the user that a specific provider has started creating their environment. {0} is the name of a provider who Dev Home will send the request to. + + + We failed to start the creation operation for the {0} provider + {Locked="{0}"} Text that tells the user that Dev Home could not start the creation operation for a specific provider. {0} is the name of a provider who Dev Home will send the request to. + + + Environment details + Heading that will show the user the details of the environment they initiated the creation process for. Environments can be local or cloud virtual machines + + + Go to Environments page + Text for button that when clicked will redirect the user to the environments page in Dev Home + + + Environment being created + Text to tell the user that an environment is being created. Environments can be local or cloud virtual machines + + + We've started creating your environment + Text to tell the user that an environment is being created. Environments can be local or cloud virtual machines + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/EnvironmentCreationOptionsTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/EnvironmentCreationOptionsTaskGroup.cs new file mode 100644 index 0000000000..130377a53d --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/EnvironmentCreationOptionsTaskGroup.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using DevHome.Common.Environments.Models; +using DevHome.Common.Environments.Services; +using DevHome.SetupFlow.Models; +using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; + +namespace DevHome.SetupFlow.TaskGroups; + +public class EnvironmentCreationOptionsTaskGroup : ISetupTaskGroup +{ + private readonly ISetupFlowStringResource _setupFlowStringResource; + + private readonly IComputeSystemManager _computeSystemManager; + + private readonly EnvironmentCreationOptionsViewModel _environmentCreationOptionsViewModel; + + private readonly CreateEnvironmentReviewViewModel _createEnvironmentReviewViewModel; + + public ComputeSystemProviderDetails ProviderDetails { get; private set; } + + public CreateEnvironmentTask CreateEnvironmentTask { get; private set; } + + public EnvironmentCreationOptionsTaskGroup( + SetupFlowViewModel setupFlowViewModel, + IComputeSystemManager computeSystemManager, + ISetupFlowStringResource setupFlowStringResource, + EnvironmentCreationOptionsViewModel environmentCreationOptionsViewModel, + CreateEnvironmentReviewViewModel createEnvironmentReviewViewModel) + { + _environmentCreationOptionsViewModel = environmentCreationOptionsViewModel; + _createEnvironmentReviewViewModel = createEnvironmentReviewViewModel; + _setupFlowStringResource = setupFlowStringResource; + _computeSystemManager = computeSystemManager; + CreateEnvironmentTask = new CreateEnvironmentTask(_computeSystemManager, _setupFlowStringResource, setupFlowViewModel); + } + + public IEnumerable SetupTasks => new List() { CreateEnvironmentTask }; + + public IEnumerable DSCTasks => new List(); + + public SetupPageViewModelBase GetSetupPageViewModel() => _environmentCreationOptionsViewModel; + + public ReviewTabViewModelBase GetReviewTabViewModel() => _createEnvironmentReviewViewModel; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SelectEnvironmentProviderTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SelectEnvironmentProviderTaskGroup.cs new file mode 100644 index 0000000000..fcb18c29ea --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SelectEnvironmentProviderTaskGroup.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; +using DevHome.SetupFlow.Models; +using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; + +namespace DevHome.SetupFlow.TaskGroups; + +public class SelectEnvironmentProviderTaskGroup : ISetupTaskGroup +{ + private readonly SelectEnvironmentProviderViewModel _selectEnvironmentProviderViewModel; + + public SelectEnvironmentProviderTaskGroup(SelectEnvironmentProviderViewModel selectEnvironmentProviderViewModel) + { + _selectEnvironmentProviderViewModel = selectEnvironmentProviderViewModel; + } + + // No setup tasks needed for this task group. + public IEnumerable SetupTasks => new List(); + + // No dsc tasks needed for this task group. + public IEnumerable DSCTasks => new List(); + + public SetupPageViewModelBase GetSetupPageViewModel() => _selectEnvironmentProviderViewModel; + + // Review tab not needed for this task group. + public ReviewTabViewModelBase GetReviewTabViewModel() => null; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs b/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs index 99f6c12de2..474a5c1c5c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs @@ -104,7 +104,7 @@ public static bool IsDevDriveFeatureEnabled } catch (Exception ex) { - Log.Error($"Unable to query for Dev Drive enablement: {ex.Message}"); + Log.Error(ex, $"Unable to query for Dev Drive enablement: {ex.Message}"); return false; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs index 113296efda..e8b80f9336 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -339,6 +340,28 @@ public async Task ShowCustomizeDevDriveWindow() ToggleCloneButton(); } + [RelayCommand] + public void DevDriveCloneLocationChanged() + { + var location = (EditDevDriveViewModel.DevDrive != null) ? EditDevDriveViewModel.GetDriveDisplayName() : string.Empty; + + if (!string.IsNullOrEmpty(location)) + { + SaveCloneLocation(location); + } + } + + [RelayCommand] + public void SaveCloneLocation(string location) + { + // In cases where location is empty don't update the cloneLocation. Only update when there are actual values. + FolderPickerViewModel.CloneLocation = location; + + FolderPickerViewModel.ValidateCloneLocation(); + + ToggleCloneButton(); + } + /// /// Indicates if the ListView is currently filtering items. A result of manually filtering a list view /// is that the SelectionChanged is fired for any selected item that is removed and the item isn't "re-selected" @@ -574,6 +597,14 @@ private async Task AddAccountClicked() } } + [RelayCommand] + public void SaveRepoUrl(string repoUrl) + { + Url = repoUrl; + + ToggleCloneButton(); + } + /// /// Filters all repos down to any that start with text. /// A side-effect of filtering is that SelectionChanged fires for every selected repo but only on removal. @@ -706,7 +737,7 @@ public void GetExtensions() var extensions = extensionWrappers.Where( extension => extension.HasProviderType(ProviderType.Repository) && - extension.HasProviderType(ProviderType.DeveloperId)).OrderBy(extensionWrapper => extensionWrapper.Name); + extension.HasProviderType(ProviderType.DeveloperId)).OrderBy(extensionWrapper => extensionWrapper.ExtensionDisplayName); _providers = new RepositoryProviders(extensions); @@ -1014,8 +1045,8 @@ public void AddOrRemoveRepository(string accountName, IList repositories cloningInformation.RepositoryProvider = _providers.GetSDKProvider(_selectedRepoProvider); cloningInformation.ProviderName = _providers.DisplayName(_selectedRepoProvider); cloningInformation.OwningAccount = developerId; - cloningInformation.EditClonePathAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageEditClonePathAutomationProperties, $"{_selectedRepoProvider}/{repositoryToAdd}"); - cloningInformation.RemoveFromCloningAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageRemoveRepoAutomationProperties, $"{_selectedRepoProvider}/{repositoryToAdd}"); + cloningInformation.EditClonePathAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageEditClonePathAutomationProperties, Path.Join(_selectedRepoProvider, repositoryToAdd.RepoDisplayName)); + cloningInformation.RemoveFromCloningAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageRemoveRepoAutomationProperties, Path.Join(_selectedRepoProvider, repositoryToAdd.RepoDisplayName)); EverythingToClone.Add(cloningInformation); } } @@ -1051,7 +1082,7 @@ private void ValidateUriAndChangeUiIfBad(string url, out Uri uri) } catch (Exception e) { - _log.Error($"Invalid URL {uri.OriginalString}", e); + _log.Error(e, $"Invalid URL {uri.OriginalString}"); UrlParsingError = _stringResource.GetLocalized(StringResourceKey.UrlValidationBadUrl); ShouldShowUrlError = true; return; @@ -1248,7 +1279,7 @@ private async Task InitiateAddAccountUserExperienceAsync(RepositoryProvider prov } catch (Exception ex) { - _log.Error($"Exception thrown while calling show logon session", ex); + _log.Error(ex, $"Exception thrown while calling show logon session"); } } } @@ -1335,7 +1366,7 @@ private async Task CoordinateTasks(string loginId, Task PickConfigurationFileAsync() } catch (Exception e) { - _log.Error($"Failed to open file picker.", e); + _log.Error(e, $"Failed to open file picker."); return false; } } @@ -184,7 +184,7 @@ private async Task LoadConfigurationFileInternalAsync(StorageFile file) } catch (OpenConfigurationSetException e) { - _log.Error($"Opening configuration set failed.", e); + _log.Error(e, $"Opening configuration set failed."); await _mainWindow.ShowErrorMessageDialogAsync( StringResource.GetLocalized(StringResourceKey.ConfigurationViewTitle, file.Name), GetErrorMessage(e), @@ -192,7 +192,7 @@ await _mainWindow.ShowErrorMessageDialogAsync( } catch (Exception e) { - _log.Error($"Unknown error while opening configuration set.", e); + _log.Error(e, $"Unknown error while opening configuration set."); await _mainWindow.ShowErrorMessageDialogAsync( file.Name, diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs index 48cc57a038..8f2162075b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs @@ -269,7 +269,7 @@ public async Task ChooseFolderLocationAsync() } catch (Exception e) { - _log.Error("Failed to open folder picker.", e); + _log.Error(e, "Failed to open folder picker."); } } @@ -509,7 +509,7 @@ private void RefreshDriveLetterToSizeMapping() } catch (Exception ex) { - _log.Error($"Failed to refresh the drive letter to size mapping.", ex); + _log.Error(ex, $"Failed to refresh the drive letter to size mapping."); // Clear the mapping since it can't be refreshed. This shouldn't happen unless DriveInfo.GetDrives() fails. In that case we won't know which drive // in the list is causing GetDrives()'s to throw. If there are values inside the dictionary at this point, they could be stale. Clearing the list diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemProviderViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemProviderViewModel.cs new file mode 100644 index 0000000000..10719a3ce9 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemProviderViewModel.cs @@ -0,0 +1,45 @@ +// 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 CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Environments.Models; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; + +namespace DevHome.SetupFlow.ViewModels.Environments; + +public partial class ComputeSystemProviderViewModel : ObservableObject +{ + private readonly string _packageFullName; + + public ComputeSystemProviderDetails ProviderDetails { get; private set; } + + [ObservableProperty] + private string _displayName; + + [ObservableProperty] + private ImageIcon _icon; + + [ObservableProperty] + private bool _isSelected; + + public ComputeSystemProviderViewModel(ComputeSystemProviderDetails providerDetails) + { + ProviderDetails = providerDetails; + _packageFullName = ProviderDetails.ExtensionWrapper.PackageFullName; + Icon = GetImageIcon(); + DisplayName = ProviderDetails.ComputeSystemProvider.DisplayName; + } + + private ImageIcon GetImageIcon() + { + var imageIcon = new ImageIcon(); + imageIcon.Source = CardProperty.ConvertMsResourceToIcon(ProviderDetails.ComputeSystemProvider.Icon, _packageFullName); + return imageIcon; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs index a50b4f1391..123d843062 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs @@ -146,7 +146,7 @@ public void FilterComputeSystemCards(string text) } catch (Exception ex) { - _log.Error($"Failed to filter Compute system cards. Error: {ex.Message}"); + _log.Error(ex, $"Failed to filter Compute system cards. Error: {ex.Message}"); } return true; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/CreateEnvironmentReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/CreateEnvironmentReviewViewModel.cs new file mode 100644 index 0000000000..543ef7d544 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/CreateEnvironmentReviewViewModel.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.SetupFlow.Services; + +namespace DevHome.SetupFlow.ViewModels.Environments; + +public partial class CreateEnvironmentReviewViewModel : ReviewTabViewModelBase +{ + private readonly ISetupFlowStringResource _stringResource; + + public override bool HasItems => true; + + public CreateEnvironmentReviewViewModel( + ISetupFlowStringResource stringResource) + { + _stringResource = stringResource; + TabTitle = stringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewTabTitle); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs new file mode 100644 index 0000000000..4a5f1a3115 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Threading.Tasks; +using AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.DevHomeAdaptiveCards.Parsers; +using DevHome.Common.Environments.Models; +using DevHome.Common.Models; +using DevHome.Common.Renderers; +using DevHome.Common.Services; +using DevHome.SetupFlow.Exceptions; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.Services; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using WinUIEx; + +namespace DevHome.SetupFlow.ViewModels.Environments; + +/// +/// View model for the Configure Environment page in the setup flow. This page will display an adaptive card that is provided by the selected +/// compute system provider. The adaptive card will be display in the middle of the page and will contain compute system provider specific UI +/// for the user to configure their creation options. +/// +public partial class EnvironmentCreationOptionsViewModel : SetupPageViewModelBase, IRecipient +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(SelectEnvironmentProviderViewModel)); + + private readonly AdaptiveCardRenderingService _adaptiveCardRenderingService; + + private readonly WindowEx _windowEx; + + private readonly AdaptiveElementParserRegistration _elementRegistration = new(); + + private readonly AdaptiveActionParserRegistration _actionRegistration = new(); + + private readonly SetupFlowViewModel _setupFlowViewModel; + + private ComputeSystemProviderDetails _curProviderDetails; + + private AdaptiveCardRenderer _adaptiveCardRenderer; + + private ComputeSystemProviderDetails _upcomingProviderDetails; + + private ExtensionAdaptiveCardSession _extensionAdaptiveCardSession; + + private ExtensionAdaptiveCard _extensionAdaptiveCard; + + private RenderedAdaptiveCard _renderedAdaptiveCard; + + private AdaptiveInputs _userInputsFromAdaptiveCard; + + [ObservableProperty] + private bool _isAdaptiveCardSessionLoaded; + + [ObservableProperty] + private string _sessionErrorMessage; + + public EnvironmentCreationOptionsViewModel( + ISetupFlowStringResource stringResource, + SetupFlowOrchestrator orchestrator, + SetupFlowViewModel setupFlow, + WindowEx windowEx, + AdaptiveCardRenderingService renderingService) + : base(stringResource, orchestrator) + { + PageTitle = stringResource.GetLocalized(StringResourceKey.ConfigureEnvironmentPageTitle); + _setupFlowViewModel = setupFlow; + _setupFlowViewModel.EndSetupFlow += OnEndSetupFlow; + _windowEx = windowEx; + + // Register for changes to the selected provider. This will be triggered when the user selects a provider. + // from the SelectEnvironmentProviderViewModel. This is a weak reference so that the recipient can be garbage collected. + WeakReferenceMessenger.Default.Register(this); + + // Register to receive the EnvironmentCreationOptionsViewRequestMessage so that we can populate the configure environment page with the current + // adaptive card information. This handles the case where the view is loaded after the view model has finished loading the adaptive card. + WeakReferenceMessenger.Default.Register(this, OnEnvironmentOptionsViewRequest); + + WeakReferenceMessenger.Default.Register(this, OnReviewPageViewRequest); + + // register the supported element parsers + _elementRegistration.Set(DevHomeSettingsCard.AdaptiveElementType, new DevHomeSettingsCardParser()); + _elementRegistration.Set(DevHomeSettingsCardChoiceSet.AdaptiveElementType, new DevHomeSettingsCardChoiceSetParser()); + _elementRegistration.Set(DevHomeLaunchContentDialogButton.AdaptiveElementType, new DevHomeLaunchContentDialogButtonParser()); + _elementRegistration.Set(DevHomeContentDialogContent.AdaptiveElementType, new DevHomeContentDialogContentParser()); + _adaptiveCardRenderingService = renderingService; + Orchestrator.CurrentSetupFlowKind = SetupFlowKind.CreateEnvironment; + } + + /// + /// Weak reference message handler for when the selected provider changes in the select environment provider page. This will be triggered when the user + /// selects an item in the Select Environment Provider page. The next time the user goes to this configure environments page, we'll update the UI + /// with an adaptive card from the newly selected provider. + /// + /// Message data that contains the new provider details. + public void Receive(CreationProviderChangedMessage message) + { + _upcomingProviderDetails = message.Value; + } + + private void OnEndSetupFlow(object sender, EventArgs e) + { + ResetAdaptiveCardConfiguration(); + WeakReferenceMessenger.Default.UnregisterAll(this); + _setupFlowViewModel.EndSetupFlow -= OnEndSetupFlow; + Orchestrator.CurrentSetupFlowKind = SetupFlowKind.LocalMachine; + } + + protected async override Task OnFirstNavigateToAsync() + { + _adaptiveCardRenderer = await GetAdaptiveCardRenderer(); + } + + /// + /// Make sure we only get the list of ComputeSystems from the ComputeSystemManager once when the page is first navigated to. + /// All other times will be through the use of the sync button. + /// + protected async override Task OnEachNavigateToAsync() + { + await Task.CompletedTask; + + var curSelectedProviderId = _curProviderDetails?.ComputeSystemProvider?.Id ?? string.Empty; + var upcomingSelectedProviderId = _upcomingProviderDetails?.ComputeSystemProvider?.Id; + + // Selected compute system provider may havechanged so we need to update the adaptive card in the UI + // with new a adaptive card from the new provider. + _curProviderDetails = _upcomingProviderDetails; + + IsAdaptiveCardSessionLoaded = false; + + // Its possible that an extension could take a long time to load the adaptive card session. + // So we run this on a background thread to prevent the UI from freezing. + _ = Task.Run(() => + { + var developerIdWrapper = _curProviderDetails.DeveloperIds.First(); + var result = _curProviderDetails.ComputeSystemProvider.CreateAdaptiveCardSessionForDeveloperId(developerIdWrapper.DeveloperId, ComputeSystemAdaptiveCardKind.CreateComputeSystem); + UpdateExtensionAdaptiveCard(result); + }); + } + + /// + /// Gets and configures the adaptive card that will be displayed on the configure environment page. + /// + public void UpdateExtensionAdaptiveCard(ComputeSystemAdaptiveCardResult adaptiveCardSessionResult) + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + try + { + CanGoToNextPage = false; + + // Reset error state and remove event handler from previous session. + ResetAdaptiveCardConfiguration(); + + if (adaptiveCardSessionResult.Result.Status == ProviderOperationStatus.Failure) + { + _log.Error($"{adaptiveCardSessionResult.Result.DisplayMessage} - {adaptiveCardSessionResult.Result.DiagnosticText}"); + throw new AdaptiveCardNotRetrievedException(adaptiveCardSessionResult.Result.DisplayMessage); + } + + // Create a new adaptive card session wrapper and add event handlers for when the session stops. + _extensionAdaptiveCardSession = new ExtensionAdaptiveCardSession(adaptiveCardSessionResult.ComputeSystemCardSession); + _extensionAdaptiveCardSession.Stopped += OnAdaptiveCardSessionStopped; + + // Create the Dev Home sdk extension adaptive card with our custom element and action parsers and send + // it to the extension who will update the card with an adaptive card template and data for the template. + // We use the OnAdaptiveCardUpdated method to update Dev Home's UI when IExtensionAdaptiveCard.Update is called. + _extensionAdaptiveCard = new ExtensionAdaptiveCard(_elementRegistration, _actionRegistration); + _extensionAdaptiveCard.UiUpdate += OnAdaptiveCardUpdated; + + // Initialize the adaptive card session with the extension adaptive card template and data with an initial + // call to IExtensionAdaptiveCard.Update. + _extensionAdaptiveCardSession.Initialize(_extensionAdaptiveCard); + } + catch (Exception ex) + { + _log.Error(ex, $"Failed to get creation options adaptive card from provider {_curProviderDetails.ComputeSystemProvider.Id}."); + SessionErrorMessage = ex.Message; + } + }); + } + + /// + /// When the is updated by the extension we need to render the new adaptive card in the UI. + /// This method does the work needed to create an adaptive card renderer, render the adaptive card and send the new adaptive card + /// any view that is listening for the message. + /// + public void OnAdaptiveCardUpdated(object sender, AdaptiveCard adaptiveCard) + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + // Render the adaptive card and set the action event handler. + _renderedAdaptiveCard = _adaptiveCardRenderer.RenderAdaptiveCard(adaptiveCard); + _renderedAdaptiveCard.Action += OnRenderedAdaptiveCardAction; + + // Send new card to listeners + _userInputsFromAdaptiveCard = _renderedAdaptiveCard.UserInputs; + WeakReferenceMessenger.Default.Send(new NewAdaptiveCardAvailableMessage(new RenderedAdaptiveCardData(Orchestrator.CurrentPageViewModel, _renderedAdaptiveCard))); + IsAdaptiveCardSessionLoaded = true; + + // We set CanGoToNextPage to true here because we can only validate the inputs when the user interacts with the adaptive card + // via the action buttons. + CanGoToNextPage = true; + }); + } + + /// + /// When the user interacts with the adaptive card by clicking the next or previous buttons in the Setup flow, we need to send + /// the inputs and actions back to the extension. The extension will then process the inputs and actions and update the adaptive card + /// Which will ultimately cause the method to be called. + /// + /// The rendered adaptive card whose submite or execute action was just invoked + /// The action and user inputs from within the adaptive card + private void OnRenderedAdaptiveCardAction(object sender, AdaptiveActionEventArgs args) + { + _windowEx.DispatcherQueue.TryEnqueue(async () => + { + IsAdaptiveCardSessionLoaded = false; + + // Send the inputs and actions that the user entered back to the extension. + await _extensionAdaptiveCardSession.OnAction(args.Action.ToJson().Stringify(), args.Inputs.AsJson().Stringify()); + }); + } + + private void ResetAdaptiveCardConfiguration() + { + SessionErrorMessage = null; + if (_extensionAdaptiveCardSession != null) + { + _extensionAdaptiveCardSession.Stopped -= OnAdaptiveCardSessionStopped; + } + + if (_extensionAdaptiveCard != null) + { + _extensionAdaptiveCard.UiUpdate -= OnAdaptiveCardUpdated; + } + + if (_renderedAdaptiveCard != null) + { + _renderedAdaptiveCard.Action -= OnRenderedAdaptiveCardAction; + } + } + + /// + /// The configure environment view page will request an adaptive card to display in the UI if it loads after the extension sends out the CreationOptionsViewPageRequestMessage. + /// + /// The class that should be receiving the request + /// The payload of the message request + private void OnEnvironmentOptionsViewRequest(EnvironmentCreationOptionsViewModel recipient, CreationOptionsViewPageRequestMessage message) + { + message.Reply(_renderedAdaptiveCard); + } + + /// + /// The review environments view / summary page view will request an adaptive card to display in the UI if it loads after this view model sends out the original RenderedAdaptiveCard message. + /// this can happen when the user navigates away from the review page to another page in Dev Home. E.g the settings page, then navigates back to the review page. At this point the review + /// page is unloaded when the user navigates away from it. When they navigate back to it, a new view will be created and loaded, so we need to request the adaptive again from this view model. + /// + /// The class that should be receiving the request + /// The payload of the message request + private void OnReviewPageViewRequest(EnvironmentCreationOptionsViewModel recipient, CreationOptionsReviewPageDataRequestMessage message) + { + // Only send the adaptive card if the session has loaded. If the session hasn't loaded yet, we'll send an empty response. The review page should be sent the adaptive card + // once the session has loaded in the OnAdaptiveCardUpdated method. + if (!IsAdaptiveCardSessionLoaded && Orchestrator?.CurrentPageViewModel is not SummaryViewModel) + { + return; + } + + message.Reply(_renderedAdaptiveCard); + } + + /// + /// When the extension indicates that the session has stopped, we need to get the result json from the session. Once we get this + /// we can send a message to the CreateEnvironmentTask to let it know that the adaptive card session has ended. + /// It will then update its setup tasks with information to create the compute system. + /// + /// The extension session object who stopped the session + /// Data payload that contains the users provided input + private void OnAdaptiveCardSessionStopped(ExtensionAdaptiveCardSession sender, ExtensionAdaptiveCardSessionStoppedEventArgs args) + { + // Send message to the CreateEnvironmentTask to let it know that the adaptive card session has ended. + // the task will use the ResultJson to create the compute system. + WeakReferenceMessenger.Default.Send(new CreationAdaptiveCardSessionEndedMessage(new CreationAdaptiveCardSessionEndedData(args.ResultJson, _curProviderDetails))); + sender.Stopped -= OnAdaptiveCardSessionStopped; + _extensionAdaptiveCard.UiUpdate -= OnAdaptiveCardUpdated; + } + + /// + /// Gets the adaptive card renderer that will be used to render the adaptive card in the UI. Its important to recreate the ItemsViewChoiceSet every time we want to + /// render an adaptive card because the parenting the ItemsView control to multiple parents will cause an exception to be thrown. + /// + private async Task GetAdaptiveCardRenderer() + { + var renderer = await _adaptiveCardRenderingService.GetRendererAsync(); + renderer.ElementRenderers.Set(DevHomeSettingsCardChoiceSet.AdaptiveElementType, new ItemsViewChoiceSet("SettingsCardWithButtonThatLaunchesContentDialog")); + + // We need to keep the same renderer for the ActionSet that is hooked up to the orchestrator as it will have the adaptive card + // context needed to invoke the adaptive card actions from outside the adaptive card. + renderer.ElementRenderers.Set("ActionSet", Orchestrator.DevHomeActionSetRenderer); + return renderer; + } + + /// + protected override AdaptiveInputs GetAdaptiveCardUserInputs() + { + return _userInputsFromAdaptiveCard; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs new file mode 100644 index 0000000000..dedd93a429 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.Common.Contracts.Services; +using DevHome.Common.Environments.Models; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.Services; +using Serilog; +using WinUIEx; + +namespace DevHome.SetupFlow.ViewModels.Environments; + +public partial class SelectEnvironmentProviderViewModel : SetupPageViewModelBase +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(SelectEnvironmentProviderViewModel)); + + private readonly IComputeSystemService _computeSystemService; + + public ComputeSystemProviderDetails SelectedProvider { get; private set; } + + [ObservableProperty] + private bool _areProvidersLoaded; + + [ObservableProperty] + private int _selectedProviderIndex; + + [ObservableProperty] + private ObservableCollection _providersViewModels; + + public SelectEnvironmentProviderViewModel( + ISetupFlowStringResource stringResource, + SetupFlowOrchestrator orchestrator, + IComputeSystemService computeSystemService) + : base(stringResource, orchestrator) + { + PageTitle = stringResource.GetLocalized(StringResourceKey.SelectEnvironmentPageTitle); + _computeSystemService = computeSystemService; + } + + private async Task LoadProvidersAsync() + { + AreProvidersLoaded = false; + Orchestrator.NotifyNavigationCanExecuteChanged(); + + var providerDetails = await Task.Run(_computeSystemService.GetComputeSystemProvidersAsync); + ProvidersViewModels = new(); + foreach (var providerDetail in providerDetails) + { + ProvidersViewModels.Add(new ComputeSystemProviderViewModel(providerDetail)); + } + + AreProvidersLoaded = true; + } + + protected async override Task OnFirstNavigateToAsync() + { + CanGoToNextPage = false; + await LoadProvidersAsync(); + } + + [RelayCommand] + private void ItemsViewSelectionChanged(ComputeSystemProviderViewModel sender) + { + if (sender != null) + { + // When navigating between the select providers page and the configure creation options page + // visual selection is lost, so we need deselect the providers first. Then select correct one. + // this will ensure that the correct provider is visually selected when navigating back to the select providers page. + foreach (var provider in ProvidersViewModels) + { + provider.IsSelected = false; + } + + sender.IsSelected = true; + SelectedProvider = sender.ProviderDetails; + + // Using the default channel to send the message to the recipient. In this case, the EnvironmentCreationOptionsViewModel. + // In the future if we support a multi-instance setup flow, we can use a custom channel/a message broker to send messages. + // For now, we are using the default channel. + WeakReferenceMessenger.Default.Send(new CreationProviderChangedMessage(SelectedProvider)); + CanGoToNextPage = true; + Orchestrator.NotifyNavigationCanExecuteChanged(); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs index 9d7999dfd9..f4e5b397c9 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs @@ -10,7 +10,6 @@ using DevHome.SetupFlow.Services; using Microsoft.UI.Xaml; using Serilog; -using Windows.Storage.Pickers; using WinUIEx; namespace DevHome.SetupFlow.ViewModels; @@ -146,7 +145,7 @@ private async Task PickCloneDirectoryAsync() } catch (Exception e) { - _log.Error("Failed to open folder picker", e); + _log.Error(e, "Failed to open folder picker"); return null; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs index 18a5c5c17d..76a5319861 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs @@ -34,6 +34,8 @@ public partial class MainPageViewModel : SetupPageViewModelBase private const string EnvironmentsSetupFlowFeatureName = "EnvironmentsSetupTargetFlow"; + private const string EnvironmentsCreationFlowFeatureName = "EnvironmentsCreationFlow"; + private readonly IHost _host; private readonly IWindowsPackageManager _wpm; private readonly IDesiredStateConfiguration _dsc; @@ -59,6 +61,8 @@ public partial class MainPageViewModel : SetupPageViewModelBase public bool ShouldShowSetupTargetItem => _experimentationService.IsFeatureEnabled(EnvironmentsSetupFlowFeatureName); + public bool ShouldShowCreateEnvironmentItem => _experimentationService.IsFeatureEnabled(EnvironmentsCreationFlowFeatureName); + /// /// Event raised when the user elects to start the setup flow. /// The orchestrator for the whole flow subscribes to this event to handle @@ -184,6 +188,20 @@ private void StartRepoConfig(string flowTitle) _host.GetService()); } + /// + /// Starts the create environment flow. + /// + [RelayCommand] + public void StartCreateEnvironment(string flowTitle) + { + _log.Information("Starting flow for environment creation"); + StartSetupFlowForTaskGroups( + flowTitle, + "CreateEnvironment", + _host.GetService(), + _host.GetService()); + } + /// /// Starts a setup flow that only includes app management. /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs index 693271087e..f8728f3722 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs @@ -93,7 +93,7 @@ private async Task LoadCatalogsAsync() } catch (Exception e) { - _log.Error($"Failed to load catalogs.", e); + _log.Error(e, $"Failed to load catalogs."); } finally { diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs index 7f9ef63faa..8926e1b559 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs @@ -41,6 +41,15 @@ public partial class ReviewViewModel : SetupPageViewModelBase [NotifyCanExecuteChangedFor(nameof(SetUpCommand))] private bool _canSetUp; + [ObservableProperty] + private string _reviewPageTitle; + + [ObservableProperty] + private string _reviewPageExpanderDescription; + + [ObservableProperty] + private string _reviewPageDescription; + public bool HasApplicationsToInstall => Orchestrator.GetTaskGroup()?.SetupTasks.Any() == true; public bool RequiresTermsAgreement => HasApplicationsToInstall; @@ -88,6 +97,8 @@ public ReviewViewModel( { NextPageButtonText = StringResource.GetLocalized(StringResourceKey.SetUpButton); PageTitle = StringResource.GetLocalized(StringResourceKey.ReviewPageTitle); + ReviewPageExpanderDescription = StringResource.GetLocalized(StringResourceKey.ReviewExpanderDescription); + ReviewPageDescription = StringResource.GetLocalized(StringResourceKey.SetupShellReviewPageDescription); _setupFlowOrchestrator = orchestrator; _configFileBuilder = configFileBuilder; @@ -104,7 +115,18 @@ protected async override Task OnEachNavigateToAsync() .ToList(); SelectedReviewTab = ReviewTabs.FirstOrDefault(); + // If the CreateEnvironmentTaskGroup is present, update the setup button text to "Create Environment" + // and page title to "Review your environment" + if (Orchestrator.GetTaskGroup() != null) + { + NextPageButtonText = StringResource.GetLocalized(StringResourceKey.CreateEnvironmentButtonText); + PageTitle = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewPageTitle); + ReviewPageExpanderDescription = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewExpanderDescription); + ReviewPageDescription = StringResource.GetLocalized(StringResourceKey.SetupShellReviewPageDescriptionForEnvironmentCreation); + } + NextPageButtonToolTipText = HasTasksToSetUp ? null : StringResource.GetLocalized(StringResourceKey.ReviewNothingToSetUpToolTip); + UpdateCanSetUp(); await Task.CompletedTask; @@ -142,7 +164,7 @@ private async Task OnSetUpAsync() } catch (Exception e) { - _log.Error($"Failed to initialize elevated process.", e); + _log.Error(e, $"Failed to initialize elevated process."); } } @@ -166,7 +188,7 @@ private async Task DownloadConfigurationAsync() } catch (Exception e) { - _log.Error($"Failed to download configuration file.", e); + _log.Error(e, $"Failed to download configuration file."); } } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs index 5cb8bfd161..a80727ff66 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs @@ -128,7 +128,7 @@ public SearchViewModel(IWindowsPackageManager wpm, ISetupFlowStringResource stri } catch (Exception e) { - _log.Error($"Search error.", e); + _log.Error(e, $"Search error."); return (SearchResultStatus.ExceptionThrown, null); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs index f6f3b23546..feba7c037b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs @@ -14,6 +14,7 @@ using DevHome.SetupFlow.Services; using DevHome.Telemetry; using Microsoft.Extensions.Hosting; +using Microsoft.UI.Xaml.Navigation; using Serilog; using Windows.Storage; @@ -26,6 +27,8 @@ public partial class SetupFlowViewModel : ObservableObject private readonly MainPageViewModel _mainPageViewModel; private readonly PackageProvider _packageProvider; + private readonly string _creationFlowNavigationParameter = "StartCreationFlow"; + public SetupFlowOrchestrator Orchestrator { get; } public event EventHandler EndSetupFlow = (s, e) => { }; @@ -119,4 +122,25 @@ public async Task StartFileActivationFlowAsync(StorageFile file) Orchestrator.FlowPages = [_mainPageViewModel]; await _mainPageViewModel.StartConfigurationFileAsync(file); } + + public void StartCreationFlowAsync() + { + Orchestrator.FlowPages = [_mainPageViewModel]; + _mainPageViewModel.StartCreateEnvironment(string.Empty); + } + + public void OnNavigatedTo(NavigationEventArgs args) + { + // The setup flow isn't setup to support using the navigation service to navigate to specific + // pages. Instead we need to navigate to the main page and then start the creation flow template manually. + var parameter = args.Parameter?.ToString(); + + if ((!string.IsNullOrEmpty(parameter)) && + _creationFlowNavigationParameter.Equals(parameter, StringComparison.OrdinalIgnoreCase) && + Orchestrator.CurrentSetupFlowKind != SetupFlowKind.CreateEnvironment) + { + Cancel(); + StartCreationFlowAsync(); + } + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs index d9b669bb3f..cc36f1b534 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs @@ -3,7 +3,11 @@ using System.Linq; using System.Threading.Tasks; +using AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Rendering.WinUI3; using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.DevHomeAdaptiveCards.Parsers; using DevHome.Common.TelemetryEvents.SetupFlow; using DevHome.SetupFlow.Services; using DevHome.Telemetry; @@ -189,4 +193,26 @@ protected async virtual Task OnFirstNavigateFromAsync() // Do nothing await Task.CompletedTask; } + + /// + /// Hook so the orchestrator can validate if the user can navigate to the next page when a page is rendering + /// an adaptive card that is hooked up to the . + /// + /// + /// The orchestrator takes care of calling this when appropriate through . + /// This runs on the UI thread, but the cost of validating the inputs should be minimal. + /// + protected virtual AdaptiveInputs GetAdaptiveCardUserInputs() + { + return new AdaptiveInputs(); + } + + /// + /// Performs the work to validate the user inputs when navigating to the next page when the page is rendering an adaptive card. + /// + /// The adaptive card inputs for the adaptive card currently presented to the user on the setup flow page + public AdaptiveInputs GetAdaptiveCardUserInputsForNavigationValidation() + { + return GetAdaptiveCardUserInputs(); + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs index 44ed8eabbe..80078db0f2 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs @@ -196,7 +196,7 @@ private bool ShouldShowListInUI(ComputeSystemsListViewModel listViewModel, strin } catch (Exception ex) { - _log.Error($"Error filtering ComputeSystemsListViewModel", ex); + _log.Error(ex, $"Error filtering ComputeSystemsListViewModel"); } return true; @@ -366,7 +366,7 @@ public async Task LoadAllComputeSystemsInTheUI() } catch (Exception ex) { - _log.Error($"Error loading ComputeSystemViewModels data", ex); + _log.Error(ex, $"Error loading ComputeSystemViewModels data"); } ShouldShowShimmerBelowList = false; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs index 884831843e..b6e45248dc 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs @@ -108,6 +108,20 @@ public ObservableCollection AppsDownloaded } } + public bool WasCreateEnvironmentOperationStarted + { + get + { + var taskGroup = Orchestrator.GetTaskGroup(); + if (taskGroup == null) + { + return false; + } + + return taskGroup.CreateEnvironmentTask.CreationOperationStarted; + } + } + public List AppsDownloadedInstallationNotes => AppsDownloaded.Where(p => !string.IsNullOrEmpty(p.InstallationNotes)).ToList(); public IList ConfigurationUnitResults => _configurationUnitResults.Value; @@ -134,6 +148,9 @@ public ObservableCollection AppsDownloaded [ObservableProperty] private string _applicationsClonedText; + [ObservableProperty] + private string _summaryPageEnvironmentCreatingText; + [RelayCommand] public void RemoveRestartGrid() { @@ -157,7 +174,6 @@ public void GoToMainPage() _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToMainPage"); } - [RelayCommand] public void GoToDashboard() { TelemetryFactory.Get().Log("Summary_NavigateTo_Event", LogLevel.Critical, new NavigateFromSummaryEvent("Dashboard"), Orchestrator.ActivityId); @@ -165,6 +181,26 @@ public void GoToDashboard() _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToDashboard"); } + [RelayCommand] + public void RedirectToNextPage() + { + if (WasCreateEnvironmentOperationStarted) + { + GoToEnvironmentsPage(); + return; + } + + // Default behavior is to go to the dashboard + GoToDashboard(); + } + + public void GoToEnvironmentsPage() + { + TelemetryFactory.Get().Log("Summary_NavigateTo_Event", LogLevel.Critical, new NavigateFromSummaryEvent("Environments"), Orchestrator.ActivityId); + _host.GetService().NavigateTo(KnownPageKeys.Environments); + _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToEnvironments"); + } + [RelayCommand] public void GoToDevHomeSettings() { @@ -180,6 +216,12 @@ public void GoToForDevelopersSettingsPage() Task.Run(() => Launcher.LaunchUriAsync(new Uri("ms-settings:developers"))).Wait(); } + [ObservableProperty] + private string _pageRedirectButtonText; + + [ObservableProperty] + private string _pageHeaderText; + public SummaryViewModel( ISetupFlowStringResource stringResource, SetupFlowOrchestrator orchestrator, @@ -199,6 +241,8 @@ public SummaryViewModel( _showRestartNeeded = Visibility.Collapsed; _appManagementInitializer = appManagementInitializer; _cloneRepoNextSteps = new(); + PageRedirectButtonText = StringResource.GetLocalized(StringResourceKey.SummaryPageOpenDashboard); + PageHeaderText = StringResource.GetLocalized(StringResourceKey.SummaryPageHeader); IsNavigationBarVisible = true; IsStepPage = false; @@ -259,6 +303,12 @@ protected async override Task OnFirstNavigateToAsync() TelemetryFactory.Get().LogCritical("Summary_NavigatedTo_Event", false, Orchestrator.ActivityId); } + if (WasCreateEnvironmentOperationStarted) + { + PageRedirectButtonText = StringResource.GetLocalized(StringResourceKey.SummaryPageRedirectToEnvironmentPageButton); + PageHeaderText = StringResource.GetLocalized(StringResourceKey.SummaryPageHeaderForEnvironmentCreationText); + } + await ReloadCatalogsAsync(); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml index 06639efbad..ce148ff92f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml @@ -61,9 +61,14 @@ + Margin="0, 20, 0, 0"> + + + + + + @@ -211,17 +216,29 @@ + x:Name="DevDriveCloneLocationAliasTextBox"> + + + + + + + Visibility="{x:Bind AddRepoViewModel.FolderPickerViewModel.InDevDriveScenario, Mode=OneWay, Converter={StaticResource NegatedBoolToVisibilityConverter}}"> + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs index b45115b4f1..01e125a4ad 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs @@ -110,29 +110,6 @@ public void DeveloperIdChangedEventHandler(object sender, IDeveloperId developer } } - /// - /// Validate the user put in a rooted, non-null path. - /// - private void CloneLocation_TextChanged(object sender, TextChangedEventArgs e) - { - // just in case something other than a text box calls this. - if (sender is TextBox cloneLocationTextBox) - { - var location = cloneLocationTextBox.Text; - if (string.Equals(cloneLocationTextBox.Name, "DevDriveCloneLocationAliasTextBox", StringComparison.Ordinal)) - { - location = (AddRepoViewModel.EditDevDriveViewModel.DevDrive != null) ? AddRepoViewModel.EditDevDriveViewModel.GetDriveDisplayName() : string.Empty; - } - - // In cases where location is empty don't update the cloneLocation. Only update when there are actual values. - AddRepoViewModel.FolderPickerViewModel.CloneLocation = string.IsNullOrEmpty(location) ? AddRepoViewModel.FolderPickerViewModel.CloneLocation : location; - } - - AddRepoViewModel.FolderPickerViewModel.ValidateCloneLocation(); - - AddRepoViewModel.ToggleCloneButton(); - } - /// /// If any items in reposToSelect exist in the UI, select them. /// An side-effect of SelectRange is SelectionChanged is fired for each item SelectRange is called on. @@ -306,17 +283,6 @@ private void MakeNewDevDriveCheckBox_Click(object sender, RoutedEventArgs e) } } - private void RepoUrlTextBox_TextChanged(object sender, RoutedEventArgs e) - { - // just in case something other than a text box calls this. - if (sender is TextBox) - { - AddRepoViewModel.Url = (sender as TextBox).Text; - } - - AddRepoViewModel.ToggleCloneButton(); - } - /// /// Putting the event in the view so SelectRange can be called. /// SelectRange needs a reference to the ListView. diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/EditClonePathDialog.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/EditClonePathDialog.xaml index 002122e47d..96f485a7ef 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/EditClonePathDialog.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/EditClonePathDialog.xaml @@ -53,7 +53,7 @@ - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml new file mode 100644 index 0000000000..8a98075a45 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs new file mode 100644 index 0000000000..24ca943ebb --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Serilog; + +namespace DevHome.SetupFlow.Views.Environments; + +public sealed partial class CreateEnvironmentReviewView : UserControl, IRecipient +{ + // Logging to capture any adaptive card rendering exceptions so the app doesn't crash + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(CreateEnvironmentReviewView)); + + public CreateEnvironmentReviewViewModel ViewModel => (CreateEnvironmentReviewViewModel)this.DataContext; + + public CreateEnvironmentReviewView() + { + this.InitializeComponent(); + WeakReferenceMessenger.Default.Register(this); + } + + private void ViewUnloaded(object sender, RoutedEventArgs e) + { + AdaptiveCardGrid.Children.Clear(); + WeakReferenceMessenger.Default.UnregisterAll(this); + } + + /// + /// Recieves the adaptive card from the view model, when the view model finishes loading it. + /// + public void Receive(NewAdaptiveCardAvailableMessage message) + { + // Only process the message if the view model is the ReviewViewModel + if (message.Value.CurrentSetupFlowViewModel is ReviewViewModel) + { + AddAdaptiveCardToUI(message.Value.RenderedAdaptiveCard); + } + } + + /// + /// Request the adaptive cad from the view model + /// + private void ViewLoaded(object sender, RoutedEventArgs e) + { + var message = WeakReferenceMessenger.Default.Send(); + if (!message.HasReceivedResponse) + { + return; + } + + AddAdaptiveCardToUI(message.Response); + } + + private void AddAdaptiveCardToUI(RenderedAdaptiveCard renderedAdaptiveCard) + { + try + { + var frameworkElement = renderedAdaptiveCard?.FrameworkElement; + if (frameworkElement == null) + { + return; + } + + AdaptiveCardGrid.Children.Clear(); + AdaptiveCardGrid.Children.Add(frameworkElement); + } + catch (Exception ex) + { + // Log the exception + _log.Error(ex, "Error adding adaptive card UI in CreateEnvironmentReviewView"); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml new file mode 100644 index 0000000000..fa967365f7 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs new file mode 100644 index 0000000000..7a438b6ae8 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.ViewModels.Environments; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Serilog; + +namespace DevHome.SetupFlow.Views.Environments; + +public sealed partial class EnvironmentCreationOptionsView : UserControl, IRecipient +{ + // Logging to capture any adaptive card rendering exceptions so the app doesn't crash + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(EnvironmentCreationOptionsView)); + + public EnvironmentCreationOptionsViewModel ViewModel => (EnvironmentCreationOptionsViewModel)this.DataContext; + + public EnvironmentCreationOptionsView() + { + this.InitializeComponent(); + WeakReferenceMessenger.Default.Register(this); + } + + private void ViewUnloaded(object sender, RoutedEventArgs e) + { + AdaptiveCardGrid.Children.Clear(); + WeakReferenceMessenger.Default.UnregisterAll(this); + } + + /// + /// Request the adaptive cad from the view model + /// + private void ViewLoaded(object sender, RoutedEventArgs e) + { + var message = WeakReferenceMessenger.Default.Send(); + if (!message.HasReceivedResponse) + { + return; + } + + AddAdaptiveCardToUI(message.Response); + } + + /// + /// Receive the adaptive card from the view model, when the view model finishes loading it. + /// Note: There are times when the view is loaded after the view model has finished loading the adaptive card. + /// In these cases it would have "missed" the push message. This is where the ViewLoaded method comes in. + /// + public void Receive(NewAdaptiveCardAvailableMessage message) + { + // Only process the message if the view model is the EnvironmentCreationOptionsViewModel + if (message.Value.CurrentSetupFlowViewModel is EnvironmentCreationOptionsViewModel) + { + AddAdaptiveCardToUI(message.Value.RenderedAdaptiveCard); + } + } + + private void AddAdaptiveCardToUI(RenderedAdaptiveCard adaptiveCardData) + { + try + { + if (adaptiveCardData?.FrameworkElement != null) + { + AdaptiveCardGrid.Children.Clear(); + AdaptiveCardGrid.Children.Add(adaptiveCardData.FrameworkElement); + } + } + catch (Exception ex) + { + // Log the exception + _log.Error(ex, "Error adding adaptive card UI in EnvironmentCreationOptionsView"); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml new file mode 100644 index 0000000000..30efe7f7f5 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml.cs new file mode 100644 index 0000000000..70707a5aa5 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.SetupFlow.ViewModels.Environments; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.Views.Environments; + +public sealed partial class SelectEnvironmentProviderView : UserControl +{ + public SelectEnvironmentProviderViewModel ViewModel => (SelectEnvironmentProviderViewModel)this.DataContext; + + public SelectEnvironmentProviderView() + { + this.InitializeComponent(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml index 36bb2ce586..8168967908 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml @@ -164,6 +164,24 @@ + + + + + + + + + - -