diff --git a/Build.ps1 b/Build.ps1
index 78f6de3eca..50c6e43d6d 100644
--- a/Build.ps1
+++ b/Build.ps1
@@ -49,6 +49,23 @@ $env:sdk_version = build\Scripts\CreateBuildInfo.ps1 -Version $VersionOfSDK -IsS
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')
+function Write-XmlDocumentToFile {
+ param (
+ [System.Xml.XmlDocument]$xmlDocument,
+ [string]$filePath
+ )
+
+ $settings = New-Object System.Xml.XmlWriterSettings
+ $settings.Indent = $true
+ $settings.CheckCharacters = $false
+ $settings.NewLineChars = "`r`n"
+
+ $writer = [System.Xml.XmlWriter]::Create($filePath, $settings)
+ $xmlDocument.WriteTo($writer)
+ $writer.Flush()
+ $writer.Close()
+}
+
if ($IsAzurePipelineBuild) {
Copy-Item (Join-Path $env:Build_RootDirectory "build\nuget.config.internal") -Destination (Join-Path $env:Build_RootDirectory "nuget.config")
}
@@ -127,6 +144,11 @@ Try {
$uapExtension = [System.Xml.Linq.XName]::Get("{http://schemas.microsoft.com/appx/manifest/uap/windows10/3}Extension");
$uapAppExtension = [System.Xml.Linq.XName]::Get("{http://schemas.microsoft.com/appx/manifest/uap/windows10/3}AppExtension");
+ # Update C++ version resources and header
+ $cppHeader = (Join-Path $env:Build_RootDirectory "build\cppversion\version.h")
+ $updatebinverpath = (Join-Path $env:Build_RootDirectory "build\scripts\update-binver.ps1")
+ & $updatebinverpath -TargetFile $cppHeader -BuildVersion $env:msix_version
+
# Update the appxmanifest
$appxmanifestPath = (Join-Path $env:Build_RootDirectory "src\Package.appxmanifest")
$appxmanifest = [System.Xml.Linq.XDocument]::Load($appxmanifestPath)
@@ -151,7 +173,7 @@ Try {
}
}
}
- $appxmanifest.Save($appxmanifestPath)
+ Write-XmlDocumentToFile -xmlDocument $appxmanifest -filePath $appxmanifestPath
# This is needed for vcxproj
& $nugetPath restore
@@ -164,6 +186,7 @@ Try {
("DevHome.sln"),
("/p:Platform="+$platform),
("/p:Configuration="+$configuration),
+ ("/p:Version="+$env:msix_version),
("/restore"),
("/binaryLogger:DevHome.$platform.$configuration.binlog"),
("/p:AppxPackageOutput=$appxPackageDir\DevHome-$platform.msix"),
@@ -186,6 +209,11 @@ Try {
}
}
+ # reset version file back to original values
+ $cppHeader = (Join-Path $env:Build_RootDirectory "build\cppversion\version.h")
+ $updatebinverpath = (Join-Path $env:Build_RootDirectory "build\scripts\update-binver.ps1")
+ & $updatebinverpath -TargetFile $cppHeader -BuildVersion "1.0.0.0"
+
# Reset the appxmanifest to prevent unnecessary code changes
$appxmanifest = [System.Xml.Linq.XDocument]::Load($appxmanifestPath)
$appxmanifest.Root.Element($xIdentity).Attribute("Version").Value = "0.0.0.0"
@@ -203,7 +231,7 @@ Try {
}
}
}
- $appxmanifest.Save($appxmanifestPath)
+ Write-XmlDocumentToFile -xmlDocument $appxmanifest -filePath $appxmanifestPath
}
if (($BuildStep -ieq "stubpackages")) {
diff --git a/DevHome.sln b/DevHome.sln
index dac61696af..f721edeb0a 100644
--- a/DevHome.sln
+++ b/DevHome.sln
@@ -122,6 +122,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevSetupAgent.Test", "exten
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HyperVExtension.HostGuestCommunication", "extensions\HyperVExtension\src\HyperVExtension.HostGuestCommunication\HyperVExtension.HostGuestCommunication.csproj", "{D759CD66-494C-4A00-8075-8B65A9891349}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.PI", "tools\PI\DevHome.PI\DevHome.PI.csproj", "{CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PI", "PI", "{DB3D0F2C-1A7F-44B4-B408-B21A56212985}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Customization", "Customization", "{623998FD-B0A6-4980-95D5-A5072301CA10}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.Customization", "tools\Customization\DevHome.Customization\DevHome.Customization.csproj", "{AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}"
@@ -142,6 +146,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.EnvironmentVariable
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.Telemetry.Native", "telemetry\DevHome.Telemetry.Native\DevHome.Telemetry.Native.vcxproj", "{8EB52F7D-D216-49FF-BF16-DE06E4695950}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{E768781A-D1F7-4C03-B46D-E76354FAB587}"
+ ProjectSection(SolutionItems) = preProject
+ tools\scripts\CaptureDevHomeLogs.ps1 = tools\scripts\CaptureDevHomeLogs.ps1
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|arm64 = Debug|arm64
@@ -614,6 +623,18 @@ Global
{D759CD66-494C-4A00-8075-8B65A9891349}.Release|x64.Build.0 = Release|x64
{D759CD66-494C-4A00-8075-8B65A9891349}.Release|x86.ActiveCfg = Release|x86
{D759CD66-494C-4A00-8075-8B65A9891349}.Release|x86.Build.0 = Release|x86
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|arm64.ActiveCfg = Debug|ARM64
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|arm64.Build.0 = Debug|ARM64
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|x64.ActiveCfg = Debug|x64
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|x64.Build.0 = Debug|x64
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|x86.ActiveCfg = Debug|x86
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|x86.Build.0 = Debug|x86
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|arm64.ActiveCfg = Release|ARM64
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|arm64.Build.0 = Release|ARM64
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|x64.ActiveCfg = Release|x64
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|x64.Build.0 = Release|x64
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|x86.ActiveCfg = Release|x86
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|x86.Build.0 = Release|x86
{AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Debug|arm64.ActiveCfg = Debug|arm64
{AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Debug|arm64.Build.0 = Debug|arm64
{AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Debug|x64.ActiveCfg = Debug|x64
@@ -745,6 +766,8 @@ Global
{F4095FD3-6A3F-490B-966D-E63059612EE6} = {3E3791DF-070D-4ADE-96E8-93D6FBD53953}
{0E05A442-BDC7-43D4-A000-F8C986826716} = {3E3791DF-070D-4ADE-96E8-93D6FBD53953}
{D759CD66-494C-4A00-8075-8B65A9891349} = {81AACED5-CFB5-47A6-AFD6-4625AADCFFA3}
+ {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725} = {DB3D0F2C-1A7F-44B4-B408-B21A56212985}
+ {DB3D0F2C-1A7F-44B4-B408-B21A56212985} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD}
{623998FD-B0A6-4980-95D5-A5072301CA10} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD}
{AF527EA4-6A24-4BD6-BC6E-A5863DC3489C} = {623998FD-B0A6-4980-95D5-A5072301CA10}
{FAB6FAA7-ADF4-4B65-9831-0C819915E6E1} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD}
@@ -752,8 +775,13 @@ Global
{1317314E-9BDD-4F1C-A76F-22121637A091} = {FAB6FAA7-ADF4-4B65-9831-0C819915E6E1}
{2E5629CA-0D1B-42B1-8D6E-934A6E1E18D9} = {FAB6FAA7-ADF4-4B65-9831-0C819915E6E1}
{5F9749BC-F34E-4F45-933F-61E0F3ED521F} = {FAB6FAA7-ADF4-4B65-9831-0C819915E6E1}
+ {E768781A-D1F7-4C03-B46D-E76354FAB587} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {030B5641-B206-46BB-BF71-36FF009088FA}
EndGlobalSection
+ GlobalSection(SharedMSBuildProjectFiles) = preSolution
+ build\cppversion\version.vcxitems*{092ac740-da01-4872-8e93-b9557dad6be5}*SharedItemsImports = 5
+ build\cppversion\version.vcxitems*{60e0fd98-5396-436d-bab7-187a853a5dc6}*SharedItemsImports = 5
+ EndGlobalSection
EndGlobal
diff --git a/Directory.CppBuild.props b/Directory.CppBuild.props
index 280febbe6d..59efed82bd 100644
--- a/Directory.CppBuild.props
+++ b/Directory.CppBuild.props
@@ -5,4 +5,8 @@
$(SolutionDir)tools\bin\$(CppPlatformTarget)\$(Configuration)\
$(CppBaseOutDir)$(MSBuildProjectName)\
+
+
+
+
\ No newline at end of file
diff --git a/TestingScenarios.md b/TestingScenarios.md
index 9421fede57..c99bcbc31d 100644
--- a/TestingScenarios.md
+++ b/TestingScenarios.md
@@ -12,4 +12,4 @@ These are the testing scenarios that need to be validated before shipping a new
1. [Managing Environments](tools/Environments/DevHome.Environments/TestingScenarios/ManageEnvironments.md)
1. [Creating Environment](tools/SetupFlow/DevHome.SetupFlow.UnitTest/TestingScenarios/CreateEnvironment.md)
1. [Setting up an Environment](tools/SetupFlow/DevHome.SetupFlow.UnitTest/TestingScenarios/SetupEnvironment.md)
-
+1. [Quickstart Playground](tools/SetupFlow/DevHome.SetupFlow.UnitTest/TestingScenarios/QuickstartPlayground.md)
diff --git a/build/TriggerReleaseBuild.yml b/build/TriggerReleaseBuild.yml
index 941be2a87f..97a2ec9654 100644
--- a/build/TriggerReleaseBuild.yml
+++ b/build/TriggerReleaseBuild.yml
@@ -1,6 +1,38 @@
trigger:
- release
-steps:
-- script: echo Triggering ADO Build
- displayName: 'Triggering ADO Build'
\ No newline at end of file
+resources:
+ repositories:
+ - repository: templates_onebranch
+ type: git
+ name: OneBranch.Pipelines/GovernedTemplates
+ ref: refs/heads/main
+ - repository: m365Pipelines
+ type: git
+ name: 1ESPipelineTemplates/M365GPT
+ ref: refs/tags/release
+
+extends:
+ template: v1/M365.Official.PipelineTemplate.yml@m365Pipelines
+ parameters:
+ sdl:
+ roslyn:
+ enabled: true
+ arrow:
+ serviceConnection: DevHome Build VM Generation
+ baseline:
+ baselineFile: $(Build.SourcesDirectory)\guardian\SDL\.gdnbaselines
+ pool:
+ name: Azure-Pipelines-1ESPT-ExDShared
+ image: windows-2022
+ os: windows
+ customBuildTags:
+ - ES365AIMigrationTooling
+ stages:
+ - stage: Trigger_Build
+ dependsOn: []
+ jobs:
+ - job: Trigger_Build
+ steps:
+ - script: echo Triggering ADO Build
+ displayName: 'Triggering ADO Build'
\ No newline at end of file
diff --git a/build/TriggerStagingBuild.yml b/build/TriggerStagingBuild.yml
index 3d4f50ebe9..c13329cf17 100644
--- a/build/TriggerStagingBuild.yml
+++ b/build/TriggerStagingBuild.yml
@@ -1,6 +1,38 @@
trigger:
- staging
-steps:
-- script: echo Triggering ADO Build
- displayName: 'Triggering ADO Build'
\ No newline at end of file
+resources:
+ repositories:
+ - repository: templates_onebranch
+ type: git
+ name: OneBranch.Pipelines/GovernedTemplates
+ ref: refs/heads/main
+ - repository: m365Pipelines
+ type: git
+ name: 1ESPipelineTemplates/M365GPT
+ ref: refs/tags/release
+
+extends:
+ template: v1/M365.Official.PipelineTemplate.yml@m365Pipelines
+ parameters:
+ sdl:
+ roslyn:
+ enabled: true
+ arrow:
+ serviceConnection: DevHome Build VM Generation
+ baseline:
+ baselineFile: $(Build.SourcesDirectory)\guardian\SDL\.gdnbaselines
+ pool:
+ name: Azure-Pipelines-1ESPT-ExDShared
+ image: windows-2022
+ os: windows
+ customBuildTags:
+ - ES365AIMigrationTooling
+ stages:
+ - stage: Trigger_Build
+ dependsOn: []
+ jobs:
+ - job: Trigger_Build
+ steps:
+ - script: echo Triggering ADO Build
+ displayName: 'Triggering ADO Build'
\ No newline at end of file
diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml
index 7e4a24542c..66ffde2521 100644
--- a/build/azure-pipelines.yml
+++ b/build/azure-pipelines.yml
@@ -21,7 +21,7 @@ parameters:
variables:
# MSIXVersion's second part should always be odd to account for stub app's version
- MSIXVersion: '0.1401'
+ MSIXVersion: '0.1501'
VersionOfSDK: '0.600'
solution: '**/DevHome.sln'
appxPackageDir: 'AppxPackages'
@@ -66,8 +66,6 @@ extends:
- task: NuGetToolInstaller@1
- task: NuGetAuthenticate@1
- inputs:
- nuGetServiceConnections: 'DevHomeInternal'
- task: PowerShell@2
displayName: Build SDK
@@ -109,8 +107,6 @@ extends:
- task: NuGetToolInstaller@1
- task: NuGetAuthenticate@1
- inputs:
- nuGetServiceConnections: 'DevHomeInternal'
- task: PowerShell@2
displayName: Build Stub Packages
@@ -139,8 +135,6 @@ extends:
- task: NuGetToolInstaller@1
- task: NuGetAuthenticate@1
- inputs:
- nuGetServiceConnections: 'DevHomeInternal'
- task: PowerShell@2
displayName: Replace Stubbed Files
@@ -153,7 +147,7 @@ extends:
artifactName: 'SdkNugetPackage'
targetPath: '$(Pipeline.Workspace)\sdkArtifacts\'
- - task: MicrosoftTDBuild.tdbuild-task.tdbuild-task.TouchdownBuildTask@3
+ - task: MicrosoftTDBuild.tdbuild-task.tdbuild-task.TouchdownBuildTask@4
displayName: Send and Download Localization Files for Artifacts
condition: and(eq(variables['EnableLocalization'], 'true'), eq(variables['UpdateLocalization'], 'true'))
inputs:
@@ -168,7 +162,7 @@ extends:
appendRelativeDir: true
pseudoSetting: Included
- - task: MicrosoftTDBuild.tdbuild-task.tdbuild-task.TouchdownBuildTask@3
+ - task: MicrosoftTDBuild.tdbuild-task.tdbuild-task.TouchdownBuildTask@4
displayName: Download and Use Localization Files
condition: eq(variables['EnableLocalization'], 'true')
retryCountOnTaskFailure: 2
@@ -241,46 +235,45 @@ extends:
$(Build.SourcesDirectory)\**\obj\**\*.r2r.ni.pdb
- - task: EsrpCodeSigning@2
- inputs:
- ConnectedServiceName: 'Xlang Code Signing'
- FolderPath: '$(appxPackageDir)\${{ configuration }}'
- Pattern: '*.msix'
- signConfigType: 'inlineSignParams'
- inlineOperation: |
- [
- {
- "keycode": "CP-230012",
- "operationSetCode": "SigntoolvNextSign",
- "parameters": [
- {
- "parameterName": "OpusName",
- "parameterValue": "Microsoft"
- },
- {
- "parameterName": "OpusInfo",
- "parameterValue": "http://www.microsoft.com"
- },
- {
- "parameterName": "PageHash",
- "parameterValue": "/NPH"
- },
- {
- "parameterName": "FileDigest",
- "parameterValue": "/fd sha256"
- },
- {
- "parameterName": "TimeStamp",
- "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
- }
- ],
- "toolName": "signtool.exe",
- "toolVersion": "6.2.9304.0"
- }
- ]
- SessionTimeout: '60'
- MaxConcurrency: '50'
- MaxRetryAttempts: '5'
+ - template: ./build/templates/EsrpSigning-Steps.yml@self
+ parameters:
+ displayName: Submit *.msix to ESRP for code signing
+ inputs:
+ FolderPath: '$(appxPackageDir)\${{ configuration }}'
+ Pattern: '*.msix'
+ UseMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: >-
+ [
+ {
+ "keycode": "CP-230012",
+ "operationSetCode": "SigntoolvNextSign",
+ "parameters": [
+ {
+ "parameterName": "OpusName",
+ "parameterValue": "Microsoft"
+ },
+ {
+ "parameterName": "OpusInfo",
+ "parameterValue": "http://www.microsoft.com"
+ },
+ {
+ "parameterName": "PageHash",
+ "parameterValue": "/NPH"
+ },
+ {
+ "parameterName": "FileDigest",
+ "parameterValue": "/fd sha256"
+ },
+ {
+ "parameterName": "TimeStamp",
+ "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
+ }
+ ],
+ "toolName": "signtool.exe",
+ "toolVersion": "6.2.9304.0"
+ }
+ ]
# Commented out until our implementation is fixed
# - task: AzureKeyVault@1
@@ -394,47 +387,46 @@ extends:
filePath: 'build/scripts/UnbundleStubPackage.ps1'
arguments: -InputPath $(appxPackageDir)\Staging -OutputLocation $(appxPackageDir)\${{ configuration }}\AppxMetadata\Stub
- - task: EsrpCodeSigning@2
- condition: and(eq(variables['BuildingBranch'], 'release'), eq('${{ configuration }}', 'Release'))
- inputs:
- ConnectedServiceName: 'Xlang Code Signing'
- FolderPath: '$(appxPackageDir)\${{ configuration }}\AppxMetadata\Stub'
- Pattern: '*.msix'
- signConfigType: 'inlineSignParams'
- inlineOperation: |
- [
- {
- "keycode": "CP-230012",
- "operationSetCode": "SigntoolvNextSign",
- "parameters": [
- {
- "parameterName": "OpusName",
- "parameterValue": "Microsoft"
- },
- {
- "parameterName": "OpusInfo",
- "parameterValue": "http://www.microsoft.com"
- },
- {
- "parameterName": "PageHash",
- "parameterValue": "/NPH"
- },
- {
- "parameterName": "FileDigest",
- "parameterValue": "/fd sha256"
- },
+ - ${{ if and(eq(variables['BuildingBranch'], 'release'), eq('${{ configuration }}', 'Release')) }}:
+ - template: ./build/templates/EsrpSigning-Steps.yml@self
+ parameters:
+ displayName: Submit *.msix to ESRP for code signing
+ inputs:
+ FolderPath: '$(appxPackageDir)\${{ configuration }}\AppxMetadata\Stub'
+ Pattern: '*.msix'
+ UseMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
{
- "parameterName": "TimeStamp",
- "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
+ "keycode": "CP-230012",
+ "operationSetCode": "SigntoolvNextSign",
+ "parameters": [
+ {
+ "parameterName": "OpusName",
+ "parameterValue": "Microsoft"
+ },
+ {
+ "parameterName": "OpusInfo",
+ "parameterValue": "http://www.microsoft.com"
+ },
+ {
+ "parameterName": "PageHash",
+ "parameterValue": "/NPH"
+ },
+ {
+ "parameterName": "FileDigest",
+ "parameterValue": "/fd sha256"
+ },
+ {
+ "parameterName": "TimeStamp",
+ "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
+ }
+ ],
+ "toolName": "signtool.exe",
+ "toolVersion": "6.2.9304.0"
}
- ],
- "toolName": "signtool.exe",
- "toolVersion": "6.2.9304.0"
- }
- ]
- SessionTimeout: '60'
- MaxConcurrency: '50'
- MaxRetryAttempts: '5'
+ ]
- task: PowerShell@2
displayName: Build MsixBundle
@@ -442,46 +434,45 @@ extends:
filePath: 'Build.ps1'
arguments: -Configuration "${{ configuration }}" -Version $(MSIXVersion) -BuildStep "msixbundle" -IsAzurePipelineBuild
- - task: EsrpCodeSigning@2
- inputs:
- ConnectedServiceName: 'Xlang Code Signing'
- FolderPath: 'AppxBundles\${{ configuration }}'
- Pattern: '*.msixbundle'
- signConfigType: 'inlineSignParams'
- inlineOperation: |
- [
- {
- "keycode": "CP-230012",
- "operationSetCode": "SigntoolvNextSign",
- "parameters": [
- {
- "parameterName": "OpusName",
- "parameterValue": "Microsoft"
- },
- {
- "parameterName": "OpusInfo",
- "parameterValue": "http://www.microsoft.com"
- },
- {
- "parameterName": "PageHash",
- "parameterValue": "/NPH"
- },
- {
- "parameterName": "FileDigest",
- "parameterValue": "/fd sha256"
- },
- {
- "parameterName": "TimeStamp",
- "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
- }
- ],
- "toolName": "signtool.exe",
- "toolVersion": "6.2.9304.0"
- }
- ]
- SessionTimeout: '60'
- MaxConcurrency: '50'
- MaxRetryAttempts: '5'
+ - template: ./build/templates/EsrpSigning-Steps.yml@self
+ parameters:
+ displayName: Submit *.msixbundle to ESRP for code signing
+ inputs:
+ FolderPath: 'AppxBundles\${{ configuration }}'
+ Pattern: '*.msixbundle'
+ UseMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
+ {
+ "keycode": "CP-230012",
+ "operationSetCode": "SigntoolvNextSign",
+ "parameters": [
+ {
+ "parameterName": "OpusName",
+ "parameterValue": "Microsoft"
+ },
+ {
+ "parameterName": "OpusInfo",
+ "parameterValue": "http://www.microsoft.com"
+ },
+ {
+ "parameterName": "PageHash",
+ "parameterValue": "/NPH"
+ },
+ {
+ "parameterName": "FileDigest",
+ "parameterValue": "/fd sha256"
+ },
+ {
+ "parameterName": "TimeStamp",
+ "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
+ }
+ ],
+ "toolName": "signtool.exe",
+ "toolVersion": "6.2.9304.0"
+ }
+ ]
templateContext:
outputs:
@@ -515,7 +506,7 @@ extends:
artifactName: MsixBundle_Release
targetPath: StorePublish
- - task: MicrosoftTDBuild.tdbuild-task.tdbuild-task.TouchdownBuildTask@3
+ - task: MicrosoftTDBuild.tdbuild-task.tdbuild-task.TouchdownBuildTask@4
displayName: Download and Use Localization Files
condition: eq(variables['EnableLocalization'], 'true')
retryCountOnTaskFailure: 2
@@ -540,11 +531,11 @@ extends:
$Files | % { Move-Item -Verbose $_.Directory $_.Directory.Parent.Parent -EA:Ignore }
pwsh: true
- - task: MS-RDX-MRO.windows-store-publish-dev.package-task.store-package@2
+ - task: MS-RDX-MRO.windows-store-publish-dev.package-task.store-package@3
displayName: 'Create Staging StoreBroker Package'
condition: eq(variables['BuildingBranch'], 'staging')
inputs:
- serviceEndpoint: 'DevHomeCanary StoreBroker'
+ serviceEndpoint: 'DevHomeCanary StoreBroker ServiceConnection'
sbConfigPath: '$(System.DefaultWorkingDirectory)\build\store\canary\SBConfig.json'
sourceFolder: 'StorePublish'
contents: '*.msixbundle'
@@ -553,11 +544,11 @@ extends:
outSBName: DevHomeStoreSubmissionPackage
pdpInclude: 'PDP.xml'
- - task: MS-RDX-MRO.windows-store-publish-dev.publish-task.store-publish@2
+ - task: MS-RDX-MRO.windows-store-publish-dev.publish-task.store-publish@3
displayName: 'Publish Staging StoreBroker Package'
condition: eq(variables['BuildingBranch'], 'staging')
inputs:
- serviceEndpoint: 'DevHomeCanary StoreBroker'
+ serviceEndpoint: 'DevHomeCanary StoreBroker ServiceConnection'
appId: 9MX22N5S7HRD
inputMethod: JsonAndZip
jsonPath: '$(System.DefaultWorkingDirectory)\SBOutDir\DevHomeStoreSubmissionPackage.json'
@@ -568,11 +559,11 @@ extends:
jsonZipUpdateMetadata: true
updateImages: true
- - task: MS-RDX-MRO.windows-store-publish-dev.package-task.store-package@2
+ - task: MS-RDX-MRO.windows-store-publish-dev.package-task.store-package@3
displayName: 'Create Release StoreBroker Package'
condition: eq(variables['BuildingBranch'], 'release')
inputs:
- serviceEndpoint: 'DevHome StoreBroker'
+ serviceEndpoint: 'DevHome StoreBroker ServiceConnection'
sbConfigPath: '$(System.DefaultWorkingDirectory)\build\store\preview\SBConfig.json'
sourceFolder: 'StorePublish'
contents: '*.msixbundle'
@@ -581,11 +572,11 @@ extends:
outSBName: DevHomeStoreSubmissionPackage
pdpInclude: 'PDP.xml'
- - task: MS-RDX-MRO.windows-store-publish-dev.publish-task.store-publish@2
+ - task: MS-RDX-MRO.windows-store-publish-dev.publish-task.store-publish@3
displayName: 'Publish Release StoreBroker Package'
condition: eq(variables['BuildingBranch'], 'release')
inputs:
- serviceEndpoint: 'DevHome StoreBroker'
+ serviceEndpoint: 'DevHome StoreBroker ServiceConnection'
appId: 9N8MHTPHNGVV
inputMethod: JsonAndZip
jsonPath: '$(System.DefaultWorkingDirectory)\SBOutDir\DevHomeStoreSubmissionPackage.json'
diff --git a/build/cppversion/version.h b/build/cppversion/version.h
new file mode 100644
index 0000000000..86885d7378
--- /dev/null
+++ b/build/cppversion/version.h
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#define STRINGIZE2(s) #s
+#define STRINGIZE(s) STRINGIZE2(s)
+
+#define VERSION_MAJOR 1
+#define VERSION_MINOR 0
+#define VERSION_BUILD 0
+#define VERSION_REVISION 0
+
+#define VER_FILE_DESCRIPTION_STR "Dev Home"
+#define VER_ORIGINAL_FILENAME_STR "DevHome"
+#define VER_FILE_VERSION VERSION_MAJOR, VERSION_MINOR, VERSION_BUILD, VERSION_REVISION
+#define VER_FILE_VERSION_STR STRINGIZE(VERSION_MAJOR) \
+ "." STRINGIZE(VERSION_MINOR) \
+ "." STRINGIZE(VERSION_BUILD) \
+ "." STRINGIZE(VERSION_REVISION) \
+
+#define VER_PRODUCTNAME_STR "Dev Home"
+#define VER_PRODUCT_VERSION VER_FILE_VERSION
+#define VER_PRODUCT_VERSION_STR VER_FILE_VERSION_STR
+#define VER_INTERNAL_NAME_STR VER_ORIGINAL_FILENAME_STR
+#define VER_COPYRIGHT_STR "Copyright (c) Microsoft Corporation"
+
+#ifdef _DEBUG
+#define VER_VER_DEBUG VS_FF_DEBUG
+#else
+#define VER_VER_DEBUG 0
+#endif
+
+#define VER_FILEOS VOS_NT_WINDOWS32
+#define VER_FILEFLAGS VER_VER_DEBUG
+#define VER_FILETYPE VFT_APP
+
diff --git a/build/cppversion/version.rc b/build/cppversion/version.rc
new file mode 100644
index 0000000000..74e46c30cd
--- /dev/null
+++ b/build/cppversion/version.rc
@@ -0,0 +1,96 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+//
+#include "version.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VER_FILE_VERSION
+ PRODUCTVERSION VER_PRODUCT_VERSION
+ FILEFLAGSMASK 0x3fL
+ FILEFLAGS VER_FILEFLAGS
+ FILEOS VER_FILEOS
+ FILETYPE VER_FILETYPE
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0"
+ BEGIN
+ VALUE "FileDescription", VER_FILE_DESCRIPTION_STR "\0"
+ VALUE "FileVersion", VER_FILE_VERSION_STR "\0"
+ VALUE "InternalName", VER_INTERNAL_NAME_STR "\0"
+ VALUE "LegalCopyright", VER_COPYRIGHT_STR "\0"
+ VALUE "OriginalFilename", VER_ORIGINAL_FILENAME_STR "\0"
+ VALUE "ProductName", VER_PRODUCTNAME_STR
+ VALUE "ProductVersion", VER_PRODUCT_VERSION_STR "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
+
diff --git a/build/cppversion/version.vcxitems b/build/cppversion/version.vcxitems
new file mode 100644
index 0000000000..27136662bc
--- /dev/null
+++ b/build/cppversion/version.vcxitems
@@ -0,0 +1,23 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ {6e36ddd7-1602-474e-b1d7-d0a7e1d5ad86}
+
+
+
+ %(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory)include
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build/scripts/CreateBuildInfo.ps1 b/build/scripts/CreateBuildInfo.ps1
index 4acf970541..232711ad55 100644
--- a/build/scripts/CreateBuildInfo.ps1
+++ b/build/scripts/CreateBuildInfo.ps1
@@ -6,7 +6,7 @@ Param(
)
$Major = "0"
-$Minor = "14"
+$Minor = "15"
$Patch = "99" # default to 99 for local builds
$versionSplit = $Version.Split(".");
diff --git a/build/scripts/update-binver.ps1 b/build/scripts/update-binver.ps1
new file mode 100644
index 0000000000..386d7f2c90
--- /dev/null
+++ b/build/scripts/update-binver.ps1
@@ -0,0 +1,66 @@
+<#
+.SYNOPSIS
+ Updates the given version header file to match the version info passed in as a string.
+.DESCRIPTION
+ Updates the given header file with the version information passed in. The version information is passed in as a string in the format "Major.Minor.Build.Revision".
+ to match. See the existing version.h for the format.
+.PARAMETER TargetFile
+ The file to update with version information.
+.PARAMETER BuildVersion
+ The build version to use.
+#>
+param(
+ [Parameter(Mandatory=$true)]
+ [string]$TargetFile,
+
+ [Parameter(Mandatory=$true)]
+ [string]$BuildVersion = "1.0.0.0"
+)
+
+
+$VersionParts = $BuildVersion.Split('.')
+$MajorVersion = $VersionParts[0]
+$MinorVersion = $VersionParts[1]
+$BuildVersion = $VersionParts[2]
+
+Write-Host "Using version: $MajorVersion.$MinorVersion.$BuildVersion.0"
+
+if (![String]::IsNullOrEmpty($TargetFile))
+{
+ $Local:FullPath = Resolve-Path $TargetFile
+ Write-Host "Updating file: $Local:FullPath"
+ if (Test-Path $TargetFile)
+ {
+ $Local:ResultContent = ""
+ foreach ($Local:line in [System.IO.File]::ReadLines($Local:FullPath))
+ {
+ if ($Local:line.StartsWith("#define VERSION_MAJOR"))
+ {
+ $Local:ResultContent += "#define VERSION_MAJOR $MajorVersion";
+ }
+ elseif ($Local:line.StartsWith("#define VERSION_MINOR"))
+ {
+ $Local:ResultContent += "#define VERSION_MINOR $MinorVersion";
+ }
+ elseif ($Local:line.StartsWith("#define VERSION_BUILD"))
+ {
+ $Local:ResultContent += "#define VERSION_BUILD $BuildVersion";
+ }
+ else
+ {
+ $Local:ResultContent += $Local:line;
+ }
+
+ # Add a newline if the line does not already have one
+ if (!$Local:line.EndsWith("`n"))
+ {
+ $Local:ResultContent += "`n";
+ }
+ }
+ Set-Content -Path $Local:FullPath -Value $Local:ResultContent -NoNewline
+ }
+ else
+ {
+ Write-Error "Did not find target file: $TargetFile"
+ }
+}
diff --git a/build/templates/EsrpSigning-Steps.yml b/build/templates/EsrpSigning-Steps.yml
new file mode 100644
index 0000000000..63eb25b47c
--- /dev/null
+++ b/build/templates/EsrpSigning-Steps.yml
@@ -0,0 +1,22 @@
+parameters:
+ - name: displayName
+ type: string
+ default: ESRP Code Signing
+ - name: inputs
+ type: object
+ default: {}
+
+steps:
+ - task: EsrpCodeSigning@5
+ displayName: ${{ parameters.displayName }}
+ inputs:
+ ConnectedServiceName: $(EsrpConnectedServiceName)
+ AppRegistrationClientId: $(EsrpAppRegistrationClientId)
+ AppRegistrationTenantId: $(EsrpAppRegistrationTenantId)
+ AuthAKVName: $(EsrpAuthAKVName)
+ AuthCertName: $(EsrpAuthCertName)
+ AuthSignCertName: $(EsrpAuthSignCertName)
+ SessionTimeout: '60'
+ MaxConcurrency: '50'
+ MaxRetryAttempts: '5'
+ ${{ insert }}: ${{ parameters.inputs }}
diff --git a/common/DevHome.Common.csproj b/common/DevHome.Common.csproj
index 5e046bf8ec..d730ac47e8 100644
--- a/common/DevHome.Common.csproj
+++ b/common/DevHome.Common.csproj
@@ -25,7 +25,7 @@
-
+
@@ -55,7 +55,7 @@
-
+
diff --git a/common/Environments/CustomControls/CardHeader.xaml b/common/Environments/CustomControls/CardHeader.xaml
index 21b3e5270e..6c20d72f53 100644
--- a/common/Environments/CustomControls/CardHeader.xaml
+++ b/common/Environments/CustomControls/CardHeader.xaml
@@ -52,6 +52,7 @@
IsTabStop="False"
HorizontalContentAlignment="Right"
VerticalContentAlignment="Stretch"
- ContentTemplate="{x:Bind ActionControlTemplate, Mode=OneWay}"/>
+ ContentTemplate="{x:Bind ActionControlTemplate, Mode=OneWay}"
+ Visibility="{x:Bind OperationsVisibility, Mode=OneWay}"/>
diff --git a/common/Environments/CustomControls/CardHeader.xaml.cs b/common/Environments/CustomControls/CardHeader.xaml.cs
index c243ff0925..8ec5361e11 100644
--- a/common/Environments/CustomControls/CardHeader.xaml.cs
+++ b/common/Environments/CustomControls/CardHeader.xaml.cs
@@ -26,6 +26,12 @@ public string HeaderCaption
set => SetValue(HeaderCaptionProperty, value);
}
+ public bool OperationsVisibility
+ {
+ get => (bool)GetValue(OperationsVisibilityProperty);
+ set => SetValue(OperationsVisibilityProperty, value);
+ }
+
public BitmapImage HeaderIcon
{
get => (BitmapImage)GetValue(HeaderIconProperty);
@@ -35,4 +41,5 @@ public BitmapImage HeaderIcon
private static readonly DependencyProperty ActionControlTemplateProperty = DependencyProperty.Register(nameof(ActionControlTemplate), typeof(DataTemplate), typeof(CardHeader), new PropertyMetadata(null));
private static readonly DependencyProperty HeaderCaptionProperty = DependencyProperty.Register(nameof(HeaderCaption), typeof(string), typeof(CardHeader), new PropertyMetadata(null));
private static readonly DependencyProperty HeaderIconProperty = DependencyProperty.Register(nameof(HeaderIcon), typeof(BitmapImage), typeof(CardHeader), new PropertyMetadata(null));
+ private static readonly DependencyProperty OperationsVisibilityProperty = DependencyProperty.Register(nameof(HeaderCaption), typeof(bool), typeof(CardHeader), new PropertyMetadata(true));
}
diff --git a/common/Environments/Exceptions/EnvironmentNotificationScriptException.cs b/common/Environments/Exceptions/EnvironmentNotificationScriptException.cs
new file mode 100644
index 0000000000..2e37aa0812
--- /dev/null
+++ b/common/Environments/Exceptions/EnvironmentNotificationScriptException.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace DevHome.Common.Environments.Exceptions;
+
+public class EnvironmentNotificationScriptException : Exception
+{
+ public EnvironmentNotificationScriptException(string message)
+ : base(message)
+ {
+ }
+}
diff --git a/common/Environments/Helpers/ComputeSystemHelpers.cs b/common/Environments/Helpers/ComputeSystemHelpers.cs
index e2635a4ee7..ec305bbfee 100644
--- a/common/Environments/Helpers/ComputeSystemHelpers.cs
+++ b/common/Environments/Helpers/ComputeSystemHelpers.cs
@@ -8,6 +8,8 @@
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using DevHome.Common.Environments.Models;
+using DevHome.Common.TelemetryEvents.SetupFlow.Environments;
+using Microsoft.Extensions.Hosting;
using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.Windows.DevHome.SDK;
using Serilog;
@@ -168,4 +170,23 @@ public static bool RemoveAllItemsAndReplace(ObservableCollection collectio
return false;
}
+
+ public static (string DisplayMessage, string DiagnosticText, EnvironmentsTelemetryStatus Status) LogResult(ProviderOperationResult? result, ILogger logger)
+ {
+ var telemetryStatus = EnvironmentsTelemetryStatus.Succeeded;
+
+ if (result == null)
+ {
+ var logErrorMsg = $"The returned result object was null";
+ logger.Error(logErrorMsg);
+ return (logErrorMsg, logErrorMsg, EnvironmentsTelemetryStatus.Failed);
+ }
+ else if (result.Status == ProviderOperationStatus.Failure)
+ {
+ logger.Error(result.ExtendedError, $"Operation failed with error:{result.DiagnosticText}");
+ telemetryStatus = EnvironmentsTelemetryStatus.Failed;
+ }
+
+ return (result.DisplayMessage, result.DiagnosticText, telemetryStatus);
+ }
}
diff --git a/common/Environments/Helpers/EnvironmentsNotificationHelper.cs b/common/Environments/Helpers/EnvironmentsNotificationHelper.cs
index 98d56cd1c4..d9622ce29b 100644
--- a/common/Environments/Helpers/EnvironmentsNotificationHelper.cs
+++ b/common/Environments/Helpers/EnvironmentsNotificationHelper.cs
@@ -2,16 +2,21 @@
// Licensed under the MIT License.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI.Behaviors;
+using DevHome.Common.Environments.Exceptions;
using DevHome.Common.Environments.Models;
using DevHome.Common.Environments.Scripts;
using DevHome.Common.Extensions;
using DevHome.Common.Helpers;
using DevHome.Common.Services;
+using DevHome.Common.TelemetryEvents.Environments;
+using DevHome.Common.TelemetryEvents.SetupFlow.Environments;
+using DevHome.Telemetry;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.DevHome.SDK;
using Serilog;
@@ -155,6 +160,11 @@ public void DisplayComputeSystemOperationError(string providerDisplayName, strin
[RelayCommand]
private void RestartComputer()
{
+ TelemetryFactory.Get().Log(
+ "Environment_RestartComputer_Event",
+ LogLevel.Critical,
+ new EnvironmentRestartUserEvent());
+
var startInfo = new ProcessStartInfo
{
WindowStyle = ProcessWindowStyle.Hidden,
@@ -195,7 +205,9 @@ private void AddUserToHyperVAdminGroupAndEnableHyperV(Notification notification)
startInfo.Verb = "runas";
var process = new Process();
- process.StartInfo = startInfo;
+ process.StartInfo = startInfo;
+ var telemetryEnablementMap = new Dictionary();
+
Task.Run(() =>
{
// Since a UAC prompt will be shown, we need to wait for the process to exit
@@ -214,37 +226,64 @@ private void AddUserToHyperVAdminGroupAndEnableHyperV(Notification notification)
case 0:
// The script successfully added the user to the Hyper-V Admin Group and enabled the Hyper-V Feature.
_shouldShowHyperVRebootButton = true;
+ telemetryEnablementMap.Add(FeatureEnablementKind.HyperVFeature, EnvironmentsTelemetryStatus.Succeeded);
+ telemetryEnablementMap.Add(FeatureEnablementKind.HyperVAdminGroup, EnvironmentsTelemetryStatus.Succeeded);
+ LogEnablementTelemetry(telemetryEnablementMap);
ShowRestartNotification();
return;
case 2:
// Hyper-V Feature is already enabled and the script successfully added the user to the Hyper-V Admin group.
_shouldShowHyperVRebootButton = true;
+ telemetryEnablementMap.Add(FeatureEnablementKind.HyperVAdminGroup, EnvironmentsTelemetryStatus.Succeeded);
+ LogEnablementTelemetry(telemetryEnablementMap);
ShowRestartNotification();
return;
case 3:
// Hyper-V Feature is already enabled and the script failed to add the user to the Hyper-V Admin group.
+ telemetryEnablementMap.Add(FeatureEnablementKind.HyperVAdminGroup, EnvironmentsTelemetryStatus.Failed);
+ LogEnablementTelemetry(telemetryEnablementMap, "Failed to enable Hyper-V Feature: exit code 3");
ShowErrorWithRebootAfterExecutionMessage(_stringResource.GetLocalized("UserNotAddedToHyperVAdminGroupMessage"));
return;
case 4:
// The user is already in the Hyper-V Admin group and the script successfully enabled the Hyper-Feature.
+ telemetryEnablementMap.Add(FeatureEnablementKind.HyperVFeature, EnvironmentsTelemetryStatus.Succeeded);
+ LogEnablementTelemetry(telemetryEnablementMap);
_shouldShowHyperVRebootButton = true;
ShowRestartNotification();
return;
case 5:
// The user is already in the Hyper-V Admin group and the script failed to enable the Hyper-Feature.
+ telemetryEnablementMap.Add(FeatureEnablementKind.HyperVFeature, EnvironmentsTelemetryStatus.Failed);
+ LogEnablementTelemetry(telemetryEnablementMap, "Failed to enable Hyper-V Feature: exit code 5");
ShowErrorWithRebootAfterExecutionMessage(_stringResource.GetLocalized("UnableToEnableHyperVFeatureMessage"));
return;
case 6:
- // Display nothing as there is no work to be done
+ // Display nothing as there is no work to be done, as the feature is enabled and user is in the group.
+ telemetryEnablementMap.Add(FeatureEnablementKind.HyperVFeature, EnvironmentsTelemetryStatus.NoOperation);
+ telemetryEnablementMap.Add(FeatureEnablementKind.HyperVAdminGroup, EnvironmentsTelemetryStatus.NoOperation);
+ LogEnablementTelemetry(telemetryEnablementMap);
return;
}
+
+ throw new EnvironmentNotificationScriptException($"HyperV Enablement Script failed with exit code: {process.ExitCode}");
}
catch (Exception ex)
{
_log.Error(ex, "Script failed, we may not have been able to add user to Hyper-V admin group or enable Hyper-V");
+ telemetryEnablementMap.Add(FeatureEnablementKind.HyperVFeature, EnvironmentsTelemetryStatus.Unknown);
+ telemetryEnablementMap.Add(FeatureEnablementKind.HyperVAdminGroup, EnvironmentsTelemetryStatus.Unknown);
+ LogEnablementTelemetry(telemetryEnablementMap, ex.Message);
}
ShowErrorWithRebootAfterExecutionMessage(_stringResource.GetLocalized("UnableToAddUserToHyperVAdminAndEnableHyperVMessage"));
});
+ }
+
+ private void LogEnablementTelemetry(Dictionary features, string? failureMessage = null)
+ {
+ TelemetryFactory.Get().Log(
+ "Environment_FeatureEnablement_Event",
+ LogLevel.Critical,
+ new EnvironmentEnablementEvent(features, failureMessage));
}
}
diff --git a/common/Environments/Models/ComputeSystemCache.cs b/common/Environments/Models/ComputeSystemCache.cs
index 56eaf4ecd5..e3653e8f8a 100644
--- a/common/Environments/Models/ComputeSystemCache.cs
+++ b/common/Environments/Models/ComputeSystemCache.cs
@@ -237,16 +237,6 @@ public async Task FetchDataAsync()
_ = await GetStateAsync();
var supportedOperations = SupportedOperations?.Value ?? ComputeSystemOperations.None;
- if (supportedOperations.HasFlag(ComputeSystemOperations.PinToStartMenu))
- {
- _ = await GetIsPinnedToStartMenuAsync();
- }
-
- if (supportedOperations.HasFlag(ComputeSystemOperations.PinToTaskbar))
- {
- _ = await GetIsPinnedToTaskbarAsync();
- }
-
_ = await GetComputeSystemThumbnailAsync(string.Empty);
_ = await GetComputeSystemPropertiesAsync(string.Empty);
}
diff --git a/common/Environments/Models/CreateComputeSystemOperation.cs b/common/Environments/Models/CreateComputeSystemOperation.cs
index a562e27aa6..6768594515 100644
--- a/common/Environments/Models/CreateComputeSystemOperation.cs
+++ b/common/Environments/Models/CreateComputeSystemOperation.cs
@@ -22,6 +22,8 @@ public class CreateComputeSystemOperation : IDisposable
{
private readonly ILogger _log = Log.ForContext("SourceContext", nameof(CreateComputeSystemOperation));
+ private readonly Guid _activityId;
+
// These operations are stored by the ComputeSystemManager who can then provide them to the environments
// page to be displayed to the user.
public Guid OperationId { get; } = Guid.NewGuid();
@@ -81,7 +83,11 @@ public class CreateComputeSystemOperation : IDisposable
private bool _disposedValue;
- public CreateComputeSystemOperation(ICreateComputeSystemOperation createComputeSystemOperation, ComputeSystemProviderDetails providerDetails, string userInputJson)
+ public CreateComputeSystemOperation(
+ ICreateComputeSystemOperation createComputeSystemOperation,
+ ComputeSystemProviderDetails providerDetails,
+ string userInputJson,
+ Guid activityId)
{
_createComputeSystemOperation = createComputeSystemOperation;
ProviderDetails = providerDetails;
@@ -100,6 +106,8 @@ public CreateComputeSystemOperation(ICreateComputeSystemOperation createComputeS
break;
}
}
+
+ _activityId = activityId;
}
public void StartOperation()
@@ -109,11 +117,6 @@ public void StartOperation()
{
try
{
- TelemetryFactory.Get().Log(
- "Environment_Creation_Event",
- LogLevel.Critical,
- new EnvironmentCreationUserEvent(ProviderDetails.ComputeSystemProvider.Id, EnvironmentsTelemetryStatus.Started));
-
CreateComputeSystemResult = await _createComputeSystemOperation.StartAsync().AsTask(_cancellationTokenSource.Token);
Completed?.Invoke(this, CreateComputeSystemResult);
}
@@ -123,19 +126,13 @@ public void StartOperation()
CreateComputeSystemResult = new CreateComputeSystemResult(ex, StringResourceHelper.GetResource("CreationOperationStoppedUnexpectedly"), ex.Message);
Completed?.Invoke(this, CreateComputeSystemResult);
}
-
- var completionStatus = EnvironmentsTelemetryStatus.Succeeded;
-
- if ((CreateComputeSystemResult == null) || (CreateComputeSystemResult.Result.Status == ProviderOperationStatus.Failure))
- {
- completionStatus = EnvironmentsTelemetryStatus.Failed;
- LogFailure(CreateComputeSystemResult);
- }
-
+
+ var (displayMessage, diagnosticText, telemetryStatus) = ComputeSystemHelpers.LogResult(CreateComputeSystemResult?.Result, _log);
TelemetryFactory.Get().Log(
"Environment_Creation_Event",
LogLevel.Critical,
- new EnvironmentCreationUserEvent(ProviderDetails.ComputeSystemProvider.Id, completionStatus));
+ new EnvironmentCreationEvent(ProviderDetails.ComputeSystemProvider.Id, telemetryStatus, displayMessage, diagnosticText),
+ _activityId);
RemoveEventHandlers();
});
@@ -200,16 +197,4 @@ public void Dispose()
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
-
- private void LogFailure(CreateComputeSystemResult? createComputeSystemResult)
- {
- if (createComputeSystemResult == null)
- {
- _log.Error($"The CreateComputeSystemResult object sent by {ProviderDetails.ComputeSystemProvider.Id} for the creation of {EnvironmentName} was null");
- }
- else
- {
- _log.Error(createComputeSystemResult.Result.ExtendedError, $"Creation failed for {EnvironmentName} with error:{createComputeSystemResult.Result.DiagnosticText}");
- }
- }
}
diff --git a/common/Environments/Styles/HorizontalCardStyles.xaml b/common/Environments/Styles/HorizontalCardStyles.xaml
index 3ef4708fc8..7a9e98c2b8 100644
--- a/common/Environments/Styles/HorizontalCardStyles.xaml
+++ b/common/Environments/Styles/HorizontalCardStyles.xaml
@@ -61,6 +61,7 @@
TargetType="Grid">
+
diff --git a/common/Extensions/StackedNotificationsBehaviorExtensions.cs b/common/Extensions/StackedNotificationsBehaviorExtensions.cs
index 93b665302d..c93bbec88b 100644
--- a/common/Extensions/StackedNotificationsBehaviorExtensions.cs
+++ b/common/Extensions/StackedNotificationsBehaviorExtensions.cs
@@ -1,17 +1,12 @@
// 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.Input;
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Behaviors;
+using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
-using WinUIEx;
namespace DevHome.Common.Extensions;
@@ -25,9 +20,9 @@ public static void ShowWithWindowExtension(
IRelayCommand? command = null,
string? buttonContent = null)
{
- var dispatcherQueue = Application.Current.GetService().DispatcherQueue;
+ var dispatcherQueue = Application.Current.GetService();
- dispatcherQueue.EnqueueAsync(() =>
+ dispatcherQueue?.EnqueueAsync(() =>
{
var notificationToShow = new Notification
{
@@ -55,9 +50,9 @@ public static void ShowWithWindowExtension(
public static void RemoveWithWindowExtension(this StackedNotificationsBehavior behavior, Notification notification)
{
- var dispatcherQueue = Application.Current.GetService().DispatcherQueue;
+ var dispatcherQueue = Application.Current.GetService();
- dispatcherQueue.EnqueueAsync(() =>
+ dispatcherQueue?.EnqueueAsync(() =>
{
behavior.Remove(notification);
});
@@ -65,7 +60,7 @@ public static void RemoveWithWindowExtension(this StackedNotificationsBehavior b
public static void ClearWithWindowExtension(this StackedNotificationsBehavior behavior)
{
- var dispatcherQueue = Application.Current.GetService().DispatcherQueue;
+ var dispatcherQueue = Application.Current.GetService().DispatcherQueue;
dispatcherQueue.EnqueueAsync(() =>
{
diff --git a/common/Extensions/WindowExExtensions.cs b/common/Extensions/WindowExExtensions.cs
index 07202c7e70..554f57a27d 100644
--- a/common/Extensions/WindowExExtensions.cs
+++ b/common/Extensions/WindowExExtensions.cs
@@ -2,16 +2,20 @@
// Licensed under the MIT License.
using System;
+using System.Runtime.InteropServices;
using System.Threading.Tasks;
using DevHome.Common.Helpers;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
-using WinUIEx;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.Graphics.Gdi;
+using Windows.Win32.UI.WindowsAndMessaging;
namespace DevHome.Common.Extensions;
///
-/// This class add extension methods to the class.
+/// This class add extension methods to the class.
///
public static class WindowExExtensions
{
@@ -24,7 +28,7 @@ public static class WindowExExtensions
/// Dialog title.
/// Dialog content.
/// Close button text.
- public static async Task ShowErrorMessageDialogAsync(this WindowEx window, string title, string content, string buttonText)
+ public static async Task ShowErrorMessageDialogAsync(this Window window, string title, string content, string buttonText)
{
await window.ShowMessageDialogAsync(dialog =>
{
@@ -32,7 +36,7 @@ await window.ShowMessageDialogAsync(dialog =>
dialog.Content = new TextBlock()
{
Text = content,
- TextWrapping = Microsoft.UI.Xaml.TextWrapping.WrapWholeWords,
+ TextWrapping = TextWrapping.WrapWholeWords,
};
dialog.PrimaryButtonText = buttonText;
});
@@ -42,11 +46,11 @@ await window.ShowMessageDialogAsync(dialog =>
/// Generic implementation for creating and displaying a message dialog on
/// a window.
///
- /// This extension method overloads .
+ /// This extension method overloads .
///
/// Target window.
/// Action performed on the created dialog.
- private static async Task ShowMessageDialogAsync(this WindowEx window, Action action)
+ public static async Task ShowMessageDialogAsync(this Window window, Action action)
{
var dialog = new ContentDialog()
{
@@ -61,7 +65,7 @@ private static async Task ShowMessageDialogAsync(this WindowEx window, Action
/// Target window
/// New theme.
- public static void SetRequestedTheme(this WindowEx window, ElementTheme theme)
+ public static void SetRequestedTheme(this Window window, ElementTheme theme)
{
if (window.Content is FrameworkElement rootElement)
{
@@ -69,4 +73,37 @@ public static void SetRequestedTheme(this WindowEx window, ElementTheme theme)
TitleBarHelper.UpdateTitleBar(window, rootElement.ActualTheme);
}
}
+
+ ///
+ /// Gets the native HWND pointer handle for the window
+ ///
+ /// The window to return the handle for
+ /// HWND handle
+ public static IntPtr GetWindowHandle(this Microsoft.UI.Xaml.Window window)
+ => window is null ? throw new ArgumentNullException(nameof(window)) : WinRT.Interop.WindowNative.GetWindowHandle(window);
+
+ ///
+ /// Centers the window on the current monitor
+ ///
+ /// The window to center
+ /// Width of the window in device independent pixels, or null if keeping the current size
+ /// Height of the window in device independent pixels, or null if keeping the current size
+ public static void CenterOnScreen(this Microsoft.UI.Xaml.Window window, double? width = null, double? height = null)
+ {
+ var hwnd = window.GetWindowHandle();
+ var hwndDesktop = PInvoke.MonitorFromWindow((HWND)hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
+ var info = default(MONITORINFO);
+ info.cbSize = (uint)Marshal.SizeOf(info);
+ PInvoke.GetMonitorInfo(hwndDesktop, ref info);
+ var dpi = PInvoke.GetDpiForWindow((HWND)hwnd);
+ PInvoke.GetWindowRect((HWND)hwnd, out RECT windowRect);
+ var scalingFactor = dpi / 96d;
+ var w = width.HasValue ? (int)(width * scalingFactor) : windowRect.right - windowRect.left;
+ var h = height.HasValue ? (int)(height * scalingFactor) : windowRect.bottom - windowRect.top;
+ var cx = (info.rcMonitor.left + info.rcMonitor.right) / 2;
+ var cy = (info.rcMonitor.bottom + info.rcMonitor.top) / 2;
+ var left = cx - (w / 2);
+ var top = cy - (h / 2);
+ PInvoke.SetWindowPos((HWND)hwnd, (HWND)IntPtr.Zero, left, top, w, h, SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW);
+ }
}
diff --git a/common/Helpers/RuntimeHelper.cs b/common/Helpers/RuntimeHelper.cs
index 7ab541f6f2..1fb3af8d43 100644
--- a/common/Helpers/RuntimeHelper.cs
+++ b/common/Helpers/RuntimeHelper.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System;
+using System.Security.Principal;
using Windows.Win32;
using Windows.Win32.Foundation;
@@ -27,4 +28,10 @@ public static bool IsOnWindows11
return version.Major >= 10 && version.Build >= 22000;
}
}
+
+ public static bool IsCurrentProcessRunningAsAdmin()
+ {
+ var identity = WindowsIdentity.GetCurrent();
+ return identity.Owner?.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid) ?? false;
+ }
}
diff --git a/common/NativeMethods.txt b/common/NativeMethods.txt
index 8507f58dc3..ea4531780a 100644
--- a/common/NativeMethods.txt
+++ b/common/NativeMethods.txt
@@ -13,3 +13,8 @@ SHLoadIndirectString
StrFormatByteSizeEx
SFBS_FLAGS
MAX_PATH
+GetDpiForWindow
+GetWindowRect
+GetMonitorInfo
+SetWindowPos
+MonitorFromWindow
diff --git a/common/Renderers/ChooseFileAction.cs b/common/Renderers/ChooseFileAction.cs
new file mode 100644
index 0000000000..9abc3dbe41
--- /dev/null
+++ b/common/Renderers/ChooseFileAction.cs
@@ -0,0 +1,170 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using AdaptiveCards.ObjectModel.WinUI3;
+using AdaptiveCards.Rendering.WinUI3;
+using DevHome.Common.Extensions;
+using DevHome.Common.Services;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Data.Json;
+using Windows.Storage.Pickers;
+using WinRT.Interop;
+using WinUIEx;
+
+namespace DevHome.Common.Renderers;
+
+public class ChooseFileAction : IAdaptiveActionElement
+{
+ // ChooseFile properties
+ public string FilePath { get; set; } = string.Empty;
+
+ public string Verb { get; set; } = string.Empty;
+
+ public bool UseIcon { get; set; }
+
+ public static readonly string CustomTypeString = "Action.ChooseFile";
+
+ // Inherited properties
+ public ActionType ActionType => ActionType.Custom;
+
+ public string ActionTypeString => CustomTypeString;
+
+ public JsonObject? AdditionalProperties { get; set; }
+
+ public IAdaptiveActionElement? FallbackContent { get; set; }
+
+ public FallbackType FallbackType { get; set; } = FallbackType.Drop;
+
+ public string IconUrl { get; set; } = string.Empty;
+
+ public string? Id { get; set; } = CustomTypeString + "Id";
+
+ public bool IsEnabled { get; set; } = true;
+
+ public AdaptiveCards.ObjectModel.WinUI3.ActionMode Mode { get; set; }
+
+ public ActionRole Role { get; set; }
+
+ public string Style { get; set; } = string.Empty;
+
+ public string Title { get; set; } = string.Empty;
+
+ public string Tooltip { get; set; } = string.Empty;
+
+ public JsonObject ToJson()
+ {
+ var json = new JsonObject
+ {
+ ["type"] = JsonValue.CreateStringValue(ActionTypeString),
+ ["filePath"] = JsonValue.CreateStringValue(FilePath),
+ ["verb"] = JsonValue.CreateStringValue(Verb),
+ };
+
+ if (AdditionalProperties != null)
+ {
+ foreach (var prop in AdditionalProperties)
+ {
+ json.Add(prop.Key, prop.Value);
+ }
+ }
+
+ return json;
+ }
+
+ /// Launches the file picker dialog to select a file.
+ /// true if a file was selected, false otherwise.
+ public bool LaunchFilePicker()
+ {
+ var filePicker = new FileOpenPicker();
+ filePicker.FileTypeFilter.Add("*");
+
+ var mainWindow = Application.Current.GetService();
+ if (mainWindow != null)
+ {
+ var hwnd = WindowNative.GetWindowHandle(mainWindow);
+ InitializeWithWindow.Initialize(filePicker, hwnd);
+ }
+
+ var file = filePicker.PickSingleFileAsync().AsTask().Result;
+ if (file != null)
+ {
+ FilePath = file.Path;
+ return true;
+ }
+
+ return false;
+ }
+}
+
+public class ChooseFileParser : IAdaptiveActionParser
+{
+ public IAdaptiveActionElement FromJson(
+ JsonObject inputJson,
+ AdaptiveElementParserRegistration elementParsers,
+ AdaptiveActionParserRegistration actionParsers,
+ IList warnings)
+ {
+ var stringResource = new StringResource("DevHome.Common.pri", "DevHome.Common/Resources");
+
+ var chooseFileAction = new ChooseFileAction
+ {
+ Title = stringResource.GetLocalized("ChooseFileActionTitle"),
+ Tooltip = stringResource.GetLocalized("ChooseFileActionToolTip"),
+
+ // Parse the JSON properties of the action.
+ // The Verb ChooseFile is not meant to be localized.
+ Verb = inputJson.GetNamedString("verb", "ChooseFile"),
+ UseIcon = inputJson.GetNamedBoolean("useIcon", false),
+ };
+
+ return chooseFileAction;
+ }
+}
+
+public class ChooseFileActionRenderer : IAdaptiveActionRenderer
+{
+ public UIElement Render(IAdaptiveActionElement element, AdaptiveRenderContext context, AdaptiveRenderArgs renderArgs)
+ {
+ var renderer = new AdaptiveExecuteActionRenderer();
+
+ if (element as ChooseFileAction is ChooseFileAction chooseFileElement)
+ {
+ // Card author is not allowed to specify a custom icon for the file picker action.
+ chooseFileElement.IconUrl = string.Empty;
+
+ var button = renderer.Render(element, context, renderArgs) as Button;
+ if (button != null)
+ {
+ var content = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = 8,
+ };
+ if (chooseFileElement.UseIcon)
+ {
+ content.Children.Add(new FontIcon
+ {
+ Glyph = "\xED25",
+ });
+ }
+
+ if (!string.IsNullOrEmpty(chooseFileElement.Title))
+ {
+ content.Children.Add(new TextBlock
+ {
+ Text = chooseFileElement.Title,
+ });
+ }
+
+ button.Content = content;
+
+ return button;
+ }
+ }
+
+ return renderer.Render(element, context, renderArgs);
+ }
+}
diff --git a/common/Renderers/LabelGroup.cs b/common/Renderers/LabelGroup.cs
index 83ef7e6df9..4352cb9859 100644
--- a/common/Renderers/LabelGroup.cs
+++ b/common/Renderers/LabelGroup.cs
@@ -95,6 +95,8 @@ public UIElement Render(IAdaptiveCardElement element, AdaptiveRenderContext cont
{
Name = LabelGroup.CustomTypeString,
Orientation = Orientation.Horizontal,
+ HorizontalSpacing = 4,
+ VerticalSpacing = 4,
};
if (element is LabelGroup labelGroup)
@@ -108,7 +110,6 @@ public UIElement Render(IAdaptiveCardElement element, AdaptiveRenderContext cont
{
Background = GetBrushFromColor(label.Item2, 0.4),
Padding = new Thickness(7, 2, 7, 2),
- Margin = new Thickness(0, 0, 10, 0),
};
if (labelGroup.RoundedCorners)
{
diff --git a/common/Renderers/TextInputRenderer.cs b/common/Renderers/TextInputRenderer.cs
new file mode 100644
index 0000000000..0352c49faf
--- /dev/null
+++ b/common/Renderers/TextInputRenderer.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Linq;
+using AdaptiveCards.ObjectModel.WinUI3;
+using AdaptiveCards.Rendering.WinUI3;
+using CommunityToolkit.WinUI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Automation;
+using Microsoft.UI.Xaml.Controls;
+
+namespace DevHome.Common.Renderers;
+
+public class TextInputRenderer : IAdaptiveElementRenderer
+{
+ public UIElement Render(IAdaptiveCardElement element, AdaptiveRenderContext context, AdaptiveRenderArgs renderArgs)
+ {
+ var renderer = new AdaptiveTextInputRenderer();
+ var elementToReturn = renderer.Render(element, context, renderArgs);
+
+ if (element as AdaptiveTextInput is AdaptiveTextInput textInputElement)
+ {
+ if (textInputElement.InlineAction == null)
+ {
+ return elementToReturn;
+ }
+
+ if (textInputElement.InlineAction is not ChooseFileAction)
+ {
+ return elementToReturn;
+ }
+
+ // If the Input has an inline action, the element will have a button as a descendant.
+ // Since guidance suggests inline actions use an icon rather than text, we can safely
+ // set the content of the button to an icon.
+ foreach (var descendant in elementToReturn.FindDescendants())
+ {
+ if (descendant is Button inlineActionButton)
+ {
+ inlineActionButton.Padding = new Thickness(5);
+ inlineActionButton.Content = new FontIcon
+ {
+ Glyph = "\xED25",
+ };
+ }
+ }
+ }
+
+ return elementToReturn;
+ }
+}
diff --git a/common/Services/AdaptiveCardRenderingService.cs b/common/Services/AdaptiveCardRenderingService.cs
index 3e712f476d..b9f4f9cf0b 100644
--- a/common/Services/AdaptiveCardRenderingService.cs
+++ b/common/Services/AdaptiveCardRenderingService.cs
@@ -7,10 +7,10 @@
using AdaptiveCards.Rendering.WinUI3;
using DevHome.Common.Renderers;
using DevHome.Contracts.Services;
+using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Serilog;
using Windows.Storage;
-using WinUIEx;
namespace DevHome.Common.Services;
@@ -20,7 +20,7 @@ public class AdaptiveCardRenderingService : IAdaptiveCardRenderingService, IDisp
public event EventHandler RendererUpdated = (_, _) => { };
- private readonly WindowEx _windowEx;
+ private readonly DispatcherQueue _dispatcherQueue;
private readonly IThemeSelectorService _themeSelectorService;
@@ -30,9 +30,9 @@ public class AdaptiveCardRenderingService : IAdaptiveCardRenderingService, IDisp
private bool _disposedValue;
- public AdaptiveCardRenderingService(WindowEx windowEx, IThemeSelectorService themeSelectorService)
+ public AdaptiveCardRenderingService(DispatcherQueue dispatcherQueue, IThemeSelectorService themeSelectorService)
{
- _windowEx = windowEx;
+ _dispatcherQueue = dispatcherQueue;
_themeSelectorService = themeSelectorService;
_themeSelectorService.ThemeChanged += OnThemeChanged;
}
@@ -87,6 +87,8 @@ private async Task ConfigureAdaptiveCardRendererAsync()
// Add custom Adaptive Card renderer.
_renderer.ElementRenderers.Set(LabelGroup.CustomTypeString, new LabelGroupRenderer());
_renderer.ElementRenderers.Set("Input.ChoiceSet", new AccessibleChoiceSet());
+ _renderer.ElementRenderers.Set("Input.Text", new TextInputRenderer());
+ _renderer.ActionRenderers.Set(ChooseFileAction.CustomTypeString, new ChooseFileActionRenderer());
// A different host config is used to render widgets (adaptive cards) in light and dark themes.
await UpdateHostConfig();
@@ -122,7 +124,7 @@ private async Task UpdateHostConfig()
_log.Error(ex, "Error retrieving HostConfig");
}
- _windowEx.DispatcherQueue.TryEnqueue(() =>
+ _dispatcherQueue.TryEnqueue(() =>
{
if (!string.IsNullOrEmpty(hostConfigContents))
{
diff --git a/common/Services/StringResource.cs b/common/Services/StringResource.cs
index 068f7b5f78..c4628b65f2 100644
--- a/common/Services/StringResource.cs
+++ b/common/Services/StringResource.cs
@@ -48,7 +48,12 @@ public string GetLocalized(string key, params object[] args)
try
{
value = _resourceLoader.GetString(key);
- value = string.Format(CultureInfo.CurrentCulture, value, args);
+
+ // only replace the placeholders if args is not empty
+ if (args.Length > 0)
+ {
+ value = string.Format(CultureInfo.CurrentCulture, value, args);
+ }
}
catch
{
diff --git a/common/Strings/en-us/Resources.resw b/common/Strings/en-us/Resources.resw
index a3a3ad5a31..a2d1803b91 100644
--- a/common/Strings/en-us/Resources.resw
+++ b/common/Strings/en-us/Resources.resw
@@ -317,4 +317,10 @@
Go to extensions library
Text for when the environment page in Dev Home is empty and the user has no extensions installed that support environments
+
+ Choose file
+
+
+ Choose file
+
\ No newline at end of file
diff --git a/common/TelemetryEvents/EnvironmentRedirectionUserEvent.cs b/common/TelemetryEvents/EnvironmentRedirectionUserEvent.cs
new file mode 100644
index 0000000000..8e6af37312
--- /dev/null
+++ b/common/TelemetryEvents/EnvironmentRedirectionUserEvent.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using DevHome.Telemetry;
+using Microsoft.Diagnostics.Telemetry;
+using Microsoft.Diagnostics.Telemetry.Internal;
+
+namespace DevHome.Common.TelemetryEvents;
+
+[EventData]
+public class EnvironmentRedirectionUserEvent : EventBase
+{
+ public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage;
+
+ public string NavigationAction { get; }
+
+ public string OriginPage { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public EnvironmentRedirectionUserEvent(string navigationAction, string originPage)
+ {
+ NavigationAction = navigationAction;
+ OriginPage = originPage;
+ }
+
+ // Inherited but unused.
+ public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings)
+ {
+ }
+}
diff --git a/common/TelemetryEvents/Environments/EnvironmentEnablementEvent.cs b/common/TelemetryEvents/Environments/EnvironmentEnablementEvent.cs
new file mode 100644
index 0000000000..1a2e66dce8
--- /dev/null
+++ b/common/TelemetryEvents/Environments/EnvironmentEnablementEvent.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+using DevHome.Common.TelemetryEvents.SetupFlow.Environments;
+using DevHome.Telemetry;
+using Microsoft.Diagnostics.Telemetry;
+using Microsoft.Diagnostics.Telemetry.Internal;
+
+namespace DevHome.Common.TelemetryEvents.Environments;
+
+public enum FeatureEnablementKind
+{
+ HyperVFeature,
+ HyperVAdminGroup,
+}
+
+[EventData]
+public class EnvironmentEnablementEvent : EventBase
+{
+ public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServicePerformance;
+
+ public string Features { get; }
+
+ public string? FailureMessage { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Dictionary of the enablement type and its status
+ /// Associated error text if it exists after an enablement attempt
+ public EnvironmentEnablementEvent(Dictionary features, string? failureMessage = null)
+ {
+ Features = ConvertFeaturesToString(features);
+ FailureMessage = failureMessage;
+ }
+
+ // Inherited but unused.
+ public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings)
+ {
+ }
+
+ private string ConvertFeaturesToString(Dictionary features)
+ {
+ var featureList = new List();
+ foreach (var feature in features)
+ {
+ featureList.Add($"{feature.Key}:{feature.Value}");
+ }
+
+ return string.Join(',', featureList);
+ }
+}
diff --git a/common/TelemetryEvents/Environments/EnvironmentOperationUserEvent.cs b/common/TelemetryEvents/Environments/EnvironmentOperationEvent.cs
similarity index 61%
rename from common/TelemetryEvents/Environments/EnvironmentOperationUserEvent.cs
rename to common/TelemetryEvents/Environments/EnvironmentOperationEvent.cs
index a920f34133..6a6ec79ef0 100644
--- a/common/TelemetryEvents/Environments/EnvironmentOperationUserEvent.cs
+++ b/common/TelemetryEvents/Environments/EnvironmentOperationEvent.cs
@@ -12,9 +12,9 @@
namespace DevHome.Common.TelemetryEvents.Environments;
[EventData]
-public class EnvironmentOperationUserEvent : EventBase
+public class EnvironmentOperationEvent : EventBase
{
- public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage;
+ public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServicePerformance;
public string ProviderId { get; }
@@ -22,29 +22,38 @@ public class EnvironmentOperationUserEvent : EventBase
public string OperationName { get; }
- public string ActivityId { get; }
+ public string? AdditionalContext { get; }
- public string AdditionalContext { get; }
+ public string? DisplayMessage { get; }
+
+ public string? DiagnosticText { get; }
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- /// The status of the launch operation
+ /// The status of the compute system operation
/// An enum representing the compute system operation that was invoked
/// The Id of the compute system provider that owns the compute system that is being launched
/// The context in which the operation is running as. E.g the Pin to start operation can be for Pinning or Unpinning
- /// The activity Id associated with the compute system operation that was invoked by the user
- public EnvironmentOperationUserEvent(EnvironmentsTelemetryStatus status, ComputeSystemOperations computeSystemOperation, string providerId, string additionalContext, Guid activityId)
+ /// Associated error text that was displayed to the user
+ /// Associated error text for the operation
+ public EnvironmentOperationEvent(
+ EnvironmentsTelemetryStatus status,
+ ComputeSystemOperations computeSystemOperation,
+ string providerId,
+ string? additionalContext = null,
+ string? displayMessage = null,
+ string? diagnosticText = null)
{
Status = status.ToString();
OperationName = computeSystemOperation.ToString();
ProviderId = providerId;
AdditionalContext = additionalContext;
- ActivityId = activityId.ToString();
+ DisplayMessage = displayMessage;
+ DiagnosticText = diagnosticText;
}
public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings)
{
- // The only sensitive string is the developerID. GetHashedDeveloperId is used to hash the developerId.
}
}
diff --git a/common/TelemetryEvents/Environments/EnvironmentRestartUserEvent.cs b/common/TelemetryEvents/Environments/EnvironmentRestartUserEvent.cs
new file mode 100644
index 0000000000..abfcbb5049
--- /dev/null
+++ b/common/TelemetryEvents/Environments/EnvironmentRestartUserEvent.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Diagnostics.Tracing;
+using DevHome.Common.TelemetryEvents.SetupFlow.Environments;
+using DevHome.Telemetry;
+using Microsoft.Diagnostics.Telemetry;
+using Microsoft.Diagnostics.Telemetry.Internal;
+
+namespace DevHome.Common.TelemetryEvents.Environments;
+
+[EventData]
+public class EnvironmentRestartUserEvent : EventBase
+{
+ public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage;
+
+ // Inherited but unused.
+ public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings)
+ {
+ }
+}
diff --git a/common/TelemetryEvents/SetupFlow/Environments/EnvironmentCreationUserEvent.cs b/common/TelemetryEvents/SetupFlow/Environments/EnvironmentCreationEvent.cs
similarity index 63%
rename from common/TelemetryEvents/SetupFlow/Environments/EnvironmentCreationUserEvent.cs
rename to common/TelemetryEvents/SetupFlow/Environments/EnvironmentCreationEvent.cs
index 52b91607aa..477feb69a0 100644
--- a/common/TelemetryEvents/SetupFlow/Environments/EnvironmentCreationUserEvent.cs
+++ b/common/TelemetryEvents/SetupFlow/Environments/EnvironmentCreationEvent.cs
@@ -13,27 +13,36 @@ public enum EnvironmentsTelemetryStatus
{
Started,
Succeeded,
- Failed,
+ Failed,
+ Unknown,
+ NoOperation, // Used when there is a no op kind of operation and no work was done.
}
[EventData]
-public class EnvironmentCreationUserEvent : EventBase
+public class EnvironmentCreationEvent : EventBase
{
- public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage;
+ public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServicePerformance;
public string ProviderId { get; }
public string Status { get; }
+ public string? DisplayMessage { get; }
+
+ public string? DiagnosticText { get; }
+
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The Id of the compute system provider that initiated the creation operation
/// The status of the creation operation
- public EnvironmentCreationUserEvent(string providerId, EnvironmentsTelemetryStatus status)
+ /// Associated error text for the operation
+ public EnvironmentCreationEvent(string providerId, EnvironmentsTelemetryStatus status, string? displayMessage = null, string? diagnosticText = null)
{
ProviderId = providerId;
Status = status.ToString();
+ DisplayMessage = displayMessage;
+ DiagnosticText = diagnosticText;
}
// Inherited but unused.
diff --git a/common/TelemetryEvents/SetupFlow/Environments/EnvironmentLaunchUserEvent.cs b/common/TelemetryEvents/SetupFlow/Environments/EnvironmentLaunchEvent.cs
similarity index 66%
rename from common/TelemetryEvents/SetupFlow/Environments/EnvironmentLaunchUserEvent.cs
rename to common/TelemetryEvents/SetupFlow/Environments/EnvironmentLaunchEvent.cs
index 7f0c1b1995..6d1d00848a 100644
--- a/common/TelemetryEvents/SetupFlow/Environments/EnvironmentLaunchUserEvent.cs
+++ b/common/TelemetryEvents/SetupFlow/Environments/EnvironmentLaunchEvent.cs
@@ -10,23 +10,30 @@
namespace DevHome.Common.TelemetryEvents.SetupFlow.Environments;
[EventData]
-public class EnvironmentLaunchUserEvent : EventBase
+public class EnvironmentLaunchEvent : EventBase
{
- public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage;
+ public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServicePerformance;
public string ProviderId { get; }
public string Status { get; }
+ public string? DisplayMessage { get; }
+
+ public string? DiagnosticText { get; }
+
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The Id of the compute system provider that owns the compute system that is being launched
/// The status of the launch operation
- public EnvironmentLaunchUserEvent(string providerId, EnvironmentsTelemetryStatus status)
+ /// Associated error text for the operation
+ public EnvironmentLaunchEvent(string providerId, EnvironmentsTelemetryStatus status, string? displayMessage = null, string? diagnosticText = null)
{
ProviderId = providerId;
Status = status.ToString();
+ DisplayMessage = displayMessage;
+ DiagnosticText = diagnosticText;
}
// Inherited but unused.
diff --git a/common/ToolPage.cs b/common/ToolPage.cs
index bec0df868e..4ce07613f7 100644
--- a/common/ToolPage.cs
+++ b/common/ToolPage.cs
@@ -1,11 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-using Microsoft.UI.Xaml.Controls;
+using DevHome.Common.Views;
namespace DevHome.Common;
-public abstract class ToolPage : Page
+public abstract class ToolPage : DevHomePage
{
public abstract string ShortName { get; }
}
diff --git a/common/Views/AdaptiveCardViews/ContentDialogWithNonInteractiveContent.xaml.cs b/common/Views/AdaptiveCardViews/ContentDialogWithNonInteractiveContent.xaml.cs
index 23f78ba236..8b778f41b6 100644
--- a/common/Views/AdaptiveCardViews/ContentDialogWithNonInteractiveContent.xaml.cs
+++ b/common/Views/AdaptiveCardViews/ContentDialogWithNonInteractiveContent.xaml.cs
@@ -1,14 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-using System.Threading.Tasks;
-using AdaptiveCards.Rendering.WinUI3;
using DevHome.Common.DevHomeAdaptiveCards.CardModels;
using DevHome.Common.Extensions;
using DevHome.Common.Services;
+using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
-using WinUIEx;
namespace DevHome.Common.Views.AdaptiveCardViews;
@@ -22,7 +20,7 @@ public ContentDialogWithNonInteractiveContent(DevHomeContentDialogContent conten
this.InitializeComponent();
// Since we use the renderer service to allow the card to receive theming updates, we need to ensure the UI thread is used.
- var dispatcherQueue = Application.Current.GetService().DispatcherQueue;
+ var dispatcherQueue = Application.Current.GetService();
dispatcherQueue.TryEnqueue(async () =>
{
Title = content.Title;
@@ -31,8 +29,13 @@ public ContentDialogWithNonInteractiveContent(DevHomeContentDialogContent conten
var renderer = await rendererService.GetRendererAsync();
renderer.HostConfig.ContainerStyles.Default.BackgroundColor = Microsoft.UI.Colors.Transparent;
var card = renderer.RenderAdaptiveCardFromJsonString(content.ContentDialogInternalAdaptiveCardJson?.Stringify() ?? string.Empty);
- Content = card.FrameworkElement;
+ Content = new ScrollViewer
+ {
+ VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
+ Content = card.FrameworkElement,
+ };
SecondaryButtonText = content.SecondaryButtonText;
+ this.Focus(FocusState.Programmatic);
});
}
}
diff --git a/common/Views/DevHomePage.cs b/common/Views/DevHomePage.cs
new file mode 100644
index 0000000000..75681024a7
--- /dev/null
+++ b/common/Views/DevHomePage.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace DevHome.Common.Views;
+
+///
+/// This page is used to auto focus on the first selectable element.
+/// Please inherit from this class for pages.
+/// If the Page needs custom focus logic (for example, waiting until adaptive cards are loaded)
+/// the individual Page should handle that. Take a look at EnvironmentCreationOptionsView.xaml
+/// for an example on using the autofocus behavior to focus when the element when it becomes visible.
+///
+///
+public class DevHomePage : Page
+{
+ public DevHomePage()
+ {
+ Loaded += (s, e) =>
+ {
+ Focus(FocusState.Programmatic);
+ };
+ }
+}
diff --git a/common/Views/DevHomeUserControl.cs b/common/Views/DevHomeUserControl.cs
new file mode 100644
index 0000000000..0d8f55344a
--- /dev/null
+++ b/common/Views/DevHomeUserControl.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace DevHome.Common.Views;
+
+///
+/// This UserControl is used to auto focus on the first selectable element.
+/// Please inherit from this class for UserControl.
+/// If the UserControl needs custom focus logic (for example, waiting until adaptive cards are loaded)
+/// the individual UserControl should handle that. Take a look at EnvironmentCreationOptionsView.xaml
+/// for an example on using the autofocus behavior to focus when the element when it becomes visible.
+///
+public class DevHomeUserControl : UserControl
+{
+ public DevHomeUserControl()
+ {
+ Loaded += (s, args) =>
+ {
+ Focus(FocusState.Programmatic);
+ };
+ }
+}
diff --git a/common/Windows/SecondaryWindow.cs b/common/Windows/SecondaryWindow.cs
index a5834f21cd..fce42968e1 100644
--- a/common/Windows/SecondaryWindow.cs
+++ b/common/Windows/SecondaryWindow.cs
@@ -8,23 +8,23 @@
using DevHome.Contracts.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup;
+using Windows.Graphics;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
-using WinUIEx;
namespace DevHome.Common.Windows;
[ContentProperty(Name = nameof(SecondaryWindowContent))]
-public class SecondaryWindow : WindowEx
+public class SecondaryWindow : WinUIEx.WindowEx
{
private readonly SecondaryWindowTemplate _windowTemplate;
- private WindowEx? _primaryWindow;
+ private Window? _primaryWindow;
private bool _useAppTheme;
private bool _isModal;
private bool _isTopLevel;
- private WindowEx MainWindow => Application.Current.GetService();
+ private Window MainWindow => Application.Current.GetService();
private IThemeSelectorService ThemeSelector => Application.Current.GetService();
@@ -149,7 +149,7 @@ public bool IsTopLevel
}
}
- public WindowEx? PrimaryWindow
+ public Window? PrimaryWindow
{
get => _primaryWindow;
set
@@ -195,7 +195,7 @@ public SecondaryWindow()
SystemBackdrop = PrimaryWindow.SystemBackdrop;
Title = AppInfo.GetAppNameLocalized();
- this.SetIcon(AppInfo.IconPath);
+ AppWindow.SetIcon(AppInfo.IconPath);
ShowInTaskbar();
}
@@ -212,7 +212,7 @@ public SecondaryWindow(object secondaryWindowContent)
///
///
/// This method should be called after the secondary window is shown.
- /// See also:
+ /// See also:
///
public void CenterOnWindow()
{
@@ -224,22 +224,23 @@ public void CenterOnWindow()
{
// Get DPI for primary widow
const float defaultDPI = 96f;
- var dpi = HwndExtensions.GetDpiForWindow(PrimaryWindow.GetWindowHandle()) / defaultDPI;
+ var dpi = PInvoke.GetDpiForWindow((HWND)PrimaryWindow.GetWindowHandle()) / defaultDPI;
// Extract primary window dimensions
var primaryWindowLeftOffset = PrimaryWindow.AppWindow.Position.X;
var primaryWindowTopOffset = PrimaryWindow.AppWindow.Position.Y;
- var primaryWindowHalfWidth = (PrimaryWindow.Width * dpi) / 2;
- var primaryWindowHalfHeight = (PrimaryWindow.Height * dpi) / 2;
+ var primaryWindowHalfWidth = (PrimaryWindow.AppWindow.Size.Width * dpi) / 2;
+ var primaryWindowHalfHeight = (PrimaryWindow.AppWindow.Size.Height * dpi) / 2;
// Derive secondary window dimensions
- var secondaryWindowHalfWidth = (Width * dpi) / 2;
- var secondaryWindowHalfHeight = (Height * dpi) / 2;
+ var secondaryWindowHalfWidth = (AppWindow.Size.Width * dpi) / 2;
+ var secondaryWindowHalfHeight = (AppWindow.Size.Height * dpi) / 2;
var secondaryWindowLeftOffset = primaryWindowLeftOffset + primaryWindowHalfWidth - secondaryWindowHalfWidth;
var secondaryWindowTopOffset = primaryWindowTopOffset + primaryWindowHalfHeight - secondaryWindowHalfHeight;
// Move and resize secondary window
- this.MoveAndResize(secondaryWindowLeftOffset, secondaryWindowTopOffset, Width, Height);
+ var newRect = new RectInt32((int)secondaryWindowLeftOffset, (int)secondaryWindowTopOffset, AppWindow.Size.Width, AppWindow.Size.Height);
+ AppWindow.MoveAndResize(newRect);
}
}
diff --git a/docs/readme.md b/docs/readme.md
index 31bd0c698f..122550af80 100644
--- a/docs/readme.md
+++ b/docs/readme.md
@@ -3,5 +3,5 @@
Before you dive into developing for Dev Home, check out these resources:
- [Dev Home Architecture](./architecture.md)
- [Dev Home Tools](./tools/readme.md)
-- [Dev Home Extensions](./extensions/extensions.md)
+- [Dev Home Extensions](./extensions/readme.md)
diff --git a/docs/tools/Dashboard.md b/docs/tools/Dashboard.md
index d6cc48f007..bc7810a075 100644
--- a/docs/tools/Dashboard.md
+++ b/docs/tools/Dashboard.md
@@ -12,3 +12,7 @@ Widgets are rendered by Adaptive Cards, and there are a few ways Dev Home custom
* Dev Home widgets use the [Adaptive Card schema](https://adaptivecards.io/explorer/) version 1.5, which is the most recent schema supported by the WinUI 3 Adaptive Card renderer.
* There are [HostConfig](https://learn.microsoft.com/adaptive-cards/sdk/rendering-cards/uwp/host-config) files that define common styles (e.g., font family, font sizes, default spacing) and behaviors (e.g., max number of actions) for all the widgets. There is one for [light mode](../../tools/Dashboard/DevHome.Dashboard/Assets/HostConfigLight.json) and one for [dark mode](../tools/Dashboard/DevHome.Dashboard/Assets/HostConfigDark.json).
* Dev Home supports a custom AdaptiveElement type called [`LabelGroup`](../../common/Renderers/LabelGroup.cs). This allows a card author to render a set of labels, each with a specified background color. For an example of how to use this type, please see the [GitHub Issues widget](https://github.com/microsoft/devhomegithubextension/blob/main/src/GitHubExtension/Widgets/Templates/GitHubIssuesTemplate.json).
+
+### Widget providers
+
+When creating a widget for Dev Home, you should include all manifest values described in [Widget provider package manifest XML format](https://learn.microsoft.com/windows/apps/develop/widgets/widget-provider-manifest).
diff --git a/docs/tools/common/SecondaryWindow.md b/docs/tools/common/SecondaryWindow.md
index f07a7c89f3..26a0dd3023 100644
--- a/docs/tools/common/SecondaryWindow.md
+++ b/docs/tools/common/SecondaryWindow.md
@@ -14,7 +14,7 @@ Create a secondary application window that derives from `WinUIEx.WindowEx` ensur
## Additional methods
| Property | Retrun type | Description |
| -------- | -------- | -------- |
-| CenterOnWindow() | void | If the primary window is set, center the secondary window on the primary window. Otherwise, center the secondary window on the screen by calling `WinUIEx.WindowExtensions.CenterOnScreen()`. |
+| CenterOnWindow() | void | If the primary window is set, center the secondary window on the primary window. Otherwise, center the secondary window on the screen by calling `WindowExExtensions.CenterOnScreen()`. |
## Usage
### Example 1: Set content from XAML
diff --git a/extensions/CoreWidgetProvider/Helpers/Resources.cs b/extensions/CoreWidgetProvider/Helpers/Resources.cs
index 85e5659de3..715cc0002e 100644
--- a/extensions/CoreWidgetProvider/Helpers/Resources.cs
+++ b/extensions/CoreWidgetProvider/Helpers/Resources.cs
@@ -88,6 +88,7 @@ public static string[] GetWidgetResourceIdentifiers()
"CPUUsage_Widget_Template/CPU_Speed",
"CPUUsage_Widget_Template/Processes",
"CPUUsage_Widget_Template/End_Process",
+ "Widget_Template_Button/Preview",
"Widget_Template_Button/Save",
"Widget_Template_Button/Cancel",
];
diff --git a/extensions/CoreWidgetProvider/Strings/en-US/Resources.resw b/extensions/CoreWidgetProvider/Strings/en-US/Resources.resw
index 332c784dbd..2e1a45870a 100644
--- a/extensions/CoreWidgetProvider/Strings/en-US/Resources.resw
+++ b/extensions/CoreWidgetProvider/Strings/en-US/Resources.resw
@@ -162,6 +162,10 @@
End process
+
+ Preview
+ Shown in Widget, Button text
+
Save
Shown in Widget, Button text
diff --git a/extensions/CoreWidgetProvider/Widgets/Assets/example_config b/extensions/CoreWidgetProvider/Widgets/Assets/example_config
index 7e7ae4ffb0..47c7fe95b8 100644
--- a/extensions/CoreWidgetProvider/Widgets/Assets/example_config
+++ b/extensions/CoreWidgetProvider/Widgets/Assets/example_config
@@ -16,3 +16,7 @@ Host ubuntu_test
Hostname 192.168.1.41
User ubuntussh
Port 22
+
+Host router rack_123_router
+ Hostname 192.168.2.100
+ User admin
diff --git a/extensions/CoreWidgetProvider/Widgets/Enums/WidgetAction.cs b/extensions/CoreWidgetProvider/Widgets/Enums/WidgetAction.cs
index 2e07df89dd..a14ee5c906 100644
--- a/extensions/CoreWidgetProvider/Widgets/Enums/WidgetAction.cs
+++ b/extensions/CoreWidgetProvider/Widgets/Enums/WidgetAction.cs
@@ -48,4 +48,6 @@ public enum WidgetAction
Save,
Cancel,
+
+ ChooseFile,
}
diff --git a/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs b/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs
index 09818b1e0c..5bf8176f53 100644
--- a/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs
+++ b/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs
@@ -17,7 +17,7 @@ internal sealed class SSHWalletWidget : CoreWidget
{
private static readonly string DefaultConfigFile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "\\.ssh\\config";
- private static readonly Regex HostRegex = new(@"^Host\s+(\S*)\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
+ private static readonly Regex HostRegex = new(@"^Host\s+(?:(\S*) ?)*?\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
private FileSystemWatcher? FileWatcher { get; set; }
@@ -133,6 +133,10 @@ public override void OnActionInvoked(WidgetActionInvokedArgs actionInvokedArgs)
ContentData = _savedContentData;
SetActive();
break;
+
+ case WidgetAction.ChooseFile:
+ HandleCheckPath(actionInvokedArgs);
+ break;
}
}
@@ -162,16 +166,32 @@ private void HandleCheckPath(WidgetActionInvokedArgs args)
Page = WidgetPageState.Loading;
UpdateWidget();
- // This is the action when the user clicks the submit button after entering a path while in
- // the Configure state.
Page = WidgetPageState.Configure;
var data = args.Data;
var dataObject = JsonSerializer.Deserialize(data, SourceGenerationContext.Default.DataPayload);
- if (dataObject != null && dataObject.ConfigFile != null)
+
+ var chosenPath = string.Empty;
+
+ if (dataObject == null)
+ {
+ return;
+ }
+ else if (dataObject.ConfigFile != null)
+ {
+ // The user clicked the Preview button in the Configure state.
+ chosenPath = dataObject.ConfigFile;
+ }
+ else if (dataObject.FilePath != null)
+ {
+ // The user used the File Picker to select a file in the Configure state.
+ chosenPath = dataObject.FilePath;
+ }
+
+ if (!string.IsNullOrEmpty(chosenPath))
{
var updateRequestOptions = new WidgetUpdateRequestOptions(Id)
{
- Data = GetConfiguration(dataObject.ConfigFile),
+ Data = GetConfiguration(chosenPath),
CustomState = ConfigFile,
Template = GetTemplateForPage(Page),
};
@@ -409,6 +429,12 @@ public string? ConfigFile
{
get; set;
}
+
+ [JsonPropertyName("filePath")]
+ public string? FilePath
+ {
+ get; set;
+ }
}
[JsonSourceGenerationOptions(WriteIndented = true)]
diff --git a/extensions/CoreWidgetProvider/Widgets/Templates/SSHWalletConfigurationTemplate.json b/extensions/CoreWidgetProvider/Widgets/Templates/SSHWalletConfigurationTemplate.json
index bf46b81350..3fc7eb1c8b 100644
--- a/extensions/CoreWidgetProvider/Widgets/Templates/SSHWalletConfigurationTemplate.json
+++ b/extensions/CoreWidgetProvider/Widgets/Templates/SSHWalletConfigurationTemplate.json
@@ -8,16 +8,49 @@
"id": "ConfigFile",
"label": "%SSH_Widget_Template/ConfigFilePath%",
"inlineAction": {
- "type": "Action.Execute",
- "tooltip": "%Widget_Template_Tooltip/Submit%",
- "verb": "CheckPath",
- "iconUrl": "data:image/png;base64,${submitIcon}"
+ "type": "Action.ChooseFile"
},
"spacing": "Medium",
"style": "Url",
"placeholder": "${$root.configuration.currentOrDefaultConfigFile}",
"value": "${$root.configuration.currentOrDefaultConfigFile}"
},
+ {
+ "type": "ColumnSet",
+ "spacing": "Medium",
+ "columns": [
+ {
+ "type": "Column",
+ "width": "stretch"
+ },
+ {
+ "type": "Column",
+ "width": "auto",
+ "items": [
+ {
+ "type": "Container",
+ "items": [
+ {
+ "type": "ActionSet",
+ "actions": [
+ {
+ "type": "Action.Execute",
+ "title": "%Widget_Template_Button/Preview%",
+ "verb": "CheckPath",
+ "associatedInputs": "Auto"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "Column",
+ "width": "stretch"
+ }
+ ]
+ },
{
"type": "Container",
"items": [
@@ -36,21 +69,6 @@
{
"type": "Container",
"items": [
- {
- "type": "TextBlock",
- "text": "%SSH_Widget_Template/ConfigFilePath%",
- "wrap": true,
- "spacing": "Medium",
- "size": "Small",
- "isSubtle": true
- },
- {
- "type": "TextBlock",
- "text": "${configFile}",
- "wrap": true,
- "size": "medium",
- "spacing": "None"
- },
{
"type": "TextBlock",
"text": "%SSH_Widget_Template/NumOfHosts%",
@@ -74,7 +92,7 @@
},
{
"type": "ColumnSet",
- "spacing": "ExtraLarge",
+ "spacing": "Large",
"columns": [
{
"type": "Column",
diff --git a/settings/DevHome.Settings/Strings/en-us/Resources.resw b/settings/DevHome.Settings/Strings/en-us/Resources.resw
index 812608237b..f13444365a 100644
--- a/settings/DevHome.Settings/Strings/en-us/Resources.resw
+++ b/settings/DevHome.Settings/Strings/en-us/Resources.resw
@@ -1,17 +1,17 @@
-
@@ -547,11 +547,19 @@
Quiet background processes
- Name of experimental feature 'Quiet background processes' on the 'Settings -> Experiments' page where you enable it.
+ Name of experimental feature 'Quiet background processes' on the 'Settings -> Experiments' page where you enable it.
Quiet background processes allows you to free up resources while developing
- Inline description of the Quiet background processes experimental feature on the 'Settings -> Experiments' page where you enable it.
+ Inline description of the Quiet background processes experimental feature on the 'Settings -> Experiments' page where you enable it.
+
+
+ Project Ironsides
+ Name of experimental feature 'Project Ironsides' on the 'Settings -> Experiments' page where you enable it.
+
+
+ Project Ironsides is a utlity to provide deeper insights into your applications
+ Inline description of the Project Ironsides experimental feature on the 'Settings -> Experiments' page where you enable it.
Quickstart Playground
diff --git a/settings/DevHome.Settings/Views/AboutPage.xaml b/settings/DevHome.Settings/Views/AboutPage.xaml
index 6aceb5db4f..6ac173b743 100644
--- a/settings/DevHome.Settings/Views/AboutPage.xaml
+++ b/settings/DevHome.Settings/Views/AboutPage.xaml
@@ -1,42 +1,47 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/settings/DevHome.Settings/Views/AboutPage.xaml.cs b/settings/DevHome.Settings/Views/AboutPage.xaml.cs
index 743dc23bb1..79b8c342c0 100644
--- a/settings/DevHome.Settings/Views/AboutPage.xaml.cs
+++ b/settings/DevHome.Settings/Views/AboutPage.xaml.cs
@@ -5,14 +5,14 @@
using System.Diagnostics;
using CommunityToolkit.Mvvm.Input;
using DevHome.Common.Extensions;
+using DevHome.Common.Views;
using DevHome.Settings.ViewModels;
using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
using Serilog;
namespace DevHome.Settings.Views;
-public sealed partial class AboutPage : Page
+public sealed partial class AboutPage : DevHomePage
{
public AboutViewModel ViewModel { get; }
@@ -20,17 +20,6 @@ public AboutPage()
{
ViewModel = Application.Current.GetService();
this.InitializeComponent();
-
-#if DEBUG
- Loaded += ShowViewLogsButton;
-#endif
- }
-
-#if DEBUG
- private void ShowViewLogsButton(object sender, RoutedEventArgs e)
- {
- ViewLogsSettingsCard.Visibility = Visibility.Visible;
- ViewLogsSettingsCard.Command = OpenLogsLocationCommand;
}
[RelayCommand]
@@ -47,5 +36,4 @@ private void OpenLogsLocation()
log.Error(e, $"Error opening log location");
}
}
-#endif
}
diff --git a/settings/DevHome.Settings/Views/AccountsPage.xaml b/settings/DevHome.Settings/Views/AccountsPage.xaml
index 90ebc7febc..7682d530ab 100644
--- a/settings/DevHome.Settings/Views/AccountsPage.xaml
+++ b/settings/DevHome.Settings/Views/AccountsPage.xaml
@@ -1,4 +1,4 @@
-
@@ -14,9 +15,7 @@
-
-
-
+
@@ -41,12 +40,19 @@
-
+
+ Configuration in progress
+ Title displayed to the user when they attempt to activate the app through a URI but the machine configuration flow is currently in progress.
+
Transfer developer machine settings
Title text of an instruction banner section for transferring settings from a developer machine
@@ -546,7 +550,7 @@
Body text description for a card than when clicked takes the user to a multi-step flow for setting up their machine
- [Experimental] Use natural language and AI to set up a new devcontainer-based programming environment
+ [Experimental] Use natural language and AI to set up a new devcontainer-based programming environment.
Body text description for a card than when clicked takes the user to a flow for creating a devcontainer environment for their project idea
@@ -657,7 +661,7 @@
Generate Configuration file
Text for a generating configuration file button
-
+
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
@@ -1199,6 +1203,10 @@
Enter the full path and folder name
Error message to tell the user to fully qualify their clone path
+
+ The URI activation flow cannot be initiated while machine configuration is in progress. Please complete or cancel your current configuration tasks and then try again.
+ Message displayed to the user when they attempt to activate the app through a URI but the machine configuration flow is currently in progress.
+
Enter a repository name beginning with https://
Error string to show the user if the url is not an absolute URL
@@ -1793,7 +1801,7 @@
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
+ 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
@@ -2003,4 +2011,12 @@
Error: {0}
Locked={"{0}"} Error message with additional details in {0} shown when we failed to generate the project.
+
+ Version
+ Name for the version combo box
+
+
+ Version
+ Name for the version combo box
+
\ No newline at end of file
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml b/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml
index 213fa6b626..da05f591b5 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml
+++ b/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml
@@ -77,10 +77,11 @@
-
+
diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/AppManagementTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/AppManagementTaskGroup.cs
index 28b7c62939..781a2b2ec7 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/AppManagementTaskGroup.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/AppManagementTaskGroup.cs
@@ -39,11 +39,6 @@ public AppManagementTaskGroup(
public void HandleSearchQuery(string query)
{
- var searchParameter = HttpUtility.ParseQueryString(query)["search"];
- if (!string.IsNullOrEmpty(searchParameter))
- {
- var trimmedSearchParameter = searchParameter.Trim('\"');
- _appManagementViewModel.PerformSearch(trimmedSearchParameter);
- }
+ _appManagementViewModel.PerformSearch(query);
}
}
diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs
index 4bd1bf8489..7bef0ace60 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs
@@ -6,7 +6,7 @@
using DevHome.SetupFlow.Models;
using DevHome.SetupFlow.Services;
using DevHome.SetupFlow.ViewModels;
-using WinUIEx;
+using Microsoft.UI.Dispatching;
namespace DevHome.SetupFlow.TaskGroups;
@@ -24,7 +24,7 @@ public SetupTargetTaskGroup(
IComputeSystemManager computeSystemManager,
ConfigurationFileBuilder configurationFileBuilder,
SetupFlowOrchestrator setupFlowOrchestrator,
- WindowEx windowEx)
+ DispatcherQueue dispatcherQueue)
{
_setupTargetViewModel = setupTargetViewModel;
_setupTargetReviewViewModel = setupTargetReviewViewModel;
@@ -34,7 +34,7 @@ public SetupTargetTaskGroup(
computeSystemManager,
configurationFileBuilder,
setupFlowOrchestrator,
- windowEx);
+ dispatcherQueue);
}
///
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs
index cf2d67c7f6..f1195810ce 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs
@@ -23,12 +23,12 @@
using DevHome.Telemetry;
using Microsoft.Extensions.Hosting;
using Microsoft.UI;
+using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.DevHome.SDK;
using Serilog;
using Windows.Foundation;
-using WinUIEx;
using static DevHome.SetupFlow.Models.Common;
namespace DevHome.SetupFlow.ViewModels;
@@ -445,6 +445,44 @@ private void RepoProviderSelected(string repositoryProviderName)
_selectedRepoProvider = repositoryProviderName;
}
+ ///
+ /// Adds or removes the default dev drive. This dev drive will be made at the loading screen.
+ ///
+ [RelayCommand]
+ private void MakeNewDevDrive(bool isCheckBoxChecked)
+ {
+ // Getting here means
+ // 1. The user does not have any existing dev drives
+ // 2. The user wants to clone to a new dev drive.
+ // 3. The user un-checked this and does not want a new dev drive.
+ if (isCheckBoxChecked)
+ {
+ UpdateDevDriveInfo();
+ }
+ else
+ {
+ FolderPickerViewModel.CloneLocationAlias = string.Empty;
+ FolderPickerViewModel.InDevDriveScenario = false;
+ EditDevDriveViewModel.RemoveNewDevDrive();
+ FolderPickerViewModel.EnableBrowseButton();
+ FolderPickerViewModel.CloneLocation = _addRepoDialog.OldCloneLocation;
+ }
+ }
+
+ ///
+ /// Update dialog to show Dev Drive information.
+ ///
+ public void UpdateDevDriveInfo()
+ {
+ EditDevDriveViewModel.MakeDefaultDevDrive();
+ FolderPickerViewModel.DisableBrowseButton();
+ _addRepoDialog.OldCloneLocation = FolderPickerViewModel.CloneLocation;
+ FolderPickerViewModel.CloneLocation = EditDevDriveViewModel.GetDriveDisplayName();
+ FolderPickerViewModel.CloneLocationAlias = EditDevDriveViewModel.GetDriveDisplayName(DevDriveDisplayNameKind.FormattedDriveLabelKind);
+ FolderPickerViewModel.InDevDriveScenario = true;
+ EditDevDriveViewModel.IsDevDriveCheckboxChecked = true;
+ }
+
[RelayCommand]
private void CancelButtonPressed()
{
@@ -691,7 +729,7 @@ public AddRepoViewModel(
_addRepoDialog = addRepoDialog;
_stringResource = stringResource;
_host = host;
- _dispatcherQueue = host.GetService().DispatcherQueue;
+ _dispatcherQueue = host.GetService();
_loginUiContent = new Frame();
_setupFlowOrchestrator = setupFlowOrchestrator;
@@ -717,6 +755,52 @@ public AddRepoViewModel(
_accountIndex = -1;
}
+ ///
+ /// Handles logic when the primary button is clicked. Actions change depending on the screen
+ /// then user is on.
+ ///
+ /// The search terms for repositories.
+ /// True if the close should be canceled. Otherwise false and the dialog will close.
+ public async Task PrimaryButtonClick(Dictionary searchTerms)
+ {
+ if (CurrentPage == PageKind.AddViaUrl)
+ {
+ // Get the number of repos already selected to clone in a previous instance.
+ // Used to figure out if the repo was added after the user logged into an account.
+ var numberOfReposToCloneCount = EverythingToClone.Count;
+
+ await AddRepositoryViaUri(Url, FolderPickerViewModel.CloneLocation);
+
+ // If the repo was not added.
+ if (numberOfReposToCloneCount == EverythingToClone.Count)
+ {
+ ShouldEnablePrimaryButton = false;
+ return true;
+ }
+
+ return false;
+ }
+ else if (CurrentPage == PageKind.AddViaAccount)
+ {
+ if (!string.IsNullOrEmpty(_selectedRepoProvider))
+ {
+ await ChangeToRepoPageAsync();
+ }
+
+ return true;
+ }
+ else if (CurrentPage == PageKind.SearchFields)
+ {
+ // switching to the repo page causes repos to be queried.
+ await ChangeToRepoPageAsync();
+ SearchForRepos(searchTerms);
+
+ return true;
+ }
+
+ return false;
+ }
+
///
/// Toggles the clone button. Make sure other view models have correct information.
///
@@ -981,9 +1065,18 @@ private async Task LogUserIn(string repositoryProviderName)
ShouldShowLoginUi = true;
IsCancelling = false;
+ // Store the close button text because it will change.
+ // The text of the secondary button of the content dialog changes to notify users
+ // to press the "X" button to cancel the login prompt.
+ // This is needed because the secondary button does not listen to events because
+ // the content dialog is in the middle of the Primary Button click event.
+ var closeButtonText = _addRepoDialog.CloseButtonText;
_addRepoDialog.CloseButtonText = _host.GetService().GetLocalized(StringResourceKey.UrlCancelButtonText);
+
await InitiateAddAccountUserExperienceAsync(_providers.GetProvider(repositoryProviderName), LoginUiContent);
+ _addRepoDialog.CloseButtonText = closeButtonText;
+
ShouldShowLoginUi = false;
IsLoggingIn = false;
IsCancelling = true;
@@ -1066,6 +1159,7 @@ public void AddOrRemoveRepository(string accountName, IList
public void UpdateExtensionAdaptiveCard(ComputeSystemAdaptiveCardResult adaptiveCardSessionResult)
{
- _windowEx.DispatcherQueue.TryEnqueue(() =>
+ _dispatcherQueue.TryEnqueue(() =>
{
try
{
@@ -221,7 +221,7 @@ public void UpdateExtensionAdaptiveCard(ComputeSystemAdaptiveCardResult adaptive
///
public void OnAdaptiveCardUpdated(object sender, AdaptiveCard adaptiveCard)
{
- _windowEx.DispatcherQueue.TryEnqueue(() =>
+ _dispatcherQueue.TryEnqueue(() =>
{
// Render the adaptive card and set the action event handler.
_renderedAdaptiveCard = _adaptiveCardRenderer.RenderAdaptiveCard(adaptiveCard);
@@ -247,7 +247,7 @@ public void OnAdaptiveCardUpdated(object sender, AdaptiveCard adaptiveCard)
/// The action and user inputs from within the adaptive card
private void OnRenderedAdaptiveCardAction(object sender, AdaptiveActionEventArgs args)
{
- _windowEx.DispatcherQueue.TryEnqueue(async () =>
+ _dispatcherQueue.TryEnqueue(async () =>
{
IsAdaptiveCardSessionLoaded = false;
AdaptiveCardLoadingMessage = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationAdaptiveCardLoadingMessage, _curProviderDetails.ComputeSystemProvider.DisplayName);
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs
index 4ff1b10fd7..3e7d246534 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs
@@ -1,25 +1,20 @@
// 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 CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Behaviors;
using DevHome.Common.Contracts.Services;
using DevHome.Common.Environments.Helpers;
using DevHome.Common.Environments.Models;
-using DevHome.Common.Environments.Services;
using DevHome.Common.Services;
using DevHome.SetupFlow.Models.Environments;
using DevHome.SetupFlow.Services;
-using Microsoft.UI.Xaml;
using Microsoft.Windows.DevHome.SDK;
using Serilog;
-using WinUIEx;
namespace DevHome.SetupFlow.ViewModels.Environments;
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs
index f4e5b397c9..b515822989 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 WinUIEx;
namespace DevHome.SetupFlow.ViewModels;
@@ -131,7 +130,7 @@ private async Task PickCloneDirectoryAsync()
{
_log.Information("Opening folder picker to select clone directory");
using var folderPicker = new WindowOpenFolderDialog();
- var locationToCloneTo = await folderPicker.ShowAsync(Application.Current.GetService());
+ var locationToCloneTo = await folderPicker.ShowAsync(Application.Current.GetService());
if (locationToCloneTo != null && locationToCloneTo.Path.Length > 0)
{
_log.Information($"Selected '{locationToCloneTo.Path}' as location to clone to");
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingMessageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingMessageViewModel.cs
index 3e6c248c36..358999c7d6 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingMessageViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingMessageViewModel.cs
@@ -18,18 +18,6 @@ public partial class LoadingMessageViewModel : ObservableObject
[ObservableProperty]
private string _messageToShow;
- ///
- /// If the progress ring should be shown. Only show a progress ring when the task is running.
- ///
- [ObservableProperty]
- private bool _shouldShowProgressRing;
-
- ///
- /// The status symbol icon is the red, green, or yellow icon that is next to a task when it has been completed.
- ///
- [ObservableProperty]
- private bool _shouldShowStatusSymbolIcon;
-
///
/// The icon to display in the loading screen after a task is finished.
///
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs
index e3e465f58b..cf2fa99f0a 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs
@@ -18,10 +18,10 @@
using DevHome.SetupFlow.TaskGroups;
using DevHome.Telemetry;
using Microsoft.Extensions.Hosting;
+using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
using Serilog;
-using WinUIEx;
namespace DevHome.SetupFlow.ViewModels;
@@ -44,9 +44,7 @@ public partial class LoadingViewModel : SetupPageViewModelBase
private static readonly BitmapImage LightSuccess = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/LightSuccess.png"));
private static readonly BitmapImage LightInfo = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/LightInfo.png"));
-#pragma warning disable SA1310 // Field names should not contain underscore
- private const int MAX_RETRIES = 1;
-#pragma warning restore SA1310 // Field names should not contain underscore
+ private const int MaxRetries = 1;
private int _retryCount;
@@ -77,7 +75,10 @@ public partial class LoadingViewModel : SetupPageViewModelBase
private ObservableCollection _tasksToRun;
[ObservableProperty]
- private ObservableCollection _messages;
+ private ObservableCollection _executingMessages;
+
+ [ObservableProperty]
+ private ObservableCollection _nonExecutingMessages;
[ObservableProperty]
private ObservableCollection _summaryInformation;
@@ -216,43 +217,37 @@ public void GoToSummaryPage()
public void AddMessage(string message, MessageSeverityKind severityKind = MessageSeverityKind.Info)
{
- Application.Current.GetService().DispatcherQueue.TryEnqueue(() =>
+ Application.Current.GetService().TryEnqueue(() =>
{
var messageToDisplay = _host.GetService();
messageToDisplay.MessageToShow = message;
- messageToDisplay.ShouldShowStatusSymbolIcon = false;
- messageToDisplay.ShouldShowProgressRing = false;
if (severityKind == MessageSeverityKind.Warning)
{
- messageToDisplay.ShouldShowStatusSymbolIcon = true;
messageToDisplay.StatusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkCaution : LightCaution;
}
else if (severityKind == MessageSeverityKind.Error)
{
- messageToDisplay.ShouldShowStatusSymbolIcon = true;
messageToDisplay.StatusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkError : LightError;
}
else if (severityKind == MessageSeverityKind.Success)
{
- messageToDisplay.ShouldShowStatusSymbolIcon = true;
messageToDisplay.StatusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkSuccess : LightSuccess;
}
else if (severityKind == MessageSeverityKind.Info)
{
- messageToDisplay.ShouldShowStatusSymbolIcon = true;
messageToDisplay.StatusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkInfo : LightInfo;
}
- Messages.Insert(Messages.Count - _numberOfExecutingTasks, messageToDisplay);
+ NonExecutingMessages.Add(messageToDisplay);
});
}
public void UpdateActionCenterMessage(ActionCenterMessages message, ActionMessageRequestKind requestKind)
{
- // ALl referenced to WindowEx and Application.Current will be removed in the future,
- // in the loadingViewModel.
- Application.Current.GetService().DispatcherQueue.TryEnqueue(() =>
+ // All references to DispatcherQueue and Application.Current will be removed in the future,
+ // in the LoadingViewModel.
+ Application.Current.GetService().TryEnqueue(() =>
{
// We need to add/remove the message in a temporary list and then re add the items to a new collection. This is because
// of the adaptive card panel and it receiving UI updates in the listview. There can be random crashes if we don't do this when
@@ -314,7 +309,8 @@ public LoadingViewModel(
ShowRetryButton = Visibility.Collapsed;
_failedTasks = new List();
ActionCenterItems = new();
- Messages = new();
+ ExecutingMessages = new();
+ NonExecutingMessages = new();
_activityId = orchestrator.ActivityId;
_summaryInformation = new ObservableCollection();
}
@@ -377,73 +373,109 @@ private void SetExecutingTaskAndActionCenter()
}
///
- /// Changes the internals of information according to the taskFinishedState.
+ /// Uses information and the task state to figure out what message needs to be placed into the loading screen.
///
- /// The information that will change.
- /// The status of the task.
- ///
- /// TaskInformation is an ObservableObject inside an ObservableCollection. Any changes to information
- /// will change the UI.
- ///
- private void ChangeMessage(TaskInformation information, LoadingMessageViewModel loadingMessage, TaskFinishedState taskFinishedState)
+ /// Used to know if the computer needs to reboot.
+ /// The state of the finished task.
+ /// A LoadingMessageViewModel that can be placed into the UI.
+ private LoadingMessageViewModel GenerateFinishedMessage(TaskInformation information, TaskFinishedState finishedState)
{
- _log.Debug($"Updating message for task {information.MessageToShow} with state {taskFinishedState}");
+ _log.Debug($"Updating message for task {information.MessageToShow} with state {finishedState}");
var stringToReplace = string.Empty;
BitmapImage statusSymbolIcon = null;
// Two things to do.
// 1. Change the message color and icon in information
// 2. Add a new message with the done message.
- if (taskFinishedState == TaskFinishedState.Success)
+ if (finishedState == TaskFinishedState.Success)
{
if (information.TaskToExecute.RequiresReboot)
{
- _log.Debug("Task succeeded but requires reboot; adding to action center");
stringToReplace = information.TaskToExecute.GetLoadingMessages().NeedsReboot;
statusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkCaution : LightCaution;
- ActionCenterItems.Insert(0, information.TaskToExecute.GetRebootMessage());
}
else
{
- _log.Debug("Task succeeded");
stringToReplace = information.TaskToExecute.GetLoadingMessages().Finished;
statusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkSuccess : LightSuccess;
}
+ }
+ else if (finishedState == TaskFinishedState.Failure)
+ {
+ stringToReplace = information.TaskToExecute.GetLoadingMessages().Error;
+ statusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkError : LightError;
+ }
+
+ var newLoadingScreenMessage = _host.GetService();
+ newLoadingScreenMessage.MessageToShow = stringToReplace;
+ newLoadingScreenMessage.StatusSymbolIcon = statusSymbolIcon;
+
+ return newLoadingScreenMessage;
+ }
+
+ ///
+ /// Updates the loading screen task logging UI to show that the task is finished.
+ ///
+ /// The executing message.
+ /// The finished message.
+ private void InsertFinishedMessageIntoLogScreen(LoadingMessageViewModel originalMessage, LoadingMessageViewModel finishedMessage)
+ {
+ // Remove the executing message from the list.
+ ExecutingMessages.Remove(originalMessage);
+
+ // Insert the same message. All messages in this list have their foreground set to
+ // secondary.
+ NonExecutingMessages.Add(originalMessage);
+
+ // Add the execution finished message
+ NonExecutingMessages.Add(finishedMessage);
+ }
+
+ ///
+ /// Adds an item to the ActionCenterItems collection if needs be. Updates task counters.
+ ///
+ /// Used to know if the computer needs to reboot.
+ /// The status of the finished task.
+ private void PostTaskUiUpdate(TaskInformation information, TaskFinishedState taskFinishedState)
+ {
+ if (taskFinishedState == TaskFinishedState.Success)
+ {
+ if (information.TaskToExecute.RequiresReboot)
+ {
+ _log.Debug("Task succeeded but requires reboot; adding to action center");
+ ActionCenterItems.Insert(0, information.TaskToExecute.GetRebootMessage());
+ }
+ else
+ {
+ _log.Debug("Task succeeded");
+ }
TasksFinishedSuccessfully++;
}
else if (taskFinishedState == TaskFinishedState.Failure)
{
_log.Debug("Task failed");
- stringToReplace = information.TaskToExecute.GetLoadingMessages().Error;
- statusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkError : LightError;
ActionCenterItems.Insert(0, information.TaskToExecute.GetErrorMessages());
TasksFailed++;
_log.Debug("Adding task to list for retry");
_failedTasks.Add(information);
}
+ }
- // When a task is done
- // Following logic is to keep all "executing" messages at the bottom of the list.
- // Remove the "executing" message from the list.
- Messages.Remove(loadingMessage);
-
- // Modify the message so it looks done.
- loadingMessage.ShouldShowProgressRing = false;
-
- // Insert the message right before any "executing" messages.
- Messages.Insert(Messages.Count - _numberOfExecutingTasks, loadingMessage);
-
- // Add the "Execution finished" message
- var newLoadingScreenMessage = _host.GetService();
- newLoadingScreenMessage.MessageToShow = stringToReplace;
- newLoadingScreenMessage.StatusSymbolIcon = statusSymbolIcon;
- newLoadingScreenMessage.ShouldShowProgressRing = false;
- newLoadingScreenMessage.ShouldShowStatusSymbolIcon = true;
-
- // Insert the message right before any "executing" messages.
- Messages.Insert(Messages.Count - _numberOfExecutingTasks, newLoadingScreenMessage);
+ ///
+ /// Updates the Action Center, Inserts the finished message, clears out the executing message,
+ /// and updates any task counters.
+ ///
+ /// Information on the task. Used to figure out if a reboot is needed.
+ /// The executing messages placed into the Loading Screen. This
+ /// is used to find the executing message to remove.
+ /// The state of the task. Used for a variety of things.
+ private void PerformPostTaskTasks(TaskInformation information, LoadingMessageViewModel originalMessage, TaskFinishedState finishedState)
+ {
+ PostTaskUiUpdate(information, finishedState);
+ var finishedMessage = GenerateFinishedMessage(information, finishedState);
+ InsertFinishedMessageIntoLogScreen(originalMessage, finishedMessage);
}
///
@@ -463,7 +495,7 @@ protected async override Task OnFirstNavigateToAsync()
private async Task StartAllTasks(ObservableCollection tasks)
{
_log.Information("Starting all tasks");
- var window = Application.Current.GetService();
+ var dispatcherQueue = Application.Current.GetService();
await Task.Run(async () =>
{
var tasksToRunFirst = new List();
@@ -487,13 +519,13 @@ await Task.Run(async () =>
// Run all tasks that don't need dev drive installed.
await Parallel.ForEachAsync(tasksToRunFirst, async (taskInformation, token) =>
{
- await StartTaskAndReportResult(window, taskInformation);
+ await StartTaskAndReportResult(dispatcherQueue, taskInformation);
});
// Run all the tasks that need dev drive installed.
await Parallel.ForEachAsync(tasksToRunSecond, async (taskInformation, token) =>
{
- await StartTaskAndReportResult(window, taskInformation);
+ await StartTaskAndReportResult(dispatcherQueue, taskInformation);
});
});
@@ -505,7 +537,7 @@ await Parallel.ForEachAsync(tasksToRunSecond, async (taskInformation, token) =>
_log.Information("All tasks succeeded. Moving to next page");
ExecutionFinished.Invoke(null, null);
}
- else if (_retryCount >= MAX_RETRIES)
+ else if (_retryCount >= MaxRetries)
{
_log.Information("Max number of retries reached; moving to next page");
ShowOutOfRetriesBanner = true;
@@ -530,17 +562,19 @@ await Parallel.ForEachAsync(tasksToRunSecond, async (taskInformation, token) =>
///
/// Runs the specified task and updates the UI when the task is finished.
///
- /// Used to get access to the dispatcher queue.
+ /// Dispatcher queue associated with the window for the task.
/// Information about the task to execute. Will be modified
/// An awaitable task
- private async Task StartTaskAndReportResult(WinUIEx.WindowEx window, TaskInformation taskInformation)
+ private async Task StartTaskAndReportResult(DispatcherQueue dispatcherQueue, TaskInformation taskInformation)
{
+ // loadingMessage is used in the catch.
+ var loadingMessage = _host.GetService();
+ loadingMessage.MessageToShow = taskInformation.MessageToShow;
+
// Start the task and wait for it to complete.
try
{
- var loadingMessage = _host.GetService();
- loadingMessage.MessageToShow = taskInformation.MessageToShow;
- window.DispatcherQueue.TryEnqueue(() =>
+ dispatcherQueue.TryEnqueue(() =>
{
TasksStarted++;
if (!Orchestrator.IsSettingUpATargetMachine)
@@ -552,10 +586,9 @@ private async Task StartTaskAndReportResult(WinUIEx.WindowEx window, TaskInforma
SetupTargetText = StringResource.GetLocalized(StringResourceKey.LoadingPageSetupTargetText, taskInformation.TaskToExecute.TargetName);
}
- loadingMessage.ShouldShowProgressRing = true;
- Messages.Add(loadingMessage);
+ ExecutingMessages.Add(loadingMessage);
- // Keep increment inside TryEnqueue to enforce "locking"
+ // Keep increment inside TryEnqueue to enforce locking
_numberOfExecutingTasks++;
});
@@ -570,20 +603,45 @@ private async Task StartTaskAndReportResult(WinUIEx.WindowEx window, TaskInforma
taskFinishedState = await taskInformation.TaskToExecute.Execute();
}
- window.DispatcherQueue.TryEnqueue(() =>
+ dispatcherQueue.TryEnqueue(() =>
{
- // Keep decrement inside TryEnqueue to enforce "locking"
- _numberOfExecutingTasks--;
- ChangeMessage(taskInformation, loadingMessage, taskFinishedState);
- TasksCompleted++;
- ActionCenterDisplay = StringResource.GetLocalized(StringResourceKey.ActionCenterDisplay, TasksFailed);
+ PerformPostTaskTasks(taskInformation, loadingMessage, taskFinishedState);
});
}
- catch
+ catch (Exception e)
{
- // Don't let a single task break everything
- // TODO: Show failed tasks on UI
- // https://github.com/microsoft/devhome/issues/629
+ dispatcherQueue.TryEnqueue(() =>
+ {
+ // This code block mostly duplicates logic in PerformPostTaskTasks.
+ // The difference is the message isn't stored in the task.
+ // PerformPostTaskTasks uses information inside LoadingMessageViewModel
+ // and TaskInformation to determine what message to show.
+ // Until those two peices of information are de-coupled this code should stay here.
+ var newLoadingMessage = _host.GetService();
+ newLoadingMessage.MessageToShow = $"Could not finish {taskInformation.MessageToShow} because {e.Message}";
+ newLoadingMessage.StatusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkError : LightError;
+
+ InsertFinishedMessageIntoLogScreen(loadingMessage, newLoadingMessage);
+
+ TasksFailed++;
+
+ _log.Debug("Adding task to list for retry");
+ _failedTasks.Add(taskInformation);
+
+ var actionCenterErrorMessage = new ActionCenterMessages();
+ actionCenterErrorMessage.PrimaryMessage = e.Message;
+ ActionCenterItems.Insert(0, actionCenterErrorMessage);
+ });
+
+ _log.Error(e, $"Could not finish all tasks.");
}
+
+ dispatcherQueue.TryEnqueue(() =>
+ {
+ // Keep decrement inside TryEnqueue to enforce locking
+ _numberOfExecutingTasks--;
+ TasksCompleted++;
+ ActionCenterDisplay = StringResource.GetLocalized(StringResourceKey.ActionCenterDisplay, TasksFailed);
+ });
}
}
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs
index 915400e78f..776c82f7f2 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs
@@ -145,11 +145,11 @@ public async Task StartConfigurationFileAsync(StorageFile file)
}
}
- internal void StartAppManagementFlow(string query)
+ internal void StartAppManagementFlow(string query = null)
{
- _log.Information($"Launching app management flow for query:{query}");
+ _log.Information("Launching app management flow");
var appManagementSetupFlow = _host.GetService();
- StartSetupFlowForTaskGroups(null, "App Search URI", appManagementSetupFlow);
+ StartSetupFlowForTaskGroups(null, "App Activation URI", appManagementSetupFlow);
appManagementSetupFlow.HandleSearchQuery(query);
}
@@ -250,6 +250,15 @@ private void StartQuickstart(string flowTitle)
///
[RelayCommand]
public void StartCreateEnvironment(string flowTitle)
+ {
+ StartCreateEnvironmentWithTelemetry(flowTitle, "StartCreationFlow", "Machine Configuration");
+ }
+
+ ///
+ /// Starts the create environment flow and logs that the create environment button has been clicked. This
+ /// can be generalized in the future so other flow can utilize it as well.
+ ///
+ public void StartCreateEnvironmentWithTelemetry(string flowTitle, string navigationAction, string originPage)
{
_log.Information("Starting flow for environment creation");
StartSetupFlowForTaskGroups(
@@ -257,6 +266,13 @@ public void StartCreateEnvironment(string flowTitle)
"CreateEnvironment",
_host.GetService(),
_host.GetService());
+
+ // Send telemetry so we know which page in Dev Home the user clicked the create environment button.
+ TelemetryFactory.Get().Log(
+ "Create_Environment_button_Clicked",
+ LogLevel.Critical,
+ new EnvironmentRedirectionUserEvent(navigationAction: navigationAction, originPage),
+ relatedActivityId: Orchestrator.ActivityId);
}
///
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs
index f8728f3722..6119abb93e 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs
@@ -14,8 +14,8 @@
using DevHome.SetupFlow.Behaviors;
using DevHome.SetupFlow.Services;
using DevHome.Telemetry;
+using Microsoft.UI.Dispatching;
using Serilog;
-using WinUIEx;
namespace DevHome.SetupFlow.ViewModels;
@@ -25,7 +25,7 @@ public partial class PackageCatalogListViewModel : ObservableObject, IDisposable
private readonly ICatalogDataSourceLoader _catalogDataSourceLoader;
private readonly IExtensionService _extensionService;
private readonly PackageCatalogViewModelFactory _packageCatalogViewModelFactory;
- private readonly WindowEx _windowEx;
+ private readonly DispatcherQueue _dispatcherQueue;
private readonly SemaphoreSlim _loadCatalogsSemaphore = new(1, 1);
[ObservableProperty]
@@ -56,10 +56,10 @@ public PackageCatalogListViewModel(
IExtensionService extensionService,
ICatalogDataSourceLoader catalogDataSourceLoader,
PackageCatalogViewModelFactory packageCatalogViewModelFactory,
- WindowEx windowEx)
+ DispatcherQueue dispatcherQueue)
{
_extensionService = extensionService;
- _windowEx = windowEx;
+ _dispatcherQueue = dispatcherQueue;
_catalogDataSourceLoader = catalogDataSourceLoader;
_packageCatalogViewModelFactory = packageCatalogViewModelFactory;
}
@@ -172,7 +172,7 @@ private void OnUnloaded()
private async void OnExtensionChangedAsync(object sender, EventArgs e)
{
- await _windowEx.DispatcherQueue.EnqueueAsync(() => LoadCatalogsAsync());
+ await _dispatcherQueue.EnqueueAsync(() => LoadCatalogsAsync());
}
private void Dispose(bool disposing)
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs
index a5c59743c2..6c7d38926d 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs
@@ -58,7 +58,7 @@ public partial class PackageViewModel : ObservableObject
/// Indicates if a package is selected
///
[ObservableProperty]
- [NotifyPropertyChangedFor(nameof(ButtonAutomationName))]
+ [NotifyPropertyChangedFor(nameof(ActionButtonDescription))]
[NotifyPropertyChangedFor(nameof(ButtonAutomationId))]
private bool _isSelected;
@@ -130,7 +130,7 @@ public PackageViewModel(
public bool CanInstall => _orchestrator.IsSettingUpATargetMachine || !IsInstalled || _package.InstalledVersion != SelectedVersion;
- public string ButtonAutomationName => IsSelected ?
+ public string ActionButtonDescription => IsSelected ?
_stringResource.GetLocalized(StringResourceKey.RemoveApplication) :
_stringResource.GetLocalized(StringResourceKey.AddApplication);
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/QuickstartPlaygroundViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/QuickstartPlaygroundViewModel.cs
index d30b69cf25..9ad91bc1ea 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/QuickstartPlaygroundViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/QuickstartPlaygroundViewModel.cs
@@ -24,7 +24,6 @@
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.System;
-using WinUIEx;
namespace DevHome.SetupFlow.ViewModels;
@@ -195,7 +194,7 @@ public Task SaveProject()
// TODO: Replace with WindowSaveFileDialog
var folderPicker = new FolderPicker();
- var hWnd = Application.Current.GetService().GetWindowHandle();
+ var hWnd = Application.Current.GetService().GetWindowHandle();
WinRT.Interop.InitializeWithWindow.Initialize(folderPicker, hWnd);
folderPicker.FileTypeFilter.Add("*");
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs
index fb6f899d1c..b7ad09911c 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs
@@ -16,8 +16,8 @@
using DevHome.SetupFlow.Services;
using DevHome.SetupFlow.TaskGroups;
using DevHome.Telemetry;
+using Microsoft.UI.Xaml;
using Serilog;
-using WinUIEx;
namespace DevHome.SetupFlow.ViewModels;
@@ -27,7 +27,7 @@ public partial class ReviewViewModel : SetupPageViewModelBase
private readonly SetupFlowOrchestrator _setupFlowOrchestrator;
private readonly ConfigurationFileBuilder _configFileBuilder;
- private readonly WindowEx _mainWindow;
+ private readonly Window _mainWindow;
[ObservableProperty]
private IList _reviewTabs;
@@ -95,7 +95,7 @@ public ReviewViewModel(
ISetupFlowStringResource stringResource,
SetupFlowOrchestrator orchestrator,
ConfigurationFileBuilder configFileBuilder,
- WindowEx mainWindow)
+ Window mainWindow)
: base(stringResource, orchestrator)
{
NextPageButtonText = StringResource.GetLocalized(StringResourceKey.SetUpButton);
@@ -164,7 +164,12 @@ private async Task OnSetUpAsync()
}
var flowPages = Orchestrator.FlowPages.Select(p => p.GetType().Name).ToList();
- TelemetryFactory.Get().Log("Review_SetUp", LogLevel.Critical, new ReviewSetUpCommandEvent(Orchestrator.IsSettingUpATargetMachine, flowPages));
+ TelemetryFactory.Get().Log(
+ "Review_SetUp",
+ LogLevel.Critical,
+ new ReviewSetUpCommandEvent(Orchestrator.IsSettingUpATargetMachine, flowPages),
+ relatedActivityId: Orchestrator.ActivityId);
+
await Orchestrator.GoToNextPage();
}
catch (Exception e)
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs
index b4bedba0dc..bad8444d1c 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs
@@ -9,7 +9,10 @@
using CommunityToolkit.Mvvm.Input;
using DevHome.Common.Extensions;
using DevHome.Common.Services;
+using DevHome.Common.TelemetryEvents;
+using DevHome.Common.TelemetryEvents.Environments;
using DevHome.Common.TelemetryEvents.SetupFlow;
+using DevHome.Common.TelemetryEvents.SetupFlow.Environments;
using DevHome.SetupFlow.Models;
using DevHome.SetupFlow.Services;
using DevHome.Telemetry;
@@ -123,10 +126,12 @@ public async Task StartFileActivationFlowAsync(StorageFile file)
await _mainPageViewModel.StartConfigurationFileAsync(file);
}
- public void StartCreationFlowAsync()
+ public void StartCreationFlowAsync(string originPage)
{
Orchestrator.FlowPages = [_mainPageViewModel];
- _mainPageViewModel.StartCreateEnvironment(string.Empty);
+
+ // This method is only called when the user clicks a button that redirects them to 'Create Environment' flow in the setup flow.
+ _mainPageViewModel.StartCreateEnvironmentWithTelemetry(string.Empty, _creationFlowNavigationParameter, originPage);
}
public void OnNavigatedTo(NavigationEventArgs args)
@@ -136,15 +141,19 @@ public void OnNavigatedTo(NavigationEventArgs args)
var parameter = args.Parameter?.ToString();
if ((!string.IsNullOrEmpty(parameter)) &&
- _creationFlowNavigationParameter.Equals(parameter, StringComparison.OrdinalIgnoreCase) &&
+ parameter.Contains(_creationFlowNavigationParameter, StringComparison.OrdinalIgnoreCase) &&
Orchestrator.CurrentSetupFlowKind != SetupFlowKind.CreateEnvironment)
{
+ // We expect that when navigating from anywhere in Dev Home to the create environment page
+ // that the arg.Parameter variable be semicolon delimited string with the first value being 'StartCreationFlow'
+ // and the second value being the page name that redirection came from for telemetry purposes.
+ var parameters = parameter.Split(';');
Cancel();
- StartCreationFlowAsync();
+ StartCreationFlowAsync(originPage: parameters[1]);
}
}
- public void StartAppManagementFlow(string query)
+ public void StartAppManagementFlow(string query = null)
{
Orchestrator.FlowPages = [_mainPageViewModel];
_mainPageViewModel.StartAppManagementFlow(query);
diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs
index 84737e727b..cab9f290f6 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs
@@ -3,9 +3,6 @@
using System;
using System.Collections.ObjectModel;
-using System.Configuration;
-using System.Globalization;
-using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -18,10 +15,9 @@
using DevHome.Common.Services;
using DevHome.SetupFlow.Models.Environments;
using DevHome.SetupFlow.Services;
-using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Dispatching;
using Microsoft.Windows.DevHome.SDK;
using Serilog;
-using WinUIEx;
namespace DevHome.SetupFlow.ViewModels;
@@ -29,7 +25,7 @@ public partial class SetupTargetViewModel : SetupPageViewModelBase
{
private readonly ILogger _log = Log.ForContext("SourceContext", nameof(SetupTargetViewModel));
- private readonly WindowEx _windowEx;
+ private readonly DispatcherQueue _dispatcherQueue;
private const string SortByDisplayName = "DisplayName";
@@ -87,7 +83,7 @@ public SetupTargetViewModel(
SetupFlowOrchestrator orchestrator,
IComputeSystemManager computeSystemManager,
ComputeSystemViewModelFactory computeSystemViewModelFactory,
- WindowEx windowEx)
+ DispatcherQueue dispatcherQueue)
: base(stringResource, orchestrator)
{
// Setup initial state for page.
@@ -111,7 +107,7 @@ public SetupTargetViewModel(
// Add AdvancedCollectionView to make filtering and sorting the list of ComputeSystemsListViewModels easier.
ComputeSystemsCollectionView = new AdvancedCollectionView(_computeSystemViewModelList, true);
- _windowEx = windowEx;
+ _dispatcherQueue = dispatcherQueue;
_computeSystemViewModelFactory = computeSystemViewModelFactory;
ComputeSystemManagerObj = computeSystemManager;
_setupFlowViewModel = setupFlowModel;
@@ -410,7 +406,7 @@ await Parallel.ForEachAsync(curListViewModel.ComputeSystems, async (computeSyste
}
});
- await _windowEx.DispatcherQueue.EnqueueAsync(async () =>
+ await _dispatcherQueue.EnqueueAsync(async () =>
{
foreach (var computeSystem in curListViewModel.ComputeSystems)
{
@@ -426,7 +422,7 @@ await _windowEx.DispatcherQueue.EnqueueAsync(async () =>
computeSystem,
curListViewModel.Provider,
packageFullName,
- _windowEx);
+ _dispatcherQueue);
// Don't show environments that aren't in a state to configure
if (!ShouldShowCard(card.CardState))
@@ -494,7 +490,7 @@ public void CallToActionButton()
return;
}
- Orchestrator.NavigateToOutsideFlow(KnownPageKeys.SetupFlow, "startCreationFlow");
+ Orchestrator.NavigateToOutsideFlow(KnownPageKeys.SetupFlow, "startCreationFlow;SetupEnvironmentPage");
}
private bool ShouldShowCard(ComputeSystemState state)
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml
index 78d3d3aa87..194daca8fb 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml
+++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml
@@ -170,7 +170,13 @@
into the command. I tried converting this to use a command on SelectionChanged and pass SelectedItems into the command.
I ran into issues because the viewmodel expects one repository at a time. Not a list.
Another issue is .SelectRange() needs to be called to "select" repos when the dialog is opened more than once. -->
-
+
@@ -276,12 +282,18 @@
+ x:Uid="NewDevDriveComboBox"
+ x:Name="NewDevDriveComboBox">
+
+
+
+
+
+
- /// Hold the clone location in case the user decides not to add a dev drive.
+ /// Gets or sets the clone location in case the user decides not to add a dev drive.
///
- private string _oldCloneLocation;
+ public string OldCloneLocation { get; set; }
public AddRepoDialog(
SetupFlowOrchestrator setupFlowOrchestrator,
@@ -160,90 +160,21 @@ private void RepositoriesListView_SelectionChanged(object sender, SelectionChang
///
private async void AddRepoContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
- if (AddRepoViewModel.CurrentPage == PageKind.AddViaUrl)
- {
- // Get the number of repos already selected to clone in a previous instance.
- // Used to figure out if the repo was added after the user logged into an account.
- var numberOfReposToCloneCount = AddRepoViewModel.EverythingToClone.Count;
-
- // If the user is logging in, the close button text will change.
- // Keep a copy of the original to revert when this button click is done.
- var originalCloseButtonText = AddRepoContentDialog.CloseButtonText;
-
- var deferral = args.GetDeferral();
- await AddRepoViewModel.AddRepositoryViaUri(AddRepoViewModel.Url, AddRepoViewModel.FolderPickerViewModel.CloneLocation);
-
- AddRepoContentDialog.CloseButtonText = originalCloseButtonText;
-
- // If the repo was not added.
- if (numberOfReposToCloneCount == AddRepoViewModel.EverythingToClone.Count)
- {
- AddRepoViewModel.ShouldEnablePrimaryButton = false;
- args.Cancel = true;
- deferral.Complete();
- return;
- }
+ var deferral = args.GetDeferral();
- deferral.Complete();
- }
- else if (AddRepoViewModel.CurrentPage == PageKind.AddViaAccount)
+ // Collect search inputs.
+ Dictionary searchInput = new();
+ foreach (var searchBox in ShowingSearchTermsGrid.Children)
{
- args.Cancel = true;
- var repositoryProviderName = (string)RepositoryProviderComboBox.SelectedItem;
- if (!string.IsNullOrEmpty(repositoryProviderName))
+ if (searchBox is AutoSuggestBox suggestBox)
{
- var originalCloseButtonText = AddRepoContentDialog.CloseButtonText;
-
- var deferral = args.GetDeferral();
- await AddRepoViewModel.ChangeToRepoPageAsync();
-
- AddRepoContentDialog.CloseButtonText = originalCloseButtonText;
-
- deferral.Complete();
+ searchInput.Add(suggestBox.Header as string, suggestBox.Text);
}
}
- else if (AddRepoViewModel.CurrentPage == PageKind.SearchFields)
- {
- args.Cancel = true;
- Dictionary searchInput = new();
- foreach (var searchBox in ShowingSearchTermsGrid.Children)
- {
- if (searchBox is AutoSuggestBox suggestBox)
- {
- searchInput.Add(suggestBox.Header as string, suggestBox.Text);
- }
- }
- // switching to the repo page causes repos to be queried.
- var deferral = args.GetDeferral();
- await AddRepoViewModel.ChangeToRepoPageAsync();
- AddRepoViewModel.SearchForRepos(searchInput);
- deferral.Complete();
- }
- }
+ args.Cancel = await AddRepoViewModel.PrimaryButtonClick(searchInput);
- ///
- /// Adds or removes the default dev drive. This dev drive will be made at the loading screen.
- ///
- private void MakeNewDevDriveCheckBox_Click(object sender, RoutedEventArgs e)
- {
- // Getting here means
- // 1. The user does not have any existing dev drives
- // 2. The user wants to clone to a new dev drive.
- // 3. The user un-checked this and does not want a new dev drive.
- var isChecked = (sender as CheckBox).IsChecked;
- if (isChecked.Value)
- {
- UpdateDevDriveInfo();
- }
- else
- {
- AddRepoViewModel.FolderPickerViewModel.CloneLocationAlias = string.Empty;
- AddRepoViewModel.FolderPickerViewModel.InDevDriveScenario = false;
- AddRepoViewModel.EditDevDriveViewModel.RemoveNewDevDrive();
- AddRepoViewModel.FolderPickerViewModel.EnableBrowseButton();
- AddRepoViewModel.FolderPickerViewModel.CloneLocation = _oldCloneLocation;
- }
+ deferral.Complete();
}
///
@@ -262,20 +193,6 @@ private void FilterTextBox_TextChanged(object sender, TextChangedEventArgs e)
}
}
- ///
- /// Update dialog to show Dev Drive information.
- ///
- public void UpdateDevDriveInfo()
- {
- AddRepoViewModel.EditDevDriveViewModel.MakeDefaultDevDrive();
- AddRepoViewModel.FolderPickerViewModel.DisableBrowseButton();
- _oldCloneLocation = AddRepoViewModel.FolderPickerViewModel.CloneLocation;
- AddRepoViewModel.FolderPickerViewModel.CloneLocation = AddRepoViewModel.EditDevDriveViewModel.GetDriveDisplayName();
- AddRepoViewModel.FolderPickerViewModel.CloneLocationAlias = AddRepoViewModel.EditDevDriveViewModel.GetDriveDisplayName(DevDriveDisplayNameKind.FormattedDriveLabelKind);
- AddRepoViewModel.FolderPickerViewModel.InDevDriveScenario = true;
- AddRepoViewModel.EditDevDriveViewModel.IsDevDriveCheckboxChecked = true;
- }
-
private void FilterSuggestions(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
sender.ItemsSource = _searchFieldsAndValues[sender.Header.ToString()].Where(x => x.Contains(sender.Text));
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementReviewView.xaml
index 5beffd8074..8d49950bef 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementReviewView.xaml
+++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementReviewView.xaml
@@ -60,7 +60,7 @@
-
+
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml
index 4d23463b32..e5e14aeb1e 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml
+++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml
@@ -195,7 +195,7 @@
+
+
+
@@ -46,10 +48,15 @@
+
+ Padding="-15">
+
+
+
+
-
-
-
-
-
+ 18
+ 18
@@ -135,48 +118,61 @@
VerticalScrollBarVisibility="Auto"
Grid.Column="0"
Grid.Row="2">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
32
-
+
@@ -87,19 +96,21 @@
BackgroundSource="{ThemeResource Setup_Banner_Back}"
OverlaySource="{ThemeResource Setup_Banner_Front}" />
-
+
-
-
+
+
+
-
+
@@ -163,15 +175,16 @@
-
+
@@ -179,78 +192,83 @@
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml
index 210a2d76c1..9ddd6af640 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml
+++ b/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml
@@ -68,12 +68,13 @@
-
+
@@ -103,6 +104,7 @@
-
+
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml.cs
index c0a90a74a3..350fdc2df8 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml.cs
@@ -72,7 +72,7 @@ private async Task AddRepoAsync()
if (_addRepoDialog.AddRepoViewModel.EditDevDriveViewModel.CanShowDevDriveUI && ViewModel.ShouldAutoCheckDevDriveCheckbox)
{
- _addRepoDialog.UpdateDevDriveInfo();
+ _addRepoDialog.AddRepoViewModel.UpdateDevDriveInfo();
}
_addRepoDialog.IsSecondaryButtonEnabled = true;
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml
index b270959109..de8a04aa87 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml
+++ b/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml
@@ -62,7 +62,8 @@
+ MinHeight="64" CornerRadius="{ThemeResource ControlCornerRadius}" Padding="24,0,24,7"
+ AutomationProperties.Name="{x:Bind ViewModel.ReviewPageExpanderDescription, Mode=OneWay}">
@@ -77,16 +78,18 @@
-
-
-
-
-
-
-
+
+
+
+
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml.cs
index 3a06ee8591..5977b44eb1 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml.cs
+++ b/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml.cs
@@ -6,6 +6,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
namespace DevHome.SetupFlow.Views;
@@ -31,4 +32,14 @@ private void ReviewNavigationView_Loaded(object sender, RoutedEventArgs e)
}
}
}
+
+ private void GenerateConfigInfoButton_Click(object sender, RoutedEventArgs e)
+ {
+ GenerateConfigInfoTeachingTip.IsOpen = !GenerateConfigInfoTeachingTip.IsOpen;
+ }
+
+ private void GenerateConfigInfoButton_PointerEntered(object sender, PointerRoutedEventArgs e)
+ {
+ GenerateConfigInfoButton_Click(sender, e);
+ }
}
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupTargetView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupTargetView.xaml
index 127d3aa992..84d21d9977 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupTargetView.xaml
+++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupTargetView.xaml
@@ -43,7 +43,7 @@
Style="{StaticResource HorizontalCardRootForSetupTargetFlow}"
IsTabStop="False">
+ Padding="0 -13 0 -10">
diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml
index 9ee24f1ce2..fb2f0c74e8 100644
--- a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml
+++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml
@@ -31,7 +31,7 @@
-
+
@@ -183,7 +183,7 @@
-
+
@@ -340,7 +340,7 @@
-
+
@@ -586,7 +586,7 @@
-
+
@@ -662,7 +662,7 @@
-
+
@@ -673,7 +673,7 @@
-
+
@@ -695,7 +695,7 @@
-
+
@@ -712,9 +712,10 @@
Tag="{Binding ElementName=ViewAllButton}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind InstallationNotes}"/>
-
Launch
+
+ Launch
+
Utilities
Utilities
+
+ An application for gaining deeper insights into your applications.
+
+
+ Project Ironsides
+
Edit and visualize Windows Registry files.
diff --git a/tools/Utilities/src/TelemetryEvents/UtilitiesLaunchEvent.cs b/tools/Utilities/src/TelemetryEvents/UtilitiesLaunchEvent.cs
index 37c99bc974..96a18a0bdc 100644
--- a/tools/Utilities/src/TelemetryEvents/UtilitiesLaunchEvent.cs
+++ b/tools/Utilities/src/TelemetryEvents/UtilitiesLaunchEvent.cs
@@ -12,22 +12,32 @@ namespace DevHome.Utilities.TelemetryEvents;
[EventData]
public class UtilitiesLaunchEvent : EventBase
{
- public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage;
+ public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage;
+
+ public enum Phase
+ {
+ Start,
+ Error,
+ Complete,
+ }
+
+ public string ActivityId { get; }
- public string UtilityName
- {
- get;
- }
-
- public bool LaunchedAsAdmin
- {
- get;
- }
+ public string UtilityName { get; }
- public UtilitiesLaunchEvent(string utilityName, bool launchedAsAdmin)
- {
+ public bool LaunchedAsAdmin { get; }
+
+ public Phase LaunchPhase { get; }
+
+ public string ErrorString { get; }
+
+ public UtilitiesLaunchEvent(Guid activityId, string utilityName, bool launchedAsAdmin, Phase phase, string errorString = "")
+ {
+ ActivityId = activityId.ToString();
UtilityName = utilityName;
- LaunchedAsAdmin = launchedAsAdmin;
+ LaunchedAsAdmin = launchedAsAdmin;
+ LaunchPhase = phase;
+ ErrorString = errorString;
}
public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings)
diff --git a/tools/Utilities/src/TelemetryEvents/UtilitiesMainPageViewModelEvent.cs b/tools/Utilities/src/TelemetryEvents/UtilitiesMainPageViewModelEvent.cs
new file mode 100644
index 0000000000..fb4b0413d0
--- /dev/null
+++ b/tools/Utilities/src/TelemetryEvents/UtilitiesMainPageViewModelEvent.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Diagnostics.Tracing;
+using DevHome.Telemetry;
+using Microsoft.Diagnostics.Telemetry;
+using Microsoft.Diagnostics.Telemetry.Internal;
+
+namespace DevHome.Utilities.TelemetryEvents;
+
+[EventData]
+public class UtilitiesMainPageViewModelEvent : EventBase
+{
+ public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage;
+
+ public UtilitiesMainPageViewModelEvent()
+ {
+ }
+
+ public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings)
+ {
+ // No sensitive strings to replace.
+ }
+}
diff --git a/tools/Utilities/src/ViewModels/UtilitiesMainPageViewModel.cs b/tools/Utilities/src/ViewModels/UtilitiesMainPageViewModel.cs
index 1f9354a0d5..393f2416e9 100644
--- a/tools/Utilities/src/ViewModels/UtilitiesMainPageViewModel.cs
+++ b/tools/Utilities/src/ViewModels/UtilitiesMainPageViewModel.cs
@@ -6,6 +6,8 @@
using System.IO;
using CommunityToolkit.Mvvm.ComponentModel;
using DevHome.Common.Services;
+using DevHome.Telemetry;
+using DevHome.Utilities.TelemetryEvents;
using Windows.ApplicationModel;
namespace DevHome.Utilities.ViewModels;
@@ -14,7 +16,7 @@ public partial class UtilitiesMainPageViewModel : ObservableObject
{
public ObservableCollection Utilities { get; set; }
- public UtilitiesMainPageViewModel()
+ public UtilitiesMainPageViewModel(IExperimentationService experimentationService)
{
var appExAliasAbsFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), $"Microsoft\\WindowsApps\\{Package.Current.Id.FamilyName}");
var stringResource = new StringResource("DevHome.Utilities.pri", "DevHome.Utilities/Resources");
@@ -28,6 +30,7 @@ public UtilitiesMainPageViewModel()
NavigateUri = "https://go.microsoft.com/fwlink/?Linkid=2271355",
ImageSource = Path.Combine(AppContext.BaseDirectory, "Assets\\HostsUILib", "Hosts.ico"),
SupportsLaunchAsAdmin = Microsoft.UI.Xaml.Visibility.Visible,
+ UtilityAutomationId = "DevHome.HostsFileEditor",
},
new(Path.Combine(appExAliasAbsFolderPath, "DevHome.RegistryPreviewApp.exe"))
{
@@ -36,6 +39,7 @@ public UtilitiesMainPageViewModel()
NavigateUri = "https://go.microsoft.com/fwlink/?Linkid=2270966",
ImageSource = Path.Combine(AppContext.BaseDirectory, "Assets\\RegistryPreview", "RegistryPreview.ico"),
SupportsLaunchAsAdmin = Microsoft.UI.Xaml.Visibility.Collapsed,
+ UtilityAutomationId = "DevHome.RegistryPreview",
},
new(Path.Combine(appExAliasAbsFolderPath, "DevHome.EnvironmentVariablesApp.exe"))
{
@@ -44,7 +48,18 @@ public UtilitiesMainPageViewModel()
NavigateUri = "https://go.microsoft.com/fwlink/?Linkid=2270894",
ImageSource = Path.Combine(AppContext.BaseDirectory, "Assets\\EnvironmentVariables", "EnvironmentVariables.ico"),
SupportsLaunchAsAdmin = Microsoft.UI.Xaml.Visibility.Visible,
+ UtilityAutomationId = "DevHome.EnvironmentVariables",
+ },
+ new(Path.Combine(appExAliasAbsFolderPath, "devhome.pi.exe"), experimentationService, "ProjectIronsidesExperiment")
+ {
+ Title = stringResource.GetLocalized("ProjectIronsidesTitle"),
+ Description = stringResource.GetLocalized("ProjectIronsidesDesc"),
+ NavigateUri = "https://go.microsoft.com/fwlink/?linkid=2275140",
+ ImageSource = Path.Combine(AppContext.BaseDirectory, "PI.ico"),
+ UtilityAutomationId = "DevHome.PI",
},
};
+
+ TelemetryFactory.Get().Log("Utilities_UtilitiesMainPage", LogLevel.Critical, new UtilitiesMainPageViewModelEvent());
}
}
diff --git a/tools/Utilities/src/ViewModels/UtilityViewModel.cs b/tools/Utilities/src/ViewModels/UtilityViewModel.cs
index 9f547e9ea5..3819deca5f 100644
--- a/tools/Utilities/src/ViewModels/UtilityViewModel.cs
+++ b/tools/Utilities/src/ViewModels/UtilityViewModel.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT License.
using System;
-using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -55,6 +54,9 @@ public bool Visible
[ObservableProperty]
private bool _launchAsAdmin;
+ [ObservableProperty]
+ private string _utilityAutomationId;
+
#nullable enable
public UtilityViewModel(string exeName, IExperimentationService? experimentationService = null, string? experimentalFeature = null)
{
@@ -68,7 +70,9 @@ public UtilityViewModel(string exeName, IExperimentationService? experimentation
private void Launch()
{
+ var activityId = Guid.NewGuid();
_log.Information($"Launching {_exeName}, as admin: {LaunchAsAdmin}");
+ TelemetryFactory.Get().Log("Utilities_UtilitiesLaunchEvent", LogLevel.Critical, new UtilitiesLaunchEvent(activityId, Title, LaunchAsAdmin, UtilitiesLaunchEvent.Phase.Start));
// We need to start the process with ShellExecute to run elevated
var processStartInfo = new ProcessStartInfo
@@ -85,14 +89,16 @@ private void Launch()
if (process is null)
{
_log.Error($"Failed to start process {_exeName}");
+ TelemetryFactory.Get().Log("Utilities_UtilitiesLaunchEvent", LogLevel.Critical, new UtilitiesLaunchEvent(activityId, Title, LaunchAsAdmin, UtilitiesLaunchEvent.Phase.Error));
throw new InvalidOperationException("Failed to start process");
}
}
catch (Exception ex)
{
_log.Error(ex, $"Failed to start process {_exeName}");
+ TelemetryFactory.Get().Log("Utilities_UtilitiesLaunchEvent", LogLevel.Critical, new UtilitiesLaunchEvent(activityId, Title, LaunchAsAdmin, UtilitiesLaunchEvent.Phase.Error, ex.ToString()));
}
- TelemetryFactory.Get().Log("Utilities_UtilitiesLaunchEvent", LogLevel.Critical, new UtilitiesLaunchEvent(Title, LaunchAsAdmin), null);
+ TelemetryFactory.Get().Log("Utilities_UtilitiesLaunchEvent", LogLevel.Critical, new UtilitiesLaunchEvent(activityId, Title, LaunchAsAdmin, UtilitiesLaunchEvent.Phase.Complete), null);
}
}
diff --git a/tools/Utilities/src/Views/UtilitiesMainPageView.xaml b/tools/Utilities/src/Views/UtilitiesMainPageView.xaml
index 23f2014aa4..f08da95042 100644
--- a/tools/Utilities/src/Views/UtilitiesMainPageView.xaml
+++ b/tools/Utilities/src/Views/UtilitiesMainPageView.xaml
@@ -31,8 +31,7 @@
MinRowSpacing="12"
MinColumnSpacing="12"
ItemsStretch="Fill"
- MaximumRowsOrColumns="3"
- ItemsJustification="Center" />
+ MaximumRowsOrColumns="3" />
diff --git a/tools/Utilities/src/Views/UtilityView.xaml b/tools/Utilities/src/Views/UtilityView.xaml
index d1e5c90dcd..d0a6950289 100644
--- a/tools/Utilities/src/Views/UtilityView.xaml
+++ b/tools/Utilities/src/Views/UtilityView.xaml
@@ -9,6 +9,8 @@
d:DesignHeight="300" d:DesignWidth="300">
+ Margin="15 0 0 5"
+ AutomationProperties.AutomationId="AdminToggleAutomationId"/>
+ Command="{x:Bind ViewModel.LaunchCommand}"
+ AutomationProperties.AutomationId="LaunchButtonAutomationId">
diff --git a/tools/scripts/CaptureDevHomeLogs.ps1 b/tools/scripts/CaptureDevHomeLogs.ps1
new file mode 100644
index 0000000000..882f1a7eb5
--- /dev/null
+++ b/tools/scripts/CaptureDevHomeLogs.ps1
@@ -0,0 +1,57 @@
+# Script to capture log files and create a zip file for upload to GitHub or log analysis.
+
+param(
+ [switch]$StopDevHome = $false
+)
+
+# List of supported package names for this tool. If a new Dev Home package is added
+# with the same log format, add it to this list.
+$devHomePackageNames = @(
+ 'Microsoft.Windows.DevHome_8wekyb3d8bbwe'
+ 'Microsoft.Windows.DevHome.Canary_8wekyb3d8bbwe'
+ 'Microsoft.Windows.DevHome.Dev_8wekyb3d8bbwe'
+ 'Microsoft.Windows.DevHomeAzureExtension_8wekyb3d8bbwe'
+ 'Microsoft.Windows.DevHomeAzureExtension.Canary_8wekyb3d8bbwe'
+ 'Microsoft.Windows.DevHomeAzureExtension.Dev_8wekyb3d8bbwe'
+ 'Microsoft.Windows.DevHomeGitHubExtension_8wekyb3d8bbwe'
+ 'Microsoft.Windows.DevHomeGitHubExtension.Canary_8wekyb3d8bbwe'
+ 'Microsoft.Windows.DevHomeGitHubExtension.Dev_8wekyb3d8bbwe'
+)
+
+# Terminate Dev Home processes if user passed StopDevHome flag.
+if ($StopDevHome)
+{
+ Write-Host "Stopping all Dev Home processes"
+ Get-Process *DevHome* -ErrorAction Continue | Stop-Process
+}
+
+$LogFolderName = $(Get-Date -Format yyyy-MM-dd-HHmmss_) + "DevHomeLogs"
+$TempRoot = [System.IO.Path]::GetTempPath()
+$TempFolder = Join-Path -Path $TempRoot -ChildPath $LogFolderName
+New-Item -ItemType Directory -Force -Path $TempFolder | Out-Null
+
+# Only collect logs from known Dev Home packages
+# Preserve folder structure to ensure no collisions in the archive.
+$AppDataPackagesPath = Join-Path -Path $env:LOCALAPPDATA -ChildPath Packages
+ForEach ($packageName in $devHomePackageNames)
+{
+ $packageFolderPath = Join-Path -Path $AppDataPackagesPath -ChildPath $packageName
+ if (!(Test-Path $packageFolderPath))
+ {
+ Continue
+ }
+
+ $targetRoot = Join-Path -Path $TempFolder -ChildPath $packageName
+ & robocopy /s $packageFolderPath $targetRoot *.dhlog | Out-Null
+}
+
+# Archive the folder and then remove the temp folder.
+$ArchiveFileName = $LogFolderName + ".zip"
+$ArchiveFilePath = Join-Path -Path $TempRoot -ChildPath $ArchiveFileName
+$ArchiveSourcePath = Join-Path -Path $TempFolder -ChildPath "\*"
+Compress-Archive -Path $ArchiveSourcePath -DestinationPath $ArchiveFilePath
+Remove-Item $TempFolder -Recurse
+
+Set-Clipboard -Value $ArchiveFilePath
+Write-Host "Logs are archived in the following location, which was copied to your clipboard:"
+Write-Host $ArchiveFilePath
diff --git a/uitest/Common/DevHomeApplication.cs b/uitest/Common/DevHomeApplication.cs
index 944faa176a..82c74a9fc0 100644
--- a/uitest/Common/DevHomeApplication.cs
+++ b/uitest/Common/DevHomeApplication.cs
@@ -38,7 +38,9 @@ public sealed class DevHomeApplication
private WindowsElement SettingsNavigationItem => _devHomeSession.Driver.FindElementByAccessibilityId("SettingsItem");
- private WindowsElement ExtensionsNavigationItem => _devHomeSession.Driver.FindElementByAccessibilityId("Extensions");
+ private WindowsElement ExtensionsNavigationItem => _devHomeSession.Driver.FindElementByAccessibilityId("Extensions");
+
+ private WindowsElement UtilitiesNavigationItem => _devHomeSession.Driver.FindElementByAccessibilityId("DevHome.Utilities");
private DevHomeApplication()
{
@@ -101,6 +103,13 @@ public ExtensionsPage NavigateToExtensionsPage()
ExtensionsNavigationItem.Click();
return new(_devHomeSession.Driver);
}
+
+ public UtilitiesPage NavigateToUtilitiesPage()
+ {
+ Trace.WriteLine("Navigating to Utilities");
+ UtilitiesNavigationItem.Click();
+ return new(_devHomeSession.Driver);
+ }
///
/// Start Dev Home application
diff --git a/uitest/DevHome.UITest.csproj b/uitest/DevHome.UITest.csproj
index 4d36904914..1c0fd0ffa9 100644
--- a/uitest/DevHome.UITest.csproj
+++ b/uitest/DevHome.UITest.csproj
@@ -11,10 +11,22 @@
resources.pri
$(MSBuildProjectDirectory)\Test.runsettings
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/uitest/NativeMethods.json b/uitest/NativeMethods.json
new file mode 100644
index 0000000000..1175df48bf
--- /dev/null
+++ b/uitest/NativeMethods.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://aka.ms/CsWin32.schema.json",
+ "wideCharOnly": true
+}
\ No newline at end of file
diff --git a/uitest/NativeMethods.txt b/uitest/NativeMethods.txt
new file mode 100644
index 0000000000..bb03830eab
--- /dev/null
+++ b/uitest/NativeMethods.txt
@@ -0,0 +1 @@
+OpenProcessToken
\ No newline at end of file
diff --git a/uitest/Pages/UtilitiesPage.cs b/uitest/Pages/UtilitiesPage.cs
new file mode 100644
index 0000000000..a7ae2170d4
--- /dev/null
+++ b/uitest/Pages/UtilitiesPage.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Security.Principal;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.Win32.SafeHandles;
+using OpenQA.Selenium.Appium.Windows;
+using Windows.Win32;
+
+namespace DevHome.UITest.Pages;
+
+public class UtilitiesPage : ApplicationPage
+{
+ public UtilitiesPage(WindowsDriver driver)
+ : base(driver)
+ {
+ }
+
+ public void LaunchAndVerifyUtility(string utilityName, bool launchAsAdmin = false)
+ {
+ Trace.WriteLine($"Launching {utilityName} with admin: {launchAsAdmin}");
+ WindowsElement utilityView = Driver.FindElementByAccessibilityId(utilityName);
+
+ if (launchAsAdmin)
+ {
+ var launchAsAdminToggle = utilityView.FindElementByAccessibilityId("AdminToggleAutomationId");
+ launchAsAdminToggle.Click();
+ }
+
+ var launchButton = utilityView.FindElementByAccessibilityId("LaunchButtonAutomationId");
+ launchButton.Click();
+
+ // Wait for the utility to launch
+ Thread.Sleep(2000);
+
+ var processes = Process.GetProcessesByName(utilityName);
+ Assert.IsTrue(processes.Length == 1, $"{utilityName} as admin:{launchAsAdmin} did not launch");
+
+ if (launchAsAdmin)
+ {
+ try
+ {
+ var isAdmin = false;
+ SafeFileHandle processToken;
+ var result = PInvoke.OpenProcessToken(processes.First().SafeHandle, Windows.Win32.Security.TOKEN_ACCESS_MASK.TOKEN_QUERY, out processToken);
+ if (result != 0)
+ {
+ var identity = new WindowsIdentity(processToken.DangerousGetHandle());
+ isAdmin = identity?.Owner?.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid) ?? false;
+ }
+
+ Assert.IsTrue(isAdmin, $"{utilityName} did not launch as admin");
+ }
+ catch (Win32Exception ex)
+ {
+ Assert.Fail($"Failed to check if {utilityName} launched as admin: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/uitest/UtilitiesTest.cs b/uitest/UtilitiesTest.cs
new file mode 100644
index 0000000000..cb3dba450f
--- /dev/null
+++ b/uitest/UtilitiesTest.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Diagnostics;
+using DevHome.UITest.Common;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace DevHome.Tests.UITest;
+
+[TestClass]
+public class UtilitiesTest : DevHomeTestBase
+{
+ [DataTestMethod]
+ [DataRow("DevHome.HostsFileEditor", false, DisplayName = "HostsUtility")]
+ [DataRow("DevHome.RegistryPreview", false, DisplayName = "RegistryPreview")]
+ [DataRow("DevHome.EnvironmentVariables", false, DisplayName = "EnvironmentVariables")]
+ [DataRow("DevHome.PI", false, DisplayName = "PI")]
+ [DataRow("DevHome.HostsFileEditor", true, DisplayName = "HostsUtility as Admin")]
+ [DataRow("DevHome.EnvironmentVariables", true, DisplayName = "EnvironmentVariables as Admin")]
+ [DataRow("DevHome.PI", true, DisplayName = "PI as Admin")]
+ public void LaunchUtilityTest(string utilityName, bool launchAsAdmin)
+ {
+ var utilities = Application.NavigateToUtilitiesPage();
+ utilities.LaunchAndVerifyUtility(utilityName, launchAsAdmin);
+ }
+
+ [TestCleanup]
+ public void UtilitiesTestCleanup()
+ {
+ // Kill all utilities after each test
+ string[] utilityNames = { "DevHome.HostsFileEditor", "DevHome.RegistryPreview", "DevHome.EnvironmentVariables", "DevHome.PI" };
+ foreach (var utilityName in utilityNames)
+ {
+ var processes = Process.GetProcessesByName(utilityName);
+
+ foreach (var process in processes)
+ {
+ process.Kill();
+ }
+ }
+ }
+}