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..07620f7594 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,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.EnvironmentVariable EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.Telemetry.Native", "telemetry\DevHome.Telemetry.Native\DevHome.Telemetry.Native.vcxproj", "{8EB52F7D-D216-49FF-BF16-DE06E4695950}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WindowsSandboxExtension", "WindowsSandboxExtension", "{4ACF917D-B2CC-4CF2-8EE1-0EBBB52A69F0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsSandboxExtension", "extensions\WindowsSandboxExtension\WindowsSandboxExtension.csproj", "{118E20E8-FD8A-40CF-83A5-F912B9187787}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|arm64 = Debug|arm64 @@ -614,6 +622,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 @@ -698,6 +718,18 @@ Global {8EB52F7D-D216-49FF-BF16-DE06E4695950}.Release|x64.Build.0 = Release|x64 {8EB52F7D-D216-49FF-BF16-DE06E4695950}.Release|x86.ActiveCfg = Release|Win32 {8EB52F7D-D216-49FF-BF16-DE06E4695950}.Release|x86.Build.0 = Release|Win32 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|arm64.ActiveCfg = Debug|arm64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|arm64.Build.0 = Debug|arm64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|x64.ActiveCfg = Debug|x64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|x64.Build.0 = Debug|x64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|x86.ActiveCfg = Debug|x86 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Debug|x86.Build.0 = Debug|x86 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|arm64.ActiveCfg = Release|arm64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|arm64.Build.0 = Release|arm64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|x64.ActiveCfg = Release|x64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|x64.Build.0 = Release|x64 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|x86.ActiveCfg = Release|x86 + {118E20E8-FD8A-40CF-83A5-F912B9187787}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -745,6 +777,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 +786,14 @@ 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} + {4ACF917D-B2CC-4CF2-8EE1-0EBBB52A69F0} = {DCAF188B-60C3-4EDB-8049-BAA927FBCD7D} + {118E20E8-FD8A-40CF-83A5-F912B9187787} = {4ACF917D-B2CC-4CF2-8EE1-0EBBB52A69F0} 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/build/azure-pipelines.yml b/build/azure-pipelines.yml index 7e4a24542c..e55c277094 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' @@ -241,46 +241,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 +393,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 +440,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: 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..aac684e5ac 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(false)); } 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/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..6f37578819 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) + private 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/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/Services/AdaptiveCardRenderingService.cs b/common/Services/AdaptiveCardRenderingService.cs index 3e712f476d..e5fa079cf4 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; } @@ -122,7 +122,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/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/WindowsSandboxExtension/Assets/windows-sandbox-icon.png b/extensions/WindowsSandboxExtension/Assets/windows-sandbox-icon.png new file mode 100644 index 0000000000..73ca5be9d2 Binary files /dev/null and b/extensions/WindowsSandboxExtension/Assets/windows-sandbox-icon.png differ diff --git a/extensions/WindowsSandboxExtension/Assets/windows-sandbox-thumbnail.jpg b/extensions/WindowsSandboxExtension/Assets/windows-sandbox-thumbnail.jpg new file mode 100644 index 0000000000..6569fdd746 Binary files /dev/null and b/extensions/WindowsSandboxExtension/Assets/windows-sandbox-thumbnail.jpg differ diff --git a/extensions/WindowsSandboxExtension/Constants.cs b/extensions/WindowsSandboxExtension/Constants.cs new file mode 100644 index 0000000000..167c5dfa8d --- /dev/null +++ b/extensions/WindowsSandboxExtension/Constants.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WindowsSandboxExtension; + +internal sealed class Constants +{ + public const string WindowsSandboxExe = "WindowsSandbox.exe"; + public const string ProviderDisplayName = "Windows Sandbox"; + public const string ProviderId = "Microsoft.WindowsSandbox"; + public const string Thumbnail = "ms-appx:///Assets/windows-sandbox-thumbnail.jpg"; + + // We use different icon locations for different builds. Note these are ms-resource URIs, but are used by Dev Home to load the providers icon. + // from the extension package. Extensions that implement the IComputeSystemProvider interface must provide a provider icon in this format. + // Dev Home will use SHLoadIndirectString (https://learn.microsoft.com/windows/win32/api/shlwapi/nf-shlwapi-shloadindirectstring) to load the + // location of the icon from the extension package.Once it gets this location, it will load the icon from the path and display it in the UI. + // Icons should be located in an extension resource.pri file which is generated at build time. + // See the MakePri.exe documentation for how you can view what is in the resource.pri file, so you can find the location of your icon. + // https://learn.microsoft.com/windows/uwp/app-resources/makepri-exe-command-options. (use MakePri.exe in a VS Developer Command Prompt or + // Powershell window) +#if CANARY_BUILD + public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome.Canary/Files/Assets/windows-sandbox-icon.png"; +#elif STABLE_BUILD + public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome/Files/Assets/windows-sandbox-icon.png"; +#else + public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome.Dev/Files/Assets/windows-sandbox-icon.png"; +#endif +} diff --git a/extensions/WindowsSandboxExtension/Helpers/DependencyChecker.cs b/extensions/WindowsSandboxExtension/Helpers/DependencyChecker.cs new file mode 100644 index 0000000000..cb76ef75c6 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Helpers/DependencyChecker.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Management; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; +using Windows.Management.Deployment; +using WinRT; + +namespace WindowsSandboxExtension.Helpers; + +internal sealed class DependencyChecker +{ + private const string OptionalComponentName = "Containers-DisposableClientVM"; + private const string PackageFamilyName = "MicrosoftWindows.WindowsSandbox_cw5n1h2txyewy"; + + public static bool IsOptionalComponentEnabled() + { + var searcher = new ManagementObjectSearcher($"SELECT InstallState FROM Win32_OptionalFeature WHERE Name = '{OptionalComponentName}'"); + var collection = searcher.Get(); + + foreach (ManagementObject instance in collection) + { + if (instance["InstallState"] != null) + { + var state = Convert.ToInt32(instance.GetPropertyValue("InstallState"), CultureInfo.InvariantCulture); + + // 1 means the feature is enabled + return state == 1; + } + } + + // Return false if the feature is not found + return false; + } + + public static bool IsNewWindowsSandboxExtensionInstalled() + { + PackageManager packageManager = new PackageManager(); + + var securityId = WindowsIdentity.GetCurrent().Owner?.ToString(); + var packages = packageManager.FindPackagesForUser(securityId, PackageFamilyName); + + return packages.Any(); + } +} diff --git a/extensions/WindowsSandboxExtension/Helpers/Logging.cs b/extensions/WindowsSandboxExtension/Helpers/Logging.cs new file mode 100644 index 0000000000..8821497401 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Helpers/Logging.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Storage; + +namespace WindowsSandboxExtension.Helpers; + +public class Logging +{ + public static readonly string LogExtension = ".dhlog"; + + public static readonly string LogFolderName = "Logs"; + + public static readonly string DefaultLogFileName = "WindowsSandbox"; + + private static readonly Lazy _logFolderRoot = new(() => Path.Combine(ApplicationData.Current.TemporaryFolder.Path, LogFolderName)); + + public static readonly string LogFolderRoot = _logFolderRoot.Value; +} diff --git a/extensions/WindowsSandboxExtension/Helpers/Resources.cs b/extensions/WindowsSandboxExtension/Helpers/Resources.cs new file mode 100644 index 0000000000..315be43064 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Helpers/Resources.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Windows.ApplicationModel.Resources; +using Serilog; + +namespace WindowsSandboxExtension.Helpers; + +internal sealed class Resources +{ + private static ResourceLoader? _resourceLoader; + + public static string GetResource(string identifier, ILogger? log = null) + { + try + { + if (_resourceLoader == null) + { + var path = ResourceLoader.GetDefaultResourceFilePath(); + _resourceLoader = new ResourceLoader(path); + } + + return _resourceLoader.GetString(identifier); + } + catch (Exception ex) + { + log?.Error(ex, $"Failed loading resource: {identifier}"); + + // If we fail, load the original identifier so it is obvious which resource is missing. + return identifier; + } + } +} diff --git a/extensions/WindowsSandboxExtension/NativeMethods.txt b/extensions/WindowsSandboxExtension/NativeMethods.txt new file mode 100644 index 0000000000..0caa4406b1 --- /dev/null +++ b/extensions/WindowsSandboxExtension/NativeMethods.txt @@ -0,0 +1 @@ +SetForegroundWindow \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Program.cs b/extensions/WindowsSandboxExtension/Program.cs new file mode 100644 index 0000000000..54f5b83f6f --- /dev/null +++ b/extensions/WindowsSandboxExtension/Program.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Windows.AppLifecycle; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.ApplicationModel.Activation; +using WindowsSandboxExtension.Helpers; +using WindowsSandboxExtension.Providers; +using WinRT; + +namespace WindowsSandboxExtension; + +public sealed class Program +{ + public static IHost? Host + { + get; set; + } + + [MTAThread] + public static void Main([System.Runtime.InteropServices.WindowsRuntime.ReadOnlyArray] string[] args) + { + // Set up Logging + Environment.SetEnvironmentVariable("DEVHOME_LOGS_ROOT", Path.Join(Helpers.Logging.LogFolderRoot, "WindowsSandbox")); + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings_WindowsSandbox.json") + .Build(); + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); + + Log.Information($"Launched with args: {string.Join(' ', args.ToArray())}"); + + // Force the app to be single instanced. + // Get or register the main instance. + var mainInstance = AppInstance.FindOrRegisterForKey("mainInstance"); + var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + if (!mainInstance.IsCurrent) + { + Log.Information($"Not main instance, redirecting."); + mainInstance.RedirectActivationToAsync(activationArgs).AsTask().Wait(); + Log.CloseAndFlush(); + return; + } + + // Build the host container before handling activation. + BuildHostContainer(); + + // Register for activation redirection. + AppInstance.GetCurrent().Activated += AppActivationRedirected; + + if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer") + { + HandleCOMServerActivation(); + } + else + { + Log.Warning("Not being launched as a ComServer... exiting."); + } + + Log.CloseAndFlush(); + } + + private static void AppActivationRedirected(object? sender, Microsoft.Windows.AppLifecycle.AppActivationArguments activationArgs) + { + Log.Information($"Redirected with kind: {activationArgs.Kind}"); + + // Handle COM server. + if (activationArgs.Kind == ExtendedActivationKind.Launch) + { + var launchActivatedEventArgs = activationArgs.Data as ILaunchActivatedEventArgs; + var args = launchActivatedEventArgs?.Arguments.Split(); + + if (args?.Length > 0 && args[1] == "-RegisterProcessAsComServer") + { + Log.Information($"Activation COM Registration Redirect: {string.Join(' ', args.ToList())}"); + HandleCOMServerActivation(); + } + } + } + + /// + /// Creates the host container for the Windows Sandbox Extension application. This can be used to register + /// services and other dependencies throughout the application. + /// + private static void BuildHostContainer() + { + Host = Microsoft.Extensions.Hosting.Host. + CreateDefaultBuilder(). + UseContentRoot(AppContext.BaseDirectory). + UseDefaultServiceProvider((context, options) => + { + options.ValidateOnBuild = true; + }). + ConfigureServices((context, services) => + { + // Services + services.AddHttpClient(); + services.AddSingleton(); + services.AddSingleton(); + }). + Build(); + } + + private static void HandleCOMServerActivation() + { + Debug.Assert(Host != null, "Host is null"); + Log.Information($"Activating COM Server"); + + // Register and run COM server. + // This could be called by either of the COM registrations, we will do them all to avoid deadlock and bind all on the extension's lifetime. + using var extensionServer = new Microsoft.Windows.DevHome.SDK.ExtensionServer(); + var windowsSandboxExtension = Host.Services.GetRequiredService(typeof(WindowsSandboxExtension)).As(); + + // We are instantiating extension instance once above, and returning it every time the callback in RegisterExtension below is called. + // This makes sure that only one instance of the extension is alive, which is returned every time the host asks for the IExtension object. + // If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate. + extensionServer.RegisterExtension(() => windowsSandboxExtension, true); + + // This will make the main thread wait until the event is signalled by the extension class. + // Since we have single instance of the extension object, we exit as soon as it is disposed. + windowsSandboxExtension.ExtensionDisposedEvent.WaitOne(); + Log.Information($"Extension is disposed."); + } +} diff --git a/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-arm64.pubxml b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-arm64.pubxml new file mode 100644 index 0000000000..b2f119ed57 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-arm64.pubxml @@ -0,0 +1,17 @@ + + + + + FileSystem + arm64 + win-arm64 + true + False + False + True + True + True + + \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x64.pubxml b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x64.pubxml new file mode 100644 index 0000000000..0c9801b313 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x64.pubxml @@ -0,0 +1,17 @@ + + + + + FileSystem + x64 + win-x64 + true + False + False + True + True + True + + \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x86.pubxml b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x86.pubxml new file mode 100644 index 0000000000..84817dc699 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Properties/PublishProfiles/win-x86.pubxml @@ -0,0 +1,17 @@ + + + + + FileSystem + x86 + win-x86 + true + False + False + True + True + True + + \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Properties/launchSettings.json b/extensions/WindowsSandboxExtension/Properties/launchSettings.json new file mode 100644 index 0000000000..20a32d9c87 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "WindowsSandboxExtension": { + "commandName": "Project", + "commandLineArgs": "-RegisterProcessAsComServer" + } + } +} \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Providers/WindowsSandboxComputeSystem.cs b/extensions/WindowsSandboxExtension/Providers/WindowsSandboxComputeSystem.cs new file mode 100644 index 0000000000..b80dd7303c --- /dev/null +++ b/extensions/WindowsSandboxExtension/Providers/WindowsSandboxComputeSystem.cs @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Diagnostics; +using System.Runtime.InteropServices.WindowsRuntime; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Foundation; +using Windows.Storage; +using Windows.Storage.Streams; +using Windows.Win32; +using Windows.Win32.Foundation; +using WindowsSandboxExtension.Helpers; +using WindowsSandboxExtension.Telemetry; + +using Timer = System.Timers.Timer; + +namespace WindowsSandboxExtension.Providers; + +public class WindowsSandboxComputeSystem : IComputeSystem, IDisposable +{ + private const long ByteSizeGB = 1024 * 1024 * 1024; + private const long DefaultMemorySizeInBytes = 4 * ByteSizeGB; + private const long DefaultStorageSizeInBytes = 80 * ByteSizeGB; + + private readonly Guid _id = Guid.NewGuid(); + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WindowsSandboxProvider)); + private readonly object _windowsSandboxStartLock = new(); + + private Process? _windowsSandboxExeProcess; + private ComputeSystemState _state = ComputeSystemState.Stopped; + + private ComputeSystemState State + { + get => _state; + + set + { + _state = value; + StateChanged?.Invoke(this, value); + } + } + + public string AssociatedProviderId => Constants.ProviderId; + + public string DisplayName => Resources.GetResource("WindowsSandboxDisplayName", _log); + + public string Id => _id.ToString(); + + public string SupplementalDisplayName => string.Empty; + + public ComputeSystemOperations SupportedOperations => State switch + { + ComputeSystemState.Running => ComputeSystemOperations.Terminate, + _ => ComputeSystemOperations.None, + }; + + public IDeveloperId? AssociatedDeveloperId => null; + + public event TypedEventHandler? StateChanged; + + public IAsyncOperation GetComputeSystemThumbnailAsync(string options) + { + return Task.Run(async () => + { + var uri = new Uri(Constants.Thumbnail); + var storageFile = await StorageFile.GetFileFromApplicationUriAsync(uri); + var randomAccessStream = await storageFile.OpenReadAsync(); + + // Convert the stream to a byte array + var bytes = new byte[randomAccessStream.Size]; + await randomAccessStream.ReadAsync(bytes.AsBuffer(), (uint)randomAccessStream.Size, InputStreamOptions.None); + return new ComputeSystemThumbnailResult(bytes); + }).AsAsyncOperation(); + } + + public IAsyncOperation> GetComputeSystemPropertiesAsync(string options) + { + return Task.Run(() => + { + var properties = new List + { + ComputeSystemProperty.Create(ComputeSystemPropertyKind.CpuCount, Environment.ProcessorCount), + ComputeSystemProperty.Create(ComputeSystemPropertyKind.AssignedMemorySizeInBytes, DefaultMemorySizeInBytes), + ComputeSystemProperty.Create(ComputeSystemPropertyKind.StorageSizeInBytes, DefaultStorageSizeInBytes), + }; + + return properties.AsEnumerable(); + }).AsAsyncOperation(); + } + + public IAsyncOperation GetStateAsync() + { + return Task.Run(() => + { + return new ComputeSystemStateResult(State); + }).AsAsyncOperation(); + } + + public IAsyncOperation ConnectAsync(string options) + { + return Task.Run(() => + { + try + { + // Windows Sandbox is not running. + if (_windowsSandboxExeProcess == null || _windowsSandboxExeProcess.HasExited) + { + State = ComputeSystemState.Starting; + + var system32Path = Environment.GetFolderPath(Environment.SpecialFolder.System); + var windowsSandboxExePath = Path.Combine(system32Path, Constants.WindowsSandboxExe); + + _windowsSandboxExeProcess = new(); + _windowsSandboxExeProcess.StartInfo.FileName = windowsSandboxExePath; + _windowsSandboxExeProcess.EnableRaisingEvents = true; + _windowsSandboxExeProcess.Exited += WindowsSandboxProcessExited; + + State = ComputeSystemState.Running; + TraceLogging.StartingWindowsSandbox(); + + _windowsSandboxExeProcess.Start(); + + PInvoke.SetForegroundWindow((HWND)_windowsSandboxExeProcess.MainWindowHandle); + } + + BringWindowsSandboxClientToForeground(); + + return new ComputeSystemOperationResult(); + } + catch (Exception ex) + { + State = ComputeSystemState.Unknown; + + _log.Error(ex, "Failed to start Windows Sandbox"); + TraceLogging.ExceptionThrown(ex); + + return new ComputeSystemOperationResult( + ex, + Resources.GetResource("WindowsSandboxFailedToStart", _log), + "Failed to start Windows Sandbox"); + } + }).AsAsyncOperation(); + } + + private void WindowsSandboxProcessExited(object? sender, EventArgs e) + { + State = ComputeSystemState.Stopped; + _windowsSandboxExeProcess?.Dispose(); + _windowsSandboxExeProcess = null; + } + + private Process? GetWindowsSandboxClientProcess() + { + return Process.GetProcessesByName("WindowsSandboxClient").FirstOrDefault(); + } + + private void BringWindowsSandboxClientToForeground() + { + var clientProcess = GetWindowsSandboxClientProcess(); + var windowHandle = clientProcess?.MainWindowHandle ?? IntPtr.Zero; + + PInvoke.SetForegroundWindow((HWND)windowHandle); + } + + public IAsyncOperation TerminateAsync(string options) + { + return Task.Run(() => + { + try + { + if (_windowsSandboxExeProcess == null || _windowsSandboxExeProcess.HasExited) + { + State = ComputeSystemState.Stopped; + return new ComputeSystemOperationResult(); + } + + GetWindowsSandboxClientProcess()?.Kill(); + _windowsSandboxExeProcess.Kill(); + + return new ComputeSystemOperationResult(); + } + catch (Exception ex) + { + State = ComputeSystemState.Unknown; + + _log.Error(ex, "Failed to terminate Windows Sandbox"); + TraceLogging.ExceptionThrown(ex); + + return new ComputeSystemOperationResult( + ex, + Resources.GetResource("FailedToTerminateWindowsSandbox", _log), + "Failed to terminate Windows Sandbox"); + } + }).AsAsyncOperation(); + } + + private IAsyncOperation NotImplemntedComputeSystemOperation() + { + NotImplementedException ex = new("This operation is not implemented."); + ComputeSystemOperationResult result = new(ex, Resources.GetResource("NotImplemented", _log), ex.Message); + + return Task.FromResult(result).AsAsyncOperation(); + } + + public IApplyConfigurationOperation? CreateApplyConfigurationOperation(string configuration) + { + return null; + } + + public IAsyncOperation CreateSnapshotAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation DeleteAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation DeleteSnapshotAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation ModifyPropertiesAsync(string inputJson) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation PauseAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation RestartAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation ResumeAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation RevertSnapshotAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation SaveAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation ShutDownAsync(string options) => NotImplemntedComputeSystemOperation(); + + public IAsyncOperation StartAsync(string options) => NotImplemntedComputeSystemOperation(); + + public void Dispose() + { + _windowsSandboxExeProcess?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/extensions/WindowsSandboxExtension/Providers/WindowsSandboxProvider.cs b/extensions/WindowsSandboxExtension/Providers/WindowsSandboxProvider.cs new file mode 100644 index 0000000000..fcf662b925 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Providers/WindowsSandboxProvider.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Foundation; +using WindowsSandboxExtension.Helpers; + +namespace WindowsSandboxExtension.Providers; + +internal sealed class WindowsSandboxProvider : IComputeSystemProvider +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WindowsSandboxProvider)); + + public string DisplayName => Constants.ProviderDisplayName; + + public Uri Icon => new(Constants.ExtensionIcon); + + public string Id => Constants.ProviderId; + + public ComputeSystemProviderOperations SupportedOperations => ComputeSystemProviderOperations.None; + + public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForComputeSystem(IComputeSystem computeSystem, ComputeSystemAdaptiveCardKind sessionKind) + { + return new ComputeSystemAdaptiveCardResult( + new NotImplementedException(), + Resources.GetResource("NotImplemented", _log), + "Create Windows Sandbox compute system is not implemented."); + } + + public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForDeveloperId(IDeveloperId developerId, ComputeSystemAdaptiveCardKind sessionKind) + { + return new ComputeSystemAdaptiveCardResult( + new NotImplementedException(), + Resources.GetResource("NotImplemented", _log), + "Developer Id Adaptive Card session is not implmented for Windows Sandbox."); + } + + public ICreateComputeSystemOperation? CreateCreateComputeSystemOperation(IDeveloperId developerId, string inputJson) + { + return null; + } + + public IAsyncOperation GetComputeSystemsAsync(IDeveloperId developerId) + { + return Task.Run(() => + { + List list = new(); + list.Add(new WindowsSandboxComputeSystem()); + + return new ComputeSystemsResult(list); + }).AsAsyncOperation(); + } +} diff --git a/extensions/WindowsSandboxExtension/Strings/en-US/Resources.resw b/extensions/WindowsSandboxExtension/Strings/en-US/Resources.resw new file mode 100644 index 0000000000..c968db3346 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Strings/en-US/Resources.resw @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to terminate Windows Sandbox + {Locked="Windows Sandbox"} Failed to terminate display message. + + + This operation is not yet implemented for Windows Sandbox + + + Windows Sandbox Environment + {Locked="Windows Sandbox"} The diplay name used for the default Windows Sandbox compute system. + + + Failed to start Windows Sandbox + {Locked="Windows Sandbox"} Failed to start display message. + + \ No newline at end of file diff --git a/extensions/WindowsSandboxExtension/Telemetry/TelemetryEventSource.cs b/extensions/WindowsSandboxExtension/Telemetry/TelemetryEventSource.cs new file mode 100644 index 0000000000..40a98a0a48 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Telemetry/TelemetryEventSource.cs @@ -0,0 +1,343 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#if TELEMETRYEVENTSOURCE_USE_NUGET +using Microsoft.Diagnostics.Tracing; +#else +using System.Diagnostics.Tracing; +#endif +using System; +using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute; + +#pragma warning disable 3021 // 'type' does not need a CLSCompliant attribute + +namespace Microsoft.Diagnostics.Telemetry +{ + /// + /// + /// An EventSource with extra methods and constants commonly used in Microsoft's + /// TraceLogging-based ETW. This class inherits from EventSource, and is exactly + /// the same as EventSource except that it always enables + /// EtwSelfDescribingEventFormat and never uses traits. It also provides several + /// constants and helpers commonly used by Microsoft code. + /// + /// + /// Different versions of this class use different provider traits. The provider + /// traits in this class are empty. As a result, providers using this class will + /// not join any ETW Provider Groups and will not be given any special treatment + /// by group-sensitive ETW listeners. + /// + /// + /// When including this class in your project, you may define the following + /// conditional-compilation symbols to adjust the default behaviors: + /// + /// + /// TELEMETRYEVENTSOURCE_USE_NUGET - use Microsoft.Diagnostics.Tracing instead + /// of System.Diagnostics.Tracing. + /// + /// + /// TELEMETRYEVENTSOURCE_PUBLIC - define TelemetryEventSource as public instead + /// of internal. + /// + /// +#if TELEMETRYEVENTSOURCE_PUBLIC + public +#else + internal +#endif + class TelemetryEventSource + : EventSource + { + /// + /// Keyword 0x0000100000000000 is reserved for future definition. Do + /// not use keyword 0x0000100000000000 in Microsoft-style ETW. + /// + public const EventKeywords Reserved44Keyword = (EventKeywords)0x0000100000000000; + + /// + /// Add TelemetryKeyword to eventSourceOptions.Keywords to indicate that + /// an event is for general-purpose telemetry. + /// This keyword should not be combined with MeasuresKeyword or + /// CriticalDataKeyword. + /// + public const EventKeywords TelemetryKeyword = (EventKeywords)0x0000200000000000; + + /// + /// Add MeasuresKeyword to eventSourceOptions.Keywords to indicate that + /// an event is for understanding measures and reporting scenarios. + /// This keyword should not be combined with TelemetryKeyword or + /// CriticalDataKeyword. + /// + public const EventKeywords MeasuresKeyword = (EventKeywords)0x0000400000000000; + + /// + /// Add CriticalDataKeyword to eventSourceOptions.Keywords to indicate that + /// an event powers user experiences or is critical to business intelligence. + /// This keyword should not be combined with TelemetryKeyword or + /// MeasuresKeyword. + /// + public const EventKeywords CriticalDataKeyword = (EventKeywords)0x0000800000000000; + + /// + /// Add CostDeferredLatency to eventSourceOptions.Tags to indicate that an event + /// should try to upload over free networks for a period of time before resorting + /// to upload over costed networks. + /// + public const EventTags CostDeferredLatency = (EventTags)0x040000; + + /// + /// Add CoreData to eventSourceOptions.Tags to indicate that an event + /// contains high priority "core data". + /// + public const EventTags CoreData = (EventTags)0x00080000; + + /// + /// Add InjectXToken to eventSourceOptions.Tags to indicate that an XBOX + /// identity token should be injected into the event before the event is + /// uploaded. + /// + public const EventTags InjectXToken = (EventTags)0x00100000; + + /// + /// Add RealtimeLatency to eventSourceOptions.Tags to indicate that an event + /// should be transmitted in real time (via any available connection). + /// + public const EventTags RealtimeLatency = (EventTags)0x0200000; + + /// + /// Add NormalLatency to eventSourceOptions.Tags to indicate that an event + /// should be transmitted via the preferred connection based on device policy. + /// + public const EventTags NormalLatency = (EventTags)0x0400000; + + /// + /// Add CriticalPersistence to eventSourceOptions.Tags to indicate that an + /// event should be deleted last when low on spool space. + /// + public const EventTags CriticalPersistence = (EventTags)0x0800000; + + /// + /// Add NormalPersistence to eventSourceOptions.Tags to indicate that an event + /// should be deleted first when low on spool space. + /// + public const EventTags NormalPersistence = (EventTags)0x1000000; + + /// + /// Add DropPii to eventSourceOptions.Tags to indicate that an event contains + /// PII and should be anonymized by the telemetry client. If this tag is + /// present, PartA fields that might allow identification or cross-event + /// correlation will be removed from the event. + /// + public const EventTags DropPii = (EventTags)0x02000000; + + /// + /// Add HashPii to eventSourceOptions.Tags to indicate that an event contains + /// PII and should be anonymized by the telemetry client. If this tag is + /// present, PartA fields that might allow identification or cross-event + /// correlation will be hashed (obfuscated). + /// + public const EventTags HashPii = (EventTags)0x04000000; + + /// + /// Add MarkPii to eventSourceOptions.Tags to indicate that an event contains + /// PII but may be uploaded as-is. If this tag is present, the event will be + /// marked so that it will only appear on the private stream. + /// + public const EventTags MarkPii = (EventTags)0x08000000; + + /// + /// Add DropPiiField to eventFieldAttribute.Tags to indicate that a field + /// contains PII and should be dropped by the telemetry client. + /// + public const EventFieldTags DropPiiField = (EventFieldTags)0x04000000; + + /// + /// Add HashPiiField to eventFieldAttribute.Tags to indicate that a field + /// contains PII and should be hashed (obfuscated) prior to uploading. + /// + public const EventFieldTags HashPiiField = (EventFieldTags)0x08000000; + + /// + /// Constructs a new instance of the TelemetryEventSource class with the + /// specified name. Sets the EtwSelfDescribingEventFormat option. + /// + /// The name of the event source. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + public TelemetryEventSource( + string eventSourceName) + : base( + eventSourceName, + EventSourceSettings.EtwSelfDescribingEventFormat) + { + return; + } + + /// + /// For use by derived classes that set the eventSourceName via EventSourceAttribute. + /// Sets the EtwSelfDescribingEventFormat option. + /// + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + protected TelemetryEventSource() + : base( + EventSourceSettings.EtwSelfDescribingEventFormat) + { + return; + } + + /// + /// Constructs a new instance of the TelemetryEventSource class with the + /// specified name. Sets the EtwSelfDescribingEventFormat option. + /// + /// The name of the event source. + /// The parameter is not used. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "API compatibility")] + public TelemetryEventSource( + string eventSourceName, + TelemetryGroup telemetryGroup) + : base( + eventSourceName, + EventSourceSettings.EtwSelfDescribingEventFormat) + { + return; + } + + /// + /// Returns an instance of EventSourceOptions with the TelemetryKeyword set. + /// + /// Returns an instance of EventSourceOptions with the TelemetryKeyword set. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + public static EventSourceOptions TelemetryOptions() + { + return new EventSourceOptions { Keywords = TelemetryKeyword }; + } + + /// + /// Returns an instance of EventSourceOptions with the MeasuresKeyword set. + /// + /// Returns an instance of EventSourceOptions with the MeasuresKeyword set. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + public static EventSourceOptions MeasuresOptions() + { + return new EventSourceOptions { Keywords = MeasuresKeyword }; + } + } + + /// + /// + /// The PrivTags class defines privacy tags that can be used to specify the privacy + /// category of an event. Add a privacy tag as a field with name "PartA_PrivTags". + /// As a shortcut, you can use _1 as the field name, which will automatically be + /// expanded to "PartA_PrivTags" at runtime. + /// + /// + /// Multiple tags can be OR'ed together if necessary (rarely needed). + /// + /// + /// + /// Typical usage: + /// + /// es.Write("UsageEvent", new + /// { + /// _1 = PrivTags.ProductAndServiceUsage, + /// field1 = fieldValue1, + /// field2 = fieldValue2 + /// }); + /// + /// +#if TELEMETRYEVENTSOURCE_PUBLIC + [CLSCompliant(false)] + public +#else + internal +#endif + static class PrivTags + { + /// + public const Internal.PartA_PrivTags BrowsingHistory = Internal.PartA_PrivTags.BrowsingHistory; + + /// + public const Internal.PartA_PrivTags DeviceConnectivityAndConfiguration = Internal.PartA_PrivTags.DeviceConnectivityAndConfiguration; + + /// + public const Internal.PartA_PrivTags InkingTypingAndSpeechUtterance = Internal.PartA_PrivTags.InkingTypingAndSpeechUtterance; + + /// + public const Internal.PartA_PrivTags ProductAndServicePerformance = Internal.PartA_PrivTags.ProductAndServicePerformance; + + /// + public const Internal.PartA_PrivTags ProductAndServiceUsage = Internal.PartA_PrivTags.ProductAndServiceUsage; + + /// + public const Internal.PartA_PrivTags SoftwareSetupAndInventory = Internal.PartA_PrivTags.SoftwareSetupAndInventory; + } + /// + /// Pass a TelemetryGroup value to the constructor of TelemetryEventSource + /// to control which telemetry group should be joined. + /// Note: has no effect in this version of TelemetryEventSource. + /// +#if TELEMETRYEVENTSOURCE_PUBLIC + public +#else + internal +#endif + enum TelemetryGroup + { + /// + /// The default group. Join this group to log normal, non-critical, non-coredata + /// events. + /// + MicrosoftTelemetry, + + /// + /// Join this group to log CriticalData, CoreData, or other specially approved + /// events. + /// + WindowsCoreTelemetry + } + +#pragma warning disable SA1403 // File may only contain a single namespace + namespace Internal +#pragma warning restore SA1403 // File may only contain a single namespace + { + /// + /// The complete list of privacy tags supported for events. + /// Multiple tags can be OR'ed together if an event belongs in multiple + /// categories. + /// Note that the PartA_PrivTags enum should not be used directly. + /// Instead, use values from the PrivTags class. + /// + [Flags] +#if TELEMETRYEVENTSOURCE_PUBLIC + [CLSCompliant(false)] + public +#else + internal +#endif + enum PartA_PrivTags + : ulong + { + /// + None = 0, + + /// + BrowsingHistory = 0x0000000000000002u, + + /// + DeviceConnectivityAndConfiguration = 0x0000000000000800u, + + /// + InkingTypingAndSpeechUtterance = 0x0000000000020000u, + + /// + ProductAndServicePerformance = 0x0000000001000000u, + + /// + ProductAndServiceUsage = 0x0000000002000000u, + + /// + SoftwareSetupAndInventory = 0x0000000080000000u, + } + } +} diff --git a/extensions/WindowsSandboxExtension/Telemetry/TraceLogging.cs b/extensions/WindowsSandboxExtension/Telemetry/TraceLogging.cs new file mode 100644 index 0000000000..e8a39e8b16 --- /dev/null +++ b/extensions/WindowsSandboxExtension/Telemetry/TraceLogging.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Diagnostics.Telemetry; + +namespace WindowsSandboxExtension.Telemetry; + +internal sealed class TraceLogging +{ + private const string ProviderName = "Microsoft.Windows.Containers.WindowsSandboxExtension"; + private const string StartingEventName = "StartingWindowsSandbox"; + private const string ExceptionThrownEventName = "ExceptionThrown"; + + private static readonly TelemetryEventSource EventSource = new(ProviderName); + + public static void StartingWindowsSandbox() + { + var options = TelemetryEventSource.MeasuresOptions(); + options.Level = EventLevel.Informational; + + EventSource.Write(StartingEventName, options); + } + + public static void ExceptionThrown(Exception exception) + { + var options = TelemetryEventSource.MeasuresOptions(); + options.Level = EventLevel.Error; + + EventSource.Write( + ExceptionThrownEventName, + options, + new + { + name = exception.GetType().Name, + stackTrace = exception.StackTrace, + innerName = exception.InnerException?.GetType().Name, + innerMessage = exception.InnerException?.Message, + innerStackTrace = exception.InnerException?.ToString(), + message = exception.Message, + }); + } +} diff --git a/extensions/WindowsSandboxExtension/WindowsSandboxExtension.cs b/extensions/WindowsSandboxExtension/WindowsSandboxExtension.cs new file mode 100644 index 0000000000..495dec4e7f --- /dev/null +++ b/extensions/WindowsSandboxExtension/WindowsSandboxExtension.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Runtime.InteropServices; +using Microsoft.Extensions.Hosting; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using WindowsSandboxExtension.Helpers; + +namespace WindowsSandboxExtension; + +[ComVisible(true)] +[Guid("6A52115B-083C-4FB1-85F4-BBE23289220E")] +[ComDefaultInterface(typeof(IExtension))] +internal sealed class WindowsSandboxExtension : IExtension, IDisposable +{ + private readonly IHost _host; + private readonly ILogger _logger; + private bool _disposed; + + public WindowsSandboxExtension(IHost host) + { + _host = host; + _logger = Log.ForContext("SourceContext", nameof(WindowsSandboxExtension)); + } + + public ManualResetEvent ExtensionDisposedEvent { get; } = new(false); + + public object? GetProvider(ProviderType providerType) + { + object? provider = null; + + try + { + switch (providerType) + { + case ProviderType.ComputeSystem: + + provider = GetComputeSystemProvider(); + break; + default: + _logger.Information($"Unsupported provider: {providerType}"); + break; + } + } + catch (Exception ex) + { + _logger.Error(ex, $"Failed to get provider for provider type {providerType}"); + } + + return provider; + } + + private object? GetComputeSystemProvider() + { + if (DependencyChecker.IsNewWindowsSandboxExtensionInstalled()) + { + _logger.Information("New Windows Sandbox appx package is installed."); + return null; + } + + if (!DependencyChecker.IsOptionalComponentEnabled()) + { + _logger.Information("Windows Sandbox optional component is not enabled."); + return null; + } + + return _host.Services.GetService(typeof(IComputeSystemProvider)); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + ExtensionDisposedEvent.Set(); + } + + _disposed = true; + } + } +} diff --git a/extensions/WindowsSandboxExtension/WindowsSandboxExtension.csproj b/extensions/WindowsSandboxExtension/WindowsSandboxExtension.csproj new file mode 100644 index 0000000000..4738e476b5 --- /dev/null +++ b/extensions/WindowsSandboxExtension/WindowsSandboxExtension.csproj @@ -0,0 +1,48 @@ + + + + + Exe + + + WinExe + + + + enable + enable + false + false + win-x86;win-x64;win-arm64 + WindowsSandboxExtension.Program + x86;x64;arm64 + $(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + Always + + + diff --git a/extensions/WindowsSandboxExtension/appsettings_WindowsSandbox.json b/extensions/WindowsSandboxExtension/appsettings_WindowsSandbox.json new file mode 100644 index 0000000000..c26d22a19c --- /dev/null +++ b/extensions/WindowsSandboxExtension/appsettings_WindowsSandbox.json @@ -0,0 +1,31 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Debug" ], + "MinimumLevel": "Debug", + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "restrictedToMinimumLevel": "Debug" + } + }, + { + "Name": "File", + "Args": { + "path": "%DEVHOME_LOGS_ROOT%\\WindowsSandbox.dhlog", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "restrictedToMinimumLevel": "Information", + "rollingInterval": "Day" + } + }, + { + "Name": "Debug" + } + ], + "Enrich": [ "FromLogContext" ], + "Properties": { + "SourceContext": "WindowsSandboxExtension" + } + } +} \ No newline at end of file diff --git a/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..b486092cf2 100644 --- a/settings/DevHome.Settings/Views/AboutPage.xaml +++ b/settings/DevHome.Settings/Views/AboutPage.xaml @@ -1,10 +1,11 @@ - @@ -30,13 +31,13 @@ - + 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..5b65c8758b 100644 --- a/settings/DevHome.Settings/Views/AccountsPage.xaml +++ b/settings/DevHome.Settings/Views/AccountsPage.xaml @@ -1,4 +1,4 @@ - @@ -14,9 +15,7 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/BarWindowHorizontal.xaml.cs b/tools/PI/DevHome.PI/BarWindowHorizontal.xaml.cs new file mode 100644 index 0000000000..f6fd62bd36 --- /dev/null +++ b/tools/PI/DevHome.PI/BarWindowHorizontal.xaml.cs @@ -0,0 +1,490 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using DevHome.Common.Extensions; +using DevHome.PI.Controls; +using DevHome.PI.Helpers; +using DevHome.PI.Models; +using DevHome.PI.Properties; +using DevHome.PI.ViewModels; +using Microsoft.UI.Input; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Windows.Foundation; +using Windows.UI.WindowManagement; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; +using WinRT.Interop; +using WinUIEx; +using static DevHome.PI.Helpers.WindowHelper; + +namespace DevHome.PI; + +public partial class BarWindowHorizontal : WindowEx +{ + private readonly Settings _settings = Settings.Default; + private readonly string _errorTitleText = CommonHelper.GetLocalizedString("ToolLaunchErrorTitle"); + private readonly string _errorMessageText = CommonHelper.GetLocalizedString("ToolLaunchErrorMessage"); + private readonly string _pinMenuItemText = CommonHelper.GetLocalizedString("PinMenuItemText"); + private readonly string _unpinMenuItemText = CommonHelper.GetLocalizedString("UnpinMenuItemText"); + private readonly BarWindowViewModel _viewModel; + private readonly FontIcon _pinIcon = new() { Glyph = "\uE718" }; + private readonly FontIcon _unpinIcon = new() { Glyph = "\uE77A" }; + + private ExternalTool? _selectedExternalTool; + private INotifyCollectionChanged? _externalTools; + private bool isClosing; + + // Constants that control window sizes + private const int _WindowPositionOffsetY = 30; + private const int _FloatingHorizontalBarHeight = 70; + private const int _DefaultExpandedViewTop = 30; + private const int _DefaultExpandedViewLeft = 100; + private const int _RightSideGap = 10; + + private RECT _monitorRect; + + private RestoreState _restoreState = new() + { + Top = _DefaultExpandedViewTop, + Left = _DefaultExpandedViewLeft, + BarOrientation = Orientation.Horizontal, + IsLargePanelVisible = true, + }; + + private const int _UnsnapGap = 9; + private double _dpiScale = 1.0; + + internal HWND ThisHwnd { get; private set; } + + internal ClipboardMonitor? ClipboardMonitor { get; private set; } + + public Microsoft.UI.Dispatching.DispatcherQueue TheDispatcher + { + get; set; + } + + public BarWindowHorizontal(BarWindowViewModel model) + { + _viewModel = model; + + TheDispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); + + InitializeComponent(); + _viewModel.PropertyChanged += ViewModel_PropertyChanged; + + ExtendsContentIntoTitleBar = true; + AppWindow.TitleBar.IconShowOptions = IconShowOptions.HideIconAndSystemMenu; + + // Get the default window size. We grab this in the constructor, as + // we may try and set our window size before our main panel gets + // loaded (and we call SetDefaultPosition) + var settingSize = Settings.Default.ExpandedLargeSize; + _restoreState.Height = settingSize.Height; + _restoreState.Width = settingSize.Width; + } + + private void ViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(BarWindowViewModel.ShowingExpandedContent)) + { + if (_viewModel.ShowingExpandedContent) + { + ExpandLargeContentPanel(); + } + else + { + CollapseLargeContentPanel(); + } + } + } + + private void MainPanel_Loaded(object sender, RoutedEventArgs e) + { + ThisHwnd = (HWND)WindowNative.GetWindowHandle(this); + + _settings.PropertyChanged += Settings_PropertyChanged; + + if (_settings.IsClipboardMonitoringEnabled) + { + ClipboardMonitor.Instance.Start(); + } + + InitializeExternalTools(); + + // Apply the user's chosen theme setting. + ThemeName t = ThemeName.Themes.First(t => t.Name == _settings.CurrentTheme); + SetRequestedTheme(t.Theme); + + // Calculate the DPI scale. + var dpiWindow = HwndExtensions.GetDpiForWindow(ThisHwnd); + _dpiScale = dpiWindow / 96.0; + + SetDefaultPosition(); + + SetRegionsForTitleBar(); + } + + public void SetRegionsForTitleBar() + { + var scaleAdjustment = MainPanel.XamlRoot.RasterizationScale; + + RightPaddingColumn.Width = new GridLength(AppWindow.TitleBar.RightInset / scaleAdjustment); + LeftPaddingColumn.Width = new GridLength(AppWindow.TitleBar.LeftInset / scaleAdjustment); + + var transform = ChromeButtonPanel.TransformToVisual(null); + var bounds = transform.TransformBounds(new Rect(0, 0, ChromeButtonPanel.ActualWidth, ChromeButtonPanel.ActualHeight)); + Windows.Graphics.RectInt32 chromeButtonsRect = WindowHelper.GetRect(bounds, scaleAdjustment); + + var rectArray = new Windows.Graphics.RectInt32[] { chromeButtonsRect }; + + InputNonClientPointerSource nonClientInputSrc = + InputNonClientPointerSource.GetForWindowId(AppWindow.Id); + nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray); + } + + private void InitializeExternalTools() + { + ExternalToolsHelper.Instance.Init(); + + ExternalToolsMenu.Items.Clear(); + foreach (var item in ExternalToolsHelper.Instance.AllExternalTools) + { + CreateMenuItemFromTool(item); + } + + // We have to cast to INotifyCollectionChanged explicitly because the CollectionChanged + // event in ReadOnlyObservableCollection is protected. + _externalTools = ExternalToolsHelper.Instance.AllExternalTools; + _externalTools.CollectionChanged += ExternalTools_CollectionChanged; + } + + private void ExternalTools_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems is not null) + { + foreach (ExternalTool item in e.NewItems) + { + CreateMenuItemFromTool(item); + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems is not null) + { + foreach (ExternalTool item in e.OldItems) + { + var menuItem = ExternalToolsMenu.Items.FirstOrDefault(i => ((ExternalTool)i.Tag).ID == item.ID); + if (menuItem is not null) + { + ExternalToolsMenu.Items.Remove(menuItem); + } + } + } + } + + private void CreateMenuItemFromTool(ExternalTool item) + { + var imageIcon = new ImageIcon + { + Source = item.ToolIcon, + }; + + var menuItem = new MenuFlyoutItem + { + Text = item.Name, + Tag = item, + Icon = item.MenuIcon, + }; + menuItem.Click += ExternalToolMenuItem_Click; + menuItem.RightTapped += ExternalToolMenuItem_RightTapped; + ExternalToolsMenu.Items.Add(menuItem); + + // You can't databind to MenuFlyoutItem, and the ExternalTool icon image is generated asynchronously, + // so we'll handle the PropertyChanged event in code, so we can update the icon when it gets set. + // https://github.com/microsoft/microsoft-ui-xaml/issues/1087 + item.PropertyChanged += ExternalToolItem_PropertyChanged; + } + + private void ExternalToolItem_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (sender is ExternalTool item && string.Equals(e.PropertyName, nameof(ExternalTool.MenuIcon), StringComparison.Ordinal)) + { + var menuItem = (MenuFlyoutItem?)ExternalToolsMenu.Items.FirstOrDefault(i => ((ExternalTool)i.Tag).ID == item.ID); + if (menuItem is not null) + { + menuItem.Icon = item.MenuIcon; + } + } + } + + private void ManageExternalToolsButton_Click(object sender, RoutedEventArgs e) + { + ExpandLargeContentPanel(); + ExpandedViewControl.NavigateToSettings(typeof(AdditionalToolsViewModel).FullName!); + } + + private void ExternalToolsMenu_Opening(object sender, object e) + { + // Cancel the opening of the menu if there are no items. + if (sender is MenuFlyout flyout && flyout?.Items?.Count == 0) + { + flyout.Hide(); + } + } + + private void ExternalToolMenuItem_RightTapped(object sender, RightTappedRoutedEventArgs e) + { + var menuItem = sender as MenuFlyoutItem; + if (menuItem is not null) + { + _selectedExternalTool = (ExternalTool)menuItem.Tag; + if (_selectedExternalTool.IsPinned) + { + PinUnpinMenuItem.Text = _unpinMenuItemText; + PinUnpinMenuItem.Icon = _unpinIcon; + } + else + { + PinUnpinMenuItem.Text = _pinMenuItemText; + PinUnpinMenuItem.Icon = _pinIcon; + } + + ToolContextMenu.ShowAt(menuItem, e.GetPosition(menuItem)); + } + } + + private void ExternalToolMenuItem_Click(object sender, RoutedEventArgs e) + { + if (sender is MenuFlyoutItem clickedMenuItem) + { + if (clickedMenuItem.Tag is ExternalTool tool) + { + InvokeTool(tool, TargetAppData.Instance.TargetProcess?.Id, TargetAppData.Instance.HWnd); + } + } + } + + private void ExternalToolButton_Click(object sender, RoutedEventArgs e) + { + if (sender is Button clickedButton) + { + if (clickedButton.Tag is ExternalTool tool) + { + InvokeTool(tool, TargetAppData.Instance.TargetProcess?.Id, TargetAppData.Instance.HWnd); + } + } + } + + private void InvokeTool(ExternalTool tool, int? id, HWND hWnd) + { + var process = tool.Invoke(id, hWnd); + if (process is null) + { + // A ContentDialog only renders in the space its parent occupies. Since the parent is a narrow + // bar, the dialog doesn't have enough space to render. So, we'll use MessageBox to display errors. + PInvoke.MessageBox( + ThisHwnd, + string.Format(CultureInfo.CurrentCulture, _errorMessageText, tool.Executable), + _errorTitleText, + MESSAGEBOX_STYLE.MB_ICONERROR); + } + } + + private void ExternalToolButton_PointerPressed(object sender, PointerRoutedEventArgs e) + { + if (sender is Button clickedButton) + { + _selectedExternalTool = (ExternalTool)clickedButton.Tag; + if (_selectedExternalTool.IsPinned) + { + PinUnpinMenuItem.Text = _unpinMenuItemText; + PinUnpinMenuItem.Icon = _unpinIcon; + } + else + { + PinUnpinMenuItem.Text = _pinMenuItemText; + PinUnpinMenuItem.Icon = _pinIcon; + } + } + } + + private void PinUnpinMenuItem_Click(object sender, RoutedEventArgs e) + { + // Pin or unpin the tool on the bar. + if (_selectedExternalTool is not null) + { + _selectedExternalTool.IsPinned = !_selectedExternalTool.IsPinned; + } + } + + private void UnregisterMenuItem_Click(object sender, RoutedEventArgs e) + { + if (_selectedExternalTool is not null) + { + ExternalToolsHelper.Instance.RemoveExternalTool(_selectedExternalTool); + _selectedExternalTool = null; + } + } + + private void SetDefaultPosition() + { + _monitorRect = GetMonitorRectForWindow(ThisHwnd); + var screenWidth = _monitorRect.right - _monitorRect.left; + this.Move( + (int)(((screenWidth - Width) / 2) * _dpiScale), + (int)(_WindowPositionOffsetY * _dpiScale)); + + // Get the saved settings for the ExpandedView size. On first run, this will be + // the default 0,0, so we'll set the size proportional to the monitor size. + // Subsequently, it will be whatever size the user sets. + var settingSize = Settings.Default.ExpandedLargeSize; + if (settingSize.Width == 0) + { + settingSize.Width = _monitorRect.Width * 2 / 3; + } + + if (settingSize.Height == 0) + { + settingSize.Height = _monitorRect.Height * 3 / 4; + } + + Settings.Default.ExpandedLargeSize = settingSize; + Settings.Default.Save(); + + // Set the default restore state for the ExpandedView size to the (adjusted) settings size. + _restoreState.Height = settingSize.Height; + _restoreState.Width = settingSize.Width; + } + + private void WindowEx_Closed(object sender, WindowEventArgs args) + { + ClipboardMonitor.Instance.Stop(); + + if (LargeContentPanel is not null && + LargeContentPanel.Visibility == Visibility.Visible && + this.WindowState != WindowState.Maximized) + { + CacheRestoreState(); + } + + if (!isClosing) + { + isClosing = true; + var barWindow = Application.Current.GetService().DBarWindow; + barWindow?.Close(); + isClosing = false; + } + } + + private void Settings_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Settings.IsClipboardMonitoringEnabled)) + { + if (_settings.IsClipboardMonitoringEnabled) + { + ClipboardMonitor.Instance.Start(); + } + else + { + ClipboardMonitor.Instance.Stop(); + } + } + } + + internal void SetRequestedTheme(ElementTheme theme) + { + if (Content is FrameworkElement rootElement) + { + rootElement.RequestedTheme = theme; + } + } + + private void CacheRestoreState() + { + _restoreState = new() + { + Left = AppWindow.Position.X, + Top = AppWindow.Position.Y, + Width = Width, + Height = Height, + IsLargePanelVisible = LargeContentPanel.Visibility == Visibility.Visible, + }; + + Settings.Default.ExpandedLargeSize = new System.Drawing.Size((int)Width, (int)Height); + Settings.Default.Save(); + } + + private void ExpandLargeContentPanel() + { + // We're expanding. + // Switch the bar to horizontal before we adjust the size. + LargeContentPanel.Visibility = Visibility.Visible; + MaxHeight = double.NaN; + + // If they expand to ExpandedView and they're not snapped, we can use the + // RestoreState size & position. + if (!_viewModel.IsSnapped) + { + this.MoveAndResize( + _restoreState.Left, _restoreState.Top, _restoreState.Width, _restoreState.Height); + } + else + { + // Conversely if they're snapped, the position is determined by the snap, + // and we potentially adjust the size to ensure it doesn't extend beyond the screen. + var availableWidth = _monitorRect.Width - Math.Abs(AppWindow.Position.X) - _RightSideGap; + if (availableWidth < _restoreState.Width) + { + _restoreState.Width = availableWidth; + } + + Width = _restoreState.Width; + + var availableHeight = _monitorRect.Height - Math.Abs(AppWindow.Position.Y); + if (availableHeight < _restoreState.Height) + { + _restoreState.Height = availableHeight; + } + + Height = _restoreState.Height; + } + } + + private void CollapseLargeContentPanel() + { + // Make sure we cache the state before switching to collapsed bar. + CacheRestoreState(); + LargeContentPanel.Visibility = Visibility.Collapsed; + MaxHeight = _FloatingHorizontalBarHeight; + } + + private void ProcessChooserButton_Click(object sender, RoutedEventArgs e) + { + _viewModel.ShowingExpandedContent = true; + ExpandedViewControl.NavigateTo(typeof(ProcessListPageViewModel)); + } + + internal void NavigateTo(Type viewModelType) + { + _viewModel.ShowingExpandedContent = true; + ExpandedViewControl.NavigateTo(viewModelType); + } + + internal Frame GetFrame() + { + return ExpandedViewControl.GetPageFrame(); + } + + private void MainPanel_SizeChanged(object sender, SizeChangedEventArgs e) + { + SetRegionsForTitleBar(); + } +} diff --git a/tools/PI/DevHome.PI/BarWindowVertical.xaml b/tools/PI/DevHome.PI/BarWindowVertical.xaml new file mode 100644 index 0000000000..dde57ec1b4 --- /dev/null +++ b/tools/PI/DevHome.PI/BarWindowVertical.xaml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/BarWindowVertical.xaml.cs b/tools/PI/DevHome.PI/BarWindowVertical.xaml.cs new file mode 100644 index 0000000000..b6c174ff18 --- /dev/null +++ b/tools/PI/DevHome.PI/BarWindowVertical.xaml.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using DevHome.Common.Extensions; +using DevHome.PI.Helpers; +using DevHome.PI.Models; +using DevHome.PI.Properties; +using DevHome.PI.ViewModels; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Windows.UI.WindowManagement; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Accessibility; +using Windows.Win32.UI.WindowsAndMessaging; +using WinRT.Interop; +using WinUIEx; +using static DevHome.PI.Helpers.WindowHelper; + +namespace DevHome.PI; + +public partial class BarWindowVertical : WindowEx +{ + private readonly Settings _settings = Settings.Default; + private readonly string _errorTitleText = CommonHelper.GetLocalizedString("ToolLaunchErrorTitle"); + private readonly string _errorMessageText = CommonHelper.GetLocalizedString("ToolLaunchErrorMessage"); + private readonly BarWindowViewModel _viewModel; + + private int _cursorPosX; // = 0; + private int _cursorPosY; // = 0; + private int _appWindowPosX; // = 0; + private int _appWindowPosY; // = 0; + private bool isWindowMoving; // = false; + private bool isClosing; + + private Button? _selectedExternalToolButton; + + internal HWND ThisHwnd { get; private set; } + + public Microsoft.UI.Dispatching.DispatcherQueue TheDispatcher + { + get; set; + } + + public BarWindowVertical(BarWindowViewModel model) + { + _viewModel = model; + + // The main constructor is used in all cases, including when there's no target window. + TheDispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); + + InitializeComponent(); + _viewModel.PropertyChanged += ViewModel_PropertyChanged; + } + + private void MainPanel_Loaded(object sender, RoutedEventArgs e) + { + ThisHwnd = (HWND)WindowNative.GetWindowHandle(this); + + // Apply the user's chosen theme setting. + ThemeName t = ThemeName.Themes.First(t => t.Name == _settings.CurrentTheme); + SetRequestedTheme(t.Theme); + + // Regardless of what is set in the XAML, our initial window width is too big. Setting this to 70 (same as the XAML file) + Width = 70; + } + + private void ViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (string.Equals(e.PropertyName, nameof(BarWindowViewModel.WindowPosition), StringComparison.OrdinalIgnoreCase)) + { + this.Move(_viewModel.WindowPosition.X, _viewModel.WindowPosition.Y); + } + } + + private void ExternalToolsMenu_Opening(object sender, object e) + { + // Cancel the opening of the menu if there are no items. + var flyout = sender as MenuFlyout; + if (flyout is not null && flyout.Items is not null && flyout.Items.Count == 0) + { + flyout.Hide(); + } + } + + private void ExternalToolButton_Click(object sender, RoutedEventArgs e) + { + if (sender is Button clickedButton) + { + if (clickedButton.Tag is ExternalTool tool) + { + var process = tool.Invoke(TargetAppData.Instance.TargetProcess?.Id, TargetAppData.Instance.HWnd); + + if (process == null) + { + // It appears ContentDialogs only render in the space it's parent occupies. Since the parent is a narrow + // bar, the dialog doesn't have enough space to render. So, we'll use MessageBox to display errors. + PInvoke.MessageBox( + ThisHwnd, + string.Format(CultureInfo.CurrentCulture, _errorMessageText, tool.Executable), + _errorTitleText, + MESSAGEBOX_STYLE.MB_ICONERROR); + } + } + } + } + + private void WindowEx_Closed(object sender, WindowEventArgs args) + { + if (!isClosing) + { + isClosing = true; + var barWindow = Application.Current.GetService().DBarWindow; + barWindow?.Close(); + isClosing = false; + } + } + + private void ExternalToolButton_PointerPressed(object sender, PointerRoutedEventArgs e) + { + _selectedExternalToolButton = (Button)sender; + } + + private void UnPinMenuItem_Click(object sender, RoutedEventArgs e) + { + // TODO Implement unpinning a tool from the bar, assuming we continue with the pinning feature. + } + + private void UnregisterMenuItem_Click(object sender, RoutedEventArgs e) + { + if (_selectedExternalToolButton is not null) + { + if (_selectedExternalToolButton.Tag is ExternalTool tool) + { + ExternalToolsHelper.Instance.RemoveExternalTool(tool); + } + } + } + + internal void SetRequestedTheme(ElementTheme theme) + { + if (Content is FrameworkElement rootElement) + { + rootElement.RequestedTheme = theme; + } + } + + private void CloseButton_Click(object sender, RoutedEventArgs e) + { + var primaryWindow = Application.Current.GetService(); + Close(); + primaryWindow.ClearBarWindow(); + } + + private void Window_PointerReleased(object sender, PointerRoutedEventArgs e) + { + ((UIElement)sender).ReleasePointerCaptures(); + isWindowMoving = false; + + // If we're occupying the same space as the target window, and we're not in medium/large mode, snap to the app + if (!_viewModel.IsSnapped && TargetAppData.Instance.HWnd != HWND.Null) + { + if (DoesWindow1CoverTheRightSideOfWindow2(ThisHwnd, TargetAppData.Instance.HWnd)) + { + _viewModel.IsSnapped = true; + } + } + } + + private void Window_PointerPressed(object sender, PointerRoutedEventArgs e) + { + var properties = e.GetCurrentPoint((UIElement)sender).Properties; + if (properties.IsLeftButtonPressed) + { + // Moving the window causes it to unsnap + if (_viewModel.IsSnapped) + { + _viewModel.IsSnapped = false; + } + + isWindowMoving = true; + ((UIElement)sender).CapturePointer(e.Pointer); + _appWindowPosX = AppWindow.Position.X; + _appWindowPosY = AppWindow.Position.Y; + PInvoke.GetCursorPos(out var pt); + _cursorPosX = pt.X; + _cursorPosY = pt.Y; + } + } + + private void Window_PointerMoved(object sender, PointerRoutedEventArgs e) + { + if (isWindowMoving) + { + var properties = e.GetCurrentPoint((UIElement)sender).Properties; + if (properties.IsLeftButtonPressed) + { + PInvoke.GetCursorPos(out var pt); + AppWindow.Move(new Windows.Graphics.PointInt32( + _appWindowPosX + (pt.X - _cursorPosX), _appWindowPosY + (pt.Y - _cursorPosY))); + } + + e.Handled = true; + } + } +} diff --git a/tools/PI/DevHome.PI/Contracts/ViewModels/INavigationAware.cs b/tools/PI/DevHome.PI/Contracts/ViewModels/INavigationAware.cs new file mode 100644 index 0000000000..ee1eb1c7ec --- /dev/null +++ b/tools/PI/DevHome.PI/Contracts/ViewModels/INavigationAware.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DevHome.PI.Contracts.ViewModels; + +// Similar to DevHome.Contracts.ViewModels.INavigationAware +public interface INavigationAware +{ + void OnNavigatedTo(object parameter); + + void OnNavigatedFrom(); +} diff --git a/tools/PI/DevHome.PI/Controls/AddToolControl.xaml b/tools/PI/DevHome.PI/Controls/AddToolControl.xaml new file mode 100644 index 0000000000..9a30aa8d1b --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/AddToolControl.xaml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml.cs b/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml.cs new file mode 100644 index 0000000000..5e7d995ebc --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.PI.Controls; + +public sealed partial class ExpandedViewControl : UserControl +{ + private readonly ExpandedViewControlViewModel viewModel = new(); + + public ExpandedViewControl() + { + InitializeComponent(); + viewModel.NavigationService.Frame = PageFrame; + } + + public Frame GetPageFrame() + { + return PageFrame; + } + + public void NavigateTo(Type viewModelType) + { + viewModel.NavigateTo(viewModelType); + } + + private void SettingsButton_Click(object sender, RoutedEventArgs e) + { + NavigateToSettings(typeof(SettingsPageViewModel).FullName!); + } + + public void NavigateToSettings(string viewModelType) + { + viewModel.NavigateToSettings(viewModelType); + } +} diff --git a/tools/PI/DevHome.PI/Controls/GlowButton.xaml b/tools/PI/DevHome.PI/Controls/GlowButton.xaml new file mode 100644 index 0000000000..ebc6dc6635 --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/GlowButton.xaml @@ -0,0 +1,15 @@ + + + + + diff --git a/tools/PI/DevHome.PI/Controls/GlowButton.xaml.cs b/tools/PI/DevHome.PI/Controls/GlowButton.xaml.cs new file mode 100644 index 0000000000..9a416ee19d --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/GlowButton.xaml.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Windows.Input; +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Hosting; + +namespace DevHome.PI.Controls; + +public sealed partial class GlowButton : UserControl +{ +#pragma warning disable CA2211 // Non-constant fields should not be visible +#pragma warning disable SA1401 // Fields should be private + public static DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(GlowButton), new PropertyMetadata(string.Empty)); +#pragma warning restore SA1401 // Fields should be private +#pragma warning restore CA2211 // Non-constant fields should not be visible + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public ICommand Command + { + get => (ICommand)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + public static readonly DependencyProperty CommandProperty = + DependencyProperty.Register("Command", typeof(ICommand), typeof(GlowButton), new PropertyMetadata(null)); + + private readonly Compositor compositor; + private readonly ContainerVisual buttonVisual; + private readonly ScalarKeyFrameAnimation opacityAnimation; + + public GlowButton() + { + InitializeComponent(); + compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; + buttonVisual = (ContainerVisual)ElementCompositionPreview.GetElementVisual(this); + + var result = RegisterPropertyChangedCallback(VisibilityProperty, VisibilityChanged); + opacityAnimation = CreatePulseAnimation("Opacity", 0.4f, 1.0f, TimeSpan.FromSeconds(5)); + } + + private ScalarKeyFrameAnimation CreatePulseAnimation(string property, float from, float to, TimeSpan duration) + { + var animation = compositor.CreateScalarKeyFrameAnimation(); + animation.InsertKeyFrame(0.0f, from); + animation.InsertKeyFrame(0.1f, to); + animation.InsertKeyFrame(0.3f, from); + animation.InsertKeyFrame(0.4f, to); + animation.InsertKeyFrame(0.6f, from); + animation.InsertKeyFrame(0.7f, to); + animation.InsertKeyFrame(0.8f, from); + animation.InsertKeyFrame(0.9f, to); + animation.Duration = duration; + animation.Target = property; + return animation; + } + + private void VisibilityChanged(DependencyObject sender, DependencyProperty dp) + { + if (Visibility == Visibility.Visible) + { + buttonVisual.StartAnimation("Opacity", opacityAnimation); + } + } +} diff --git a/tools/PI/DevHome.PI/Controls/ProcessSelectionButton.cs b/tools/PI/DevHome.PI/Controls/ProcessSelectionButton.cs new file mode 100644 index 0000000000..864e806ae3 --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/ProcessSelectionButton.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using DevHome.PI.Helpers; +using DevHome.PI.Models; +using Microsoft.UI.Input; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; + +namespace DevHome.PI.Controls; + +public class ProcessSelectionButton : Button +{ + public ProcessSelectionButton() + { + } + + protected override void OnPointerEntered(PointerRoutedEventArgs e) + { + base.OnPointerEntered(e); + + // When the mouse cursor is over the button, change to the default cursor. + ResetCursor(); + } + + protected override void OnPointerExited(PointerRoutedEventArgs e) + { + base.OnPointerExited(e); + + // When the mouse cursor leaves the button, change the cursor to the cross. + ChangeCursor(); + } + + protected override void OnPointerReleased(PointerRoutedEventArgs e) + { + base.OnPointerReleased(e); + + // Were we showing the select cursor? + if (ProtectedCursor == null) + { + return; + } + + Process? p; + Windows.Win32.Foundation.HWND hwnd; + + // Grab the window under the cursor and attach to that process. + WindowHelper.GetAppInfoUnderMouseCursor(out p, out hwnd); + if (p != null) + { + TargetAppData.Instance.SetNewAppData(p, hwnd); + } + + ResetCursor(); + } + + public void ChangeCursor() + { + ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.Cross); + } + + public void ResetCursor() + { + ProtectedCursor = null; + } +} diff --git a/tools/PI/DevHome.PI/Controls/ReadOnlyCheckBox.cs b/tools/PI/DevHome.PI/Controls/ReadOnlyCheckBox.cs new file mode 100644 index 0000000000..138f6cb62b --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/ReadOnlyCheckBox.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; + +namespace DevHome.PI.Controls; + +public class ReadOnlyCheckBox : CheckBox +{ + public static readonly DependencyProperty IsReadOnlyProperty = + DependencyProperty.Register( + nameof(IsReadOnly), + typeof(bool), + typeof(ReadOnlyCheckBox), + new PropertyMetadata(false)); + + public bool IsReadOnly + { + get => (bool)GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); + } + + protected override void OnToggle() + { + if (!IsReadOnly) + { + base.OnToggle(); + } + } + + protected override void OnPointerPressed(PointerRoutedEventArgs e) + { + if (!IsReadOnly) + { + base.OnPointerPressed(e); + } + } + + protected override void OnKeyDown(KeyRoutedEventArgs e) + { + if (!IsReadOnly) + { + base.OnKeyDown(e); + } + } +} diff --git a/tools/PI/DevHome.PI/DevHome.PI.csproj b/tools/PI/DevHome.PI/DevHome.PI.csproj new file mode 100644 index 0000000000..bba227fd8e --- /dev/null +++ b/tools/PI/DevHome.PI/DevHome.PI.csproj @@ -0,0 +1,207 @@ + + + + WinExe + DevHome.PI + app.manifest + x86;x64;ARM64 + $(Platform) + win-x86;win-x64;win-arm64 + Properties\PublishProfiles\win-$(Platform).pubxml + true + false + enable + 12.0 + DevHome.PI.Program + true + false + $(DefineConstants);DISABLE_XAML_GENERATED_MAIN + + + + + 10.0.19041.0 + PI.ico + PI.ico + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + $(DefaultXamlRuntime) + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Settings.settings + + + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + Always + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + MSBuild:Compile + + + MSBuild:Compile + + + + + 7 + + + 7 + + + 7 + + + 7 + + + 7 + + + 7 + + diff --git a/tools/PI/DevHome.PI/Helpers/CommonHelper.cs b/tools/PI/DevHome.PI/Helpers/CommonHelper.cs new file mode 100644 index 0000000000..4d22de4d5c --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/CommonHelper.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IO; +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using Microsoft.UI.Xaml; +using Serilog; +using Windows.ApplicationModel; + +namespace DevHome.PI.Helpers; + +internal sealed class CommonHelper +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(CommonHelper)); + + internal static string GetLocalizedString(string stringName, params object[] args) + { + var stringResource = new StringResource(); + var localizedString = stringResource.GetLocalized(stringName, args); + Debug.Assert(!string.IsNullOrEmpty(localizedString), stringName + " is empty. Check if " + stringName + " is present in Resources.resw."); + return localizedString; + } + + internal static void RunAsAdmin(int pid, string pageName) + { + var startInfo = new ProcessStartInfo(); + startInfo.WindowStyle = ProcessWindowStyle.Hidden; + + var aliasSubDirectoryPath = $"Microsoft\\WindowsApps\\{Package.Current.Id.FamilyName}\\devhome.pi.exe"; + var aliasPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), aliasSubDirectoryPath); + startInfo.FileName = aliasPath; + + // Pass pid and the page from where the admin request came from + startInfo.Arguments = $"--pid {pid} --expandWindow {pageName}"; + startInfo.UseShellExecute = true; + startInfo.Verb = "runas"; + + var process = new Process(); + process.StartInfo = startInfo; + + // Since a UAC prompt will be shown, we need to wait for the process to exit + // This can also be cancelled by the user which will result in an exception + try + { + process.Start(); + + // Close the primary window for this instance and exit + var primaryWindow = Application.Current.GetService(); + primaryWindow.Close(); + } + catch (Exception ex) + { + _log.Error(ex, "UAC to run PI as admin was denied"); + } + } +} diff --git a/tools/PI/DevHome.PI/Helpers/CommonInterop.cs b/tools/PI/DevHome.PI/Helpers/CommonInterop.cs new file mode 100644 index 0000000000..a3270345a2 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/CommonInterop.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace DevHome.PI.Helpers; + +internal sealed class CommonInterop +{ + // CSWin32 will not produce these methods for x86 so we need to define them here. + [DllImport("user32.dll", ExactSpelling = true, EntryPoint = "SetWindowLongPtrW", SetLastError = true)] + internal static extern nint SetWindowLongPtr64(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong); + + [DllImport("user32.dll", ExactSpelling = true, EntryPoint = "GetWindowLongPtrW", SetLastError = true)] + internal static extern nint GetWindowLongPtr64(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex); + + [DllImport("user32.dll", ExactSpelling = true, EntryPoint = "GetClassLongPtrW", SetLastError = true)] + internal static extern nint GetClassLongPtr64(HWND hWnd, GET_CLASS_LONG_INDEX nIndex); +} diff --git a/tools/PI/DevHome.PI/Helpers/DebugMonitor.cs b/tools/PI/DevHome.PI/Helpers/DebugMonitor.cs new file mode 100644 index 0000000000..18caa503ee --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/DebugMonitor.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Text; +using System.Threading; +using DevHome.PI.Models; + +namespace DevHome.PI.Helpers; + +public sealed class DebugMonitor : IDisposable +{ + private readonly Process targetProcess; + private readonly ObservableCollection output; + private readonly EventWaitHandle stopEvent; + private readonly string errorMessageText = CommonHelper.GetLocalizedString("WinLogsAlreadyRunningErrorMessage"); + + private const string MutexName = "DevHome.PI.DebugMonitor.SingletonMutex"; + private const string StopEventName = "DebugMonitorStopEvent"; + private const string DBWinBufferReadyName = "DBWIN_BUFFER_READY"; + private const string DBWinDataReadyName = "DBWIN_DATA_READY"; + private const string DBWinBufferName = "DBWIN_BUFFER"; + + private static readonly List IgnoreLogList = []; + + public DebugMonitor(Process targetProcess, ObservableCollection output) + { + this.targetProcess = targetProcess; + this.targetProcess.Exited += TargetProcess_Exited; + this.output = output; + + stopEvent = new EventWaitHandle(false, EventResetMode.AutoReset, StopEventName); + } + + public void Start() + { + stopEvent.Reset(); + + // Don't initiate if debugger is attached. It makes debugging very slow. + if (Debugger.IsAttached) + { + return; + } + + // Check for multiple instances. It is possible to have multiple debug monitors listen on OutputDebugString, + // but the message would be randomly distributed among all running instances. + using var singletonMutex = new Mutex(false, MutexName, out var createdNew); + if (!createdNew) + { + throw new InvalidOperationException($"Failed to get the {MutexName} mutex."); + } + + bool isNewBufferReadyEvent; + using var bufferReadyEvent = new EventWaitHandle(false, EventResetMode.AutoReset, DBWinBufferReadyName, out isNewBufferReadyEvent); + bool isNewDataReadyEvent; + using var dataReadyEvent = new EventWaitHandle(false, EventResetMode.AutoReset, DBWinDataReadyName, out isNewDataReadyEvent); + + // Don't initiate if there is an existing OutputDebugString monitor running + if (!isNewBufferReadyEvent || !isNewDataReadyEvent) + { + WinLogsEntry entry = new(DateTime.Now, WinLogCategory.Error, errorMessageText, WinLogsHelper.DebugOutputLogsName); + output.Add(entry); + return; + } + + using var memoryMappedFile = MemoryMappedFile.CreateNew(DBWinBufferName, 4096); + while (true) + { + bufferReadyEvent.Set(); + var waitResult = WaitHandle.WaitAny(new[] { stopEvent, dataReadyEvent }); + + // Stop listenting to OutputDebugString if the debugger is attached. It makes debugging very slow. + if (Debugger.IsAttached) + { + break; + } + + // Stop event is triggered. + if (waitResult == 0) + { + break; + } + + if (waitResult == 1) + { + var timeGenerated = DateTime.Now; + + // The first DWORD of the shared memory buffer contains + // the process ID of the client that sent the debug string. + using var viewStream = memoryMappedFile.CreateViewStream(0, 0, MemoryMappedFileAccess.Read); + using BinaryReader binaryReader = new BinaryReader(viewStream); + var pid = binaryReader.ReadUInt32(); + + if (pid == targetProcess.Id) + { + // Get the message from the stream. + var stringBuilder = new StringBuilder(); + while (binaryReader.PeekChar() != '\0') + { + stringBuilder.Append(binaryReader.ReadChar()); + } + + var entryMessage = stringBuilder.ToString(); + + if (!string.IsNullOrWhiteSpace(entryMessage)) + { + var hasIgnoreLog = false; + foreach (var ignoreLog in IgnoreLogList) + { + if (entryMessage.Contains(ignoreLog)) + { + hasIgnoreLog = true; + } + } + + if (!hasIgnoreLog) + { + WinLogsEntry entry = new(timeGenerated, WinLogCategory.Debug, entryMessage, WinLogsHelper.DebugOutputLogsName); + output.Add(entry); + } + } + } + } + } + } + + public void Stop() + { + if (!stopEvent.SafeWaitHandle.IsClosed) + { + stopEvent.Set(); + } + } + + public void Dispose() + { + stopEvent.Close(); + stopEvent.Dispose(); + + GC.SuppressFinalize(this); + } + + private void TargetProcess_Exited(object? sender, EventArgs e) + { + Stop(); + Dispose(); + } +} diff --git a/tools/PI/DevHome.PI/Helpers/ETWHelper.cs b/tools/PI/DevHome.PI/Helpers/ETWHelper.cs new file mode 100644 index 0000000000..0965a2dfb0 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/ETWHelper.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Security.Principal; +using DevHome.PI.Models; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.Tracing.Session; +using Serilog; + +namespace DevHome.PI.Helpers; + +internal sealed class ETWHelper : IDisposable +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(ETWHelper)); + private readonly Process targetProcess; + private readonly ObservableCollection output; + + private static readonly List ProviderList = ["1AFF6089-E863-4D36-BDFD-3581F07440BE" /*COM Tracelog*/]; + private TraceEventSession? session; + + // From: https://learn.microsoft.com/windows-server/identity/ad-ds/manage/understand-security-identifiers + private const string PerformanceLogUsersSid = "S-1-5-32-559"; + + public ETWHelper(Process targetProcess, ObservableCollection output) + { + this.targetProcess = targetProcess; + this.targetProcess.Exited += TargetProcess_Exited; + this.output = output; + } + + public void Start() + { + var isUserInPerformanceLogUsersGroup = IsUserInPerformanceLogUsersGroup(); + + if (!isUserInPerformanceLogUsersGroup) + { + isUserInPerformanceLogUsersGroup = TryAddUserToPerformanceLogUsersGroup(); + } + + if (isUserInPerformanceLogUsersGroup) + { + var sessionName = "DevHomePITrace" + Process.GetCurrentProcess().SessionId; + + // Stop and dispose any existing session + session = TraceEventSession.GetActiveSession(sessionName); + if (session is not null) + { + session.Stop(); + session.Dispose(); + } + + using (session = new TraceEventSession(sessionName)) + { + // Filter the provider events based on processId + var providerOptions = new TraceEventProviderOptions { ProcessIDFilter = [targetProcess.Id] }; + foreach (var provider in ProviderList) + { + session.EnableProvider(provider, TraceEventLevel.Always, options: providerOptions); + } + + session.Source.Dynamic.All += EventsHandler; + session.Source.UnhandledEvents += UnHandledEventsHandler; + session.Source.Process(); + } + } + } + + public void Stop() + { + session?.Stop(); + } + + public void Dispose() + { + session?.Dispose(); + GC.SuppressFinalize(this); + } + + private void EventsHandler(TraceEvent traceEvent) + { + ETWEventHandler(traceEvent.ProcessID, traceEvent.TimeStamp, traceEvent.Level, traceEvent.ToString(CultureInfo.CurrentCulture)); + } + + private void UnHandledEventsHandler(TraceEvent traceEvent) + { + var errorMessage = CommonHelper.GetLocalizedString("UnhandledTraceEventErrorMessage", traceEvent.Dump()); + ETWEventHandler(traceEvent.ProcessID, traceEvent.TimeStamp, traceEvent.Level, errorMessage); + } + + private void ETWEventHandler(int processId, DateTime timeStamp, TraceEventLevel level, string message) + { + if (processId != targetProcess.Id) + { + return; + } + + var category = WinLogsHelper.ConvertTraceEventLevelToWinLogCategory(level); + var entry = new WinLogsEntry(timeStamp, category, message, WinLogsHelper.EtwLogsName); + output.Add(entry); + } + + private void TargetProcess_Exited(object? sender, EventArgs e) + { + Stop(); + Dispose(); + } + + public static bool IsUserInPerformanceLogUsersGroup() + { + WindowsIdentity processUserIdentity = WindowsIdentity.GetCurrent(); + var isPerformanceLogSidFound = processUserIdentity.Groups?.Any(sid => sid.Value == PerformanceLogUsersSid); + return isPerformanceLogSidFound ?? false; + } + + public static bool TryAddUserToPerformanceLogUsersGroup() + { + WindowsIdentity processUserIdentity = WindowsIdentity.GetCurrent(); + var userName = processUserIdentity.Name; + if (userName is null) + { + _log.Error("Unable to get the current user name"); + return false; + } + + var startInfo = new ProcessStartInfo(); + startInfo.WindowStyle = ProcessWindowStyle.Hidden; + startInfo.FileName = Environment.SystemDirectory + "\\net.exe"; + + // Add the user to the Performance Log Users group + startInfo.Arguments = $"localgroup \"Performance Log Users\" {userName} /add"; + startInfo.UseShellExecute = true; + startInfo.Verb = "runas"; + + var process = new Process(); + process.StartInfo = startInfo; + + // Since a UAC prompt will be shown, we need to wait for the process to exit + // This can also be cancelled by the user which will result in an exception + try + { + process.Start(); + process.WaitForExit(); + + if (process.ExitCode == 0) + { + return true; + } + else + { + _log.Error("Unable to add the user to the Performance Log Users group"); + return false; + } + } + catch (Exception ex) + { + _log.Error(ex, "UAC to add the user to the Performance Log Users group was denied"); + } + + return false; + } +} diff --git a/tools/PI/DevHome.PI/Helpers/EnumStringConverter.cs b/tools/PI/DevHome.PI/Helpers/EnumStringConverter.cs new file mode 100644 index 0000000000..a89262890f --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/EnumStringConverter.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DevHome.PI.Helpers; + +#pragma warning disable SA1649 // File name should match first type name +public class EnumStringConverter : JsonConverter +#pragma warning restore SA1649 // File name should match first type name + where TEnum : struct +{ + public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + var enumString = reader.GetString(); + if (Enum.TryParse(enumString, ignoreCase: true, out TEnum result)) + { + return result; + } + + throw new JsonException($"Unable to convert \"{enumString}\" to enum type {typeof(TEnum)}."); + } + + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/tools/PI/DevHome.PI/Helpers/ErrorLookupHelper.cs b/tools/PI/DevHome.PI/Helpers/ErrorLookupHelper.cs new file mode 100644 index 0000000000..65ff1586e0 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/ErrorLookupHelper.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Data.Sqlite; +using Serilog; + +namespace DevHome.PI.Helpers; + +internal sealed class ErrorLookupHelper +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(ErrorLookupHelper)); + + private static SqliteConnectionStringBuilder? _dbConnectionString; + + private static SqliteConnectionStringBuilder DbConnectionString + { + get + { + if (_dbConnectionString == null) + { + _dbConnectionString = new SqliteConnectionStringBuilder(); + var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); + var dbPath = Path.Combine(path ?? string.Empty, "errors.db"); + + _dbConnectionString.DataSource = dbPath; + _dbConnectionString.Mode = SqliteOpenMode.ReadOnly; + } + + return _dbConnectionString; + } + } + + public static AppError[]? LookupError(int error) + { + try + { + using SqliteConnection connection = new(DbConnectionString.ConnectionString); + connection.Open(); + AppError[]? errors = LookupErrors(connection, error); + connection.Close(); + return errors; + } + catch + { + _log.Error("Failed to look up errors: {AppError}", error.ToString(CultureInfo.CurrentCulture)); + } + + return null; + } + + private static AppError[]? LookupErrors(SqliteConnection connection, int hresult) + { + // Look up a solution for an error. + SqliteCommand errorCommand = connection.CreateCommand(); + errorCommand.CommandText = @"select Name, Help from tblErrors WHERE code=@code"; + + SqliteParameter errorParam = new("@code", hresult.ToString(CultureInfo.CurrentCulture)); + errorCommand.Parameters.Add(errorParam); + SqliteDataReader errorReader = errorCommand.ExecuteReader(); + IList errors = []; + + while (errorReader.Read()) + { + AppError error = new() + { + Code = hresult, + Name = errorReader.GetString(0), + Help = errorReader.GetString(1), + }; + errors.Add(error); + } + + errorReader.Close(); + return errors.ToArray(); + } +} + +public class AppError +{ + public int Code { get; set; } + + public string Name { get; set; } = string.Empty; + + public string Help { get; set; } = string.Empty; +} diff --git a/tools/PI/DevHome.PI/Helpers/EventViewerHelper.cs b/tools/PI/DevHome.PI/Helpers/EventViewerHelper.cs new file mode 100644 index 0000000000..1ac44fe9c0 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/EventViewerHelper.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; +using DevHome.PI.Models; + +namespace DevHome.PI.Helpers; + +internal sealed class EventViewerHelper : IDisposable +{ + private readonly Process targetProcess; + private readonly ObservableCollection output; + private readonly EventLogWatcher? eventLogWatcher; + + public EventViewerHelper(Process targetProcess, ObservableCollection output) + { + this.targetProcess = targetProcess; + this.targetProcess.Exited += TargetProcess_Exited; + this.output = output; + + try + { + // Subscribe for Application events matching the processName. + var filterQuery = "*[System[Provider[@Name=\"" + targetProcess.ProcessName + "\"]]]"; + EventLogQuery subscriptionQuery = new("Application", PathType.LogName, filterQuery); + eventLogWatcher = new EventLogWatcher(subscriptionQuery); + eventLogWatcher.EventRecordWritten += new EventHandler(EventLogEventRead); + } + catch (EventLogReadingException) + { + var message = CommonHelper.GetLocalizedString("UnableToStartEventViewerErrorMessage"); + WinLogsEntry entry = new(DateTime.Now, WinLogCategory.Error, message, WinLogsHelper.EventViewerName); + output.Add(entry); + } + } + + public void Start() + { + if (eventLogWatcher is not null) + { + eventLogWatcher.Enabled = true; + } + } + + public void Stop() + { + if (eventLogWatcher is not null) + { + eventLogWatcher.Enabled = false; + } + } + + public void Dispose() + { + if (eventLogWatcher is not null) + { + eventLogWatcher.Dispose(); + } + + GC.SuppressFinalize(this); + } + + public void EventLogEventRead(object? obj, EventRecordWrittenEventArgs eventArg) + { + if (eventArg.EventRecord != null) + { + WinLogCategory category = WinLogsHelper.ConvertStandardEventLevelToWinLogCategory(eventArg.EventRecord.Level); + var message = eventArg.EventRecord.FormatDescription(); + WinLogsEntry entry = new(eventArg.EventRecord.TimeCreated, category, message, WinLogsHelper.EventViewerName); + output.Add(entry); + } + } + + private void TargetProcess_Exited(object? sender, EventArgs e) + { + Stop(); + Dispose(); + } +} diff --git a/tools/PI/DevHome.PI/Helpers/ExternalTool.cs b/tools/PI/DevHome.PI/Helpers/ExternalTool.cs new file mode 100644 index 0000000000..efcca40459 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/ExternalTool.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Text.Json.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; +using Serilog; +using Windows.Graphics.Imaging; +using Windows.Win32.Foundation; +using static DevHome.PI.Helpers.WindowHelper; + +namespace DevHome.PI.Helpers; + +public enum ExternalToolArgType +{ + None, + ProcessId, + Hwnd, +} + +// ExternalTool represents an imported tool +public partial class ExternalTool : ObservableObject +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(ExternalTool)); + + public string ID { get; private set; } + + public string Name { get; private set; } + + public string Executable { get; private set; } + + [JsonConverter(typeof(EnumStringConverter))] + public ExternalToolArgType ArgType { get; private set; } = ExternalToolArgType.None; + + public string ArgPrefix + { + get; private set; + } + + public string OtherArgs + { + get; private set; + } + + [ObservableProperty] + private bool _isPinned; + + // Note the additional "property:" syntax to ensure the JsonIgnore is propagated to the generated property. + [ObservableProperty] + [property: JsonIgnore] + private SoftwareBitmapSource? _toolIcon; + + [ObservableProperty] + [property: JsonIgnore] + private BitmapIcon? _menuIcon; + + [JsonIgnore] + private SoftwareBitmap? _softwareBitmap; + + public ExternalTool( + string name, + string executable, + ExternalToolArgType argtype, + string argprefix = "", + string otherArgs = "", + bool isPinned = false) + { + Name = name; + Executable = executable; + ArgType = argtype; + ArgPrefix = argprefix; + OtherArgs = otherArgs; + IsPinned = isPinned; + + ID = Guid.NewGuid().ToString(); + + if (!string.IsNullOrEmpty(executable)) + { + GetToolImage(); + GetMenuIcon(); + } + } + + private async void GetToolImage() + { + try + { + _softwareBitmap ??= GetSoftwareBitmapFromExecutable(Executable); + if (_softwareBitmap is not null) + { + ToolIcon = await GetSoftwareBitmapSourceFromSoftwareBitmap(_softwareBitmap); + } + } + catch (Exception ex) + { + _log.Error(ex, "Failed to get tool image"); + } + } + + private async void GetMenuIcon() + { + try + { + _softwareBitmap ??= GetSoftwareBitmapFromExecutable(Executable); + if (_softwareBitmap is not null) + { + var bitmapUri = await SaveSoftwareBitmapToTempFile(_softwareBitmap); + MenuIcon = new BitmapIcon + { + UriSource = bitmapUri, + ShowAsMonochrome = false, + }; + } + } + catch (Exception ex) + { + _log.Error(ex, "Failed to get menu icon"); + } + } + + internal string CreateFullCommandLine(int? pid, HWND? hwnd) + { + return "\"" + Executable + "\"" + CreateCommandLine(pid, hwnd); + } + + internal string CreateCommandLine(int? pid, HWND? hwnd) + { + var commandLine = $" {OtherArgs}"; + + if (ArgType == ExternalToolArgType.Hwnd && hwnd is not null) + { + commandLine = $" {ArgPrefix} {hwnd:D} {OtherArgs}"; + } + else if (ArgType == ExternalToolArgType.ProcessId && pid is not null) + { + commandLine = $" {ArgPrefix} {pid:D} {OtherArgs}"; + } + + return commandLine; + } + + internal virtual Process? Invoke(int? pid, HWND? hwnd) + { + try + { + var toolProcess = new Process(); + toolProcess.StartInfo.FileName = Executable; + toolProcess.StartInfo.Arguments = CreateCommandLine(pid, hwnd); + toolProcess.StartInfo.UseShellExecute = false; + toolProcess.StartInfo.RedirectStandardOutput = true; + toolProcess.Start(); + return toolProcess; + } + catch (Exception ex) + { + _log.Error(ex, "Tool launched failed"); + return null; + } + } +} diff --git a/tools/PI/DevHome.PI/Helpers/ExternalToolsHelper.cs b/tools/PI/DevHome.PI/Helpers/ExternalToolsHelper.cs new file mode 100644 index 0000000000..02de51d806 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/ExternalToolsHelper.cs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.IO; +using System.Reflection; +using System.Text.Json; +using DevHome.Common.Helpers; +using Serilog; +using Windows.Storage; + +namespace DevHome.PI.Helpers; + +internal sealed class ExternalToolsHelper +{ + private readonly JsonSerializerOptions serializerOptions = new() { WriteIndented = true }; + private readonly string toolInfoFileName; + + public static readonly ExternalToolsHelper Instance = new(); + + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(ExternalToolsHelper)); + + private readonly ObservableCollection filteredExternalTools = []; + + private ObservableCollection allExternalTools = []; + + // The ExternalTools menu shows all registered tools. + public ObservableCollection AllExternalTools + { + get => allExternalTools; + set + { + // We're assigning the collection once, and this also covers the case where we reassign it again: + // we need to unsubscribe from the old collection's events, subscribe to the new collection's events, + // and initialize the filtered collection. + if (allExternalTools != value) + { + if (allExternalTools != null) + { + allExternalTools.CollectionChanged -= AllExternalTools_CollectionChanged; + } + + allExternalTools = value; + if (allExternalTools != null) + { + allExternalTools.CollectionChanged += AllExternalTools_CollectionChanged; + } + + // Synchronize the filtered collection with this unfiltered one. + SynchronizeAllFilteredItems(); + } + } + } + + // The bar shows only the pinned tools. + public ReadOnlyObservableCollection FilteredExternalTools { get; private set; } + + private ExternalToolsHelper() + { + string localFolder; + if (RuntimeHelper.IsMSIX) + { + localFolder = ApplicationData.Current.LocalFolder.Path; + } + else + { + localFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) ?? string.Empty; + } + + // The file should be in this location: + // %LocalAppData%\Packages\Microsoft.Windows.DevHome_8wekyb3d8bbwe\LocalState\externaltools.json + toolInfoFileName = Path.Combine(localFolder, "externaltools.json"); + AllExternalTools = new(allExternalTools); + FilteredExternalTools = new(filteredExternalTools); + } + + internal void Init() + { + allExternalTools.Clear(); + if (File.Exists(toolInfoFileName)) + { + try + { + var jsonData = File.ReadAllText(toolInfoFileName); + var existingData = JsonSerializer.Deserialize(jsonData) ?? []; + foreach (var toolItem in existingData) + { + allExternalTools.Add(toolItem); + toolItem.PropertyChanged += ToolItem_PropertyChanged; + } + } + catch (Exception ex) + { + // TODO If we failed to parse the JSON file, we should rename it (using DateTime.Now), + // create a new one, and report to the user. + _log.Error(ex, $"Failed to parse {toolInfoFileName}"); + } + } + } + + private void ToolItem_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + // The user can change the IsPinned property of a tool, to pin or unpin it on the bar. + if (sender is ExternalTool tool && string.Equals(e.PropertyName, nameof(ExternalTool.IsPinned), StringComparison.Ordinal)) + { + if (tool.IsPinned) + { + if (!filteredExternalTools.Contains(tool)) + { + filteredExternalTools.Add(tool); + } + } + else + { + filteredExternalTools.Remove(tool); + } + } + + WriteToolsJsonFile(); + } + + public ExternalTool AddExternalTool(ExternalTool tool) + { + allExternalTools.Add(tool); + WriteToolsJsonFile(); + return tool; + } + + public void RemoveExternalTool(ExternalTool tool) + { + if (allExternalTools.Remove(tool)) + { + WriteToolsJsonFile(); + } + } + + private void WriteToolsJsonFile() + { + var updatedJson = JsonSerializer.Serialize(allExternalTools, serializerOptions); + + try + { + File.WriteAllText(toolInfoFileName, updatedJson); + } + catch (Exception ex) + { + // TODO If we're unable to write to the file, we should figure out why. + // If the file has become corrupted, we should rename it (using DateTime.Now), + // create a new one, and report to the user. If it's locked, we just report to the user. + _log.Error(ex, "WriteToolsJsonFile unable to write to file"); + } + } + + private void AllExternalTools_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // Whenever the "all tools" collection changes, we need to synchronize the filtered collection. + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewItems is not null) + { + foreach (ExternalTool newItem in e.NewItems) + { + if (newItem.IsPinned) + { + filteredExternalTools.Add(newItem); + } + + newItem.PropertyChanged += ToolItem_PropertyChanged; + } + } + + break; + + case NotifyCollectionChangedAction.Remove: + if (e.OldItems is not null) + { + foreach (ExternalTool oldItem in e.OldItems) + { + oldItem.PropertyChanged -= ToolItem_PropertyChanged; + filteredExternalTools.Remove(oldItem); + } + } + + break; + + case NotifyCollectionChangedAction.Replace: + if (e.OldItems is not null) + { + foreach (ExternalTool oldItem in e.OldItems) + { + oldItem.PropertyChanged -= ToolItem_PropertyChanged; + filteredExternalTools.Remove(oldItem); + } + } + + if (e.NewItems is not null) + { + foreach (ExternalTool newItem in e.NewItems) + { + if (newItem.IsPinned) + { + filteredExternalTools.Add(newItem); + } + + newItem.PropertyChanged += ToolItem_PropertyChanged; + } + } + + break; + + case NotifyCollectionChangedAction.Reset: + SynchronizeAllFilteredItems(); + break; + } + } + + private void SynchronizeAllFilteredItems() + { + filteredExternalTools.Clear(); + foreach (var item in AllExternalTools) + { + if (item.IsPinned) + { + filteredExternalTools.Add(item); + } + } + } +} diff --git a/tools/PI/DevHome.PI/Helpers/HotKeyHelper.cs b/tools/PI/DevHome.PI/Helpers/HotKeyHelper.cs new file mode 100644 index 0000000000..755acb86af --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/HotKeyHelper.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.UI.Xaml; +using Windows.System; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Input.KeyboardAndMouse; +using WinUIEx.Messaging; + +namespace DevHome.PI.Helpers; + +// Note: instead of making this class disposable, we're disposing the WindowMessageMonitor in +// UnregisterHotKey, and MainWindow calls this in its Closing event handler. +#pragma warning disable CA1001 // Types that own disposable fields should be disposable +public class HotKeyHelper// : IDisposable +#pragma warning restore CA1001 // Types that own disposable fields should be disposable +{ + internal ushort HotkeyID { get; private set; } + + private const string NoWindowHandleException = "Cannot get window handle: are you doing this too early?"; + private readonly HWND windowHandle; + private readonly Action onHotKeyPressed; + private readonly WindowMessageMonitor windowMessageMonitor; + + public HotKeyHelper(Window handlerWindow, Action hotKeyHandler) + { + onHotKeyPressed = hotKeyHandler; + + // Create a unique Id for this class in this instance. + var atomName = $"{Environment.CurrentManagedThreadId:X8}{GetType().FullName}"; + HotkeyID = PInvoke.GlobalAddAtom(atomName); + + // Set up the window message hook to listen for hot keys. + windowHandle = (HWND)WinRT.Interop.WindowNative.GetWindowHandle(handlerWindow); + if (windowHandle.IsNull) + { + throw new InvalidOperationException(NoWindowHandleException); + } + + windowMessageMonitor = new WindowMessageMonitor(windowHandle); + windowMessageMonitor.WindowMessageReceived += OnWindowMessageReceived; + } + + private void OnWindowMessageReceived(object? sender, WindowMessageEventArgs e) + { + if (e.Message.MessageId == PInvoke.WM_HOTKEY) + { + var keyId = (int)e.Message.WParam; + if (keyId == HotkeyID) + { + onHotKeyPressed?.Invoke((int)e.Message.LParam); + e.Handled = true; + } + } + } + + internal void RegisterHotKey(VirtualKey key, HOT_KEY_MODIFIERS modifiers) + { + PInvoke.RegisterHotKey(windowHandle, HotkeyID, modifiers, (uint)key); + } + + internal void UnregisterHotKey() + { + if (HotkeyID != 0) + { + _ = PInvoke.UnregisterHotKey(windowHandle, HotkeyID); + PInvoke.GlobalDeleteAtom(HotkeyID); + windowMessageMonitor.Dispose(); + HotkeyID = 0; + } + } +} diff --git a/tools/PI/DevHome.PI/Helpers/InsightsHelper.cs b/tools/PI/DevHome.PI/Helpers/InsightsHelper.cs new file mode 100644 index 0000000000..9d6eafc5f2 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/InsightsHelper.cs @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Text.RegularExpressions; +using DevHome.PI.Models; +using Serilog; + +namespace DevHome.PI.Helpers; + +internal sealed partial class InsightsHelper +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(InsightsHelper)); + + // TODO: Add more patterns for different insights. + // TODO: Insights patterns should be in a database of some kind. + // TODO: Pattern texts should be extracted from localized windows builds. + [GeneratedRegex( + @"The process cannot access the file '(.+?)' because it is being used by another process", + RegexOptions.IgnoreCase, "en-US")] + private static partial Regex LockedFileErrorRegex(); + + // TODO The following are examples of a simple pattern where we map error code to some help text. + // This is temporary: longer-term, we should update the errors.db + // to map the error code to a description, plus any existing documented solution options. + [GeneratedRegex(@"0xc0000409", RegexOptions.IgnoreCase, "en-US")] + private static partial Regex BufferOverflowErrorRegex(); + + [GeneratedRegex(@"0xc0000005", RegexOptions.IgnoreCase, "en-US")] + private static partial Regex MemoryErrorRegex(); + + private static readonly List RegexList = []; + + static InsightsHelper() + { + RegexList.Add(new InsightRegex(InsightType.LockedFile, LockedFileErrorRegex())); + RegexList.Add(new InsightRegex(InsightType.Security, BufferOverflowErrorRegex())); + RegexList.Add(new InsightRegex(InsightType.MemoryViolation, MemoryErrorRegex())); + } + + internal static Insight? FindPattern(string errorText) + { + Insight? newInsight = null; + + foreach (var insightRegex in RegexList) + { + var match = insightRegex.Regex.Match(errorText); + if (match.Success) + { + newInsight = new Insight + { + InsightType = insightRegex.InsightType, + }; + + // Once we flesh out our error database, we should have a more structured way to + // handle different types of insights, rather than a switch statement. + switch (insightRegex.InsightType) + { + case InsightType.LockedFile: + { + // Extract the file path from the matched group. + var pattern = string.Empty; + if (match.Groups != null && match.Groups.Count > 1) + { + pattern = match.Groups[1].Value; + } + + newInsight.Title = CommonHelper.GetLocalizedString("LockedFileInsightTitle"); + var processName = GetLockingProcess(pattern); + if (!string.IsNullOrEmpty(processName)) + { + newInsight.Description = + CommonHelper.GetLocalizedString("LockedFileInsightSpecificDescription", pattern, processName); + } + else + { + newInsight.Description = + CommonHelper.GetLocalizedString("LockedFileInsightUnknownDescription", pattern); + } + } + + break; + + case InsightType.Security: + { + var hexValue = match.Value; + var intConverter = new Int32Converter(); + var errorAsInt = (int?)intConverter.ConvertFromString(hexValue); + if (errorAsInt is not null) + { + var errors = ErrorLookupHelper.LookupError((int)errorAsInt); + if (errors is not null && errors.Length > 0) + { + var error = errors[0]; + { + newInsight.Description = + CommonHelper.GetLocalizedString("GenericInsightDescription", error.Name, error.Help); + } + } + } + + newInsight.Title = CommonHelper.GetLocalizedString("SecurityInsightTitle"); + } + + break; + + case InsightType.MemoryViolation: + { + var hexValue = match.Value; + var intConverter = new Int32Converter(); + var errorAsInt = (int?)intConverter.ConvertFromString(hexValue); + if (errorAsInt is not null) + { + var errors = ErrorLookupHelper.LookupError((int)errorAsInt); + if (errors is not null && errors.Length > 0) + { + var error = errors[0]; + { + if (IsPythonCtypesError(errorText, out var description)) + { + newInsight.Description = description; + } + else + { + newInsight.Description = + CommonHelper.GetLocalizedString("GenericInsightDescription", error.Name, error.Help); + } + } + } + } + + newInsight.Title = CommonHelper.GetLocalizedString("MemoryInsightTitle"); + } + + break; + + default: + break; + } + + break; + } + } + + return newInsight; + } + + // This is an example of an error that requires additional runtime processing to + // determine the locking process, so it cannot be handled in the error database alone. + private static string GetLockingProcess(string lockedFilePath) + { + var lockingProcess = string.Empty; + + try + { + // Determines if the specified file is locked by another process. + _ = RestartManagerHelper.GetLockingProcesses(lockedFilePath, out var processes); + if (processes != null && processes.Count > 0) + { + var process = processes[0]; + lockingProcess = process.ProcessName; + } + } + catch (Exception ex) + { + _log.Debug(ex, "Unable to determine if process is locked."); + } + + return lockingProcess; + } + + // We're special-casing Python ctypes errors here, just to exercise this type of issue + // pattern, but longer-term this should be handled by some data relationship in the errors.db. + private static bool IsPythonCtypesError(string errorText, out string description) + { + var result = false; + description = string.Empty; + var appPathPattern = @"Faulting application path: .*\\python\.exe"; + var modulePathPattern = @"Faulting module path: .*\\_ctypes\.pyd"; + var hasAppPath = Regex.IsMatch(errorText, appPathPattern); + var hasModulePath = Regex.IsMatch(errorText, modulePathPattern); + + if (hasAppPath && hasModulePath) + { + description = CommonHelper.GetLocalizedString("PythonCtypesDescription"); + result = true; + } + + return result; + } +} diff --git a/tools/PI/DevHome.PI/Helpers/RestartManagerHelper.cs b/tools/PI/DevHome.PI/Helpers/RestartManagerHelper.cs new file mode 100644 index 0000000000..ce298db6eb --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/RestartManagerHelper.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.RestartManager; + +namespace DevHome.PI.Helpers; + +internal static class RestartManagerHelper +{ + // Find out what process(es) have a lock on the specified file. + internal static WIN32_ERROR GetLockingProcesses(string filePath, out List processes) + { + var key = Guid.NewGuid().ToString(); + processes = []; + + // Start a Restart Manager session. + var result = WIN32_ERROR.ERROR_SUCCESS; + uint handle; + unsafe + { + fixed (char* p = key) + { + PInvoke.RmStartSession(out handle, p); + } + } + + if (result != 0) + { + return result; + } + + try + { + uint pnProcInfo = 0; + var lpdwRebootReasons = (uint)RM_REBOOT_REASON.RmRebootReasonNone; + + unsafe + { + fixed (char* p = filePath) + { + var filePathStr = new PCWSTR(p); + var resources = new ReadOnlySpan(&filePathStr, 1); + var uniqueProcesses = default(Span); + var serviceNames = default(ReadOnlySpan); + + // Specify the given file as a resource to be managed by the Restart Manager. + result = PInvoke.RmRegisterResources(handle, resources, uniqueProcesses, serviceNames); + if (result != 0) + { + return result; + } + } + } + + // Note: there's a race here - the first call to RmGetList returns the count of processes, + // but when we call RmGetList again to get them this number might have changed. + unsafe + { + result = PInvoke.RmGetList(handle, out var pnProcInfoNeeded, ref pnProcInfo, null, out lpdwRebootReasons); + if (result == WIN32_ERROR.ERROR_MORE_DATA) + { + var processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded]; + + fixed (RM_PROCESS_INFO* processArrayPtr = processInfo) + { + pnProcInfo = pnProcInfoNeeded; + + // Get the list of running processes that are using the given resource (file). + result = PInvoke.RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processArrayPtr, out lpdwRebootReasons); + if (result == 0) + { + // Enumerate all of the returned PIDS, get a Process for each one, and add it to the list. + processes = new List((int)pnProcInfo); + for (var i = 0; i < pnProcInfo; i++) + { + try + { + processes.Add(Process.GetProcessById((int)processInfo[i].Process.dwProcessId)); + } + catch (ArgumentException) + { + // The process might have died before we got to look at it. + } + } + } + else + { + return result; + } + } + } + else if (result != 0) + { + return result; + } + } + } + finally + { + _ = PInvoke.RmEndSession(handle); + } + + return 0; + } +} diff --git a/tools/PI/DevHome.PI/Helpers/SnapHelper.cs b/tools/PI/DevHome.PI/Helpers/SnapHelper.cs new file mode 100644 index 0000000000..094c9b6b54 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/SnapHelper.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Extensions; +using DevHome.PI.Models; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Windows.Graphics; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Accessibility; +using WinUIEx; + +namespace DevHome.PI.Helpers; + +public class SnapHelper +{ + // TODO The SnapOffsetHorizontal and UnsnapGap values don't allow for different DPIs. + private const int UnsnapGap = 9; + + // It seems the way rounded corners are implemented means that the window is really 8px + // bigger than it seems, so we'll subtract this when we do sidecar snapping. + private const int SnapOffsetHorizontal = 8; + + private readonly WINEVENTPROC _winPositionEventDelegate; + private readonly WINEVENTPROC _winFocusEventDelegate; + + private HWINEVENTHOOK _positionEventHook; + private HWINEVENTHOOK _focusEventHook; + + public SnapHelper() + { + _winPositionEventDelegate = new(WinPositionEventProc); + _winFocusEventDelegate = new(WinFocusEventProc); + } + + public void Snap() + { + Debug.Assert(_positionEventHook == HWINEVENTHOOK.Null, "Hook should be null"); + Debug.Assert(_focusEventHook == HWINEVENTHOOK.Null, "Hook should be null"); + + _positionEventHook = WindowHelper.WatchWindowPositionEvents(_winPositionEventDelegate, (uint)TargetAppData.Instance.ProcessId); + _focusEventHook = WindowHelper.WatchWindowFocusEvents(_winFocusEventDelegate, (uint)TargetAppData.Instance.ProcessId); + + SnapToWindow(); + } + + public void Unsnap() + { + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + + // Set a gap from the associated app window to provide positive feedback. + PInvoke.GetWindowRect(barWindow.CurrentHwnd, out var rect); + barWindow.UpdateBarWindowPosition(new PointInt32(rect.left + UnsnapGap, rect.top)); + + if (_positionEventHook != HWINEVENTHOOK.Null) + { + PInvoke.UnhookWinEvent(_positionEventHook); + _positionEventHook = HWINEVENTHOOK.Null; + } + + if (_focusEventHook != HWINEVENTHOOK.Null) + { + PInvoke.UnhookWinEvent(_focusEventHook); + _focusEventHook = HWINEVENTHOOK.Null; + } + } + + private void WinPositionEventProc(HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) + { + // Filter out events for non-main windows. + if (idObject != 0 || idChild != 0) + { + return; + } + + if (hwnd != TargetAppData.Instance.HWnd) + { + return; + } + + if (eventType == PInvoke.EVENT_OBJECT_LOCATIONCHANGE) + { + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + if (barWindow.IsBarSnappedToWindow()) + { + // If the window has been maximized, un-snap the bar window and free-float it. + if (PInvoke.IsZoomed(TargetAppData.Instance.HWnd)) + { + barWindow.UnsnapBarWindow(); + } + else + { + // Reposition the window to match the moved/resized/minimized/restored target window. + // If the target window was maximized and has now been restored, we want + // to resnap to it, but not do all the other work we do when we resnap + // to a new window. + SnapToWindow(); + } + } + } + + // If the window we're watching closes, we unsnap + if (eventType == PInvoke.EVENT_OBJECT_DESTROY) + { + Unsnap(); + } + } + + private void WinFocusEventProc(HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) + { + if (hwnd != TargetAppData.Instance.HWnd) + { + return; + } + + // If we're snapped to a target window, and that window loses and then regains focus, + // we need to bring our window to the front also, to be in-sync. Otherwise, we can + // end up with the target in the foreground, but our window partially obscured. + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + if (barWindow.IsBarSnappedToWindow()) + { + barWindow.ResetBarWindowOnTop(); + return; + } + } + + private void SnapToWindow() + { + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + + // If BarWindow is snapped to a TargetApp and BarWindow is in foreground, bring TargetApp to foreground. + if (barWindow.CurrentHwnd == PInvoke.GetForegroundWindow()) + { + PInvoke.SetForegroundWindow(TargetAppData.Instance.HWnd); + } + + PInvoke.GetWindowRect(TargetAppData.Instance.HWnd, out var rect); + barWindow.UpdateBarWindowPosition(new PointInt32(rect.right - SnapOffsetHorizontal, rect.top)); + + // Only reset BarWindow on top, if TargetApp is in foreground. + if (TargetAppData.Instance.HWnd == PInvoke.GetForegroundWindow()) + { + barWindow.ResetBarWindowOnTop(); + } + } +} diff --git a/tools/PI/DevHome.PI/Helpers/WatsonHelper.cs b/tools/PI/DevHome.PI/Helpers/WatsonHelper.cs new file mode 100644 index 0000000000..71f45d07a0 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/WatsonHelper.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; +using System.Globalization; +using System.IO; +using System.Linq; +using DevHome.PI.Models; + +namespace DevHome.PI.Helpers; + +internal sealed class WatsonHelper : IDisposable +{ + private const string WatsonQueryPart1 = "(*[System[Provider[@Name=\"Application Error\"]]] and *[System[EventID=1000]])"; + private const string WatsonQueryPart2 = "(*[System[Provider[@Name=\"Windows Error Reporting\"]]] and *[System[EventID=1001]])"; + + private readonly Process targetProcess; + private readonly EventLogWatcher? eventLogWatcher; + private readonly ObservableCollection? watsonOutput; + private readonly ObservableCollection? winLogsPageOutput; + + public WatsonHelper(Process targetProcess, ObservableCollection? watsonOutput, ObservableCollection? winLogsPageOutput) + { + this.targetProcess = targetProcess; + this.targetProcess.Exited += TargetProcess_Exited; + this.watsonOutput = watsonOutput; + this.winLogsPageOutput = winLogsPageOutput; + + try + { + // Subscribe for Application events matching the processName. + var filterQuery = string.Format(CultureInfo.CurrentCulture, "{0} or {1}", WatsonQueryPart1, WatsonQueryPart2); + EventLogQuery subscriptionQuery = new("Application", PathType.LogName, filterQuery); + eventLogWatcher = new EventLogWatcher(subscriptionQuery); + eventLogWatcher.EventRecordWritten += new EventHandler(EventLogEventRead); + } + catch (EventLogReadingException) + { + var message = CommonHelper.GetLocalizedString("WatsonStartErrorMessage"); + WinLogsEntry entry = new(DateTime.Now, WinLogCategory.Error, message, WinLogsHelper.WatsonName); + winLogsPageOutput?.Add(entry); + } + } + + public void Start() + { + if (eventLogWatcher is not null) + { + eventLogWatcher.Enabled = true; + } + } + + public void Stop() + { + if (eventLogWatcher is not null) + { + eventLogWatcher.Enabled = false; + } + } + + public void Dispose() + { + if (eventLogWatcher is not null) + { + eventLogWatcher.Dispose(); + } + + GC.SuppressFinalize(this); + } + + public void EventLogEventRead(object? obj, EventRecordWrittenEventArgs eventArg) + { + var eventRecord = eventArg.EventRecord; + if (eventRecord != null) + { + if (eventRecord.Id == 1000 && eventRecord.ProviderName.Equals("Application Error", StringComparison.OrdinalIgnoreCase)) + { + var filePath = eventRecord.Properties[10].Value.ToString() ?? string.Empty; + if (filePath.Contains(targetProcess.ProcessName, StringComparison.OrdinalIgnoreCase)) + { + var timeGenerated = eventRecord.TimeCreated ?? DateTime.Now; + var moduleName = eventRecord.Properties[3].Value.ToString() ?? string.Empty; + var executable = eventRecord.Properties[0].Value.ToString() ?? string.Empty; + var eventGuid = eventRecord.Properties[12].Value.ToString() ?? string.Empty; + var report = new WatsonReport(timeGenerated, moduleName, executable, eventGuid); + watsonOutput?.Add(report); + + WinLogsEntry entry = new(timeGenerated, WinLogCategory.Error, eventRecord.FormatDescription(), WinLogsHelper.WatsonName); + winLogsPageOutput?.Add(entry); + } + } + else if (eventRecord.Id == 1001 && eventRecord.ProviderName.Equals("Windows Error Reporting", StringComparison.OrdinalIgnoreCase)) + { + // See if we've already put this into our Collection. + for (var i = 0; i < watsonOutput?.Count; i++) + { + var existingReport = watsonOutput[i]; + if (existingReport.EventGuid.Equals(eventRecord.Properties[19].Value.ToString(), StringComparison.OrdinalIgnoreCase)) + { + existingReport.WatsonLog = eventRecord.FormatDescription(); + try + { + // List files available in the archive. + var directoryPath = eventRecord.Properties[16].Value.ToString(); + if (Directory.Exists(directoryPath)) + { + IEnumerable files = Directory.EnumerateFiles(directoryPath); + foreach (var file in files) + { + existingReport.WatsonReportFile = File.ReadAllText(file); + } + } + } + catch + { + } + + break; + } + } + } + } + } + + public List GetWatsonReports() + { + Dictionary reports = []; + EventLog eventLog = new("Application"); + var targetProcessName = targetProcess.ProcessName; + + foreach (EventLogEntry entry in eventLog.Entries) + { + if (entry.InstanceId == 1000 + && entry.Source.Equals("Application Error", StringComparison.OrdinalIgnoreCase) + && entry.ReplacementStrings[10].Contains(targetProcessName, StringComparison.OrdinalIgnoreCase)) + { + var timeGenerated = entry.TimeGenerated; + var moduleName = entry.ReplacementStrings[3]; + var executable = entry.ReplacementStrings[0]; + var eventGuid = entry.ReplacementStrings[12]; + var report = new WatsonReport(timeGenerated, moduleName, executable, eventGuid); + reports.Add(entry.ReplacementStrings[12], report); + } + else if (entry.InstanceId == 1001 + && entry.Source.Equals("Windows Error Reporting", StringComparison.OrdinalIgnoreCase)) + { + // See if we've already put this into our Dictionary. + if (reports.TryGetValue(entry.ReplacementStrings[19], out WatsonReport? report)) + { + report.WatsonLog = entry.Message; + + try + { + // List files available in the archive. + if (Directory.Exists(entry.ReplacementStrings[16])) + { + var files = Directory.EnumerateFiles(entry.ReplacementStrings[16]); + foreach (var file in files) + { + report.WatsonReportFile = File.ReadAllText(file); + } + } + } + catch + { + } + } + } + } + + return reports.Values.ToList(); + } + + private void TargetProcess_Exited(object? sender, EventArgs e) + { + Stop(); + Dispose(); + } +} diff --git a/tools/PI/DevHome.PI/Helpers/WinLogsHelper.cs b/tools/PI/DevHome.PI/Helpers/WinLogsHelper.cs new file mode 100644 index 0000000000..967cfe66c2 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/WinLogsHelper.cs @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using DevHome.PI.Models; +using Microsoft.Diagnostics.Tracing; +using Microsoft.UI; +using Microsoft.UI.Xaml.Media; + +namespace DevHome.PI.Helpers; + +public class WinLogsHelper : IDisposable +{ + public const string EtwLogsName = "ETW Logs"; + public const string DebugOutputLogsName = "DebugOutput"; + public const string EventViewerName = "EventViewer"; + public const string WatsonName = "Watson"; + + private readonly ETWHelper etwHelper; + private readonly DebugMonitor debugMonitor; + private readonly EventViewerHelper eventViewerHelper; + private readonly WatsonHelper watsonHelper; + private readonly ObservableCollection output; + private readonly Process targetProcess; + + private Thread? etwThread; + private Thread? debugMonitorThread; + private Thread? eventViewerThread; + private Thread? watsonThread; + + public bool IsETWEnabled { get; } + + public WinLogsHelper(Process targetProcess, ObservableCollection output) + { + this.targetProcess = targetProcess; + this.output = output; + IsETWEnabled = ETWHelper.IsUserInPerformanceLogUsersGroup(); + + // Initialize ETW logs + etwHelper = new ETWHelper(targetProcess, output); + + // Initialize DebugMonitor + debugMonitor = new DebugMonitor(targetProcess, output); + + // Initialize EventViewer + eventViewerHelper = new EventViewerHelper(targetProcess, output); + + // Initialize Watson + watsonHelper = new WatsonHelper(targetProcess, null, output); + + Start(); + } + + public void Start() + { + if (IsETWEnabled) + { + StartETWLogsThread(); + } + + StartEventViewerThread(); + StartWatsonThread(); + } + + public void Stop() + { + // Stop ETW logs + StopETWLogsThread(); + + // Stop Debug Outputs + StopDebugOutputsThread(); + + // Stop Event Viewer + StopEventViewerThread(); + + // Stop Watson + StopWatsonThread(); + } + + public void Dispose() + { + etwHelper.Dispose(); + debugMonitor.Dispose(); + eventViewerHelper.Dispose(); + watsonHelper.Dispose(); + GC.SuppressFinalize(this); + } + + private void StartETWLogsThread() + { + // Stop and close existing thread if any + StopETWLogsThread(); + + // Start a new thread + etwThread = new Thread(() => + { + etwHelper.Start(); + }); + etwThread.Name = EtwLogsName + " Thread"; + etwThread.Start(); + } + + private void StopETWLogsThread() + { + etwHelper.Stop(); + + if (Thread.CurrentThread != etwThread) + { + etwThread?.Join(); + } + } + + private void StartDebugOutputsThread() + { + // Stop and close existing thread if any + StopDebugOutputsThread(); + + // Start a new thread + debugMonitorThread = new Thread(() => + { + // Start Debug Outputs + debugMonitor.Start(); + }); + debugMonitorThread.Name = DebugOutputLogsName + " Thread"; + debugMonitorThread.Start(); + } + + private void StopDebugOutputsThread() + { + debugMonitor.Stop(); + + if (Thread.CurrentThread != debugMonitorThread) + { + debugMonitorThread?.Join(); + } + } + + private void StartEventViewerThread() + { + // Stop and close existing thread if any + StopEventViewerThread(); + + // Start a new thread + eventViewerThread = new Thread(() => + { + // Start EventViewer logs + eventViewerHelper.Start(); + }); + eventViewerThread.Name = EventViewerName + " Thread"; + eventViewerThread.Start(); + } + + private void StopEventViewerThread() + { + eventViewerHelper.Stop(); + + if (Thread.CurrentThread != eventViewerThread) + { + eventViewerThread?.Join(); + } + } + + private void StartWatsonThread() + { + // Stop and close existing thread if any + StopWatsonThread(); + + // Start a new thread + watsonThread = new Thread(() => + { + // Start Watson logs + watsonHelper.Start(); + }); + watsonThread.Name = WatsonName + " Thread"; + watsonThread.Start(); + } + + private void StopWatsonThread() + { + watsonHelper.Stop(); + + if (Thread.CurrentThread != watsonThread) + { + watsonThread?.Join(); + } + } + + public void LogStateChanged(WinLogsTool logType, bool isEnabled) + { + if (isEnabled) + { + switch (logType) + { + case WinLogsTool.ETWLogs: + StartETWLogsThread(); + break; + case WinLogsTool.DebugOutput: + StartDebugOutputsThread(); + break; + case WinLogsTool.EventViewer: + StartEventViewerThread(); + break; + case WinLogsTool.Watson: + StartWatsonThread(); + break; + } + } + else + { + switch (logType) + { + case WinLogsTool.ETWLogs: + StopETWLogsThread(); + break; + case WinLogsTool.DebugOutput: + StopDebugOutputsThread(); + break; + case WinLogsTool.EventViewer: + StopEventViewerThread(); + break; + case WinLogsTool.Watson: + StopWatsonThread(); + break; + } + } + } + + public static WinLogCategory ConvertTraceEventLevelToWinLogCategory(TraceEventLevel level) + { + var category = WinLogCategory.Information; + + switch (level) + { + case TraceEventLevel.Error: + case TraceEventLevel.Critical: + category = WinLogCategory.Error; + break; + case TraceEventLevel.Warning: + category = WinLogCategory.Warning; + break; + } + + return category; + } + + public static WinLogCategory ConvertStandardEventLevelToWinLogCategory(byte? level) + { + var category = WinLogCategory.Information; + + if (level.HasValue) + { + StandardEventLevel standardEventLevel = (StandardEventLevel)level.Value; + switch (standardEventLevel) + { + case StandardEventLevel.Error: + case StandardEventLevel.Critical: + category = WinLogCategory.Error; + break; + case StandardEventLevel.Warning: + category = WinLogCategory.Warning; + break; + } + } + + return category; + } +} diff --git a/tools/PI/DevHome.PI/Helpers/WindowHelper.cs b/tools/PI/DevHome.PI/Helpers/WindowHelper.cs new file mode 100644 index 0000000000..9c91a24a28 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/WindowHelper.cs @@ -0,0 +1,625 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; +using Serilog; +using Windows.Devices.Display; +using Windows.Devices.Enumeration; +using Windows.Foundation; +using Windows.Graphics; +using Windows.Graphics.Imaging; +using Windows.Storage; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Dwm; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.UI.Accessibility; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace DevHome.PI.Helpers; + +public class WindowHelper +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(WindowHelper)); + + private static nint GetClassLongPtr(HWND hWnd, GET_CLASS_LONG_INDEX nIndex) + { + if (IntPtr.Size == 8) + { + return CommonInterop.GetClassLongPtr64(hWnd, nIndex); + } + else + { + return (nint)PInvoke.GetClassLong(hWnd, nIndex); + } + } + + private static nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint newLong) + { + if (IntPtr.Size == 8) + { + return CommonInterop.SetWindowLongPtr64(hWnd, nIndex, newLong); + } + else + { + return (nint)PInvoke.SetWindowLong(hWnd, nIndex, (int)newLong); + } + } + + private static nint GetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex) + { + if (IntPtr.Size == 8) + { + return CommonInterop.GetWindowLongPtr64(hWnd, nIndex); + } + else + { + return PInvoke.GetWindowLong(hWnd, nIndex); + } + } + + // TODO The SnapThreshold values don't allow for different DPIs. + + // If the target window is moved to within SnapThreshold px of the edge of the screen, we unsnap. + private const int SnapThreshold = 10; + + private static unsafe BOOL EnumProc(HWND hWnd, LPARAM data) + { +#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + var enumData = (EnumWindowsData*)data.Value; +#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + + // The caller should've set this, but we'll make sure here. + enumData->OutHwnd = HWND.Null; + + // Skip this one if the window doesn't include WS_VISIBLE, or if it's minimized. + if (!PInvoke.IsWindowVisible(hWnd)) + { + return true; + } + + if (PInvoke.IsIconic(hWnd)) + { + return true; + } + + // Skip toolwindows. + var extendedStyle = GetWindowLongPtr(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + var isToolWindow = (extendedStyle & (long)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) + == (long)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW; + if (isToolWindow) + { + return true; + } + + // Skip dialogs. + if (PInvoke.GetAncestor(hWnd, GET_ANCESTOR_FLAGS.GA_ROOTOWNER) != hWnd) + { + return true; + } + + PInvoke.GetWindowRect(hWnd, out var windowRect); + var screenBounds = GetMonitorRectForWindow(hWnd); + var isOnAnyScreen = + windowRect.left < screenBounds.right && windowRect.right > screenBounds.left && + windowRect.top < screenBounds.bottom && windowRect.bottom > screenBounds.top && + windowRect.right - windowRect.left > 1 && windowRect.bottom - windowRect.top > 1; + if (!isOnAnyScreen) + { + return true; + } + + unsafe + { + // Exclude system/shell windows. + var className = stackalloc char[256]; + var classNameLength = PInvoke.GetClassName(hWnd, className, 256); + if (classNameLength == 0) + { + return true; + } + + string classNameString = new(className, 0, classNameLength); + if (classNameString == "Progman" || classNameString == "Shell_TrayWnd" || + classNameString == "WorkerW" || classNameString == "SHELLDLL_DefView" || + classNameString == "IME") + { + return true; + } + + // Exclude cloaked windows. + var cloakedVal = 0; + var hRes = PInvoke.DwmGetWindowAttribute( + hWnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, &cloakedVal, sizeof(int)); + if (hRes != 0) + { + cloakedVal = 0; + } + + if (cloakedVal != 0) + { + return true; + } + + // Skip any windows that are on our exclusion list. + var excludedProcesses = enumData->ExcludedProcesses; + if (excludedProcesses != null && !IsAcceptableWindow(hWnd, excludedProcesses)) + { + return true; + } + } + + // Skip popups, unless they're for UWP apps or for dialog-based MFC apps. + var windowStyle = (WINDOW_STYLE)GetWindowLongPtr(hWnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + var isPopup = (windowStyle & WINDOW_STYLE.WS_POPUP) == WINDOW_STYLE.WS_POPUP; + + // Dialog-based MFC apps will have WS_POPUP but also WS_OVERLAPPED. + var isOverlapped = (windowStyle & WINDOW_STYLE.WS_OVERLAPPED) == WINDOW_STYLE.WS_OVERLAPPED; + + if (isPopup) + { + var isUwpApp = IsProcessName(hWnd, "applicationframehost"); + if (isUwpApp) + { + // NOTE: We could use SHGetPropertyStoreForWindow + PKEY_AppUserModel_ID + // to get the appid for the app. + // Found a visible UWP window, stop enumerating. + enumData->OutHwnd = hWnd; + return false; + } + else if (isOverlapped) + { + // This is a top-level popup, most likely a dialog-based MFC app. + enumData->OutHwnd = hWnd; + return false; + } + + return true; + } + + // Found a window, stop enumerating. + enumData->OutHwnd = hWnd; + return false; + } + + private static Rectangle GetScreenBounds() + { + Rectangle rectangle = default; + + // Can't use async in EnumProc. + var deviceInfoOp = DeviceInformation.FindAllAsync(DisplayMonitor.GetDeviceSelector()); + deviceInfoOp.AsTask().Wait(); + var displayList = deviceInfoOp.GetResults(); + if (displayList == null || displayList.Count == 0) + { + return rectangle; + } + + var winSize = default(SizeInt32); + var displayOp = DisplayMonitor.FromInterfaceIdAsync(displayList[0].Id); + displayOp.AsTask().Wait(); + var monitorInfo = displayOp.GetResults(); + if (monitorInfo == null) + { + winSize.Width = 800; + winSize.Height = 1200; + } + else + { + winSize.Height = monitorInfo.NativeResolutionInRawPixels.Height; + winSize.Width = monitorInfo.NativeResolutionInRawPixels.Width; + } + + rectangle.Width = winSize.Width; + rectangle.Height = winSize.Height; + + return rectangle; + } + + public static Windows.Graphics.RectInt32 GetRect(Rect bounds, double scale) + { + return new Windows.Graphics.RectInt32( + _X: (int)Math.Round(bounds.X * scale), + _Y: (int)Math.Round(bounds.Y * scale), + _Width: (int)Math.Round(bounds.Width * scale), + _Height: (int)Math.Round(bounds.Height * scale)); + } + + public enum BinaryType : int + { + Unknown = -1, + X32 = 0, + X64 = 6, + } + + internal static unsafe string GetWindowTitle(HWND hWnd) + { + var length = PInvoke.GetWindowTextLength(hWnd); + var windowText = stackalloc char[length]; + _ = PInvoke.GetWindowText(hWnd, windowText, length); + return new string(windowText); + } + + internal static IntPtr LoadDefaultAppIcon() + { + IntPtr icon = PInvoke.LoadIcon(HINSTANCE.Null, PInvoke.IDI_APPLICATION); + return icon; + } + + internal static Bitmap? GetAppIcon(HWND hWnd) + { + try + { + // Try getting the big icon first. + IntPtr hIcon = default; + hIcon = PInvoke.SendMessage(hWnd, PInvoke.WM_GETICON, PInvoke.ICON_BIG, IntPtr.Zero); + + // If that failed, try getting the small icon (or the system-provided default). + if (hIcon == IntPtr.Zero) + { + hIcon = PInvoke.SendMessage(hWnd, PInvoke.WM_GETICON, PInvoke.ICON_SMALL2, IntPtr.Zero); + if (hIcon == IntPtr.Zero) + { + hIcon = (nint)GetClassLongPtr(hWnd, GET_CLASS_LONG_INDEX.GCL_HICON); + } + } + + if (hIcon != IntPtr.Zero) + { + return new Bitmap(Icon.FromHandle(hIcon).ToBitmap(), 24, 24); + } + else + { + return null; + } + } + catch (Exception) + { + return null; + } + } + + public static async Task GetWinUI3BitmapSourceFromGdiBitmap(Bitmap bmp) + { + var softwareBitmap = GetSoftwareBitmapFromGdiBitmap(bmp); + var source = new SoftwareBitmapSource(); + await source.SetBitmapAsync(softwareBitmap); + return source; + } + + public static SoftwareBitmap? GetSoftwareBitmapFromExecutable(string executable) + { + SoftwareBitmap? softwareBitmap = null; + var toolIcon = Icon.ExtractAssociatedIcon(executable); + + // Fall back to Windows default app icon. + toolIcon ??= Icon.FromHandle(LoadDefaultAppIcon()); + + if (toolIcon is not null) + { + softwareBitmap = GetSoftwareBitmapFromGdiBitmap(toolIcon.ToBitmap()); + } + + return softwareBitmap; + } + + public static SoftwareBitmap GetSoftwareBitmapFromGdiBitmap(Bitmap bmp) + { + // Get pixels as an array of bytes. + var data = bmp.LockBits( + new Rectangle(0, 0, bmp.Width, bmp.Height), + System.Drawing.Imaging.ImageLockMode.ReadOnly, + bmp.PixelFormat); + var bytes = new byte[data.Stride * data.Height]; + Marshal.Copy(data.Scan0, bytes, 0, bytes.Length); + bmp.UnlockBits(data); + + // Get WinRT SoftwareBitmap. + var softwareBitmap = new SoftwareBitmap( + BitmapPixelFormat.Bgra8, + bmp.Width, + bmp.Height, + BitmapAlphaMode.Premultiplied); + softwareBitmap.CopyFromBuffer(bytes.AsBuffer()); + + return softwareBitmap; + } + + public static async Task GetSoftwareBitmapSourceFromSoftwareBitmap(SoftwareBitmap softwareBitmap) + { + var softwareBitmapSource = new SoftwareBitmapSource(); + await softwareBitmapSource.SetBitmapAsync(softwareBitmap); + return softwareBitmapSource; + } + + public static async Task SaveSoftwareBitmapToTempFile(SoftwareBitmap softwareBitmap) + { + var tempFolder = ApplicationData.Current.TemporaryFolder; + var tempFile = await tempFolder.CreateFileAsync( + Guid.NewGuid().ToString() + ".png", CreationCollisionOption.ReplaceExisting); + + using (var stream = await tempFile.OpenAsync(FileAccessMode.ReadWrite)) + { + var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream); + encoder.SetSoftwareBitmap(softwareBitmap); + await encoder.FlushAsync(); + } + + return new Uri(tempFile.Path); + } + + internal static unsafe uint GetProcessIdFromWindow(HWND hWnd) + { + uint processID = 0; + _ = PInvoke.GetWindowThreadProcessId(hWnd, &processID); + return processID; + } + + internal static HWINEVENTHOOK WatchWindowPositionEvents(WINEVENTPROC procDelegate, uint processID) + { + var eventHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_DESTROY, + PInvoke.EVENT_OBJECT_LOCATIONCHANGE, + HMODULE.Null, + procDelegate, + processID, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT | PInvoke.WINEVENT_SKIPOWNPROCESS); + return eventHook; + } + + internal static HWINEVENTHOOK WatchWindowForegroundEvents(WINEVENTPROC procDelegate) + { + var eventHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_FOREGROUND, + PInvoke.EVENT_SYSTEM_FOREGROUND, + HMODULE.Null, + procDelegate, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT | PInvoke.WINEVENT_SKIPOWNPROCESS); + return eventHook; + } + + internal static HWINEVENTHOOK WatchWindowFocusEvents(WINEVENTPROC procDelegate, uint processID) + { + var eventHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_FOCUS, + PInvoke.EVENT_OBJECT_FOCUS, + HMODULE.Null, + procDelegate, + processID, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT | PInvoke.WINEVENT_SKIPOWNPROCESS); + return eventHook; + } + + internal sealed class EnumWindowsData + { + public HWND OutHwnd { get; set; } + + public StringCollection? ExcludedProcesses { get; set; } + + public EnumWindowsData() + { + OutHwnd = HWND.Null; + } + } + + internal static unsafe HWND FindVisibleForegroundWindow(StringCollection excludedProcesses) + { + EnumWindowsData data = new() { ExcludedProcesses = excludedProcesses }; +#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + LPARAM lparamData = new((nint)(&data)); +#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + PInvoke.EnumWindows(EnumProc, lparamData); + return data.OutHwnd; + } + + internal static bool IsAcceptableWindow(HWND hWnd, StringCollection excludedProcesses) + { + if (excludedProcesses != null && excludedProcesses.Count > 0) + { + foreach (var processName in excludedProcesses) + { + if (processName != null && IsProcessName(hWnd, processName)) + { + return false; + } + } + } + + return true; + } + + internal static unsafe bool IsProcessName(HWND hWnd, string name) + { + uint processId = 0; + _ = PInvoke.GetWindowThreadProcessId(hWnd, &processId); + var process = Process.GetProcessById((int)processId); + if (string.Equals(process.ProcessName, name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + + internal static string GetProcessName(uint processId) + { + var process = Process.GetProcessById((int)processId); + return process.ProcessName; + } + + internal static void SetWindowExTransparent(HWND hwnd) + { + var extendedStyle = (WINDOW_EX_STYLE)PInvoke.GetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + _ = PInvoke.SetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)(extendedStyle | WINDOW_EX_STYLE.WS_EX_TRANSPARENT)); + } + + internal static void SetWindowExNotTransparent(HWND hwnd) + { + var extendedStyle = (WINDOW_EX_STYLE)PInvoke.GetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + _ = PInvoke.SetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)(extendedStyle & ~WINDOW_EX_STYLE.WS_EX_TRANSPARENT)); + } + + internal static T? FindParentControl(DependencyObject child) + where T : DependencyObject + { + var parentObject = VisualTreeHelper.GetParent(child); + if (parentObject == null) + { + return null; + } + + if (parentObject is T parent) + { + return parent; + } + else + { + return FindParentControl(parentObject); + } + } + + internal static bool IsWindowSnapped(HWND hwnd) + { + if (!PInvoke.GetWindowRect(hwnd, out var windowRect)) + { + return false; + } + + var workAreaRect = GetWorkAreaRect(); + + // If the window is within the top, right or bottom (not left) snap threshold, + // consider it snapped to the edge. + var snappedToTop = Math.Abs(windowRect.top - workAreaRect.top) <= SnapThreshold; + var snappedToRight = Math.Abs(windowRect.right - workAreaRect.right) <= SnapThreshold; + var snappedToBottom = Math.Abs(windowRect.bottom - workAreaRect.bottom) <= SnapThreshold; + return snappedToTop || snappedToRight || snappedToBottom; + } + + internal static bool DoWindowsOverlap(HWND hwnd, HWND hwnd2) + { + PInvoke.GetWindowRect(hwnd, out var rect); + PInvoke.GetWindowRect(hwnd2, out var rect2); + + var overlap = rect.left < rect2.right && rect.right > rect2.left && + rect.top < rect2.bottom && rect.bottom > rect2.top; + + return overlap; + } + + internal static bool DoesWindow1CoverTheRightSideOfWindow2(HWND hwnd1, HWND hwnd2) + { + PInvoke.GetWindowRect(hwnd1, out var rect); + PInvoke.GetWindowRect(hwnd2, out var rect2); + + // We'll consider the right side of the window being the far right quarter of the window. Adjust the window's rect to match what we want + rect2.left = rect2.right - ((rect2.right - rect2.left) / 4); + + var overlap = rect.left < rect2.right && rect.right > rect2.left && + rect.top < rect2.bottom && rect.bottom > rect2.top; + + return overlap; + } + + private static RECT GetWorkAreaRect() + { + RECT rect = default; + unsafe + { + PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETWORKAREA, 0, &rect, 0); + } + + return rect; + } + + internal static RECT GetMonitorRectForWindow(HWND hWnd) + { + var monitor = PInvoke.MonitorFromWindow(hWnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST); + var monitorInfo = new MONITORINFO { cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFO)) }; + PInvoke.GetMonitorInfo(monitor, ref monitorInfo); + var screenBounds = monitorInfo.rcMonitor; + return screenBounds; + } + + internal static void GetAppInfoUnderMouseCursor(out Process? process, out HWND hwnd) + { + process = null; + + // Grab the window under the cursor and attach to that process + PInvoke.GetCursorPos(out var pt); + hwnd = PInvoke.WindowFromPoint(pt); + + if (hwnd != HWND.Null) + { + var processID = WindowHelper.GetProcessIdFromWindow(hwnd); + + if (processID != 0) + { + process = Process.GetProcessById((int)processID); + } + } + } + + // Only one ContentDialog can be shown at a time, so we have to keep track of the current one. + private static ContentDialog? ContentDialog { get; set; } + + internal static async void ShowTimedMessageDialog(FrameworkElement frameworkElement, string message, string closeButtonText) + { + if (ContentDialog is not null) + { + ContentDialog.Hide(); + ContentDialog = null; + } + + ContentDialog = new ContentDialog + { + XamlRoot = frameworkElement.XamlRoot, + RequestedTheme = frameworkElement.ActualTheme, + Content = message, + CloseButtonText = closeButtonText, + }; + + var timer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(3), + }; + timer.Tick += (s, e) => + { + timer.Stop(); + if (ContentDialog is null) + { + return; + } + + ContentDialog.Hide(); + ContentDialog = null; + }; + + try + { + await ContentDialog.ShowAsync(); + timer.Start(); + ContentDialog.Closed += (s, e) => + { + ContentDialog = null; + }; + } + catch (Exception ex) + { + _log.Error(ex, "Error showing timed message dialog"); + } + } +} diff --git a/tools/PI/DevHome.PI/Helpers/WindowHooker`1.cs b/tools/PI/DevHome.PI/Helpers/WindowHooker`1.cs new file mode 100644 index 0000000000..e821fdd864 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/WindowHooker`1.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using DevHome.PI.Helpers; +using DevHome.PI.Models; +using Serilog; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace DevHome.PI.Helpers; + +internal abstract class WindowHooker +{ + private static nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint newLong) + { + if (IntPtr.Size == 8) + { + return CommonInterop.SetWindowLongPtr64(hWnd, nIndex, newLong); + } + else + { + return (nint)PInvoke.SetWindowLong(hWnd, nIndex, (int)newLong); + } + } + + protected static readonly ILogger Log = Serilog.Log.ForContext("SourceContext", nameof(T)); + + private readonly WNDPROC windowProcHook; + + private HWND listenerHwnd; + + private WNDPROC? originalWndProc; + + protected HWND ListenerHwnd { get => listenerHwnd; set => listenerHwnd = value; } + + internal WindowHooker() + { + windowProcHook = CustomWndProc; + } + + public virtual void Start(HWND listeningWindow) + { + if (ListenerHwnd != HWND.Null) + { + // No-OP if we're already running + Debug.Assert(ListenerHwnd == listeningWindow, "Why are we trying to start with a different hwnd?"); + return; + } + + ArgumentNullException.ThrowIfNull(listeningWindow, nameof(listeningWindow)); + + var wndproc = SetWindowLongPtr(listeningWindow, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(windowProcHook)); + if (wndproc == IntPtr.Zero) + { + Log.Error("SetWindowLongPtr failed: {GetLastError}", Marshal.GetLastWin32Error().ToString(CultureInfo.InvariantCulture)); + return; + } + + originalWndProc = Marshal.GetDelegateForFunctionPointer(wndproc); + ListenerHwnd = listeningWindow; + } + + public virtual void Stop() + { + if (ListenerHwnd != HWND.Null) + { + Debug.Assert(originalWndProc != null, "Where did the original wndproc go?"); + + var result = SetWindowLongPtr(ListenerHwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(originalWndProc)); + if (result == IntPtr.Zero) + { + Log.Error("SetWindowLongPtr failed: {GetLastError}", Marshal.GetLastWin32Error().ToString(CultureInfo.InvariantCulture)); + } + + ListenerHwnd = HWND.Null; + } + } + + protected virtual LRESULT CustomWndProc(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam) + { + return PInvoke.CallWindowProc(originalWndProc, hWnd, msg, wParam, lParam); + } +} diff --git a/tools/PI/DevHome.PI/Images/PI.ico b/tools/PI/DevHome.PI/Images/PI.ico new file mode 100644 index 0000000000..a9b3b3bce9 Binary files /dev/null and b/tools/PI/DevHome.PI/Images/PI.ico differ diff --git a/tools/PI/DevHome.PI/Models/AppRuntimeInfo.cs b/tools/PI/DevHome.PI/Models/AppRuntimeInfo.cs new file mode 100644 index 0000000000..37029c7ba3 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/AppRuntimeInfo.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.PI.Helpers; +using Microsoft.UI.Xaml; + +namespace DevHome.PI.Models; + +public partial class AppRuntimeInfo : ObservableObject +{ + [ObservableProperty] + private int processId = 0; + + [ObservableProperty] + private int basePriority = 0; + + [ObservableProperty] + private int priorityClass = 0; + + [ObservableProperty] + private string mainModuleFileName = string.Empty; + + [ObservableProperty] + private WindowHelper.BinaryType binaryType = WindowHelper.BinaryType.Unknown; + + [ObservableProperty] + private bool isPackaged = false; + + [ObservableProperty] + private bool isStoreApp = false; + + [ObservableProperty] + private bool isRunningAsAdmin = false; + + [ObservableProperty] + private bool isRunningAsSystem = false; + + [ObservableProperty] + private Visibility visibility = Visibility.Visible; + + public ObservableCollection FrameworkTypes { get; } = []; + + public AppRuntimeInfo() + { + // Note these are in alphabetical order of Name. + FrameworkTypes.Add(new FrameworkType("Avalonia.Base.dll", "Avalonia")); + FrameworkTypes.Add(new FrameworkType("DXCore.dll", "DirectX")); + FrameworkTypes.Add(new FrameworkType("Microsoft.Maui.dll", "Maui")); + FrameworkTypes.Add(new FrameworkType("MFC", "MFC", false)); + FrameworkTypes.Add(new FrameworkType("Python.exe", "Python")); + FrameworkTypes.Add(new FrameworkType("Microsoft.Windows.SDK.NET.dll", "Windows App SDK")); + FrameworkTypes.Add(new FrameworkType("System.Windows.Forms.dll", "Windows Forms")); + FrameworkTypes.Add(new FrameworkType("Microsoft.WinUI.dll", "WinUI")); + FrameworkTypes.Add(new FrameworkType("PresentationFramework.dll", "WPF")); + } + + public void CheckFrameworkTypes(string moduleName) + { + foreach (var item in FrameworkTypes) + { + // Skip if already matched. + if (item.IsTypeSupported == true) + { + continue; + } + + if (item.IsExactMatch) + { + if (moduleName.Equals(item.Determinator, StringComparison.OrdinalIgnoreCase)) + { + item.IsTypeSupported = true; + } + } + else + { + if (moduleName.Contains(item.Determinator, StringComparison.OrdinalIgnoreCase)) + { + item.IsTypeSupported = true; + } + } + } + } +} diff --git a/tools/PI/DevHome.PI/Models/ClipboardContents.cs b/tools/PI/DevHome.PI/Models/ClipboardContents.cs new file mode 100644 index 0000000000..8343513eea --- /dev/null +++ b/tools/PI/DevHome.PI/Models/ClipboardContents.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DevHome.PI.Models; + +public class ClipboardContents +{ + public string Raw { get; set; } = string.Empty; + + public string Hex { get; set; } = string.Empty; + + public string Dec { get; set; } = string.Empty; + + public string Code { get; set; } = string.Empty; + + public string Help { get; set; } = string.Empty; + + public void Clear() + { + Raw = string.Empty; + Hex = string.Empty; + Dec = string.Empty; + Code = string.Empty; + Help = string.Empty; + } +} diff --git a/tools/PI/DevHome.PI/Models/ClipboardMonitor.cs b/tools/PI/DevHome.PI/Models/ClipboardMonitor.cs new file mode 100644 index 0000000000..b801ab0628 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/ClipboardMonitor.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using DevHome.Common.Extensions; +using DevHome.PI.Helpers; +using Microsoft.UI.Xaml; +using Serilog; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace DevHome.PI.Models; + +internal sealed class ClipboardMonitor : WindowHooker, INotifyPropertyChanged +{ + public static readonly ClipboardMonitor Instance = new(); + + public ClipboardContents Contents { get; private set; } = new(); + + public event PropertyChangedEventHandler? PropertyChanged; + + internal ClipboardMonitor() + { + } + + private void ClipboardChanged() + { + SafeHandle? h = null; + ClipboardContents newContents = new(); + try + { + var clipboardText = string.Empty; + PInvoke.OpenClipboard(ListenerHwnd); + h = PInvoke.GetClipboardData_SafeHandle(13 /* CF_UNICODETEXT */); + if (!h.IsInvalid) + { + unsafe + { + var p = PInvoke.GlobalLock(h); + clipboardText = Marshal.PtrToStringUni((IntPtr)p) ?? string.Empty; + } + + if (clipboardText != string.Empty) + { + newContents = ParseClipboardContents(clipboardText); + } + } + } + finally + { + if (h is not null && !h.IsInvalid) + { + PInvoke.GlobalUnlock(h); + + // You're not suppose to close this handle. + h.SetHandleAsInvalid(); + } + + PInvoke.CloseClipboard(); + + Contents = newContents; + OnPropertyChanged(nameof(Contents)); + } + } + + /* TODO This pattern matches the following: + 100 + 0x100 + 0x80040005 + -2147221499 + -1 + 0xabc + abc + ffffffff + 1de + cab + bee + + ...but sequences like "cab", "bee", "fed" could be false positives. We need + more logic to exclude these. + */ + private static readonly Regex FindNumbersRegex = + new( + pattern: @"(?:0[xX][0-9A-Fa-f]+|-?\b(?:\d+|\d*\.\d+)\b|\b[0-9A-Fa-f]+\b)", + options: RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private ClipboardContents ParseClipboardContents(string text) + { + ClipboardContents newContents = new(); + + // If this text contains a number, show it in different number bases. + var matches = FindNumbersRegex.Matches(text); + var converter = new Int32Converter(); + + foreach (var match in matches.Cast()) + { + var original = match.ToString(); + + // Assume the number is easily identifable as either base 10 or base 16... convert to int. + int? errorAsInt; + + try + { + if (converter.IsValid(original)) + { + // Int32Converter.ConvertFromString() does a pretty good job of parsing numbers, except when given a hex + // number that isn't prefixed with 0x. If it fails, try parsing it using int.Parse(). + errorAsInt = (int?)converter.ConvertFromString(original); + } + else + { + errorAsInt = int.Parse(original, NumberStyles.HexNumber, CultureInfo.CurrentCulture); + } + } + catch + { + // If this ConvertFromString() function fails due to a bad format, update the above regex to ensure + // the bad string isn't fed to this function. + Log.Warning("Failed to parse \" {original} \" to a number", original); + return newContents; + } + + newContents.Raw = original; + newContents.Hex = errorAsInt is not null ? Convert.ToString((int)errorAsInt, 16) : original; + newContents.Dec = errorAsInt is not null ? Convert.ToString((int)errorAsInt, 10) : original; + + // Is there an error code on here? + // if (ErrorLookupHelper.ContainsErrorCode(text, out var hresult)) + if (errorAsInt is not null) + { + var errors = ErrorLookupHelper.LookupError((int)errorAsInt); + if (errors is not null) + { + foreach (var error in errors) + { + // Seperate each error with a space. These errors aren't localized, so we may not need to worry + // about the space being in the wrong place. + if (newContents.Code != string.Empty) + { + newContents.Code += " "; + newContents.Help += " "; + } + + newContents.Code += error.Name; + newContents.Help += error.Help; + } + } + } + + break; + } + + return newContents; + } + + public void Start() + { + var primaryWindow = Application.Current.GetService(); + Start((HWND)primaryWindow.GetWindowHandle()); + } + + public override void Start(HWND hwndUsedForListening) + { + base.Start(hwndUsedForListening); + + var success = PInvoke.AddClipboardFormatListener(ListenerHwnd); + if (!success) + { + Log.Error("AddClipboardFormatListener failed: {GetLastError}", Marshal.GetLastWin32Error().ToString(CultureInfo.CurrentCulture)); + } + } + + public override void Stop() + { + if (ListenerHwnd != HWND.Null) + { + PInvoke.RemoveClipboardFormatListener(ListenerHwnd); + + base.Stop(); + } + } + + protected override LRESULT CustomWndProc(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam) + { + switch (msg) + { + case PInvoke.WM_CLIPBOARDUPDATE: + { + ThreadPool.QueueUserWorkItem((o) => ClipboardChanged()); + break; + } + } + + return base.CustomWndProc(hWnd, msg, wParam, lParam); + } + + private void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/tools/PI/DevHome.PI/Models/FrameworkType.cs b/tools/PI/DevHome.PI/Models/FrameworkType.cs new file mode 100644 index 0000000000..c243e2e808 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/FrameworkType.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace DevHome.PI.Models; + +public partial class FrameworkType : ObservableObject +{ + internal bool IsExactMatch { get; private set; } = true; + + internal string Determinator + { + get; set; + } + + public string Name + { + get; set; + } + + [ObservableProperty] + private bool isTypeSupported; + + public FrameworkType(string determinator, string name, bool isExactMatch = true) + { + Determinator = determinator; + Name = name; + IsExactMatch = isExactMatch; + } +} diff --git a/tools/PI/DevHome.PI/Models/Insight.cs b/tools/PI/DevHome.PI/Models/Insight.cs new file mode 100644 index 0000000000..61e819d355 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/Insight.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.RegularExpressions; + +namespace DevHome.PI.Models; + +internal enum InsightType +{ + Unknown, + LockedFile, + AccessDeniedFile, + AccessDeniedRegistry, + InvalidPath, + Security, + MemoryViolation, +} + +public sealed class Insight +{ + internal string Title { get; set; } = string.Empty; + + internal string Description { get; set; } = string.Empty; + + internal InsightType InsightType { get; set; } = InsightType.Unknown; + + internal bool IsExpanded { get; set; } +} + +internal sealed class InsightRegex +{ + internal InsightType InsightType { get; set; } + + internal Regex Regex { get; set; } + + internal InsightRegex(InsightType type, Regex regex) + { + Regex = regex; + InsightType = type; + } +} diff --git a/tools/PI/DevHome.PI/Models/NavLink.cs b/tools/PI/DevHome.PI/Models/NavLink.cs new file mode 100644 index 0000000000..2592d1fc82 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/NavLink.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace DevHome.PI.Models; + +public class NavLink +{ + public string IconText { get; internal set; } + + public string ContentText { get; internal set; } + + public NavLink(string icon, string title) + { + IconText = icon; + ContentText = title; + } +} + +public class PageNavLink : NavLink +{ + public Type? PageViewModel { get; internal set; } + + public PageNavLink(string icon, string title, Type? pageViewModel) + : base(icon, title) + { + PageViewModel = pageViewModel; + } +} diff --git a/tools/PI/DevHome.PI/Models/PerfCounters.cs b/tools/PI/DevHome.PI/Models/PerfCounters.cs new file mode 100644 index 0000000000..a5311ee387 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/PerfCounters.cs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using Serilog; + +namespace DevHome.PI.Models; + +public partial class PerfCounters : ObservableObject, IDisposable +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(PerfCounters)); + + public static readonly PerfCounters Instance = new(); + + private const string ProcessCategory = "Process"; + private const string ProcessorCategory = "Processor"; + private const string MemoryCategory = "Memory"; + private const string DiskCategory = "PhysicalDisk"; + + private const string CpuCounterName = "% Processor Time"; + private const string RamCounterName = "Working Set - Private"; + private const string SystemRamCounterName = "Committed Bytes"; + private const string SystemDiskCounterName = "% Disk Time"; + + private const string ReadCounterName = "IO Read Bytes/sec"; + private const string WriteCounterName = "IO Write Bytes/sec"; + private const string GpuEngineName = "GPU Engine"; + private const string UtilizationPercentageName = "Utilization Percentage"; + + private Process? targetProcess; + private PerformanceCounter? cpuCounter; + private List? gpuCounters; + private PerformanceCounter? ramCounter; + private PerformanceCounter? readCounter; + private PerformanceCounter? writeCounter; + + private PerformanceCounter? systemCpuCounter; + private PerformanceCounter? systemRamCounter; + private PerformanceCounter? systemDiskCounter; + + private Timer? timer; + + [ObservableProperty] + private float cpuUsage; + + [ObservableProperty] + private float gpuUsage; + + [ObservableProperty] + private float ramUsageInMB; + + [ObservableProperty] + private float diskUsage; + + [ObservableProperty] + private float networkUsage; + + [ObservableProperty] + private float systemCpuUsage; + + [ObservableProperty] + private float systemRamUsageInGB; + + [ObservableProperty] + private float systemDiskUsage; + + public PerfCounters() + { + TargetAppData.Instance.PropertyChanged += TargetApp_PropertyChanged; + + ThreadPool.QueueUserWorkItem((o) => + { + systemCpuCounter = new PerformanceCounter(ProcessorCategory, CpuCounterName, "_Total", true); + systemRamCounter = new PerformanceCounter(MemoryCategory, SystemRamCounterName, true); + systemDiskCounter = new PerformanceCounter(DiskCategory, SystemDiskCounterName, "_Total", true); + UpdateTargetProcess(TargetAppData.Instance.TargetProcess); + }); + } + + private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(TargetAppData.TargetProcess)) + { + ThreadPool.QueueUserWorkItem((o) => UpdateTargetProcess(TargetAppData.Instance.TargetProcess)); + } + else if (e.PropertyName == nameof(TargetAppData.HasExited)) + { + CloseTargetCounters(); + } + } + + private void UpdateTargetProcess(Process? process) + { + if (process == targetProcess) + { + // Already tracking this process. + return; + } + + CloseTargetCounters(); + + targetProcess = process; + if (targetProcess == null) + { + return; + } + + var processName = targetProcess.ProcessName; + cpuCounter = new PerformanceCounter(ProcessCategory, CpuCounterName, processName, true); + ramCounter = new PerformanceCounter(ProcessCategory, RamCounterName, processName, true); + gpuCounters = GetGpuCounters(targetProcess.Id); + readCounter = new PerformanceCounter(ProcessCategory, ReadCounterName, processName, true); + writeCounter = new PerformanceCounter(ProcessCategory, WriteCounterName, processName, true); + } + + public void Start() + { + Stop(); + timer = new Timer(TimerCallback, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); + } + + public void Stop() + { + timer?.Dispose(); + timer = null; + } + + private void CloseTargetCounters() + { + cpuCounter?.Close(); + cpuCounter?.Dispose(); + cpuCounter = null; + ramCounter?.Close(); + ramCounter?.Dispose(); + ramCounter = null; + + foreach (var counter in gpuCounters ?? Enumerable.Empty()) + { + counter.Close(); + counter.Dispose(); + } + + gpuCounters?.Clear(); + + readCounter?.Close(); + readCounter?.Dispose(); + readCounter = null; + writeCounter?.Close(); + writeCounter?.Dispose(); + writeCounter = null; + } + + public static List GetGpuCounters(int pid) + { + var category = new PerformanceCounterCategory(GpuEngineName); + var counterNames = category.GetInstanceNames(); + var gpuCounters = counterNames + .Where(counterName => counterName.Contains($"pid_{pid}")) + .SelectMany(category.GetCounters) + .Where(counter => counter.CounterName.Equals(UtilizationPercentageName, StringComparison.Ordinal)) + .ToList(); + return gpuCounters; + } + + private void TimerCallback(object? state) + { + try + { + CpuUsage = cpuCounter?.NextValue() / Environment.ProcessorCount ?? 0; + GpuUsage = GetGpuUsage(gpuCounters); + + // Report app memory usage in MB + RamUsageInMB = ramCounter?.NextValue() / (1024 * 1024) ?? 0; + + var readBytesPerSec = readCounter?.NextValue() ?? 0; + var writeBytesPerSec = writeCounter?.NextValue() ?? 0; + var totalDiskBytesPerSec = readBytesPerSec + writeBytesPerSec; + DiskUsage = totalDiskBytesPerSec / (1024 * 1024); + + SystemCpuUsage = systemCpuCounter?.NextValue() ?? 0; + + // Report system memory usage in GB + SystemRamUsageInGB = systemRamCounter?.NextValue() / (1024 * 1024 * 1024) ?? 0; + SystemDiskUsage = systemDiskCounter?.NextValue() ?? 0; + } + catch (Exception ex) + { + _log.Debug(ex, "Failed to update counters."); + } + } + + public static float GetGpuUsage(List? gpuCounters) + { + float result = 0; + try + { + gpuCounters?.ForEach(x => x.NextValue()); + Thread.Sleep(500); + result = gpuCounters?.Sum(x => x.NextValue()) ?? 0; + } + catch (Exception ex) + { + _log.Debug(ex, "Failed to get Gpu usage."); + } + + return result; + } + + public void Dispose() + { + cpuCounter?.Dispose(); + ramCounter?.Dispose(); + readCounter?.Dispose(); + writeCounter?.Dispose(); + + foreach (var counter in gpuCounters ?? Enumerable.Empty()) + { + counter.Dispose(); + } + + GC.SuppressFinalize(this); + } +} diff --git a/tools/PI/DevHome.PI/Models/RestoreState.cs b/tools/PI/DevHome.PI/Models/RestoreState.cs new file mode 100644 index 0000000000..5544f9b65b --- /dev/null +++ b/tools/PI/DevHome.PI/Models/RestoreState.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.PI.Models; + +internal sealed class RestoreState +{ + internal double Left { get; set; } + + internal double Top { get; set; } + + internal double Width { get; set; } + + internal double Height { get; set; } + + internal Orientation BarOrientation { get; set; } + + internal bool IsLargePanelVisible { get; set; } +} diff --git a/tools/PI/DevHome.PI/Models/Setting.cs b/tools/PI/DevHome.PI/Models/Setting.cs new file mode 100644 index 0000000000..f68bbe8dc2 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/Setting.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DevHome.PI.Models; + +public class Setting +{ + public string Path { get; } + + public string Header { get; } + + public string Description { get; } + + public string Glyph { get; } + + public Setting(string path, string header, string description, string glyph) + { + Path = path; + Header = header; + Description = description; + Glyph = glyph; + } +} diff --git a/tools/PI/DevHome.PI/Models/TargetAppData.cs b/tools/PI/DevHome.PI/Models/TargetAppData.cs new file mode 100644 index 0000000000..f05b3ad141 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/TargetAppData.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Security.Principal; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.PI.Helpers; +using Microsoft.UI.Xaml.Media.Imaging; +using Microsoft.Win32.SafeHandles; +using Windows.Graphics.Imaging; +using Windows.Win32; +using Windows.Win32.Foundation; +using static DevHome.PI.Helpers.WindowHelper; + +namespace DevHome.PI.Models; + +public partial class TargetAppData : ObservableObject +{ + public static readonly TargetAppData Instance = new(); + + public int ProcessId => TargetProcess?.Id ?? 0; + + public bool IsRunningAsSystem => TargetProcess?.SessionId == 0; + + public string Title { get; private set; } = string.Empty; + + public bool IsRunningAsAdmin + { + get + { + try + { + SafeFileHandle processToken; + var result = PInvoke.OpenProcessToken(TargetProcess?.SafeHandle, Windows.Win32.Security.TOKEN_ACCESS_MASK.TOKEN_QUERY, out processToken); + if (result != 0) + { + var identity = new WindowsIdentity(processToken.DangerousGetHandle()); + return identity?.Owner?.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid) ?? false; + } + + return false; + } + catch (Win32Exception ex) + { + if (ex.NativeErrorCode == (int)WIN32_ERROR.ERROR_ACCESS_DENIED) + { + return true; + } + + return false; + } + } + } + + [ObservableProperty] + private SoftwareBitmapSource? icon; + + [ObservableProperty] + private string appName = string.Empty; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ProcessId))] + private Process? targetProcess; + + internal HWND HWnd { get; private set; } + + [ObservableProperty] + private bool hasExited; + + private async void GetBitmap(Process process, HWND hWnd) + { + try + { + Bitmap? bitmap = null; + + if (hWnd != HWND.Null) + { + // First check if we can get an icon from the HWND + bitmap = GetAppIcon(hWnd); + } + + if (bitmap is null && process.MainWindowHandle != HWND.Null) + { + // If not, try and grab an icon from the process's main window + bitmap = GetAppIcon((HWND)process.MainWindowHandle); + } + + if (bitmap is null && process.MainModule is not null) + { + // Failing that, try and get the icon from the exe + bitmap = System.Drawing.Icon.ExtractAssociatedIcon(process.MainModule.FileName)?.ToBitmap(); + } + + // Failing that, grab the default app icon + bitmap ??= System.Drawing.Icon.FromHandle(LoadDefaultAppIcon()).ToBitmap(); + + if (bitmap is not null) + { + Icon = await GetWinUI3BitmapSourceFromGdiBitmap(bitmap); + } + else + { + Icon = null; + } + } + catch + { + Icon = null; + } + + return; + } + + private bool IsAppHost(string appName) + { + return string.Equals(appName, "ApplicationFrameHost", StringComparison.OrdinalIgnoreCase); + } + + internal void SetNewAppData(Process process, HWND hWnd) + { + TargetProcess = process; + HWnd = hWnd; + + // Reset hasExited, but don't trigger the property change event. +#pragma warning disable MVVMTK0034 // Direct field reference to [ObservableProperty] backing field + hasExited = false; +#pragma warning restore MVVMTK0034 // Direct field reference to [ObservableProperty] backing field + try + { + // These can throw if we don't have permissions to monitor process state. + TargetProcess.EnableRaisingEvents = true; + TargetProcess.Exited += TargetProcess_Exited; + } + catch + { + } + + Title = GetWindowTitle(hWnd) ?? TargetProcess.MainWindowTitle; + + // Getting the icon will be async + GetBitmap(process, hWnd); + + AppName = IsAppHost(TargetProcess.ProcessName) ? Title : TargetProcess.ProcessName; + + OnPropertyChanged(nameof(AppName)); + OnPropertyChanged(nameof(TargetProcess)); + OnPropertyChanged(nameof(HWnd)); + } + + internal void ClearAppData() + { + Title = string.Empty; + AppName = string.Empty; + Icon = null; + TargetProcess?.Dispose(); + TargetProcess = null; + + OnPropertyChanged(nameof(AppName)); + OnPropertyChanged(nameof(TargetProcess)); + OnPropertyChanged(nameof(HWnd)); + OnPropertyChanged(nameof(Icon)); + } + + private void TargetProcess_Exited(object? sender, EventArgs e) + { + // Change the property, so that we trigger the property change event. + HasExited = true; + } +} diff --git a/tools/PI/DevHome.PI/Models/ThemeName.cs b/tools/PI/DevHome.PI/Models/ThemeName.cs new file mode 100644 index 0000000000..986399780f --- /dev/null +++ b/tools/PI/DevHome.PI/Models/ThemeName.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.UI.Xaml; + +namespace DevHome.PI.Models; + +public class ThemeName +{ + public string Name { get; set; } = string.Empty; + + public ElementTheme Theme { get; set; } + + public ThemeName(string name, ElementTheme theme) => (Name, Theme) = (name, theme); + + public static List Themes { get; private set; } = + [ + new ThemeName("Light", ElementTheme.Light), + new ThemeName("Dark", ElementTheme.Dark), + new ThemeName("Default", ElementTheme.Default) + ]; +} diff --git a/tools/PI/DevHome.PI/Models/WatsonReport.cs b/tools/PI/DevHome.PI/Models/WatsonReport.cs new file mode 100644 index 0000000000..bee4d89c42 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/WatsonReport.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Globalization; + +namespace DevHome.PI.Models; + +public class WatsonReport +{ + private readonly DateTime timeGenerated; + + public string TimeGenerated => timeGenerated.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.CurrentCulture); + + public string Module { get; } + + public string Executable { get; } + + public string EventGuid { get; } + + public string? WatsonLog { get; set; } + + public string? WatsonReportFile { get; set; } + + public WatsonReport(DateTime timeGenerated, string moduleName, string executable, string eventGuid) + { + this.timeGenerated = timeGenerated; + Module = moduleName; + Executable = executable; + EventGuid = eventGuid; + } +} diff --git a/tools/PI/DevHome.PI/Models/WinLogsEntry.cs b/tools/PI/DevHome.PI/Models/WinLogsEntry.cs new file mode 100644 index 0000000000..4ead017e24 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/WinLogsEntry.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Globalization; +using DevHome.PI.Helpers; +using Microsoft.UI; +using Microsoft.UI.Xaml.Media; + +namespace DevHome.PI.Models; + +public class WinLogsEntry +{ + private readonly DateTime timeGenerated; + private readonly WinLogCategory category; + private readonly string errorText = CommonHelper.GetLocalizedString("WinLogCategoryError"); + private readonly string warningText = CommonHelper.GetLocalizedString("WinLogCategoryWarning"); + private readonly string informationText = CommonHelper.GetLocalizedString("WinLogCategoryInformation"); + private readonly string debugText = CommonHelper.GetLocalizedString("WinLogCategoryDebug"); + + public WinLogsEntry(DateTime? time, WinLogCategory category, string message, string toolName) + { + timeGenerated = time ?? DateTime.Now; + this.category = category; + this.Message = message; + this.Tool = toolName; + this.SelectedText = message; + } + + public string TimeGenerated => timeGenerated.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.CurrentCulture); + + public string Tool { get; } + + public string Category => category switch + { + WinLogCategory.Error => errorText, + WinLogCategory.Warning => warningText, + WinLogCategory.Information => informationText, + WinLogCategory.Debug => debugText, + _ => string.Empty, + }; + + public string Message { get; } + + public string SelectedText { get; set; } + + public SolidColorBrush RowColor + { + get + { + switch (category) + { + case WinLogCategory.Error: + return new SolidColorBrush(Colors.Red); + case WinLogCategory.Warning: + return new SolidColorBrush(Colors.Orange); + } + + return new SolidColorBrush(Colors.Black); + } + } +} + +public enum WinLogCategory +{ + Information = 0, + Error, + Warning, + Debug, +} + +public enum WinLogsTool +{ + Unknown = 0, + ETWLogs, + DebugOutput, + EventViewer, + Watson, +} diff --git a/tools/PI/DevHome.PI/NativeMethods.json b/tools/PI/DevHome.PI/NativeMethods.json new file mode 100644 index 0000000000..a6c9ed8b47 --- /dev/null +++ b/tools/PI/DevHome.PI/NativeMethods.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "wideCharOnly": true +} \ No newline at end of file diff --git a/tools/PI/DevHome.PI/NativeMethods.txt b/tools/PI/DevHome.PI/NativeMethods.txt new file mode 100644 index 0000000000..3555f0e25a --- /dev/null +++ b/tools/PI/DevHome.PI/NativeMethods.txt @@ -0,0 +1,105 @@ +AddClipboardFormatListener +CallWindowProc +CloseClipboard +CoCreateInstance +CombineRgn +CreateEllipticRgn +CreateRectRgn +CreateRoundRectRgn +DwmGetWindowAttribute +DwmSetWindowAttribute +DwmSetWindowAttribute +EnumThreadWindows +EnumWindows +GetAncestor +GetBinaryType +GetClassLong +GetClassName +GetClipboardData +GetCurrentPackageFullName +GetCursorPos +GetDesktopWindow +GetForegroundWindow +GetMonitorInfo +GetMonitorInfo +GetOpenFileName +GetTopWindow +GetWindowInfo +GetWindowLong +GetWindowRect +GetWindowText +GetWindowText +GetWindowTextLength +GetWindowTextLength +GetWindowThreadProcessId +GlobalAddAtom +GlobalDeleteAtom +GlobalLock +GlobalUnlock +IsIconic +IsImmersiveProcess +IsWindow +IsWindowVisible +IsZoomed +LoadIcon +LoadImage +MessageBox +MonitorFromWindow +MoveWindow +OpenClipboard +OpenProcessToken +RegisterHotKey +RemoveClipboardFormatListener +SendMessage +SetFocus +SetForegroundWindow +SetParent +SetWindowLong +SetWindowLong +SetWindowPos +SetWindowRgn +SetWinEventHook +ShowWindow +SystemParametersInfo +UnhookWinEvent +UnregisterHotKey +WaitForSingleObject +WindowFromPoint +CF_* +DWMWA_* +DWMWINDOWATTRIBUTE +EVENT_* +EVENT_OBJECT_DESTROY +EVENT_OBJECT_LOCATIONCHANGE +EVENT_SYSTEM_FOREGROUND +GET_ANCESTOR_FLAGS +GET_CLASS_LONG_INDEX +HMONITOR +HWND_* +ICON_* +ICON_* +ICON_* +IDI_* +IMAGE_* +INVALID_HANDLE_VALUE +LR_* +MOD_* +// MONITOR_DEFAULTTONEAREST +SECTION_FLAGS +SW_* +VIRTUAL_KEY +VIRTUAL_KEY +WIN32_ERROR +WINEVENT_* +WINEVENT_OUTOFCONTEXT +WINEVENT_SKIPOWNPROCESS +WM_* +WS_* +RmRegisterResources +RmStartSession +RmGetList +RmEndSession +RM_APP_TYPE +RM_UNIQUE_PROCESS +RM_PROCESS_INFO +RM_REBOOT_REASON \ No newline at end of file diff --git a/tools/PI/DevHome.PI/PI.ico b/tools/PI/DevHome.PI/PI.ico new file mode 100644 index 0000000000..a9b3b3bce9 Binary files /dev/null and b/tools/PI/DevHome.PI/PI.ico differ diff --git a/tools/PI/DevHome.PI/PIApp.xaml b/tools/PI/DevHome.PI/PIApp.xaml new file mode 100644 index 0000000000..2234450bfc --- /dev/null +++ b/tools/PI/DevHome.PI/PIApp.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + 0,0,0,4 + 0,8,0,8 + -12,0,0,0 + + 12 + 12,0 + XamlAutoFontFamily + 20 + + SemiBold + + + + diff --git a/tools/PI/DevHome.PI/PIApp.xaml.cs b/tools/PI/DevHome.PI/PIApp.xaml.cs new file mode 100644 index 0000000000..c295f6fde4 --- /dev/null +++ b/tools/PI/DevHome.PI/PIApp.xaml.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using DevHome.PI.Controls; +using DevHome.PI.Pages; +using DevHome.PI.Services; +using DevHome.PI.Telemetry; +using DevHome.PI.TelemetryEvents; +using DevHome.PI.ViewModels; +using DevHome.Telemetry; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.UI.Xaml; +using Windows.Storage; + +namespace DevHome.PI; + +public partial class App : Application, IApp +{ + // The .NET Generic Host provides dependency injection, configuration, logging, and other services. + // https://docs.microsoft.com/dotnet/core/extensions/generic-host + // https://docs.microsoft.com/dotnet/core/extensions/dependency-injection + // https://docs.microsoft.com/dotnet/core/extensions/configuration + // https://docs.microsoft.com/dotnet/core/extensions/logging + public IHost Host { get; } + + public T GetService() + where T : class => Host.GetService(); + + public Microsoft.UI.Dispatching.DispatcherQueue? UIDispatcher { get; } + + public App() + { + InitializeComponent(); + + UIDispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); + + Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder() + .ConfigureServices((context, services) => + { + // Services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Window + services.AddSingleton(); + + // Views and ViewModels + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Settings sub-pages and viewmodels. + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + }).Build(); + } + + internal static bool IsFeatureEnabled() + { + var isEnabled = false; + + ApplicationData.Current.LocalSettings.Values.TryGetValue($"ExperimentalFeature_ProjectIronsidesExperiment", out var isEnabledObj); + if (isEnabledObj is not null && isEnabledObj is string isEnabledValue) + { + isEnabled = isEnabledValue == "true"; + } + else + { +#if DEBUG + // Override on debug builds to be enabled by default + isEnabled = true; +#endif + } + + return isEnabled; + } + + internal static ITelemetry Logger => TelemetryFactory.Get(); + + internal static void LogTimeTaken(string eventName, uint timeTakenMilliseconds, Guid? relatedActivityId = null) => Logger.LogTimeTaken(eventName, timeTakenMilliseconds, relatedActivityId); + + internal static void LogCritical(string eventName, bool isError = false, Guid? relatedActivityId = null) => Logger.LogCritical(eventName, isError, relatedActivityId); + + internal static void Log(string eventName, LogLevel level, T data, Guid? relatedActivityId = null) + where T : EventBase + { + Logger.Log(eventName, level, data, relatedActivityId ?? null); + } + + internal static void LogError(string eventName, LogLevel level, T data, Guid? relatedActivityId = null) + where T : EventBase + { + Logger.LogError(eventName, level, data, relatedActivityId); + } + + internal static void Log(string eventName, LogLevel level, Guid? relatedActivityId = null) => Logger.Log(eventName, level, new UsageEventData(), relatedActivityId); +} diff --git a/tools/PI/DevHome.PI/Pages/AboutPage.xaml b/tools/PI/DevHome.PI/Pages/AboutPage.xaml new file mode 100644 index 0000000000..a3718bd637 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/AboutPage.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Pages/AboutPage.xaml.cs b/tools/PI/DevHome.PI/Pages/AboutPage.xaml.cs new file mode 100644 index 0000000000..70f9a3c5c8 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/AboutPage.xaml.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Extensions; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.PI.Pages; + +public sealed partial class AboutPage : Page +{ + public AboutViewModel ViewModel { get; } + + public AboutPage() + { + ViewModel = Application.Current.GetService(); + InitializeComponent(); + } +} diff --git a/tools/PI/DevHome.PI/Pages/AdditionalToolsPage.xaml b/tools/PI/DevHome.PI/Pages/AdditionalToolsPage.xaml new file mode 100644 index 0000000000..d5e3784a40 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/AdditionalToolsPage.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Pages/AdditionalToolsPage.xaml.cs b/tools/PI/DevHome.PI/Pages/AdditionalToolsPage.xaml.cs new file mode 100644 index 0000000000..0a0910bebe --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/AdditionalToolsPage.xaml.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Extensions; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.PI.Pages; + +public sealed partial class AdditionalToolsPage : Page +{ + public AdditionalToolsViewModel ViewModel { get; } + + public AdditionalToolsPage() + { + ViewModel = Application.Current.GetService(); + InitializeComponent(); + } +} diff --git a/tools/PI/DevHome.PI/Pages/AdvancedSettingsPage.xaml b/tools/PI/DevHome.PI/Pages/AdvancedSettingsPage.xaml new file mode 100644 index 0000000000..a3adfa16e8 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/AdvancedSettingsPage.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Pages/ProcessListPage.xaml.cs b/tools/PI/DevHome.PI/Pages/ProcessListPage.xaml.cs new file mode 100644 index 0000000000..fd26161e5d --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/ProcessListPage.xaml.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Extensions; +using DevHome.PI; +using DevHome.PI.Telemetry; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace DevHome.PI.Pages; + +public sealed partial class ProcessListPage : Page +{ + private ProcessListPageViewModel ViewModel { get; } + + public ProcessListPage() + { + ViewModel = Application.Current.GetService(); + InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + ViewModel.ResetPage(); + Application.Current.GetService().SwitchTo(Feature.ProcessList); + } +} diff --git a/tools/PI/DevHome.PI/Pages/ResourceUsagePage.xaml b/tools/PI/DevHome.PI/Pages/ResourceUsagePage.xaml new file mode 100644 index 0000000000..6b7eadb630 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/ResourceUsagePage.xaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Pages/ResourceUsagePage.xaml.cs b/tools/PI/DevHome.PI/Pages/ResourceUsagePage.xaml.cs new file mode 100644 index 0000000000..cbf6294c86 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/ResourceUsagePage.xaml.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using DevHome.Common.Extensions; +using DevHome.PI; +using DevHome.PI.Telemetry; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace DevHome.PI.Pages; + +public sealed partial class ResourceUsagePage : Page, IDisposable +{ + private ResourceUsagePageViewModel ViewModel { get; } + + public ResourceUsagePage() + { + ViewModel = Application.Current.GetService(); + InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + Application.Current.GetService().SwitchTo(Feature.ResourceUsage); + } + + public void Dispose() + { + ViewModel.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/tools/PI/DevHome.PI/Pages/SettingsPage.xaml b/tools/PI/DevHome.PI/Pages/SettingsPage.xaml new file mode 100644 index 0000000000..9987cb14c7 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/SettingsPage.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Pages/SettingsPage.xaml.cs b/tools/PI/DevHome.PI/Pages/SettingsPage.xaml.cs new file mode 100644 index 0000000000..e3a33664ca --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/SettingsPage.xaml.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.PI.Pages; + +public sealed partial class SettingsPage : Page +{ + public SettingsPageViewModel ViewModel { get; } + + public SettingsPage() + { + ViewModel = new SettingsPageViewModel(); + InitializeComponent(); + } +} diff --git a/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml b/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml new file mode 100644 index 0000000000..57be24890d --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml.cs b/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml.cs new file mode 100644 index 0000000000..c62c41333a --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using DevHome.Common.Extensions; +using DevHome.PI; +using DevHome.PI.Telemetry; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace DevHome.PI.Pages; + +public sealed partial class WatsonsPage : Page, IDisposable +{ + private WatsonPageViewModel ViewModel { get; } + + public WatsonsPage() + { + ViewModel = Application.Current.GetService(); + InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + Application.Current.GetService().SwitchTo(Feature.WERReports); + } + + public void Dispose() + { + ViewModel.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml new file mode 100644 index 0000000000..e510c2b100 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml.cs b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml.cs new file mode 100644 index 0000000000..fbdec195fa --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using DevHome.Common.Extensions; +using DevHome.PI; +using DevHome.PI.Telemetry; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace DevHome.PI.Pages; + +public partial class WinLogsPage : Page, IDisposable +{ + private WinLogsPageViewModel ViewModel { get; } + + public WinLogsPage() + { + ViewModel = Application.Current.GetService(); + InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + Application.Current.GetService().SwitchTo(Feature.WinLogs); + } + + public void Dispose() + { + ViewModel.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/tools/PI/DevHome.PI/PrimaryWindow.xaml b/tools/PI/DevHome.PI/PrimaryWindow.xaml new file mode 100644 index 0000000000..c49abe5783 --- /dev/null +++ b/tools/PI/DevHome.PI/PrimaryWindow.xaml @@ -0,0 +1,13 @@ + + + + diff --git a/tools/PI/DevHome.PI/PrimaryWindow.xaml.cs b/tools/PI/DevHome.PI/PrimaryWindow.xaml.cs new file mode 100644 index 0000000000..482e8fb67c --- /dev/null +++ b/tools/PI/DevHome.PI/PrimaryWindow.xaml.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using DevHome.PI.Helpers; +using DevHome.PI.Models; +using DevHome.PI.Properties; +using DevHome.Telemetry; +using Microsoft.UI.Xaml; +using Windows.System; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Input.KeyboardAndMouse; +using WinUIEx; +using static DevHome.PI.Helpers.WindowHelper; + +namespace DevHome.PI; + +public sealed partial class PrimaryWindow : WindowEx +{ + private const VirtualKey HotKey = VirtualKey.F12; + + private const HOT_KEY_MODIFIERS KeyModifier = HOT_KEY_MODIFIERS.MOD_WIN; + private HotKeyHelper? hotKeyHelper; + + public BarWindow? DBarWindow { get; private set; } + + public PrimaryWindow() + { + InitializeComponent(); + } + + public void ShowBarWindow() + { + if (DBarWindow == null) + { + DBarWindow = new(); + } + else + { + // Activate is unreliable so use SetForegroundWindow + PInvoke.SetForegroundWindow(DBarWindow.CurrentHwnd); + } + } + + public void ClearBarWindow() + { + DBarWindow = null; + } + + private void Window_Loaded(object sender, RoutedEventArgs e) + { + hotKeyHelper = new(this, HandleHotKey); + hotKeyHelper.RegisterHotKey(HotKey, KeyModifier); + + App.Log("DevHome.PI_MainWindows_Loaded", LogLevel.Measure); + } + + private void WindowEx_Closed(object sender, WindowEventArgs args) + { + DBarWindow?.Close(); + hotKeyHelper?.UnregisterHotKey(); + } + + public void HandleHotKey(int keyId) + { + var hWnd = FindVisibleForegroundWindow(Settings.Default.ExcludedProcesses); + + if (hWnd != IntPtr.Zero) + { + Process? process = null; + + try + { + var processId = GetProcessIdFromWindow(hWnd); + if (processId != 0) + { + process = Process.GetProcessById((int)processId); + } + } + catch + { + } + + if (process == null) + { + // Process must have died before we had a chance to grab it's process object. + return; + } + + TargetAppData.Instance.SetNewAppData(process, hWnd); + } + + DBarWindow ??= new(); + } +} diff --git a/tools/PI/DevHome.PI/Program.cs b/tools/PI/DevHome.PI/Program.cs new file mode 100644 index 0000000000..8fa098611e --- /dev/null +++ b/tools/PI/DevHome.PI/Program.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using DevHome.Common.Helpers; +using DevHome.PI.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.Windows.AppLifecycle; +using Serilog; +using Windows.ApplicationModel.Activation; +using WinRT; + +namespace DevHome.PI; + +public static class Program +{ + private static App? _app; + private static bool firstActivation = true; + + [global::System.Runtime.InteropServices.DllImport("Microsoft.ui.xaml.dll")] + [global::System.Runtime.InteropServices.DefaultDllImportSearchPaths(global::System.Runtime.InteropServices.DllImportSearchPath.SafeDirectories)] + private static extern void XamlCheckProcessRequirements(); + + private const string MainInstanceKey = "mainInstance"; + + private const string ElevatedInstanceKey = "elevatedInstance"; + + [STAThread] + public static void Main(string[] args) + { + // Set up Logging + Environment.SetEnvironmentVariable("DEVHOME_LOGS_ROOT", Path.Join(Common.Logging.LogFolderRoot, "DevHomePI")); + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings_pi.json") + .Build(); + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); + + var stopEvent = new EventWaitHandle(false, EventResetMode.ManualReset, $"DevHomePI-{Environment.ProcessId}"); + ThreadPool.QueueUserWorkItem((o) => + { + var waitResult = stopEvent.WaitOne(); + + _app?.UIDispatcher?.TryEnqueue(() => + { + var primaryWindow = Application.Current.GetService(); + primaryWindow.Close(); + }); + }); + + try + { + XamlCheckProcessRequirements(); + + WinRT.ComWrappersSupport.InitializeComWrappers(); + + var isRedirect = DecideRedirection().GetAwaiter().GetResult(); + + if (!isRedirect) + { + Log.Information("Starting application"); + Application.Start((p) => + { + var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + var context = new DispatcherQueueSynchronizationContext(dispatcherQueue); + SynchronizationContext.SetSynchronizationContext(context); + _app = new App(); + OnActivated(null, AppInstance.GetCurrent().GetActivatedEventArgs()); + }); + } + + stopEvent.Close(); + stopEvent.Dispose(); + } + catch (Exception ex) + { + Log.Fatal(ex, "Application start-up failed"); + } + finally + { + Log.CloseAndFlush(); + } + } + + private static async Task DecideRedirection() + { + var activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + var isElevatedInstancePresent = false; + var isUnElevatedInstancePresent = false; + var instanceList = AppInstance.GetInstances(); + foreach (var appInstance in instanceList) + { + if (appInstance.Key.Equals(MainInstanceKey, StringComparison.OrdinalIgnoreCase)) + { + isUnElevatedInstancePresent = true; + } + else if (appInstance.Key.Equals(ElevatedInstanceKey, StringComparison.OrdinalIgnoreCase)) + { + isElevatedInstancePresent = true; + } + } + + AppInstance instance; + if (isElevatedInstancePresent) + { + // Redirect to the elevated instance if present. + instance = AppInstance.FindOrRegisterForKey(ElevatedInstanceKey); + } + else if (RuntimeHelper.IsCurrentProcessRunningAsAdmin()) + { + // Wait for unelevated instance to exit + while (isUnElevatedInstancePresent) + { + isUnElevatedInstancePresent = false; + instanceList = AppInstance.GetInstances(); + foreach (var appInstance in instanceList) + { + if (appInstance.Key.Equals(MainInstanceKey, StringComparison.OrdinalIgnoreCase)) + { + isUnElevatedInstancePresent = true; + var stopAppInstance = new EventWaitHandle(false, EventResetMode.ManualReset, $"DevHomePI-{appInstance.ProcessId}"); + stopAppInstance.Set(); + } + } + } + + // Register the elevated instance key + instance = AppInstance.FindOrRegisterForKey(ElevatedInstanceKey); + } + else + { + instance = AppInstance.FindOrRegisterForKey(MainInstanceKey); + } + + var isRedirect = false; + if (instance.IsCurrent) + { + instance.Activated += OnActivated; + } + else + { + // Redirect the activation (and args) to the registered instance, and exit. + await instance.RedirectActivationToAsync(activatedEventArgs); + isRedirect = true; + } + + return isRedirect; + } + + private static void OnActivated(object? sender, Microsoft.Windows.AppLifecycle.AppActivationArguments e) + { + if (e.Kind == Microsoft.Windows.AppLifecycle.ExtendedActivationKind.Launch) + { + var commandLine = e.Data.As().Arguments; + + // Convert commandLine into a string array. We just can't split based just on spaces, in case there are spaces inclosed in quotes + // i.e. --application "My App" + var commandLineArgs = Regex.Matches(commandLine, @"[\""].+?[\""]|[^ ]+").Select(m => m.Value).ToArray(); + + // TODO: This should be replaced with system.commandline Microsoft.Extensions.Configuration + // is not intended to be a general purpose commandline parser, but rather only supports /key=value or /key value pairs + var builder = new ConfigurationBuilder(); + builder.AddCommandLine(commandLineArgs); + var config = builder.Build(); + + Process? targetProcess = null; + var targetApp = config["application"]; + var targetPid = config["pid"]; + var pageToExpand = config["expandWindow"]; + + try + { + if (targetApp != null) + { + Debug.Assert(targetApp != string.Empty, "Why is appname empty?"); + + Process[] processes = Process.GetProcessesByName(targetApp); + if (processes.Length > 0) + { + targetProcess = processes[0]; + } + } + else if (targetPid != null) + { + var pid = int.Parse(targetPid, CultureInfo.CurrentCulture); + targetProcess = Process.GetProcessById(pid); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to find target process {TargetApp} {TargetPid}", targetApp, targetPid); + } + + if (config["startuptask"] != null) + { + // Start the app in the background to handle the startup task and register the hotkey + if (firstActivation && !App.IsFeatureEnabled()) + { + // Exit the process if PI Expermental feature is not enabled and its the first activation in the process + Log.Information("Experimental feature is not enabled. Exiting the process."); + Process.GetCurrentProcess().Kill(true); + } + } + else + { + Debug.Assert(_app != null, "Why is _app null on a redirection?"); + + // Be sure to set the target app on the UI thread + _app?.UIDispatcher?.TryEnqueue(() => + { + if (targetProcess != null) + { + TargetAppData.Instance.SetNewAppData(targetProcess, Windows.Win32.Foundation.HWND.Null); + } + + // Show the bar window + var primaryWindow = Application.Current.GetService(); + primaryWindow.ShowBarWindow(); + + if (pageToExpand != null) + { + var barWindow = primaryWindow.DBarWindow; + Debug.Assert(barWindow is not null, "We show the bar window, so it cannot be null here"); + + var pageType = Type.GetType($"DevHome.PI.ViewModels.{pageToExpand}"); + if (pageType is not null) + { + barWindow.NavigateTo(pageType); + } + } + }); + } + } + + firstActivation = false; + } +} diff --git a/tools/PI/DevHome.PI/Properties/PublishProfiles/win-arm64.pubxml b/tools/PI/DevHome.PI/Properties/PublishProfiles/win-arm64.pubxml new file mode 100644 index 0000000000..227cf87736 --- /dev/null +++ b/tools/PI/DevHome.PI/Properties/PublishProfiles/win-arm64.pubxml @@ -0,0 +1,16 @@ + + + + + FileSystem + arm64 + win-arm64 + true + False + False + True + True + + \ No newline at end of file diff --git a/tools/PI/DevHome.PI/Properties/PublishProfiles/win-x64.pubxml b/tools/PI/DevHome.PI/Properties/PublishProfiles/win-x64.pubxml new file mode 100644 index 0000000000..19ae2a6b9c --- /dev/null +++ b/tools/PI/DevHome.PI/Properties/PublishProfiles/win-x64.pubxml @@ -0,0 +1,16 @@ + + + + + FileSystem + x64 + win-x64 + true + False + False + True + True + + \ No newline at end of file diff --git a/tools/PI/DevHome.PI/Properties/PublishProfiles/win-x86.pubxml b/tools/PI/DevHome.PI/Properties/PublishProfiles/win-x86.pubxml new file mode 100644 index 0000000000..dace1fa912 --- /dev/null +++ b/tools/PI/DevHome.PI/Properties/PublishProfiles/win-x86.pubxml @@ -0,0 +1,16 @@ + + + + + FileSystem + x86 + win-x86 + true + False + False + True + True + + \ No newline at end of file diff --git a/tools/PI/DevHome.PI/Properties/Settings.Designer.cs b/tools/PI/DevHome.PI/Properties/Settings.Designer.cs new file mode 100644 index 0000000000..42ed0a5fea --- /dev/null +++ b/tools/PI/DevHome.PI/Properties/Settings.Designer.cs @@ -0,0 +1,196 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace DevHome.PI.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.10.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool IsProcessFilterIncludeConHost { + get { + return ((bool)(this["IsProcessFilterIncludeConHost"])); + } + set { + this["IsProcessFilterIncludeConHost"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool IsProcessFilterIncludeDllHost { + get { + return ((bool)(this["IsProcessFilterIncludeDllHost"])); + } + set { + this["IsProcessFilterIncludeDllHost"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool IsProcessFilterIncludeSvcHost { + get { + return ((bool)(this["IsProcessFilterIncludeSvcHost"])); + } + set { + this["IsProcessFilterIncludeSvcHost"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool IsProcessFilterIncludeWebview { + get { + return ((bool)(this["IsProcessFilterIncludeWebview"])); + } + set { + this["IsProcessFilterIncludeWebview"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool IsProcessFilterIncludeRtb { + get { + return ((bool)(this["IsProcessFilterIncludeRtb"])); + } + set { + this["IsProcessFilterIncludeRtb"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool IsProcessFilterIncludeWmi { + get { + return ((bool)(this["IsProcessFilterIncludeWmi"])); + } + set { + this["IsProcessFilterIncludeWmi"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool IsProcessFilterIncludeWudf { + get { + return ((bool)(this["IsProcessFilterIncludeWudf"])); + } + set { + this["IsProcessFilterIncludeWudf"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool IsProcessFilterIncludeBgTaskHost { + get { + return ((bool)(this["IsProcessFilterIncludeBgTaskHost"])); + } + set { + this["IsProcessFilterIncludeBgTaskHost"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("Default")] + public string CurrentTheme { + get { + return ((string)(this["CurrentTheme"])); + } + set { + this["CurrentTheme"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool IsCpuUsageMonitoringEnabled { + get { + return ((bool)(this["IsCpuUsageMonitoringEnabled"])); + } + set { + this["IsCpuUsageMonitoringEnabled"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool IsInsightsOnStartupEnabled { + get { + return ((bool)(this["IsInsightsOnStartupEnabled"])); + } + set { + this["IsInsightsOnStartupEnabled"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool IsClipboardMonitoringEnabled { + get { + return ((bool)(this["IsClipboardMonitoringEnabled"])); + } + set { + this["IsClipboardMonitoringEnabled"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("\r\n\r\n DevHome.PI\r\n DevEnv\r\n")] + public global::System.Collections.Specialized.StringCollection ExcludedProcesses { + get { + return ((global::System.Collections.Specialized.StringCollection)(this["ExcludedProcesses"])); + } + set { + this["ExcludedProcesses"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0, 0")] + public global::System.Drawing.Size ExpandedLargeSize { + get { + return ((global::System.Drawing.Size)(this["ExpandedLargeSize"])); + } + set { + this["ExpandedLargeSize"] = value; + } + } + } +} diff --git a/tools/PI/DevHome.PI/Properties/Settings.settings b/tools/PI/DevHome.PI/Properties/Settings.settings new file mode 100644 index 0000000000..d4eae755b3 --- /dev/null +++ b/tools/PI/DevHome.PI/Properties/Settings.settings @@ -0,0 +1,55 @@ + + + + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + Default + + + True + + + False + + + True + + + <?xml version="1.0" encoding="utf-16"?> +<ArrayOfString xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <string>DevHome.PI</string> + <string>DevEnv</string> +</ArrayOfString> + + + 0, 0 + + + \ No newline at end of file diff --git a/tools/PI/DevHome.PI/Properties/launchSettings.json b/tools/PI/DevHome.PI/Properties/launchSettings.json new file mode 100644 index 0000000000..42306ffcb9 --- /dev/null +++ b/tools/PI/DevHome.PI/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "DevHome.PI (Unpackaged)": { + "commandName": "Project", + "nativeDebugging": true + } + } +} \ No newline at end of file diff --git a/tools/PI/DevHome.PI/Services/PIAppInfoService.cs b/tools/PI/DevHome.PI/Services/PIAppInfoService.cs new file mode 100644 index 0000000000..c22ed663c5 --- /dev/null +++ b/tools/PI/DevHome.PI/Services/PIAppInfoService.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Reflection; +using System.Security.Principal; +using DevHome.Common.Helpers; +using Windows.ApplicationModel; + +namespace DevHome.PI.Services; + +public class PIAppInfoService +{ + public string IconPath { get; } = Path.Combine(AppContext.BaseDirectory, "Images/PI.ico"); + + public Version GetAppVersion() + { + if (RuntimeHelper.IsMSIX) + { + var packageVersion = Package.Current.Id.Version; + return new(packageVersion.Major, packageVersion.Minor, packageVersion.Build, packageVersion.Revision); + } + else + { + return Assembly.GetExecutingAssembly().GetName().Version!; + } + } +} diff --git a/tools/PI/DevHome.PI/Services/PINavigationService.cs b/tools/PI/DevHome.PI/Services/PINavigationService.cs new file mode 100644 index 0000000000..4f9aff96bc --- /dev/null +++ b/tools/PI/DevHome.PI/Services/PINavigationService.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using DevHome.PI.Contracts.ViewModels; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace DevHome.PI.Services; + +// Similar to DevHome.Services.NavigationService +internal sealed class PINavigationService : INavigationService +{ + private readonly IPageService pageService; + private object? lastParameterUsed; + private Frame? frame; + private string? defaultPage; + + public object? LastParameterUsed => lastParameterUsed; + + public event NavigatedEventHandler? Navigated; + + public Frame? Frame + { + get + { + if (frame == null) + { + var barWindow = Application.Current.GetService().DBarWindow; + frame = barWindow?.GetFrame(); + if (frame is not null) + { + RegisterFrameEvents(); + } + } + + return frame; + } + + set + { + UnregisterFrameEvents(); + frame = value; + RegisterFrameEvents(); + } + } + + public string DefaultPage + { + get => defaultPage ?? typeof(AppDetailsPageViewModel).FullName ?? string.Empty; + set => defaultPage = value; + } + + [MemberNotNullWhen(true, nameof(Frame), nameof(frame))] + public bool CanGoBack => Frame != null && Frame.CanGoBack; + + [MemberNotNullWhen(true, nameof(Frame), nameof(frame))] + public bool CanGoForward => Frame != null && Frame.CanGoForward; + + public PINavigationService(IPageService pageService) + { + this.pageService = pageService; + } + + private void RegisterFrameEvents() + { + if (frame != null) + { + frame.Navigated += OnNavigated; + } + } + + private void UnregisterFrameEvents() + { + if (frame != null) + { + frame.Navigated -= OnNavigated; + } + } + + public bool GoBack() + { + if (CanGoBack) + { + var vmBeforeNavigation = GetPageViewModel(frame); + frame.GoBack(); + if (vmBeforeNavigation is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + + return true; + } + + return false; + } + + public bool GoForward() + { + if (CanGoForward) + { + var vmBeforeNavigation = GetPageViewModel(frame); + frame.GoForward(); + if (vmBeforeNavigation is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + + return true; + } + + return false; + } + + public bool NavigateTo(string pageKey, object? parameter = null, bool clearNavigation = false) + { + var pageType = pageService.GetPageType(pageKey); + + if (frame != null && (frame.Content?.GetType() != pageType || (parameter != null && !parameter.Equals(lastParameterUsed)))) + { + frame.Tag = clearNavigation; + var vmBeforeNavigation = GetPageViewModel(frame); + var navigated = frame.Navigate(pageType, parameter); + if (navigated) + { + lastParameterUsed = parameter; + if (vmBeforeNavigation is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + } + + return navigated; + } + + return false; + } + + private void OnNavigated(object sender, NavigationEventArgs e) + { + if (sender is Frame frame) + { + var clearNavigation = (bool)frame.Tag; + if (clearNavigation) + { + frame.BackStack.Clear(); + } + + if (GetPageViewModel(frame) is INavigationAware navigationAware) + { + navigationAware.OnNavigatedTo(e.Parameter); + } + + Navigated?.Invoke(sender, e); + } + } + + public static object? GetPageViewModel(Frame frame) + { + return frame.Content?.GetType().GetProperty("viewModel")?.GetValue(frame.Content, null); + } +} diff --git a/tools/PI/DevHome.PI/Services/PIPageService.cs b/tools/PI/DevHome.PI/Services/PIPageService.cs new file mode 100644 index 0000000000..ff4d6972ad --- /dev/null +++ b/tools/PI/DevHome.PI/Services/PIPageService.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Services; +using DevHome.PI.Pages; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.PI.Services; + +// Similar to DevHome.Services.PageService +internal sealed class PIPageService : IPageService +{ + private readonly Dictionary pages = new(); + + public PIPageService() + { + Configure(); + Configure(); + Configure(); + Configure(); + Configure(); + Configure(); + Configure(); + Configure(); + + // Settings sub-pages. + Configure(); + Configure(); + Configure(); + Configure(); + } + + public Type GetPageType(string key) + { + Type? pageType; + lock (pages) + { + if (!pages.TryGetValue(key, out pageType)) + { + throw new ArgumentException($"Page not found: {key}. Did you forget to call PageService.Configure?"); + } + } + + return pageType; + } + + public void Configure() + where T_VM : ObservableObject + where T_V : Page + { + lock (pages) + { + var key = typeof(T_VM).FullName!; + if (pages.ContainsKey(key)) + { + throw new ArgumentException($"The key {key} is already configured in PageService"); + } + + var type = typeof(T_V); + if (pages.Any(p => p.Value == type)) + { + throw new ArgumentException($"This type is already configured with key {pages.First(p => p.Value == type).Key}"); + } + + pages.Add(key, type); + } + } +} diff --git a/tools/PI/DevHome.PI/Strings/en-us/Resources.resw b/tools/PI/DevHome.PI/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..df7d94e097 --- /dev/null +++ b/tools/PI/DevHome.PI/Strings/en-us/Resources.resw @@ -0,0 +1,916 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Minimize + Tooltip for the minimize button in the BarWindow + + + Maximize + Tooltip for the maximize button in the BarWindow + + + Restore + Tooltip for the restore button in the BarWindow + + + Pin + A label for the menuitem to pin a tool on the bar. + + + Unregister + The label for Unregister menu flyout for an icon in the toolbar. This will cause the program to no longer be registered with the bar. + + + Switch between horizontal and vertical layout + The tooltip for a button which toggles the orientation of the bar between horizontal and vertical + + + Dissociate so that PI is not tracking any app + The tooltip for a button which causes PI to no longer track any app + + + Expand or collapse the large content panel + The tooltip for a button which expands or collapses the large content panel. + + + Un-snap: make the bar free-floating + The tooltip for a button that causes the bar to unsnap from the current window. + + + Snap the bar to the current foreground window + The tooltip for a button that causes the bar to snap to the current foreground window. + + + Settings + The tooltip for a button that takes the user to the settings for PI. + + + External tools: add/remove, configure and launch + The tooltip for a button that takes the user to the external tools section of PI. + + + Error + The title of an error message dialog when something goes wrong. + + + Failed to launch: {0} + Locked={"{0}"} Text for when there was an error launching an external tool. {0} is the name of an executable(.exe) file. + + + App details + The contents of a text block refering to details about an application. + + + ID + The contents of a text block refering to identity (ID) of an application. + + + Base priority + The contents of a text block refering to base CPU priority of an application. + + + Priority class + The contents of a text block refering to scheduling priority of an application. + + + Main module + This refers to the main module of an application like winword.exe or notepad.exe as an example. + + + Binary type + This refers to the type of application binary, console/windows etc. + + + MSIX packaged + This refers to whether the application is using MSIX packaging technology. + + + Microsoft Store app + This refers to whether the application was acquired via the Microsoft Store. + + + Loaded modules + The contents of a text block refering to loaded application modules. + + + File Version Info + The contents of a text block refering to version of the module file loaded by an application. + + + Memory Size + The contents of a text block refering to size in memory of the module. + + + Filter... + The contents of a combo box that allows editing to filter out elements. + + + BgTaskHost + {Locked}The contents of a check box that refers to specific process names. + + + ConHost + {Locked}The contents of a check box that refers to specific process names. + + + DllHost + {Locked}The contents of a check box that refers to specific process names. + + + SvcHost + {Locked}The contents of a check box that refers to specific process names. + + + MsEdgeWebView2 + {Locked}The contents of a check box that refers to specific process names. + + + RuntimeBroker + {Locked}The contents of a check box that refers to specific process names. + + + WmiPrvSe + {Locked}The contents of a check box that refers to specific process names. + + + WudfHost + {Locked}The contents of a check box that refers to specific process names. + + + Refresh the process list + The contents of a button tool tip that will refresh the contents of the list. + + + PID + Process identifier. + + + Executable + The name of a process (winword.exe or notepad.exe for example). + + + Resource usage + The contents of a text block refering to process resource usage information. + + + Is responding + Refers to whether the application or process is responsive, or if it is stuck. + + + CPU usage + Refers to how much of the central processing unit (CPU) the process is utilizing. + + + GPU usage + Refers to how much of the graphical processing unit (GPU) the process is utilizing. + + + Memory usage + Refers to how much random access memory (RAM) the process is utilizing. + + + Disk usage + Refers to how much disk storage (hard drive/solid state drive) the process is utilizing. + + + WER Reports + The contents of a WER crash dumps. + + + Time generated + The time at which the watson report was generated. + + + Faulting executable + Refers to the process/application executable which caused the fault which generated the crash report + + + Info about the Watson crash + Refers to the Watson diagnostic information about a process crash. + + + Windows logs + The contents of a text block refering to Windows logs. + + + Event Tracing for Windows (ETW) logs + Whether to enable Event Trace for Windows (ETW) logs in the log view output. + + + Debug output from the target app + Tooltip for whether to enable debug output (from OutputDebugString API) in the log view output. + + + Entries from the event log for the target app + Tooltip for whether to enable event viewer logs in the log view output. + + + Entries from the event log for the target app + Tooltip for whether to watson report logs in the log view output. + + + Clear the list + Tooltip for a button that will clear the log list. + + + Category + A header for what category the log entry belongs. + + + Tool + A header for what tool generated the log entry. + + + Message + The contents of a log entry. + + + Could not start OutputDebugString monitor since there is one already running. + An error message indicating that the resource is mutually exclusive due to an instance already running. + + + Tool Name + The name the user can assign to the tool they are adding. + + + Tool path + The name the user can assign to the tool they are adding. + + + Browse + A button to browse the filesystem. + + + Basic arguments + Refers to arguments that can be passed to a process when it is created + + + None + Refers to the type of data being shown + + + Hwnd + Refers to a Windows operating system data type (HWND) a window handle + + + ProcessId + Refers to a Windows operating system process identifier (PID) + + + Prefix (if any) + Refers to arguments that can be passed to a process when it is created + + + Any other arguments + Refers to arguments that can be passed to a process when it is created + + + Sample command line + What the commandline to start the executable/application will look like + + + OK + An affirmative value for a button + + + Cancel + Cancels the current operation the user is performing + + + Name + The name of a tool that could be edited. + + + Unregister + Text on a button for unregistering a tool. + + + Settings + The contents of a text block refering to settings. + + + Preferences + The contents of a text block refering to a user's settings preferences. + + + Theme selection + The contents of a text block refering to an application's color theme. + + + Light + Refers to a color theme or scheme + + + Dark + Refers to a color theme or scheme + + + Windows default + Refers to a color theme or scheme + + + Additional tools + Refers to tools the user has added. + + + Add a tool to the bar + Refers to tools a user could add to the bar UI. + + + Edit the configuration for registered tools + Refers to tools a user could edit that are already registered to the bar UI. + + + Monitor CPU usage for the target app + Refers tracing central processing unit (CPU) usage of the app the user has selected. + + + Show Insights on start-up + Refers showing insightful suggestions to the user about the app on startup. + + + Enable Clipboard monitoring + Refers monitoring the user's clipboard for error messages. + + + Processes to exclude when finding the foreground window. Enter the process names here, comma-separated. + Refers application process names for example notepad.exe, winword.exe etc, in a comma-separated list. + + + Save + Saves the data that the user has input. + + + About + A header for an about page. + + + App Info + The contents of a text block refering to the main window title. + + + File error + The contents of a text block refering to the main window title. + + + The Event Log lists several entries where the app has tried to open a file at 'C:\\Temp\\Scratch\\Foo.jpg'. That folder path exists, and you do have permission to access it, but the file specified does not exist in that path. However, I can see that there is a file with that name at a different folder path: 'C:\\Temp\\Foo.jpg' - perhaps this is the one you're looking for. + The contents of a text label. + + + See more + The contents of a text label. + + + Resource usage + The contents of a text label for computer resource usage (CPU/GPU/Disk/Network etc). + + + Over the last 24 hours, this app has exceeded 30% of CPU usage on 247 separate occasions. If the app is doing compute-intensive operations, this might be reasonable, but you should examine your code and your tests to ensure that this is within expected parameters. + The contents of a text label for computer resource usage (CPU/GPU/Disk/Network etc). + + + See more + The contents of a text label. + + + Error on clipboard + This refers to error codes being detected on the clipboard (HRESULT/NTSTATUS failures etc) + + + Hex + This refers to a short hand for hexadecimal number notation. + + + Dec + This refers to a short hand for decimal number notation. + + + Code + This refers to a short hand for an error code representation of the contents of the clipboard. + + + Is running as Admin + This refers to whether the application is running as admin + + + Is running as System + This refers to whether the application is running as System + + + Run PI as Admin to get more data + When the user clicks this button we relaunch PI as admin to get more app details + + + Go to the key insights + The tooltip for a button that takes the user to the key insights page + + + Process list + The contents of a text block referring to Process list. + + + {0} (terminated) + Locked={"{0}"} Text indicating that the process has terminated. {0} is the process name. + + + Insights + The contents of a text block referring to Insights. + + + Help + Description of the error code + + + pid: {0:0.0} + Locked={"{0:0.0}"} Showing a process identifier. {0} is a percentage 0-100 + + + cpu: {0:0.0}% + Locked={"{0:0.0}"} Showing CPU utilization. {0} is a percentage 0-100 + + + memory: {0:N2} MB + Locked={"{0:N2}"} Showing RAM utilization. MB stands for megabytes. {0} is a number. + + + dsk: {0:N0} MB/s + Locked={"{0:N0}"} Showing disk utilization. MB/s stands for megabytes per second. {0} is a number. + + + cpu: n/a % + Showing CPU utilization which is not available/applicable. + + + memory: n/a MB + Showing RAM utilization which is not available/applicable. MB stands for megabytes. + + + dsk: n/a MB/s + Showing disk utilization which is not available/applicable. MB/s stands for megabytes per second. + + + {0} CPU usage: {1:0.0}% + Locked={"{0},{1:0.0}"} A notification for CPU usage. {0} is the name of a process (notepad.exe for example). {1} is a percentage + + + GOT UNHANDLED EVENT: {0} + Locked={"{0}"} An error message when an unhandled trace event is encountered. {0} is a verbose version of the event + + + Could not start EventViewer + An error message when the program is unable to start the Windows Event Viewer + + + Locked file + Describes a computer file that is locked for exclusive access + + + The app is trying to access the file '{0}', but this is locked by '{1}'. To fix this, close '{1}' + Locked={"{0}", "{1}"} A description to help a user that is dealing with a file locked for exclusive access. {0} is a file name. {1} is the name of an executable(.exe) process. + + + The app is trying to access the file '{0}', but this is locked by an unknown process + Locked={"{0}"} A description to help a user that is dealing with a file locked for exclusive access. {0} is a file name. + + + Could not start Watson + Refers to being unable to start monitoring the Watson error logs. + + + Error + Refers to an error level message. + + + Warning + Refers to a warning level message. + + + Information + Refers to an information level message. + + + Debug + Refers to a debug level message. + + + Preferences + Refers to a user's settings preferences. + + + Additional tools + Refers to a additional executable tools the user can add to the product + + + Advanced settings + Refers to a additional more complex settings. + + + About + Refers to additional information about the product. + + + {0:0.0}% + Locked={"{0:0.0}"} Showing CPU utilization. {0} is a percentage 0-100 + + + {0:N0} MB/s + Locked={"{0:N0}"} Showing disk utilization. MB/s stands for megabytes per second. {0} is a number. + + + {0:0.0}% + Locked={"{0:0.0}"} Showing GPU utilization. {0} is a percentage 0-100 + + + {0:N2} MB + Locked={"{0:N2}"} Showing RAM utilization. MB stands for megabytes. {0} is a number. + + + CPU usage of the attached process. + Refers to the amount of CPU a process is using, shown in the bar. + + + Process ID of the attached process. + Refers to the app's PID in the bar + + + {0:0.0}% + Locked={"{0:0.0}"} Showing Disk utilization. {0} is a percentage 0-100 + + + CPU usage of the attached process. + Refers to the amount of CPU a process is using, shown in the bottom bar. + + + Disk usage of the attached process. + Refers to the amount of disk usage a process is using, shown in the bottom bar. + + + Memory usage of the attached process. + Refers to the amount of Memory a process is using, shown in the bottom bar. + + + Process ID of the attached process. + Refers to the app's PID in the bottom bar + + + {0:N2} GB + Locked={"{0:N2}"} Showing RAM utilization. GB stands for gigabytes. {0} is a number. + + + Amount of CPU used by the overall system. + Tooltip for the system CPU usage box in the bar + + + Amount of disk utilization by the overall system. + Tooltip for the system disk usage box in the bar + + + Amount of committed memory used by the overall system. + Tooltip for the system memory usage box in the bar + + + Project Ironsides + {Locked}The name of the PI application + + + Dev Home Project Ironsides + {Locked}The description of the PI application + + + Hotkey for Dev Home PI + The description of the startup task for Dev Home PI. This shows up in task manager/Windows Settings > Startup Apps. + + + Is Python + This refers to whether the application is a Python app. + + + Memory violation + Describes an issue related to a memory access violation. + + + This is a Python app that is using the ctypes foreign function library. This provides C-compatible data types, and allows calling functions in DLLs, and using raw pointers. A memory access violation can result if the code misuses pointers. + Describes an issue with a specific Python memory access violation. + + + Security issue + Describes an issue related to security. + + + Faulting module + Refers to the module which caused the fault which generated the crash report + + + Error {0}: {1} + Locked={"{0}", "{1}"} A description to help a user that is dealing with non-specific error situation. {0} is the error name. {1} is the error description. + + + Framework types used in this app + This refers to the set of application frameworks this application uses. + + + Base address + The contents of a text block referring to the base address of a loaded module. + + + Entrypoint address + The contents of a text block referring to the entrypoint address of a loaded module. + + + Pinned + Text to show when the switch is turned on, to pin the tool to the bar. + + + Not pinned + Text to show when the switch is turned off, to not pin the tool to the bar. + + + Unpin + A label for the menuitem to unpin a tool on the bar. + + + About + Header for the Settings item for product information and privacy statement. + + + Info, Privacy Statement + Sub-header for the Settings item for product information and privacy statement. + + + Additional tools + Header for the Settings item for configuring additional tools. + + + Add or edit tool configurations + Sub-header for the Settings item for configuring additional tools. + + + Advanced settings + Header for the Settings item for configuring advanced features. + + + Configure advanced features + Sub-header for the Settings item for configuring advanced features. + + + Preferences + Header for the Settings item for user preferences. + + + Themes + Sub-header for the Settings item for user preferences. + + + Settings + The title of the main Settings page. + + + Theme + Title for the theme settings sub-page item. + + + Dark + Refers to a color theme or scheme. + + + Windows default + Refers to a color theme or scheme. + + + Light + Refers to a color theme or scheme. + + + Dev Home PI + This is the name of the product. + + + © Microsoft. All rights reserved. + This is the copyright notice for the product. + + + Related links + Header for a set of hyperlinks to information about the product. + + + Documentation + Label for the link to the documentation for the product. + + + https://github.com/microsoft/devhome/issues/2796 + Link to the documentation for the product. + + + Privacy Statement + Label for the link to the standard privacy policy statement. + + + https://go.microsoft.com/fwlink/?linkid=2234882 + Link to the standard privacy policy statement. + + + Source code + Label for the link to the source code for the product. + + + https://go.microsoft.com/fwlink/?linkid=2234396 + Link to the source code for the product. + + + Register an external tool with Dev Home PI + Header for the settings item to add a tool to the product. + + + Edit or unregister existing tools + Header for the setting item to edit or unregister tools from the product. + + + The starting priority for threads created within this process + Tooltip for the Base Priority field. + + + The CPU architecture of the target process + Tooltip for the Binary Type field. + + + The windowing or application frameworks used in this app + Tooltip for the Framework Types field. + + + The process ID of the target process + Tooltip for the PID field. + + + True if the target process is running under an Administrator account + Tooltip for the Running as Admin field. + + + True if the target process is running as a System process, based on its Session ID + Tooltip for the Running as System field. + + + The executable used to start the process + Tooltip for the Main Module field. + + + True if this app was deployed via the Microsoft Store + Tooltip for the Microsoft Store field. + + + True if this process is packaged with MSIX + Tooltip for the MSIX Packaged field. + + + Threads with different priorities in the process run relative to the priority class of the process + Tooltip for the Priority Class field. + + + Close + Text used on the Close button on a message dialog. + + + External tool {0} registered + Message shown to the user when an external tool is registered. + + + Executable + The executable path of an external tool. + + + Invalid tool path or name + Text for the message to show to the user when the supplied path or name is not valid. + + + Select a process to investigate + Describes using a button in PI's UX to investigate another process running on the system + + + Attach the diagnostic bar to the target application + Describes using a button in PI's UX + + + Dev Home PI + The product name + + + Close + Describes a button that will close PI's window + + \ No newline at end of file diff --git a/tools/PI/DevHome.PI/Telemetry/FeatureState.cs b/tools/PI/DevHome.PI/Telemetry/FeatureState.cs new file mode 100644 index 0000000000..a9ad779703 --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/FeatureState.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using DevHome.PI.Telemetry; +using Microsoft.Diagnostics.Tracing.Parsers.MicrosoftWindowsWPF; +using Windows.Foundation.Metadata; +using static DevHome.PI.Telemetry.FeatureState; + +namespace DevHome.PI.Telemetry; + +// NOTE: The below flags can never be deleted. We should only rename features that were removed from the product. +// The enum can only take additions over time. +[Flags] +internal enum Feature +{ + None = 0x0, + AppDetails = 0x1, + ResourceUsage = 0x2, + LoadedModule = 0x4, + WERReports = 0x8, + WinLogs = 0x10, + ProcessList = 0x20, + Insights = 0x40, + InsightsAtStartup = 0x80, + MonitorCPU = 0x100, +} + +internal enum FeatureShareType +{ + Exclusive, + Shared, +} + +internal sealed class FeatureState +{ + internal static readonly Dictionary Features = new() + { + { Feature.AppDetails, FeatureShareType.Exclusive }, + { Feature.ResourceUsage, FeatureShareType.Exclusive }, + { Feature.LoadedModule, FeatureShareType.Exclusive }, + { Feature.WERReports, FeatureShareType.Exclusive }, + { Feature.WinLogs, FeatureShareType.Exclusive }, + { Feature.ProcessList, FeatureShareType.Exclusive }, + { Feature.Insights, FeatureShareType.Exclusive }, + { Feature.InsightsAtStartup, FeatureShareType.Shared }, + { Feature.MonitorCPU, FeatureShareType.Shared }, + }; + + internal static bool IsExclusive(Feature feature) + { + if (FeatureState.Features[feature] == FeatureShareType.Exclusive) + { + return true; + } + + return false; + } +} + +public static class EnumExtensions +{ + public static IEnumerable GetFlags(this T en) + where T : struct, Enum + { + return Enum.GetValues().Where(member => en.HasFlag(member)).ToArray(); + } +} diff --git a/tools/PI/DevHome.PI/Telemetry/FocusStartEventData.cs b/tools/PI/DevHome.PI/Telemetry/FocusStartEventData.cs new file mode 100644 index 0000000000..0e24d073d9 --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/FocusStartEventData.cs @@ -0,0 +1,19 @@ +// 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.PI.TelemetryEvents; + +[EventData] +public class FocusStartEventData : TimedStartEventBase +{ + internal FocusStartEventData(string featureName, DateTime featureStartTime) + : base(featureName, featureStartTime) + { + } +} diff --git a/tools/PI/DevHome.PI/Telemetry/FocusStopEventData.cs b/tools/PI/DevHome.PI/Telemetry/FocusStopEventData.cs new file mode 100644 index 0000000000..adf32b5dca --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/FocusStopEventData.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; + +namespace DevHome.PI.TelemetryEvents; + +[EventData] +public class FocusStopEventData : TimedStopEventBase +{ + internal FocusStopEventData(string featureName, DateTime featureStartTime) + : base(featureName, featureStartTime) + { + } +} diff --git a/tools/PI/DevHome.PI/Telemetry/LogStateChangedEventData.cs b/tools/PI/DevHome.PI/Telemetry/LogStateChangedEventData.cs new file mode 100644 index 0000000000..6e19bd4fb5 --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/LogStateChangedEventData.cs @@ -0,0 +1,30 @@ +// 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.PI.TelemetryEvents; + +[EventData] +public class LogStateChangedEventData : EventBase +{ + internal LogStateChangedEventData(string stateName, string stateValue) + { + StateName = stateName; + StateValue = stateValue; + } + + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage; + + public string StateName { get; private set; } + + public string StateValue { get; private set; } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + } +} diff --git a/tools/PI/DevHome.PI/Telemetry/TelemetryReporter.cs b/tools/PI/DevHome.PI/Telemetry/TelemetryReporter.cs new file mode 100644 index 0000000000..2df58496c8 --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/TelemetryReporter.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using DevHome.PI; +using DevHome.PI.Models; +using DevHome.PI.Telemetry; +using DevHome.PI.TelemetryEvents; +using DevHome.Telemetry; +using Microsoft.UI.Xaml; +using Serilog; +using WinUIEx; +using static DevHome.PI.Telemetry.FeatureState; + +namespace DevHome.PI.Telemetry; + +internal sealed class TelemetryReporter : IDisposable +{ + private readonly FeatureState currentFeatureState = new(); + private WindowEventGenerator? eventGenerator; + private static readonly Mutex FeatureMutex = new(); + private static readonly ILogger Log = Serilog.Log.ForContext("SourceContext", nameof(TelemetryReporter)); + + private static Feature currentFeatureSet; + + internal static Feature CurrentFeatureSet => currentFeatureSet; + + public TelemetryReporter() + { + } + + internal static void SetWindow(Window win) + { + ArgumentNullException.ThrowIfNull(win, nameof(win)); + + // Make sure to apply all this to the already existing service object. + var rep = Application.Current.GetService(); + rep.eventGenerator = new WindowEventGenerator(win); + rep.eventGenerator.InteractiveUsageVisibilityEvent += rep.InteractiveUsageVisibilityEvent; + rep.eventGenerator.InteractiveUsageFocusEvent += rep.InteractiveUsageFocusEvent; + } + + public void SwitchTo(Feature requestedFeature) + { + if (FeatureMutex.WaitOne()) + { + if (FeatureState.IsExclusive(requestedFeature)) + { + var foundFirst = false; + + // Use a copy of the flags, since we'll be modifying them inside the loop. + var currentFeatureSetIterator = currentFeatureSet.GetFlags(); + foreach (var flag in currentFeatureSetIterator) + { + // Skip empty flag. + if (flag == Feature.None) + { + continue; + } + + if (foundFirst) + { + continue; + } + + // If the requested feature is exclusive and the current iteration feature is exclusive, + // remove it as we're about to add an exclusive. + if (FeatureState.IsExclusive(flag)) + { + // First fire any events to stop the previous features telemetry. + // Debug.Assert(eventGenerator != null, "eventGenerator is null"); + if (eventGenerator != null) + { + var eventArgs = new WindowEventGenerator.InteractiveUsageEventArgs(WindowEventGenerator.InteractiveUsageEventType.Stop); + if (eventGenerator.CurrentlyVisible) + { + InteractiveUsageVisibilityEvent(this, eventArgs); + } + + if (eventGenerator.CurrentlyFocused) + { + InteractiveUsageFocusEvent(this, eventArgs); + } + } + + // Remove existing exclusive features. + currentFeatureSet &= ~flag; + + Log.Debug("Removed an exclusive feature {0}.", flag); + Log.Debug("currentFeatureSet = {0}", currentFeatureSet.ToString()); + + // Leave a mark that we've encountered our first exclusive feature. There should only ever be one. + foundFirst = true; + } + } + } + + // Add the feature to the set. + currentFeatureSet |= requestedFeature; + Log.Debug("Added the exclusive feature {0}.", requestedFeature); + Log.Debug("currentFeatureSet = {0}", currentFeatureSet.ToString()); + } + + // TODO: Codepath probably should throw exception... (failed to acquire mutex...) + } + + public Feature CurrentExclusive + { + get + { + foreach (var flag in currentFeatureSet.GetFlags()) + { + // Skip empty flag. + if (flag == Feature.None) + { + continue; + } + + // Return the first exclusive. + if (FeatureState.IsExclusive(flag)) + { + return currentFeatureSet & flag; + } + } + + return Feature.None; + } + } + + private void InteractiveUsageVisibilityEvent(object? sender, WindowEventGenerator.InteractiveUsageEventArgs e) + { + var app = App.Current as DevHome.PI.App; + if (app != null) + { + var featureName = CurrentExclusive.ToString(); + + if (e.UsageType == WindowEventGenerator.InteractiveUsageEventType.Start) + { + App.Log("DevHome.PI_VisibilityStart", LogLevel.Measure, new VisibilityStartEventData(featureName, DateTime.Now)); + Log.Debug("Visibility Start!"); + } + else + { + App.Log("DevHome.PI_VisibilityStop", LogLevel.Measure, new VisibilityStopEventData(featureName, DateTime.Now)); + Log.Debug("Visibility Stop"); + } + } + } + + private void InteractiveUsageFocusEvent(object? sender, WindowEventGenerator.InteractiveUsageEventArgs e) + { + var app = App.Current as DevHome.PI.App; + if (app != null) + { + var featureName = CurrentExclusive.ToString(); + + if (e.UsageType == WindowEventGenerator.InteractiveUsageEventType.Start) + { + App.Log("DevHome.PI_FocusStart", LogLevel.Measure, new VisibilityStartEventData(featureName, DateTime.Now)); + Log.Debug("Focus Start!"); + } + else + { + App.Log("DevHome.PI_FocusStop", LogLevel.Measure, new VisibilityStopEventData(featureName, DateTime.Now)); + Log.Debug("Focus Stop"); + } + } + } + + public void Dispose() + { + eventGenerator?.Dispose(); + } +} diff --git a/tools/PI/DevHome.PI/Telemetry/TimedStartEventBase.cs b/tools/PI/DevHome.PI/Telemetry/TimedStartEventBase.cs new file mode 100644 index 0000000000..a111567df2 --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/TimedStartEventBase.cs @@ -0,0 +1,30 @@ +// 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.PI.TelemetryEvents; + +[EventData] +public class TimedStartEventBase : EventBase +{ + protected TimedStartEventBase(string featureName, DateTime featureStartTime) + { + Name = featureName; + StartTime = featureStartTime; + } + + protected string Name { get; private set; } + + protected DateTime StartTime { get; private set; } + + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage; + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + } +} diff --git a/tools/PI/DevHome.PI/Telemetry/TimedStopEventBase.cs b/tools/PI/DevHome.PI/Telemetry/TimedStopEventBase.cs new file mode 100644 index 0000000000..914c50cead --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/TimedStopEventBase.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using System.Xml.Linq; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; + +namespace DevHome.PI.TelemetryEvents; + +[EventData] +public class TimedStopEventBase : EventBase +{ + internal TimedStopEventBase(string featureName, DateTime featureStopTime) + { + Name = featureName; + StopTime = featureStopTime; + } + + public string Name { get; private set; } + + public DateTime StopTime { get; private set; } + + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage; + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + } +} diff --git a/tools/PI/DevHome.PI/Telemetry/UsageEventData.cs b/tools/PI/DevHome.PI/Telemetry/UsageEventData.cs new file mode 100644 index 0000000000..e5c0ec8086 --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/UsageEventData.cs @@ -0,0 +1,21 @@ +// 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.PI.TelemetryEvents; + +// Used to produce basic single-line telemetry events based on a simple string. +[EventData] +public class UsageEventData : EventBase +{ + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage; + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + } +} diff --git a/tools/PI/DevHome.PI/Telemetry/VisibilityStartEventData.cs b/tools/PI/DevHome.PI/Telemetry/VisibilityStartEventData.cs new file mode 100644 index 0000000000..66c77f46c1 --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/VisibilityStartEventData.cs @@ -0,0 +1,19 @@ +// 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.PI.TelemetryEvents; + +[EventData] +public class VisibilityStartEventData : TimedStartEventBase +{ + internal VisibilityStartEventData(string featureName, DateTime featureStartTime) + : base(featureName, featureStartTime) + { + } +} diff --git a/tools/PI/DevHome.PI/Telemetry/VisibilityStopEventData.cs b/tools/PI/DevHome.PI/Telemetry/VisibilityStopEventData.cs new file mode 100644 index 0000000000..bd9a00af9a --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/VisibilityStopEventData.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using System.Xml.Linq; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; + +namespace DevHome.PI.TelemetryEvents; + +[EventData] +public class VisibilityStopEventData : TimedStopEventBase +{ + internal VisibilityStopEventData(string featureName, DateTime featureStartTime) + : base(featureName, featureStartTime) + { + } +} diff --git a/tools/PI/DevHome.PI/Telemetry/WindowEventGenerator.cs b/tools/PI/DevHome.PI/Telemetry/WindowEventGenerator.cs new file mode 100644 index 0000000000..559dc3f618 --- /dev/null +++ b/tools/PI/DevHome.PI/Telemetry/WindowEventGenerator.cs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading; +using DevHome.Common.Extensions; +using DevHome.PI.Helpers; +using Microsoft.UI.Xaml; +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace DevHome.Telemetry; + +internal sealed class WindowEventGenerator : WindowHooker, IDisposable +{ + private readonly Window window; + private readonly Mutex visibilityMutex = new(); + private readonly Mutex focusMutex = new(); + private bool currentlyVisible; + private bool currentlyFocused; + + internal bool CurrentlyVisible => currentlyVisible; + + internal bool CurrentlyFocused => currentlyFocused; + + internal enum InteractiveUsageEventType + { + Start, + Stop, + } + + internal sealed class InteractiveUsageEventArgs : EventArgs + { + internal InteractiveUsageEventArgs(InteractiveUsageEventType usageType) + { + UsageType = usageType; + } + + public InteractiveUsageEventType UsageType { get; private set; } + } + + public event System.EventHandler? InteractiveUsageVisibilityEvent; + + public event System.EventHandler? InteractiveUsageFocusEvent; + + public WindowEventGenerator(Window window) + { + this.window = window; + Start(new HWND(window.GetWindowHandle())); + } + + private void FireInteractiveUsageVisibilityEvent(bool newValue) + { + InteractiveUsageEventArgs interactiveArgs; + var oldValue = false; + var triggerEvent = false; + { + if (visibilityMutex.WaitOne()) + { + oldValue = currentlyVisible; + + if (newValue != oldValue) + { + triggerEvent = true; + currentlyVisible = newValue; + } + } + } + + if (triggerEvent) + { + if (newValue) + { + interactiveArgs = new InteractiveUsageEventArgs(InteractiveUsageEventType.Start); + } + else + { + interactiveArgs = new InteractiveUsageEventArgs(InteractiveUsageEventType.Stop); + } + + var raiseEvent = InteractiveUsageVisibilityEvent; + if (raiseEvent != null) + { + raiseEvent(this, interactiveArgs); + } + } + } + + private void FireInteractiveUsageFocusEvent(bool newValue) + { + InteractiveUsageEventArgs interactiveArgs; + var oldValue = false; + var triggerEvent = false; + { + if (focusMutex.WaitOne()) + { + oldValue = currentlyFocused; + + if (newValue != oldValue) + { + triggerEvent = true; + currentlyFocused = newValue; + } + } + } + + if (triggerEvent) + { + if (newValue) + { + interactiveArgs = new InteractiveUsageEventArgs(InteractiveUsageEventType.Start); + } + else + { + interactiveArgs = new InteractiveUsageEventArgs(InteractiveUsageEventType.Stop); + } + + var raiseEvent = InteractiveUsageFocusEvent; + if (raiseEvent != null) + { + raiseEvent(this, interactiveArgs); + } + } + } + + protected override LRESULT CustomWndProc(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam) + { + switch (msg) + { + case PInvoke.WM_ACTIVATE: + { + if (wParam == 0) + { + FireInteractiveUsageVisibilityEvent(false); + } + else + { + FireInteractiveUsageVisibilityEvent(true); + } + + break; + } + + case PInvoke.WM_SETFOCUS: + { + FireInteractiveUsageFocusEvent(true); + break; + } + + case PInvoke.WM_KILLFOCUS: + { + FireInteractiveUsageFocusEvent(false); + break; + } + } + + return base.CustomWndProc(hWnd, msg, wParam, lParam); + } + + public void Dispose() + { + visibilityMutex.Dispose(); + focusMutex.Dispose(); + } +} diff --git a/tools/PI/DevHome.PI/ViewModels/AboutViewModel.cs b/tools/PI/DevHome.PI/ViewModels/AboutViewModel.cs new file mode 100644 index 0000000000..4222d747e5 --- /dev/null +++ b/tools/PI/DevHome.PI/ViewModels/AboutViewModel.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Extensions; +using DevHome.Common.Models; +using DevHome.PI.Helpers; +using DevHome.PI.Services; +using Microsoft.UI.Xaml; +using Serilog; + +namespace DevHome.PI.ViewModels; + +public partial class AboutViewModel : ObservableObject +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(AboutViewModel)); + + public ObservableCollection Breadcrumbs { get; } + + [ObservableProperty] + private string versionDescription; + + public AboutViewModel() + { + versionDescription = GetVersionDescription(); + + Breadcrumbs = new ObservableCollection + { + new(CommonHelper.GetLocalizedString("SettingsPageHeader"), typeof(SettingsPageViewModel).FullName!), + new(CommonHelper.GetLocalizedString("SettingsAboutHeader"), typeof(AboutViewModel).FullName!), + }; + } + + private static string GetVersionDescription() + { + var appInfoService = Application.Current.GetService(); + var version = appInfoService.GetAppVersion(); + return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; + } +} diff --git a/tools/PI/DevHome.PI/ViewModels/AdditionalToolsViewModel.cs b/tools/PI/DevHome.PI/ViewModels/AdditionalToolsViewModel.cs new file mode 100644 index 0000000000..6766217e29 --- /dev/null +++ b/tools/PI/DevHome.PI/ViewModels/AdditionalToolsViewModel.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Models; +using DevHome.PI.Helpers; +using Serilog; + +namespace DevHome.PI.ViewModels; + +public partial class AdditionalToolsViewModel : ObservableObject +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(AdditionalToolsViewModel)); + + public ObservableCollection Breadcrumbs { get; } + + public AdditionalToolsViewModel() + { + Breadcrumbs = new ObservableCollection + { + new(CommonHelper.GetLocalizedString("SettingsPageHeader"), typeof(SettingsPageViewModel).FullName!), + new(CommonHelper.GetLocalizedString("SettingsAdditionalToolsHeader"), typeof(AdditionalToolsViewModel).FullName!), + }; + } +} diff --git a/tools/PI/DevHome.PI/ViewModels/AdvancedSettingsViewModel.cs b/tools/PI/DevHome.PI/ViewModels/AdvancedSettingsViewModel.cs new file mode 100644 index 0000000000..9e9d9c95e9 --- /dev/null +++ b/tools/PI/DevHome.PI/ViewModels/AdvancedSettingsViewModel.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Models; +using DevHome.PI.Helpers; +using Serilog; + +namespace DevHome.PI.ViewModels; + +public partial class AdvancedSettingsViewModel : ObservableObject +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(AdvancedSettingsViewModel)); + + public ObservableCollection Breadcrumbs { get; } + + public AdvancedSettingsViewModel() + { + Breadcrumbs = new ObservableCollection + { + new(CommonHelper.GetLocalizedString("SettingsPageHeader"), typeof(SettingsPageViewModel).FullName!), + new(CommonHelper.GetLocalizedString("SettingsAdvancedSettingsHeader"), typeof(AdvancedSettingsViewModel).FullName!), + }; + } +} diff --git a/tools/PI/DevHome.PI/ViewModels/AppDetailsPageViewModel.cs b/tools/PI/DevHome.PI/ViewModels/AppDetailsPageViewModel.cs new file mode 100644 index 0000000000..c3699e389c --- /dev/null +++ b/tools/PI/DevHome.PI/ViewModels/AppDetailsPageViewModel.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.ComponentModel; +using System.Diagnostics; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Helpers; +using DevHome.PI.Helpers; +using DevHome.PI.Models; +using Microsoft.UI.Xaml; +using Serilog; +using Windows.Win32; + +namespace DevHome.PI.ViewModels; + +public partial class AppDetailsPageViewModel : ObservableObject +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(AppDetailsPageViewModel)); + + [ObservableProperty] + private AppRuntimeInfo appInfo; + + [ObservableProperty] + private Visibility runAsAdminVisibility = Visibility.Collapsed; + + [ObservableProperty] + private Visibility processRunningParamsVisibility = Visibility.Collapsed; + + private Process? targetProcess; + + public AppDetailsPageViewModel() + { + TargetAppData.Instance.PropertyChanged += TargetApp_PropertyChanged; + AppInfo = new(); + + var process = TargetAppData.Instance.TargetProcess; + if (process is not null) + { + UpdateTargetProcess(process); + } + } + + public void UpdateTargetProcess(Process process) + { + if (targetProcess != process) + { + targetProcess = process; + RunAsAdminVisibility = Visibility.Collapsed; + AppInfo = new(); + + try + { + AppInfo.ProcessId = targetProcess.Id; + + if (process.HasExited) + { + AppInfo.Visibility = Visibility.Collapsed; + ProcessRunningParamsVisibility = Visibility.Collapsed; + } + else + { + AppInfo.Visibility = Visibility.Visible; + ProcessRunningParamsVisibility = Visibility.Visible; + AppInfo.IsRunningAsSystem = TargetAppData.Instance.IsRunningAsSystem; + AppInfo.IsRunningAsAdmin = TargetAppData.Instance.IsRunningAsAdmin; + AppInfo.BasePriority = targetProcess.BasePriority; + AppInfo.PriorityClass = (int)targetProcess.PriorityClass; + + if (targetProcess.MainModule != null) + { + AppInfo.MainModuleFileName = targetProcess.MainModule.FileName; + uint binaryTypeValue; + + // TODO GetBinaryType only distinguishes x86 from x64. It doesn't allow for ARM or ARM64. + PInvoke.GetBinaryType(AppInfo.MainModuleFileName, out binaryTypeValue); + AppInfo.BinaryType = (WindowHelper.BinaryType)binaryTypeValue; + } + + foreach (ProcessModule module in targetProcess.Modules) + { + AppInfo.CheckFrameworkTypes(module.ModuleName); + } + + AppInfo.IsStoreApp = PInvoke.IsImmersiveProcess(targetProcess.SafeHandle); + } + } + catch (Win32Exception ex) + { + // This can throw if the process is running elevated and we are not. + _log.Error(ex, "Unable to contruct an AppInfo for target process."); + if (ex.NativeErrorCode == (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_ACCESS_DENIED) + { + // Hide properties that cannot be retrieved when the target app is elevated and PI is not. + AppInfo.Visibility = Visibility.Collapsed; + + // Only show the button when not running as admin. This is possible when the target app is a system app. + if (!RuntimeHelper.IsCurrentProcessRunningAsAdmin()) + { + RunAsAdminVisibility = Visibility.Visible; + } + } + } + } + } + + private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(TargetAppData.TargetProcess)) + { + if (TargetAppData.Instance.TargetProcess is not null) + { + UpdateTargetProcess(TargetAppData.Instance.TargetProcess); + } + } + } + + [RelayCommand] + private void RunAsAdmin() + { + if (targetProcess is not null) + { + CommonHelper.RunAsAdmin(targetProcess.Id, nameof(AppDetailsPageViewModel)); + } + } +} diff --git a/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs b/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs new file mode 100644 index 0000000000..711b4f4c95 --- /dev/null +++ b/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs @@ -0,0 +1,283 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Extensions; +using DevHome.PI.Helpers; +using DevHome.PI.Models; +using Microsoft.Diagnostics.Tracing.StackSources; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.Graphics; +using Windows.Win32.Foundation; + +namespace DevHome.PI.ViewModels; + +public partial class BarWindowViewModel : ObservableObject +{ + private const string _UnsnapButtonText = "\ue89f"; + private const string _SnapButtonText = "\ue8a0"; + + private readonly Microsoft.UI.Dispatching.DispatcherQueue _dispatcher; + + private readonly ObservableCollection