diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 78affbb16f1..f2f478cb4a7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -7,7 +7,8 @@ body: - type: markdown attributes: value: Please make sure to [search for existing issues](https://github.com/microsoft/PowerToys/issues) before filing a new one! -- type: input +- id: version + type: input attributes: label: Microsoft PowerToys version placeholder: 0.70.0 @@ -32,14 +33,6 @@ body: validations: required: true -- type: dropdown - attributes: - label: Running as admin - description: Are you running PowerToys as Admin? - options: - - "Yes" - - "No" - - type: dropdown attributes: label: Area(s) with issue? @@ -80,7 +73,8 @@ body: validations: required: true -- type: textarea +- id: repro + type: textarea attributes: label: Steps to reproduce description: We highly suggest including screenshots and a bug report log (System tray > Report bug). @@ -101,8 +95,22 @@ body: placeholder: What happened instead? validations: required: false + +- id: additionalInfo + type: textarea + attributes: + label: Additional Information + placeholder: | + OS version + .Net version + System Language + User or System Installation + Running as admin + validations: + required: false -- type: textarea +- id: otherSoftware + type: textarea attributes: label: Other Software description: If you're reporting a bug about our interaction with other software, what software? What versions? diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 0cfc3835594..a1c82d51ff4 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -377,6 +377,7 @@ drivedetectionwarning dshow DSTINVERT DUMMYUNIONNAME +dupenv dutil DVASPECT DVASPECTINFO @@ -640,7 +641,9 @@ IBeam ICapture IClass ICONERROR +ICONINFORMATION IData +IDCANCEL IDD IDesktop IDirect diff --git a/src/runner/Resources.resx b/src/runner/Resources.resx index 902e3a08740..97b674770b4 100644 --- a/src/runner/Resources.resx +++ b/src/runner/Resources.resx @@ -134,4 +134,10 @@ Administrator + + Bug Report + + + Bug report is being generated + diff --git a/src/runner/bug_report.cpp b/src/runner/bug_report.cpp index 9abfe6fa180..88b20ac0297 100644 --- a/src/runner/bug_report.cpp +++ b/src/runner/bug_report.cpp @@ -1,33 +1,366 @@ #include "pch.h" #include "bug_report.h" #include "Generated files/resource.h" +#include +#include #include #include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +using namespace std; +using namespace registry::install_scope; +namespace fs = std::filesystem; std::atomic_bool isBugReportThreadRunning = false; -void launch_bug_report() noexcept +std::string bugReportResult; + +bool LaunchBugReport() { std::wstring bug_report_path = get_module_folderpath(); bug_report_path += L"\\Tools\\PowerToys.BugReportTool.exe"; + Logger::info("Starting the bug report tool from {}", WideStringToString(bug_report_path)); + + bool success = true; + + try + { + SHELLEXECUTEINFOW sei{ sizeof(sei) }; + sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE }; + sei.lpFile = bug_report_path.c_str(); + sei.nShow = SW_HIDE; + + if (ShellExecuteExW(&sei)) + { + WaitForSingleObject(sei.hProcess, INFINITE); + CloseHandle(sei.hProcess); + + // Find the newest bug report file on the desktop + bugReportResult = FindNewestBugReportFile(); + Logger::info("Bug report generated: {}", bugReportResult); + } + else + { + bugReportResult = "Failed to start bug report tool."; + auto message = get_last_error_message(GetLastError()); + + if (message.has_value()) + { + bugReportResult = "Failed to start bug report tool. Internal error: " + WideStringToString(message.value()); + } + else + { + bugReportResult = "Failed to start bug report tool with unknown error."; + } + success = false; + } + } + catch (std::exception& ex) + { + bugReportResult = std::string{ ex.what() }; + Logger::error("Exception caught in LaunchBugReport: {}", ex.what()); + success = false; + } + + isBugReportThreadRunning.store(false); + + return success; +} +void InitializeReportBugLinkAsync() +{ bool expected_isBugReportThreadRunning = false; - if (isBugReportThreadRunning.compare_exchange_strong(expected_isBugReportThreadRunning, true)) - { - std::thread([bug_report_path]() { - SHELLEXECUTEINFOW sei{ sizeof(sei) }; - sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE }; - sei.lpFile = bug_report_path.c_str(); - sei.nShow = SW_HIDE; - if (ShellExecuteExW(&sei)) + + try + { + if (isBugReportThreadRunning.compare_exchange_strong(expected_isBugReportThreadRunning, true)) + { + std::thread([] { + std::string gitHubURL; + bool launchBugReportResult; + + notifications::show_toast(GET_RESOURCE_STRING(IDS_BUGREPORT_TEXT), GET_RESOURCE_STRING(IDS_BUGREPORT_TITLE)); + + // Launch the bug report task + auto bugReportTask = std::async(std::launch::async, [&launchBugReportResult] { + launchBugReportResult = LaunchBugReport(); + }); + + bugReportTask.wait(); + + if (launchBugReportResult && !bugReportResult.empty()) + { + Logger::info("Bug report successfully generated."); + + std::wstring wVersion = get_product_version(); + std::string version; + std::transform(wVersion.begin() + 1, wVersion.end(), std::back_inserter(version), [](wchar_t c) { + return static_cast(c); + }); + + std::string additionalInfo = "OS Build Version: " + GetOSVersion() + "%0a" + ".NET Version: " + GetDotNetVersion() + "%0a%0a"; + GeneralSettings generalSettings = get_general_settings(); + std::string isElevatedRun = generalSettings.isElevated ? "Running as admin: Yes" : "Running as admin: No"; + + std::string windowsSettings = ReportWindowsSettings(); + + const InstallScope current_install_scope = get_current_install_scope(); + + std::string installScope = current_install_scope == InstallScope::PerUser ? "Installation : User" : "Installation : System"; + + additionalInfo += windowsSettings + "%0a" + installScope + "%0a" + isElevatedRun; + + gitHubURL = "https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=Issue-Bug%2CNeeds-Triage&template=bug_report.yml" + + std::string("&version=") + version + + std::string("&additionalInfo=") + additionalInfo; + + std::wstring wideBugReportResult = L"Bug report generated on your desktop. Please attach the file to the GitHub issue.\n\n" + stringToWideString(bugReportResult); + MessageBox(nullptr, wideBugReportResult.c_str(), L"Bug Report", MB_OK | MB_ICONINFORMATION | MB_SETFOREGROUND); + } + else + { + std::wstring message; + + if (bugReportResult.empty()) + { + message = L" Failed to generate bug report. bugReportResult is empty."; + } + else + { + + // Convert std::string to std::wstring + std::wstring wideBugReportResult = stringToWideString(bugReportResult); + + // Prepare the message + message = L"LaunchBugReport failed: " + wideBugReportResult; + + } + + Logger::error(message); + + MessageBox(nullptr, message.c_str(), L"Bug Report", MB_OK | MB_ICONERROR | MB_SETFOREGROUND); + gitHubURL = "https://aka.ms/powerToysReportBug"; + } + + // Open the URL + std::wstring wGitHubURL(gitHubURL.begin(), gitHubURL.end()); + ShellExecuteW(nullptr, L"open", wGitHubURL.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + }).detach(); + } + else + { + Logger::warn("Bug report thread is already running."); + } + } + catch (std::exception& ex) + { + Logger::error("Exception in InitializeReportBugLinkAsync: {}", ex.what()); + } +} + +std::string FindNewestBugReportFile() +{ + char* desktopPathC; + size_t len; + + Logger::info("Searching for the newest bug report file on the desktop."); + + if (_dupenv_s(&desktopPathC, &len, "USERPROFILE") != 0 || desktopPathC == nullptr) + { + Logger::error("Failed to get USERPROFILE environment variable."); + return ""; + } + + std::string desktopPath(desktopPathC); + free(desktopPathC); + + desktopPath += "\\Desktop"; + fs::path desktopDir(desktopPath); + + if (!fs::exists(desktopDir) || !fs::is_directory(desktopDir)) + { + Logger::error("Desktop directory not found or is not a directory: {}", desktopPath); + + return ""; + } + + std::string newestFile; + std::time_t newestTime = 0; + + for (const auto& entry : fs::directory_iterator(desktopDir)) + { + if (entry.is_regular_file() && entry.path().filename().wstring().find(L"PowerToysReport_") == 0) + { + std::time_t fileTime = fs::last_write_time(entry).time_since_epoch().count(); + if (fileTime > newestTime) + { + newestTime = fileTime; + newestFile = entry.path().string(); + } + } + } + + if (newestFile.empty()) + { + Logger::warn("No bug report files found on the desktop."); + } + else + { + Logger::info("Newest bug report file found: " + newestFile); + } + + return newestFile; +} + +std::wstring ReadRegistryString(HKEY hKeyRoot, const std::wstring& subKey, const std::wstring& valueName) +{ + HKEY hKey; + if (RegOpenKeyEx(hKeyRoot, subKey.c_str(), 0, KEY_READ, &hKey) != ERROR_SUCCESS) + { + return L""; + } + + wchar_t value[256]; + DWORD bufferSize = sizeof(value); + DWORD type; + if (RegQueryValueEx(hKey, valueName.c_str(), 0, &type, (LPBYTE)value, &bufferSize) != ERROR_SUCCESS || type != REG_SZ) + { + RegCloseKey(hKey); + return L""; + } + + RegCloseKey(hKey); + return std::wstring(value); +} + +// Helper function to convert std::wstring to std::string +std::string WideStringToString(const std::wstring& wstr) +{ + if (wstr.empty()) + return std::string(); + int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], static_cast(wstr.size()), NULL, 0, NULL, NULL); + std::string str(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &wstr[0], static_cast(wstr.size()), &str[0], size_needed, NULL, NULL); + return str; +} + +std::wstring stringToWideString(const std::string& str) +{ + if (str.empty()) + return std::wstring(); + int size_needed = MultiByteToWideChar(CP_UTF8, 0, &str[0], static_cast(str.size()), NULL, 0); + std::wstring wstr(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, &str[0], static_cast(str.size()), &wstr[0], size_needed); + return wstr; +} + +// Function to get the .NET version +std::string GetDotNetVersion() +{ + try + { + std::string dotnetInfo = (exec_and_read_output(L"dotnet --list-runtimes")).value(); + if (dotnetInfo.empty()) + { + return "Unknown .NET Version"; + } + + std::regex versionRegex(R"((\d+\.\d+\.\d+))"); + std::sregex_iterator begin(dotnetInfo.begin(), dotnetInfo.end(), versionRegex), end; + + std::string latestVersion; + for (std::sregex_iterator i = begin; i != end; ++i) + { + std::string version = (*i).str(); + if (version > latestVersion) { - WaitForSingleObject(sei.hProcess, INFINITE); - CloseHandle(sei.hProcess); - static const std::wstring bugreport_success = GET_RESOURCE_STRING(IDS_BUGREPORT_SUCCESS); - MessageBoxW(nullptr, bugreport_success.c_str(), L"PowerToys", MB_OK); + latestVersion = version; } + } - isBugReportThreadRunning.store(false); - }).detach(); + return latestVersion.empty() ? "Unknown .NET Version" : ".NET " + latestVersion; + } + catch (const std::exception& e) + { + return "Failed to get .NET Version: " + std::string(e.what()); } } + + +std::string GetOSVersion() +{ + OSVERSIONINFOEXW osInfo = { 0 }; + try + { + NTSTATUS(WINAPI * RtlGetVersion) + (LPOSVERSIONINFOEXW) = nullptr; + *reinterpret_cast(&RtlGetVersion) = GetProcAddress(GetModuleHandleA("ntdll"), "RtlGetVersion"); + if (RtlGetVersion) + { + osInfo.dwOSVersionInfoSize = sizeof(osInfo); + RtlGetVersion(&osInfo); + } + } + catch (...) + { + return "Unknown Windows Version"; + } + + try + { + std::ostringstream osVersion; + osVersion << osInfo.dwMajorVersion << "." << osInfo.dwMinorVersion << "." << osInfo.dwBuildNumber; + return osVersion.str(); + } + catch (...) + { + return "Unknown Windows Version"; + } +} + + +std::string GetModuleFolderPath() +{ + char buffer[MAX_PATH]; + GetModuleFileNameA(NULL, buffer, MAX_PATH); + std::string::size_type pos = std::string(buffer).find_last_of("\\/"); + return std::string(buffer).substr(0, pos); +} + +std::string ReportWindowsSettings() +{ + std::wstring userLanguage; + std::wstring userLocale; + std::string result; + + try + { + const auto lang = winrt::Windows::System::UserProfile::GlobalizationPreferences::Languages().GetAt(0); + userLanguage = winrt::Windows::Globalization::Language{ lang }.DisplayName().c_str(); + wchar_t localeName[LOCALE_NAME_MAX_LENGTH]{}; + if (!LCIDToLocaleName(GetThreadLocale(), localeName, LOCALE_NAME_MAX_LENGTH, 0)) + { + throw -1; + } + userLocale = localeName; + } + catch (...) + { + return "Failed to get windows settings %0a"; + } + + result = "System Language: " + WideStringToString(userLanguage) + "%0a"; + result += "User Locale: " + WideStringToString(userLocale) + "%0a"; + + return result; +} \ No newline at end of file diff --git a/src/runner/bug_report.h b/src/runner/bug_report.h index 2d7084ea213..43f245090b8 100644 --- a/src/runner/bug_report.h +++ b/src/runner/bug_report.h @@ -1,3 +1,18 @@ #pragma once +#include +#include +#include +#include +#include -void launch_bug_report() noexcept; \ No newline at end of file +void InitializeReportBugLinkAsync(); +bool LaunchBugReport(); +std::string FindNewestBugReportFile(); +std::string GetDotNetVersion(); +std::string GetOSVersion(); +std::string GetModuleFolderPath(); +std::string WideStringToString(const std::wstring& wstr); +std::wstring stringToWideString(const std::string& str); +std::string ReportWindowsSettings(); + +extern std::atomic_bool isBugReportThreadRunning; diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp index 8a99290f2d9..c39e995534b 100644 --- a/src/runner/settings_window.cpp +++ b/src/runner/settings_window.cpp @@ -223,7 +223,7 @@ void dispatch_received_json(const std::wstring& json_to_parse) } else if (name == L"bugreport") { - launch_bug_report(); + InitializeReportBugLinkAsync(); } else if (name == L"killrunner") { diff --git a/src/runner/tray_icon.cpp b/src/runner/tray_icon.cpp index 015fb158b8d..962a5b98c06 100644 --- a/src/runner/tray_icon.cpp +++ b/src/runner/tray_icon.cpp @@ -101,7 +101,7 @@ void handle_tray_command(HWND window, const WPARAM command_id, LPARAM lparam) break; case ID_REPORT_BUG_COMMAND: { - launch_bug_report(); + InitializeReportBugLinkAsync(); break; } diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index 1670acb5538..3ab2e9a912f 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -24,6 +24,8 @@ + + @@ -76,6 +78,7 @@ + @@ -142,6 +145,12 @@ Always + + MSBuild:Compile + + + MSBuild:Compile + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml new file mode 100644 index 00000000000..1b00403251f --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml @@ -0,0 +1,18 @@ + + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml.cs similarity index 58% rename from src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.cs rename to src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml.cs index a1cc6b078ce..757ceb91f41 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml.cs @@ -2,14 +2,15 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; +using Microsoft.UI.Xaml.Controls; namespace Microsoft.PowerToys.Settings.UI.Controls { - public class PageLink + public sealed partial class LoadingMessage : ContentDialog { - public string Text { get; set; } - - public Uri Link { get; set; } + public LoadingMessage() + { + InitializeComponent(); + } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml new file mode 100644 index 00000000000..fe63532889f --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml @@ -0,0 +1,17 @@ + + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml.cs new file mode 100644 index 00000000000..6ff63928e54 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.System; + +namespace Microsoft.PowerToys.Settings.UI.Controls +{ + public sealed partial class PageLink : UserControl + { + public PageLink() + { + this.InitializeComponent(); + } + + public string Text { get; set; } + + public Uri Link { get; set; } + + public ICommand Command { get; set; } + + public object CommandParameter { get; set; } + + private async void OnClick(object sender, RoutedEventArgs e) + { + if (Command != null && Command.CanExecute(CommandParameter)) + { + if (Command is AsyncRelayCommand asyncCommand) + { + await asyncCommand.ExecuteAsync(CommandParameter); + } + else + { + Command.Execute(CommandParameter); + } + + // Check if CommandParameter has been updated + if (CommandParameter is string uriString && !string.IsNullOrEmpty(uriString)) + { + _ = Launcher.LaunchUriAsync(new Uri(uriString)); + } + else if (Link != null) + { + _ = Launcher.LaunchUriAsync(Link); + } + } + else if (Link != null) + { + var uri = CommandParameter as string ?? Link.ToString(); + _ = Launcher.LaunchUriAsync(new Uri(uri)); + } + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml index 3443c03e0f8..9698cf6e6e8 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml @@ -368,7 +368,11 @@ - + diff --git a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs index 8d55056f64d..0d112c03f70 100644 --- a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs @@ -3,26 +3,41 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Abstractions; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; +using System.Web; +using System.Windows; +using CommunityToolkit.Common; +using CommunityToolkit.Mvvm.Input; using global::PowerToys.GPOWrapper; using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Controls; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Win32; +using Windows.Globalization; +using Windows.System.Profile; namespace Microsoft.PowerToys.Settings.UI.ViewModels { public class GeneralViewModel : Observable { + private static readonly object LockObject = new object(); + private static bool isBugReportThreadRunning; + private GeneralSettings GeneralSettingsConfig { get; set; } private UpdatingSettings UpdatingSettingsConfig { get; set; } @@ -57,6 +72,8 @@ public class GeneralViewModel : Observable public string RunningAsAdminDefaultText { get; set; } + public AsyncRelayCommand InitializeReportBugLinkCommand { get; } + private string _settingsConfigFileFolder = string.Empty; private IFileSystemWatcher _fileWatcher; @@ -74,6 +91,7 @@ public GeneralViewModel(ISettingsRepository settingsRepository, SelectSettingBackupDirEventHandler = new ButtonClickCommand(SelectSettingBackupDir); RestoreConfigsEventHandler = new ButtonClickCommand(RestoreConfigsClick); RefreshBackupStatusEventHandler = new ButtonClickCommand(RefreshBackupStatusEventHandlerClick); + InitializeReportBugLinkCommand = new AsyncRelayCommand(InitializeReportBugLinkAsync); HideBackupAndRestoreMessageAreaAction = hideBackupAndRestoreMessageAreaAction; DoBackupAndRestoreDryRun = doBackupAndRestoreDryRun; PickSingleFolderDialog = pickSingleFolderDialog; @@ -177,7 +195,304 @@ public GeneralViewModel(ISettingsRepository settingsRepository, private string _settingsBackupMessage; private string _backupRestoreMessageSeverity; + private string reportBugLink = "https://aka.ms/powerToysReportBug"; + + public enum InstallScope + { + PerMachine = 0, + PerUser, + } + + private const string InstallScopeRegKey = @"Software\Classes\powertoys\"; + // Gets or sets a value indicating whether run powertoys on start-up. + public string ReportBugLink + { + get => reportBugLink; + set + { + reportBugLink = value; + OnPropertyChanged(nameof(ReportBugLink)); + } + } + + public async Task InitializeReportBugLinkAsync() + { + string gitHubURL = string.Empty; + var version = HttpUtility.UrlEncode(Helper.GetProductVersion().TrimStart('v')); + + var otherSoftwareText = "OS Build Version: " + GetOSVersion() + "\n.NET Version: " + GetDotNetVersion() + "\n\n"; + var additionalInfo = HttpUtility.UrlEncode(otherSoftwareText); + var isElevatedRun = IsElevated ? "Running as admin: Yes" : "Running as admin: No"; + var windowsSettings = ReportWindowsSettings(); + + var current_install_scope = GetCurrentInstallScope(); + + var installScope = current_install_scope == InstallScope.PerUser ? "Installation : User" : "Installation : System"; + + additionalInfo += windowsSettings + "%0a" + installScope + "%0a" + isElevatedRun; + + var loadingMessage = new LoadingMessage(); + loadingMessage.XamlRoot = App.GetSettingsWindow().Content.XamlRoot; + + var cts = new CancellationTokenSource(); + + try + { + var showDialogTask = loadingMessage.ShowAsync().AsTask(); + var bugReportTask = Task.Run(() => LaunchBugReport(cts.Token), cts.Token); + + // Wait for either the dialog to be closed or the bug report to be generated + var completedTask = await Task.WhenAny(showDialogTask, bugReportTask); + + if (completedTask == showDialogTask && showDialogTask.GetResultOrDefault() == ContentDialogResult.Primary) + { + // Cancel the bug report task if the dialog was closed with Cancel + await cts.CancelAsync(); + } + else if (completedTask == bugReportTask) + { + loadingMessage.Hide(); + + // Bug report task completed + string reportResult = await bugReportTask; + + gitHubURL = "https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=Issue-Bug%2CNeeds-Triage&template=bug_report.yml" + + "&version=" + version + + "&additionalInfo=" + additionalInfo; + + var dialog = new ContentDialog + { + Title = string.IsNullOrEmpty(reportResult) ? string.Empty : reportResult, + Content = string.IsNullOrEmpty(reportResult) ? "Failed to generate bug report." : "Bug report generated on your desktop. Please attach the file to the GitHub issue.", + CloseButtonText = "OK", + XamlRoot = loadingMessage.XamlRoot, + }; + + await dialog.ShowAsync(); + } + } + catch (Exception ex) + { + await cts.CancelAsync(); + loadingMessage.Hide(); + var errorDialog = new ContentDialog + { + Title = "Error", + Content = $"An error occurred: {ex.Message}", + CloseButtonText = "OK", + XamlRoot = loadingMessage.XamlRoot, + }; + await errorDialog.ShowAsync(); + } + finally + { + ReportBugLink = !string.IsNullOrEmpty(gitHubURL) ? gitHubURL : "https://aka.ms/powerToysReportBug"; + } + } + + // Updated LaunchBugReport method to support cancellation + public string LaunchBugReport(CancellationToken token) + { + string bugReportPath = GetModuleFolderPath() + "\\..\\Tools\\PowerToys.BugReportTool.exe"; + string bugReportFileName = string.Empty; + + lock (LockObject) + { + if (!isBugReportThreadRunning) + { + isBugReportThreadRunning = true; + Thread bugReportThread = new Thread(() => + { + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = bugReportPath, + CreateNoWindow = true, + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + try + { + using (Process process = Process.Start(startInfo)) + { + while (!process.HasExited) + { + if (token.IsCancellationRequested) + { + process.Kill(); + return; + } + + Thread.Sleep(100); + } + } + + // Find the newest bug report file on the desktop + bugReportFileName = FindNewestBugReportFile(); + } + catch (Exception ex) + { + MessageBox.Show("Failed to start bug report tool: " + ex.Message, "Error"); + } + finally + { + isBugReportThreadRunning = false; + } + }); + bugReportThread.IsBackground = true; + bugReportThread.Start(); + bugReportThread.Join(); + } + } + + return bugReportFileName; + } + + private static string GetModuleFolderPath() + { + // You would implement this method to get the path to your module folder in C# + return Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + } + + private string FindNewestBugReportFile() + { + string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + DirectoryInfo directoryInfo = new DirectoryInfo(desktopPath); + + // Get all files starting with "PowerToysReport_" + FileInfo[] files = directoryInfo.GetFiles("PowerToysReport_*"); + + if (files.Length == 0) + { + return string.Empty; + } + + // Find the newest file + FileInfo newestFile = files.OrderByDescending(f => f.LastWriteTime).FirstOrDefault(); + return newestFile?.Name; + } + + public static string GetDotNetVersion() + { + var output = ExecuteCommand("dotnet --list-runtimes"); + if (string.IsNullOrEmpty(output)) + { + return "Unknown .NET Version"; + } + + var versions = output + .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) + .Select(line => + { + var versionString = line.Split(' ')[1]; + if (Version.TryParse(versionString, out var version)) + { + return version; + } + + return new Version(0, 0, 0); + }) + .Where(version => version > new Version(0, 0, 0)) + .ToList(); + + var latestVersion = versions.Max(); + return $".NET {latestVersion}"; + } + + private static string ExecuteCommand(string command) + { + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/C {command}", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + }, + }; + + process.Start(); + using var reader = process.StandardOutput; + return reader.ReadToEnd(); + } + catch (Exception ex) + { + return $"Failed to execute command: {ex.Message}"; + } + } + + private string GetOSVersion() + { + var attrNames = new List { "OSVersionFull" }; + var attrData = AnalyticsInfo.GetSystemPropertiesAsync(attrNames).AsTask().GetAwaiter().GetResult(); + var osVersion = string.Empty; + if (attrData.ContainsKey("OSVersionFull")) + { + osVersion = attrData["OSVersionFull"]; + var versionParts = osVersion.Split('.'); + if (versionParts.Length >= 3) + { + osVersion = $"{versionParts[0]}.{versionParts[1]}.{versionParts[2]}"; + } + } + + return osVersion.ToString(); + } + + public static string ReportWindowsSettings() + { + string userLanguage; + string userLocale; + string result; + + try + { + var languages = ApplicationLanguages.Languages; + userLanguage = new Language(languages[0]).DisplayName; + + userLocale = CultureInfo.CurrentCulture.Name; + } + catch (Exception) + { + return "Failed to get windows settings\n"; + } + + result = "System Language: " + userLanguage + "%0a"; + result += "User Locale: " + userLocale + "%0a"; + + return result; + } + + public static InstallScope GetCurrentInstallScope() + { + // Check HKLM first + if (Registry.LocalMachine.OpenSubKey(InstallScopeRegKey) != null) + { + return InstallScope.PerMachine; + } + + // If not found, check HKCU + var userKey = Registry.CurrentUser.OpenSubKey(InstallScopeRegKey); + if (userKey != null) + { + var installScope = userKey.GetValue("InstallScope") as string; + userKey.Close(); + if (!string.IsNullOrEmpty(installScope) && installScope.Contains("perUser")) + { + return InstallScope.PerUser; + } + } + + return InstallScope.PerMachine; // Default if no specific registry key found + } + public bool Startup { get