diff --git a/DevHome.sln b/DevHome.sln index dac61696af..c02985c1ea 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}" @@ -614,6 +618,18 @@ Global {D759CD66-494C-4A00-8075-8B65A9891349}.Release|x64.Build.0 = Release|x64 {D759CD66-494C-4A00-8075-8B65A9891349}.Release|x86.ActiveCfg = Release|x86 {D759CD66-494C-4A00-8075-8B65A9891349}.Release|x86.Build.0 = Release|x86 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|arm64.ActiveCfg = Debug|ARM64 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|arm64.Build.0 = Debug|ARM64 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|x64.ActiveCfg = Debug|x64 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|x64.Build.0 = Debug|x64 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|x86.ActiveCfg = Debug|x86 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Debug|x86.Build.0 = Debug|x86 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|arm64.ActiveCfg = Release|ARM64 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|arm64.Build.0 = Release|ARM64 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|x64.ActiveCfg = Release|x64 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|x64.Build.0 = Release|x64 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|x86.ActiveCfg = Release|x86 + {CAAC0CDF-9AB8-4F43-A3EB-38D785AF5725}.Release|x86.Build.0 = Release|x86 {AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Debug|arm64.ActiveCfg = Debug|arm64 {AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Debug|arm64.Build.0 = Debug|arm64 {AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Debug|x64.ActiveCfg = Debug|x64 @@ -745,6 +761,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} diff --git a/common/DevHome.Common.csproj b/common/DevHome.Common.csproj index 5e046bf8ec..8898b85f83 100644 --- a/common/DevHome.Common.csproj +++ b/common/DevHome.Common.csproj @@ -55,7 +55,7 @@ - + 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/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/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/src/App.xaml.cs b/src/App.xaml.cs index bcaf2fa4ab..49dd47ec9a 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -209,6 +209,8 @@ await Task.WhenAll( private async void OnActivated(object? sender, AppActivationArguments args) { + ShowMainWindow(); + // Note: Keep the reference to 'args.Data' object, as 'args' may be // disposed before the async operation completes (RpcCallFailed: 0x800706be) var localArgsDataReference = args.Data; diff --git a/src/DevHome.csproj b/src/DevHome.csproj index 2185386a5a..18555a5eb2 100644 --- a/src/DevHome.csproj +++ b/src/DevHome.csproj @@ -77,7 +77,7 @@ - + @@ -86,6 +86,7 @@ + diff --git a/src/NavConfig.jsonc b/src/NavConfig.jsonc index 948f31682c..3c47f1a159 100644 --- a/src/NavConfig.jsonc +++ b/src/NavConfig.jsonc @@ -86,6 +86,27 @@ "visible": true } ] + }, + { + "identity": "ProjectIronsidesExperiment", + "enabledByDefault": false, + "buildTypeOverrides": [ + { + "buildType": "dev", + "enabledByDefault": true, + "visible": true + }, + { + "buildType": "canary", + "enabledByDefault": true, + "visible": true + }, + { + "buildType": "stable", + "enabledByDefault": false, + "visible": true + } + ] } ] } diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest index 9d26a5e0c8..66de1e44c5 100644 --- a/src/Package.appxmanifest +++ b/src/Package.appxmanifest @@ -286,6 +286,23 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Program.cs b/src/Program.cs index 259b62e784..aa96561bed 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,17 +1,26 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics; +using DevHome.Services; +using Microsoft.Extensions.Configuration; using Microsoft.UI.Dispatching; +using Serilog; namespace DevHome; public static class Program { + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(Program)); private static App? _app; [STAThread] public static void Main(string[] args) { + // Be sure to parse these args in this instance of the exe... don't redirect this to another instance for parsing which + // may be running in a different security context. + ParseCommandLine(args); + WinRT.ComWrappersSupport.InitializeComWrappers(); var isRedirect = DecideRedirection().GetAwaiter().GetResult(); @@ -34,11 +43,7 @@ private static async Task DecideRedirection() var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs(); var isRedirect = false; - if (mainInstance.IsCurrent) - { - mainInstance.Activated += OnActivated; - } - else + if (!mainInstance.IsCurrent) { // Redirect the activation (and args) to the "main" instance, and exit. await mainInstance.RedirectActivationToAsync(activatedEventArgs); @@ -48,8 +53,38 @@ private static async Task DecideRedirection() return isRedirect; } - private static void OnActivated(object? sender, Microsoft.Windows.AppLifecycle.AppActivationArguments e) + // Currently DevHome supports one set of command line arguments, most useful when debugging different apps within the Dev Home package. + // + // For example: + // --utilitylaunch DevHome.PI.Exe --utilityLaunchArgs "--application problemapp2" + // + // --utilityLaunch is the name of the utility to launch + // --utilityLaunchArgs are the arguments to pass to the utility. This is optional, but be sure to include the quotes if you have spaces in the arguments. + private static void ParseCommandLine(string[] args) { - _app?.ShowMainWindow(); + var builder = new ConfigurationBuilder(); + builder.AddCommandLine(args); + var config = builder.Build(); + + var utilityToLaunch = config["utilitylaunch"]; + var utilityLaunchArgs = config["utilitylaunchargs"]; + + if (!string.IsNullOrEmpty(utilityToLaunch)) + { + try + { + var processStartInfo = new ProcessStartInfo + { + FileName = utilityToLaunch, + Arguments = utilityLaunchArgs, + }; + + Process.Start(processStartInfo); + } + catch (Exception ex) + { + _log.Error(ex, $"Error launching utility: {ex.Message}"); + } + } } } diff --git a/src/Strings/en-us/Resources.resw b/src/Strings/en-us/Resources.resw index 1e15ef3671..a03ca81571 100644 --- a/src/Strings/en-us/Resources.resw +++ b/src/Strings/en-us/Resources.resw @@ -1,17 +1,17 @@  - diff --git a/tools/PI/DevHome.PI/App.config b/tools/PI/DevHome.PI/App.config new file mode 100644 index 0000000000..a0e35ba70a --- /dev/null +++ b/tools/PI/DevHome.PI/App.config @@ -0,0 +1,91 @@ + + + + +
+ + + + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + + + + + + + + + + + + 50 + 350 + 964 + 680 + + + + + Default + + + True + + + False + + + + + 70 + 942 + 640 + 222 + + + + + True + + + + + DevHome.PI + DevEnv + + + + + 0, 0 + + + + \ No newline at end of file diff --git a/tools/PI/DevHome.PI/Assets/LargeTile.scale-100.png b/tools/PI/DevHome.PI/Assets/LargeTile.scale-100.png new file mode 100644 index 0000000000..21b4ce23b0 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/LargeTile.scale-100.png differ diff --git a/tools/PI/DevHome.PI/Assets/LargeTile.scale-125.png b/tools/PI/DevHome.PI/Assets/LargeTile.scale-125.png new file mode 100644 index 0000000000..a220a5c00b Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/LargeTile.scale-125.png differ diff --git a/tools/PI/DevHome.PI/Assets/LargeTile.scale-150.png b/tools/PI/DevHome.PI/Assets/LargeTile.scale-150.png new file mode 100644 index 0000000000..e371b9c48c Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/LargeTile.scale-150.png differ diff --git a/tools/PI/DevHome.PI/Assets/LargeTile.scale-200.png b/tools/PI/DevHome.PI/Assets/LargeTile.scale-200.png new file mode 100644 index 0000000000..2e0bc6d103 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/LargeTile.scale-200.png differ diff --git a/tools/PI/DevHome.PI/Assets/LargeTile.scale-400.png b/tools/PI/DevHome.PI/Assets/LargeTile.scale-400.png new file mode 100644 index 0000000000..20aae7a3ac Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/LargeTile.scale-400.png differ diff --git a/tools/PI/DevHome.PI/Assets/SmallTile.scale-100.png b/tools/PI/DevHome.PI/Assets/SmallTile.scale-100.png new file mode 100644 index 0000000000..4fb43db6c9 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/SmallTile.scale-100.png differ diff --git a/tools/PI/DevHome.PI/Assets/SmallTile.scale-125.png b/tools/PI/DevHome.PI/Assets/SmallTile.scale-125.png new file mode 100644 index 0000000000..d6fb697270 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/SmallTile.scale-125.png differ diff --git a/tools/PI/DevHome.PI/Assets/SmallTile.scale-150.png b/tools/PI/DevHome.PI/Assets/SmallTile.scale-150.png new file mode 100644 index 0000000000..94ae71b65e Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/SmallTile.scale-150.png differ diff --git a/tools/PI/DevHome.PI/Assets/SmallTile.scale-200.png b/tools/PI/DevHome.PI/Assets/SmallTile.scale-200.png new file mode 100644 index 0000000000..d3f373fa7f Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/SmallTile.scale-200.png differ diff --git a/tools/PI/DevHome.PI/Assets/SmallTile.scale-400.png b/tools/PI/DevHome.PI/Assets/SmallTile.scale-400.png new file mode 100644 index 0000000000..4207b09585 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/SmallTile.scale-400.png differ diff --git a/tools/PI/DevHome.PI/Assets/SplashScreen.scale-100.png b/tools/PI/DevHome.PI/Assets/SplashScreen.scale-100.png new file mode 100644 index 0000000000..419dc47eb8 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/SplashScreen.scale-100.png differ diff --git a/tools/PI/DevHome.PI/Assets/SplashScreen.scale-125.png b/tools/PI/DevHome.PI/Assets/SplashScreen.scale-125.png new file mode 100644 index 0000000000..964d494333 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/SplashScreen.scale-125.png differ diff --git a/tools/PI/DevHome.PI/Assets/SplashScreen.scale-150.png b/tools/PI/DevHome.PI/Assets/SplashScreen.scale-150.png new file mode 100644 index 0000000000..88f5a5c310 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/SplashScreen.scale-150.png differ diff --git a/tools/PI/DevHome.PI/Assets/SplashScreen.scale-200.png b/tools/PI/DevHome.PI/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000..ca6ba21537 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/SplashScreen.scale-200.png differ diff --git a/tools/PI/DevHome.PI/Assets/SplashScreen.scale-400.png b/tools/PI/DevHome.PI/Assets/SplashScreen.scale-400.png new file mode 100644 index 0000000000..cc930ad3fe Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/SplashScreen.scale-400.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-100.png b/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-100.png new file mode 100644 index 0000000000..b245597f1f Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-100.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-125.png b/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-125.png new file mode 100644 index 0000000000..d089b82dd5 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-125.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-150.png b/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-150.png new file mode 100644 index 0000000000..d3494b147b Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-150.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-200.png b/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..e1fd5f5f3e Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-200.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-400.png b/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-400.png new file mode 100644 index 0000000000..5025a1dabd Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square150x150Logo.scale-400.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 0000000000..79240cc51b Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 0000000000..0184e07313 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 0000000000..516a72eff2 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 0000000000..ccd38cb652 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 0000000000..5ecd627b82 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 0000000000..79240cc51b Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-24.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-24.png new file mode 100644 index 0000000000..0184e07313 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-24.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 0000000000..516a72eff2 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 0000000000..ccd38cb652 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 0000000000..5ecd627b82 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-100.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-100.png new file mode 100644 index 0000000000..54a7ca03ab Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-100.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-125.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-125.png new file mode 100644 index 0000000000..6b82c206cf Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-125.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-150.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-150.png new file mode 100644 index 0000000000..bfd92e92f3 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-150.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-200.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..a85794002f Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-200.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-400.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-400.png new file mode 100644 index 0000000000..a6790f198a Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.scale-400.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-16.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000000..b348a6af40 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-16.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-24.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000000..d0ec86e550 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-24.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-256.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000000..bd28ced589 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-256.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-32.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000000..b922287fed Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-32.png differ diff --git a/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-48.png b/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000000..3f1d8537ba Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Square44x44Logo.targetsize-48.png differ diff --git a/tools/PI/DevHome.PI/Assets/StoreLogo.backup.png b/tools/PI/DevHome.PI/Assets/StoreLogo.backup.png new file mode 100644 index 0000000000..7385b56c0e Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/StoreLogo.backup.png differ diff --git a/tools/PI/DevHome.PI/Assets/StoreLogo.scale-100.png b/tools/PI/DevHome.PI/Assets/StoreLogo.scale-100.png new file mode 100644 index 0000000000..09db289b3b Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/StoreLogo.scale-100.png differ diff --git a/tools/PI/DevHome.PI/Assets/StoreLogo.scale-125.png b/tools/PI/DevHome.PI/Assets/StoreLogo.scale-125.png new file mode 100644 index 0000000000..07e090f005 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/StoreLogo.scale-125.png differ diff --git a/tools/PI/DevHome.PI/Assets/StoreLogo.scale-150.png b/tools/PI/DevHome.PI/Assets/StoreLogo.scale-150.png new file mode 100644 index 0000000000..88a2abfb0f Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/StoreLogo.scale-150.png differ diff --git a/tools/PI/DevHome.PI/Assets/StoreLogo.scale-200.png b/tools/PI/DevHome.PI/Assets/StoreLogo.scale-200.png new file mode 100644 index 0000000000..f8820d9109 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/StoreLogo.scale-200.png differ diff --git a/tools/PI/DevHome.PI/Assets/StoreLogo.scale-400.png b/tools/PI/DevHome.PI/Assets/StoreLogo.scale-400.png new file mode 100644 index 0000000000..027cfdd182 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/StoreLogo.scale-400.png differ diff --git a/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-100.png b/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-100.png new file mode 100644 index 0000000000..e0be5f18a9 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-100.png differ diff --git a/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-125.png b/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-125.png new file mode 100644 index 0000000000..54eca79a0e Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-125.png differ diff --git a/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-150.png b/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-150.png new file mode 100644 index 0000000000..7e72f1e000 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-150.png differ diff --git a/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-200.png b/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..419dc47eb8 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-200.png differ diff --git a/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-400.png b/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-400.png new file mode 100644 index 0000000000..ca6ba21537 Binary files /dev/null and b/tools/PI/DevHome.PI/Assets/Wide310x150Logo.scale-400.png differ diff --git a/tools/PI/DevHome.PI/BarWindow.xaml b/tools/PI/DevHome.PI/BarWindow.xaml new file mode 100644 index 0000000000..44799e8737 --- /dev/null +++ b/tools/PI/DevHome.PI/BarWindow.xaml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/BarWindow.xaml.cs b/tools/PI/DevHome.PI/BarWindow.xaml.cs new file mode 100644 index 0000000000..0a25336942 --- /dev/null +++ b/tools/PI/DevHome.PI/BarWindow.xaml.cs @@ -0,0 +1,722 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +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.Telemetry; +using DevHome.PI.ViewModels; +using Microsoft.UI; +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 BarWindow : WindowEx, INotifyPropertyChanged +{ + private readonly Settings settings = Settings.Default; + private readonly string errorTitleText = CommonHelper.GetLocalizedString("ToolLaunchErrorTitle"); + private readonly string errorMessageText = CommonHelper.GetLocalizedString("ToolLaunchErrorMessage"); + private readonly BarWindowViewModel viewModel = new(); + + // Constants that control window sizes + private const int WindowPositionOffsetY = 30; + private const int FloatingHorizontalBarWidth = 700; + private const int FloatingHorizontalBarHeight = 70; + private const int FloatingVerticalBarWidth = 70; + private const int FloatingVerticalBarHeight = 700; + private const int DefaultExpandedViewTop = 30; + private const int DefaultExpandedViewLeft = 100; + private const int RightSideGap = 10; + + private readonly GridLength _gridLengthStar = new(1, GridUnitType.Star); + private int cursorPosX; // = 0; + private int cursorPosY; // = 0; + private int appWindowPosX; // = 0; + private int appWindowPosY; // = 0; + private bool isWindowMoving; // = false; + + private Orientation _barOrientation = Orientation.Horizontal; + + public Orientation BarOrientation + { + get => _barOrientation; + set + { + _barOrientation = value; + + if (value == Orientation.Horizontal) + { + SBarHorizontal.Visibility = Visibility.Visible; + SBarVertical.Visibility = Visibility.Collapsed; + ExternalToolsRepeater.Layout = Application.Current.Resources["ExternalToolsHorizontalLayout"] as StackLayout; + MainPanelMiddleRowDefinition.Height = GridLength.Auto; + MainPanelLastRowDefinition.Height = _gridLengthStar; + SystemResourceStackPanel.SetValue(Grid.RowProperty, 0); + SystemResourceStackPanel.SetValue(Grid.ColumnProperty, 2); + TopGrid.HorizontalAlignment = HorizontalAlignment.Stretch; + } + else + { + SBarHorizontal.Visibility = Visibility.Collapsed; + SBarVertical.Visibility = Visibility.Visible; + ExternalToolsRepeater.Layout = Application.Current.Resources["ExternalToolsVerticalLayout"] as StackLayout; + MainPanelMiddleRowDefinition.Height = _gridLengthStar; + MainPanelLastRowDefinition.Height = GridLength.Auto; + SystemResourceStackPanel.SetValue(Grid.RowProperty, 2); + SystemResourceStackPanel.SetValue(Grid.ColumnProperty, 0); + TopGrid.HorizontalAlignment = HorizontalAlignment.Center; + } + + OnPropertyChanged(nameof(BarOrientation)); + } + } + + 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; + + private bool _isSnapped; + + private bool IsSnapped + { + get => _isSnapped; + set + { + _isSnapped = value; + SBarHorizontal.IsSnapped = value; + SBarVertical.IsSnapped = value; + } + } + + private bool _isMaximized; + + private bool IsMaximized + { + get => _isMaximized; + set + { + _isMaximized = value; + SBarHorizontal.IsMaximized = value; + SBarVertical.IsMaximized = value; + + if (value) + { + WindowState = WindowState.Maximized; + } + } + } + + private readonly ObservableCollection + + + + + + 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..3b0db0c8fc --- /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.Common.Extensions; +using DevHome.PI.Properties; +using DevHome.PI.SettingsUi; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using WinUIEx; + +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) + { + SettingsToolWindow settingsTool = new(Settings.Default.SettingsToolPosition); + settingsTool.Show(); + } +} 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..9fa1a49175 --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/ProcessSelectionButton.cs @@ -0,0 +1,68 @@ +// 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 (this.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() + { + this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.Cross); + } + + public void ResetCursor() + { + this.ProtectedCursor = null; + } +} diff --git a/tools/PI/DevHome.PI/Controls/SystemBar.cs b/tools/PI/DevHome.PI/Controls/SystemBar.cs new file mode 100644 index 0000000000..afb963aeb0 --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/SystemBar.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.ComponentModel; +using System.Diagnostics; +using DevHome.Common.Extensions; +using DevHome.PI.Models; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.Win32.Foundation; + +namespace DevHome.PI.Controls; + +// This is the base class for a system/chrome/title bar. There are at least 2 consumers of this, +// one which implements a system bar in the horizontal position, another in a vertical position +public partial class SystemBar : UserControl, INotifyPropertyChanged +{ + private bool _isSnapped; + + // This is used to toggle the "snap to an app" icon to have different values + // based on if we are snapped or not + public bool IsSnapped + { + get => _isSnapped; + set + { + _isSnapped = value; + CurrentSnapButtonText = _isSnapped ? UnsnapButtonText : SnapButtonText; + } + } + + private bool _isSnappingEnabled; + + // This is used to enable the "snap to an app" icon if we're allowed to snap + public bool IsSnappingEnabled + { + get => _isSnappingEnabled; + set + { + _isSnappingEnabled = value; + OnPropertyChanged(nameof(IsSnappingEnabled)); + } + } + + private bool _isMaximized; + + // This is used determine if the system bar should treat the window as maximized or not. That + // allows us to enable/disable the "Maximize" button and the "Restore" button in the titlebar + public bool IsMaximized + { + get => _isMaximized; + set + { + _isMaximized = value; + MaximizeButtonVisibility = _isMaximized ? Visibility.Collapsed : Visibility.Visible; + RestoreButtonVisibility = _isMaximized ? Visibility.Visible : Visibility.Collapsed; + OnPropertyChanged(nameof(MaximizeButtonVisibility)); + OnPropertyChanged(nameof(RestoreButtonVisibility)); + } + } + + protected Visibility MaximizeButtonVisibility { get; private set; } = Visibility.Visible; + + protected Visibility RestoreButtonVisibility { get; private set; } = Visibility.Collapsed; + + private const string UnsnapButtonText = "\ue89f"; + private const string SnapButtonText = "\ue8a0"; + + private string _currentSnapButtonText = SnapButtonText; + + protected string CurrentSnapButtonText + { + get => _currentSnapButtonText; + private set + { + if (_currentSnapButtonText != value) + { + _currentSnapButtonText = value; + OnPropertyChanged(nameof(CurrentSnapButtonText)); + } + } + } + + public SystemBar() + { + } + + public void Initialize() + { + IsSnappingEnabled = TargetAppData.Instance.HWnd != HWND.Null; + TargetAppData.Instance.PropertyChanged += TargetApp_PropertyChanged; + } + + private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(TargetAppData.HWnd)) + { + IsSnappingEnabled = TargetAppData.Instance.HWnd != HWND.Null; + } + } + + protected void SnapButton_Click(object sender, RoutedEventArgs e) + { + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + barWindow.PerformSnapAction(); + } + + protected void MinimizeButton_Click(object sender, RoutedEventArgs e) + { + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + barWindow.HandleMinimizeRequest(); + } + + protected void MaximizeButton_Click(object sender, RoutedEventArgs e) + { + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + barWindow.HandleMaximizeRequest(); + } + + protected void RestoreButton_Click(object sender, RoutedEventArgs e) + { + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + barWindow.HandleRestoreRequest(); + } + + protected void CloseButton_Click(object sender, RoutedEventArgs e) + { + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + barWindow.HandleCloseRequest(); + } + + protected void CloseAllMenuItem_Click(object sender, RoutedEventArgs e) + { + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + barWindow.HandleCloseAllRequest(); + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/tools/PI/DevHome.PI/Controls/SystemBarHorizontal.xaml b/tools/PI/DevHome.PI/Controls/SystemBarHorizontal.xaml new file mode 100644 index 0000000000..6679b54fcb --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/SystemBarHorizontal.xaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Controls/SystemBarHorizontal.xaml.cs b/tools/PI/DevHome.PI/Controls/SystemBarHorizontal.xaml.cs new file mode 100644 index 0000000000..49f3f04239 --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/SystemBarHorizontal.xaml.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DevHome.PI.Controls; + +// This is the class for a horiztonal system/chrome/title bar. +public sealed partial class SystemBarHorizontal : SystemBar +{ + public SystemBarHorizontal() + { + this.InitializeComponent(); + } +} diff --git a/tools/PI/DevHome.PI/Controls/SystemBarVertical.xaml b/tools/PI/DevHome.PI/Controls/SystemBarVertical.xaml new file mode 100644 index 0000000000..7e2605c99b --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/SystemBarVertical.xaml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Controls/SystemBarVertical.xaml.cs b/tools/PI/DevHome.PI/Controls/SystemBarVertical.xaml.cs new file mode 100644 index 0000000000..f5741c7977 --- /dev/null +++ b/tools/PI/DevHome.PI/Controls/SystemBarVertical.xaml.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DevHome.PI.Controls; + +// This is the class for a vertical system/chrome/title bar. +public sealed partial class SystemBarVertical : SystemBar +{ + public SystemBarVertical() + { + this.InitializeComponent(); + } +} diff --git a/tools/PI/DevHome.PI/DevHome.PI.csproj b/tools/PI/DevHome.PI/DevHome.PI.csproj new file mode 100644 index 0000000000..22959b2a32 --- /dev/null +++ b/tools/PI/DevHome.PI/DevHome.PI.csproj @@ -0,0 +1,206 @@ + + + + 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 + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + Always + + + MSBuild:Compile + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + MSBuild:Compile + + + MSBuild:Compile + + + 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..9bfc24db8d --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/ExternalTool.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Text.Json.Serialization; +using Microsoft.UI.Xaml.Media.Imaging; +using Serilog; +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 class ExternalTool : INotifyPropertyChanged +{ + 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; + } + + [JsonIgnore] + private SoftwareBitmapSource? _toolIcon; + + [JsonIgnore] + public SoftwareBitmapSource? ToolIcon + { + get => _toolIcon; + private set + { + _toolIcon = value; + OnPropertyChanged(nameof(ToolIcon)); + } + } + + public ExternalTool( + string name, + string executable, + ExternalToolArgType argtype, + string argprefix = "", + string otherArgs = "") + { + Name = name; + Executable = executable; + ArgType = argtype; + ArgPrefix = argprefix; + OtherArgs = otherArgs; + + ID = Guid.NewGuid().ToString(); + + if (!string.IsNullOrEmpty(executable)) + { + GetToolImage(); + } + } + + private async void GetToolImage() + { + try + { + var toolIcon = System.Drawing.Icon.ExtractAssociatedIcon(Executable); + + // Fall back to Windows default app icon. + toolIcon ??= System.Drawing.Icon.FromHandle(LoadDefaultAppIcon()); + + if (toolIcon is not null) + { + ToolIcon = await WindowHelper.GetWinUI3BitmapSourceFromGdiBitmap(toolIcon.ToBitmap()); + } + } + catch (Exception ex) + { + _log.Debug(ex, "Unable to fetch tool image."); + } + } + + 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; + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/tools/PI/DevHome.PI/Helpers/ExternalToolsHelper.cs b/tools/PI/DevHome.PI/Helpers/ExternalToolsHelper.cs new file mode 100644 index 0000000000..e410984690 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/ExternalToolsHelper.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +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 ObservableCollection externalTools = []; + private readonly string toolInfoFileName; + + public static readonly ExternalToolsHelper Instance = new(); + + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(ExternalToolsHelper)); + + public ReadOnlyObservableCollection ExternalTools { get; set; } + + private ExternalToolsHelper() + { + string localFolder; + if (RuntimeHelper.IsMSIX) + { + localFolder = ApplicationData.Current.LocalFolder.Path; + } + else + { + localFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) ?? string.Empty; + } + + toolInfoFileName = Path.Combine(localFolder, "externaltools.json"); + ExternalTools = new(externalTools); + } + + internal void Init() + { + if (File.Exists(toolInfoFileName)) + { + try + { + var jsonData = File.ReadAllText(toolInfoFileName); + var existingData = JsonSerializer.Deserialize(jsonData) ?? []; + foreach (var data in existingData) + { + externalTools.Add(data); + } + } + catch (Exception ex) + { + // TODO If we failed parsing the JSON file... should we just delete it? + _log.Error(ex, "Failed to parse {tool}", toolInfoFileName); + } + } + } + + public ExternalTool AddExternalTool(ExternalTool tool) + { + externalTools.Add(tool); + + // Write out to JSON file + var updatedJson = JsonSerializer.Serialize(externalTools, serializerOptions); + + try + { + File.WriteAllText(toolInfoFileName, updatedJson); + } + catch (Exception ex) + { + // TODO What should we do if we're unable to write to the file? + _log.Error(ex, "AddExternalTool unable to write to file"); + } + + return tool; + } + + public void RemoveExternalTool(ExternalTool tool) + { + if (externalTools.Remove(tool)) + { + // Write out to JSON file + var updatedJson = JsonSerializer.Serialize(externalTools, serializerOptions); + + try + { + File.WriteAllText(toolInfoFileName, updatedJson); + } + catch (Exception ex) + { + _log.Error(ex, "RemoveExternalTool unable to write to file"); + } + } + } +} 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/WatsonHelper.cs b/tools/PI/DevHome.PI/Helpers/WatsonHelper.cs new file mode 100644 index 0000000000..4a7d8e308b --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/WatsonHelper.cs @@ -0,0 +1,182 @@ +// 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"); + + foreach (EventLogEntry entry in eventLog.Entries) + { + if (entry.InstanceId == 1000 + && entry.Source.Equals("Application Error", StringComparison.OrdinalIgnoreCase) + && entry.ReplacementStrings[10].Contains(targetProcess.ProcessName, 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..c9f927d040 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/WindowHelper.cs @@ -0,0 +1,526 @@ +// 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.Media; +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.Devices.Display; +using Windows.Devices.Enumeration; +using Windows.Graphics; +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 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 SnapOffsetHorizontal and SnapThreshold values don't allow for different DPIs. + + // 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; + + // 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 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(System.Drawing.Bitmap bmp) + { + // get pixels as an array of bytes + var data = bmp.LockBits( + new System.Drawing.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 Windows.Graphics.Imaging.SoftwareBitmap( + Windows.Graphics.Imaging.BitmapPixelFormat.Bgra8, + bmp.Width, + bmp.Height, + Windows.Graphics.Imaging.BitmapAlphaMode.Premultiplied); + softwareBitmap.CopyFromBuffer(bytes.AsBuffer()); + + // build WinUI3 SoftwareBitmapSource + var source = new SoftwareBitmapSource(); + await source.SetBitmapAsync(softwareBitmap); + return source; + } + + 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 void SnapToWindow(IntPtr targetHwnd, IntPtr dbarHwnd, SizeInt32 size) + { + PInvoke.GetWindowRect((HWND)targetHwnd, out var rect); + PInvoke.MoveWindow((HWND)dbarHwnd, rect.right - SnapOffsetHorizontal, rect.top, size.Width, size.Height, true); + } + + 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; + } + + // TODO Allow for the taskbar when returning screen size. + 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); + } + } + } +} 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..4b1bea3a0e --- /dev/null +++ b/tools/PI/DevHome.PI/Models/AppRuntimeInfo.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +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 usesWpf = false; + + [ObservableProperty] + private bool usesWinForms = false; + + [ObservableProperty] + private bool usesMfc = false; + + [ObservableProperty] + private bool isStoreApp = false; + + [ObservableProperty] + private bool isAvalonia = false; + + [ObservableProperty] + private bool isMaui = false; + + [ObservableProperty] + private bool usesWinAppSdk = false; + + [ObservableProperty] + private bool usesWinUi = false; + + [ObservableProperty] + private bool usesDirectX = false; + + [ObservableProperty] + private bool isRunningAsAdmin = false; + + [ObservableProperty] + private bool isRunningAsSystem = false; + + [ObservableProperty] + private Visibility visibility = Visibility.Visible; +} 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..8b4256f09b --- /dev/null +++ b/tools/PI/DevHome.PI/Models/ClipboardMonitor.cs @@ -0,0 +1,201 @@ +// 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 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 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/Insight.cs b/tools/PI/DevHome.PI/Models/Insight.cs new file mode 100644 index 0000000000..313f3a6678 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/Insight.cs @@ -0,0 +1,39 @@ +// 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 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..d790424bb1 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/NavLink.cs @@ -0,0 +1,22 @@ +// 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 Type? PageViewModel { get; internal set; } + + public NavLink(string i, string c, Type? pageViewModel) + { + IconText = i; + ContentText = c; + 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..b578c4f167 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/PerfCounters.cs @@ -0,0 +1,214 @@ +// 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)); + } + } + + 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(); + ramCounter?.Close(); + + foreach (var counter in gpuCounters ?? Enumerable.Empty()) + { + counter.Close(); + } + + readCounter?.Close(); + writeCounter?.Close(); + } + + 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/TargetAppData.cs b/tools/PI/DevHome.PI/Models/TargetAppData.cs new file mode 100644 index 0000000000..3d7ac59c60 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/TargetAppData.cs @@ -0,0 +1,171 @@ +// 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.Win32; +using Windows.Win32.Foundation; +using static DevHome.PI.Helpers.WindowHelper; + +namespace DevHome.PI.Models; + +public partial class TargetAppData : ObservableObject, INotifyPropertyChanged +{ + 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 WindowHelper.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..cef6068f6b --- /dev/null +++ b/tools/PI/DevHome.PI/PIApp.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/PIApp.xaml.cs b/tools/PI/DevHome.PI/PIApp.xaml.cs new file mode 100644 index 0000000000..7eda977308 --- /dev/null +++ b/tools/PI/DevHome.PI/PIApp.xaml.cs @@ -0,0 +1,109 @@ +// 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(); + + // 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(); + }).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/AppDetailsPage.xaml b/tools/PI/DevHome.PI/Pages/AppDetailsPage.xaml new file mode 100644 index 0000000000..f276fbb18f --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/AppDetailsPage.xaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..883a9b5eef --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/ProcessListPage.xaml.cs @@ -0,0 +1,29 @@ +// 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); + 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..1eac2c4194 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/ResourceUsagePage.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/WatsonsPage.xaml b/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml new file mode 100644 index 0000000000..093456b705 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..36d499c702 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..d69dc7b87d --- /dev/null +++ b/tools/PI/DevHome.PI/PrimaryWindow.xaml.cs @@ -0,0 +1,112 @@ +// 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((HWND)DBarWindow.GetWindowHandle()); + } + } + + 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; + } + + if (DBarWindow == null) + { + DBarWindow = new(process, hWnd); + DBarWindow.Closed += (s, e) => ClearBarWindow(); + } + else + { + TargetAppData.Instance.SetNewAppData(process, hWnd); + } + } + else + { + // There's no foreground window. Start with the full window, open to the process list. + if (DBarWindow == null) + { + DBarWindow = new(); + } + } + } +} diff --git a/tools/PI/DevHome.PI/Program.cs b/tools/PI/DevHome.PI/Program.cs new file mode 100644 index 0000000000..d4505c07a6 --- /dev/null +++ b/tools/PI/DevHome.PI/Program.cs @@ -0,0 +1,215 @@ +// 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(); + + 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()); + }); + } + } + catch (Exception ex) + { + Log.Fatal(ex, "Application start-up failed"); + } + finally + { + Log.CloseAndFlush(); + } + } + + private static async Task DecideRedirection() + { + AppInstance instance; + var activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + if (RuntimeHelper.IsCurrentProcessRunningAsAdmin()) + { + // Wait for unelevated instance to exit + var isUnElevatedInstancePresent = false; + do + { + isUnElevatedInstancePresent = false; + var instanceList = AppInstance.GetInstances(); + foreach (var appInstance in instanceList) + { + if (appInstance.Key.Equals(MainInstanceKey, StringComparison.OrdinalIgnoreCase)) + { + isUnElevatedInstancePresent = true; + } + } + } + while (isUnElevatedInstancePresent); + + // 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); + } + + barWindow.ShouldExpandLargeWindow = true; + } + }); + } + } + + 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..2bd5c11139 --- /dev/null +++ b/tools/PI/DevHome.PI/Properties/Settings.Designer.cs @@ -0,0 +1,280 @@ +//------------------------------------------------------------------------------ +// +// 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.9.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 IsProcessFilterIncludeEdge { + get { + return ((bool)(this["IsProcessFilterIncludeEdge"])); + } + set { + this["IsProcessFilterIncludeEdge"] = 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("")] + public string status1Source { + get { + return ((string)(this["status1Source"])); + } + set { + this["status1Source"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string status2Source { + get { + return ((string)(this["status2Source"])); + } + set { + this["status2Source"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string status3Source { + get { + return ((string)(this["status3Source"])); + } + set { + this["status3Source"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute(@" + + 50 + 350 + 964 + 680 +")] + public global::System.Collections.Specialized.StringCollection SettingsToolPosition { + get { + return ((global::System.Collections.Specialized.StringCollection)(this["SettingsToolPosition"])); + } + set { + this["SettingsToolPosition"] = 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(@" + + 70 + 942 + 640 + 222 +")] + public global::System.Collections.Specialized.StringCollection ErrorLookupToolPosition { + get { + return ((global::System.Collections.Specialized.StringCollection)(this["ErrorLookupToolPosition"])); + } + set { + this["ErrorLookupToolPosition"] = 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..c382c2da83 --- /dev/null +++ b/tools/PI/DevHome.PI/Properties/Settings.settings @@ -0,0 +1,82 @@ + + + + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + + + + + + + + + + <?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>50</string> + <string>350</string> + <string>964</string> + <string>680</string> +</ArrayOfString> + + + Default + + + True + + + False + + + <?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>70</string> + <string>942</string> + <string>640</string> + <string>222</string> +</ArrayOfString> + + + 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/PINavigationService.cs b/tools/PI/DevHome.PI/Services/PINavigationService.cs new file mode 100644 index 0000000000..35317d02b2 --- /dev/null +++ b/tools/PI/DevHome.PI/Services/PINavigationService.cs @@ -0,0 +1,167 @@ +// 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.Controls; +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..fd2a4e56e5 --- /dev/null +++ b/tools/PI/DevHome.PI/Services/PIPageService.cs @@ -0,0 +1,66 @@ +// 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(); + } + + 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/SettingsUi/AddToolControl.xaml b/tools/PI/DevHome.PI/SettingsUi/AddToolControl.xaml new file mode 100644 index 0000000000..ec22882a7d --- /dev/null +++ b/tools/PI/DevHome.PI/SettingsUi/AddToolControl.xaml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +