#include "pch.h" #include "Generated Files/resource.h" #include "RcResource.h" #include #include #include #include #include #include #include #include #include #include #include #include "DotnetInstallation.h" #include "progressbar_window.h" static bool g_Silent = false; #define STR_HELPER(x) #x #define STR(x) STR_HELPER(x) namespace // Strings in this namespace should not be localized { const wchar_t APPLICATION_ID[] = L"PowerToysInstaller"; const char EXE_LOG_FILENAME[] = "powertoys-bootstrapper-exe-" STR(VERSION_MAJOR) "." STR(VERSION_MINOR) "." STR(VERSION_REVISION) ".log"; const char MSI_LOG_FILENAME[] = "powertoys-bootstrapper-msi-" STR(VERSION_MAJOR) "." STR(VERSION_MINOR) "." STR(VERSION_REVISION) ".log"; } #undef STR #undef STR_HELPER namespace fs = std::filesystem; std::optional ExtractEmbeddedInstaller(const fs::path extractPath) { auto executableRes = RcResource::create(IDR_BIN_MSIINSTALLER, L"BIN"); if (!executableRes) { return std::nullopt; } auto installerPath = extractPath / L"PowerToysBootstrappedInstaller-" PRODUCT_VERSION_STRING L".msi"; return executableRes->saveAsFile(installerPath) ? std::make_optional(std::move(installerPath)) : std::nullopt; } void SetupLogger(fs::path directory, const spdlog::level::level_enum severity) { std::shared_ptr logger; auto nullLogger = spdlog::null_logger_mt("null"); try { if (severity != spdlog::level::off) { logger = spdlog::basic_logger_mt("file", (directory / EXE_LOG_FILENAME).wstring()); std::error_code _; const DWORD msiSev = severity == spdlog::level::debug ? INSTALLLOGMODE_VERBOSE : INSTALLLOGMODE_ERROR; const auto msiLogPath = directory / MSI_LOG_FILENAME; MsiEnableLogW(msiSev, msiLogPath.c_str(), INSTALLLOGATTRIBUTES_APPEND); } else { logger = nullLogger; } logger->set_pattern("[%L][%d-%m-%C-%T] %v"); logger->set_level(severity); spdlog::set_default_logger(std::move(logger)); spdlog::set_level(severity); spdlog::flush_every(std::chrono::seconds(5)); } catch (...) { spdlog::set_default_logger(nullLogger); } } void CleanupSettingsFromOlderVersions() { try { const auto logSettingsFile = fs::path{ PTSettingsHelper::get_root_save_folder_location() } / PTSettingsHelper::log_settings_filename; if (fs::is_regular_file(logSettingsFile)) { fs::remove(logSettingsFile); spdlog::info("Removed old log settings file"); } else { spdlog::info("Old log settings file wasn't found"); } } catch (...) { spdlog::error("Failed to cleanup old log settings"); } } void ShowMessageBoxError(const wchar_t* message) { if (!g_Silent) { MessageBoxW(nullptr, message, GET_RESOURCE_STRING(IDS_BOOTSTRAPPER_PROGRESS_TITLE).c_str(), MB_OK | MB_ICONERROR); } } void ShowMessageBoxError(const UINT messageId) { ShowMessageBoxError(GET_RESOURCE_STRING(messageId).c_str()); } bool uninstall_msi_version(const std::wstring& package_path) { const auto uninstall_result = MsiInstallProductW(package_path.c_str(), L"REMOVE=ALL"); return ERROR_SUCCESS == uninstall_result; } std::optional get_installed_powertoys_version() { auto installed_path = GetMsiPackageInstalledPath(); if (!installed_path) { return std::nullopt; } *installed_path += L"\\PowerToys.exe"; // Get the version information for the file requested const DWORD fvSize = GetFileVersionInfoSizeW(installed_path->c_str(), nullptr); if (!fvSize) { return std::nullopt; } auto pbVersionInfo = std::make_unique(fvSize); if (!GetFileVersionInfoW(installed_path->c_str(), 0, fvSize, pbVersionInfo.get())) { return std::nullopt; } VS_FIXEDFILEINFO* fileInfo = nullptr; UINT fileInfoLen = 0; if (!VerQueryValueW(pbVersionInfo.get(), L"\\", reinterpret_cast(&fileInfo), &fileInfoLen)) { return std::nullopt; } return VersionHelper{ (fileInfo->dwFileVersionMS >> 16) & 0xffff, (fileInfo->dwFileVersionMS >> 0) & 0xffff, (fileInfo->dwFileVersionLS >> 16) & 0xffff }; } int Bootstrapper(HINSTANCE hInstance) { winrt::init_apartment(); char* programFilesDir = nullptr; size_t size = 0; std::string defaultInstallDir; if (!_dupenv_s(&programFilesDir, &size, "PROGRAMFILES")) { defaultInstallDir += programFilesDir; defaultInstallDir += "\\PowerToys"; } fs::path logDir = PTSettingsHelper::get_root_save_folder_location(); cxxopts::Options options{ "PowerToysBootstrapper" }; // clang-format off options.add_options() ("h,help", "Show help") ("no_full_ui", "Use reduced UI for MSI") ("s,silent", "Suppress all UI, notifications and does not start PowerToys") ("no_start_pt", "Do not launch PowerToys after the installation is complete") ("start_pt", "Always launch PowerToys after the installation is complete") ("skip_dotnet_install", "Skip dotnet 3.X installation even if it's not detected") ("log_level", "Log level. Possible values: off|debug|error", cxxopts::value()->default_value("off")) ("log_dir", "Log directory", cxxopts::value()->default_value(logDir.string())) ("install_dir", "Installation directory", cxxopts::value()->default_value(defaultInstallDir)) ("extract_msi", "Extract MSI to the working directory and exit. Use only if you must access MSI directly."); // clang-format on cxxopts::ParseResult cmdArgs; bool showHelp = false; try { cmdArgs = options.parse(__argc, const_cast(__argv)); } catch (...) { showHelp = true; } showHelp = showHelp || cmdArgs["help"].as(); if (showHelp) { std::ostringstream helpMsg; helpMsg << options.help(); MessageBoxA(nullptr, helpMsg.str().c_str(), "Help", MB_OK | MB_ICONINFORMATION); return 0; } g_Silent = cmdArgs["silent"].as(); const bool noFullUI = cmdArgs["no_full_ui"].as(); const bool skipDotnetInstall = cmdArgs["skip_dotnet_install"].as(); const bool noStartPT = cmdArgs["no_start_pt"].as(); const bool startPT = cmdArgs["start_pt"].as(); const auto logLevel = cmdArgs["log_level"].as(); const auto logDirArg = cmdArgs["log_dir"].as(); const auto installDirArg = cmdArgs["install_dir"].as(); const bool extractMsiOnly = cmdArgs["extract_msi"].as(); std::wstring installFolderProp; if (!installDirArg.empty()) { std::string installDir; if (installDirArg.find(' ') != std::string::npos) { installDir = "\"" + installDirArg + "\""; } else { installDir = installDirArg; } installFolderProp = std::wstring(installDir.length(), L' '); std::copy(installDir.begin(), installDir.end(), installFolderProp.begin()); installFolderProp = L"INSTALLFOLDER=" + installFolderProp; } try { fs::path logDirArgPath = logDirArg; if (fs::exists(logDirArgPath) && fs::is_directory(logDirArgPath)) { logDir = logDirArgPath; } } catch (...) { } spdlog::level::level_enum severity = spdlog::level::debug; if (logLevel == "error") { severity = spdlog::level::err; } SetupLogger(logDir, severity); spdlog::debug("PowerToys Bootstrapper is launched\nnoFullUI: {}\nsilent: {}\nno_start_pt: {}\nskip_dotnet_install: {}\nlog_level: {}\ninstall_dir: {}\nextract_msi: {}\n", noFullUI, g_Silent, noStartPT, skipDotnetInstall, logLevel, installDirArg, extractMsiOnly); // If a user requested an MSI -> extract it and exit if (extractMsiOnly) { if (const auto installerPath = ExtractEmbeddedInstaller(fs::current_path())) { spdlog::debug("MSI installer extracted to {}", installerPath->string()); } else { spdlog::error("MSI installer couldn't be extracted"); } return 0; } const VersionHelper myVersion(VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION); // Do not support installing on Windows < 1903 if (!Is19H1OrHigher()) { ShowMessageBoxError(IDS_OLD_WINDOWS_ERROR); spdlog::error("PowerToys {} requires at least Windows 1903 to run.", myVersion.toString()); return 1; } // Check if there's a newer version installed const auto installedVersion = get_installed_powertoys_version(); if (installedVersion && *installedVersion >= myVersion) { spdlog::error(L"Detected a newer version {} vs {}", (*installedVersion).toWstring(), myVersion.toWstring()); ShowMessageBoxError(IDS_NEWER_VERSION_ERROR); return 0; } // Setup MSI UI visibility and restart as elevated if required if (!noFullUI) { MsiSetInternalUI(INSTALLUILEVEL_FULL, nullptr); } if (g_Silent) { if (is_process_elevated()) { MsiSetInternalUI(INSTALLUILEVEL_NONE, nullptr); } else { spdlog::debug("MSI doesn't support silent mode without elevation => restarting elevated"); // MSI fails to run in silent mode due to a suppressed UAC w/o elevation, // so we restart ourselves elevated with the same args std::wstring params; int nCmdArgs = 0; LPWSTR* argList = CommandLineToArgvW(GetCommandLineW(), &nCmdArgs); for (int i = 1; i < nCmdArgs; ++i) { if (std::wstring_view{ argList[i] }.find(L' ') != std::wstring_view::npos) { params += L'"'; params += argList[i]; params += L'"'; } else { params += argList[i]; } if (i != nCmdArgs - 1) { params += L' '; } } const auto processHandle = run_elevated(argList[0], params.c_str()); if (!processHandle) { spdlog::error("Couldn't restart elevated to enable silent mode! ({})", GetLastError()); return 1; } if (WaitForSingleObject(processHandle, 3600000) == WAIT_OBJECT_0) { DWORD exitCode = 0; GetExitCodeProcess(processHandle, &exitCode); return exitCode; } else { spdlog::error("Elevated setup process timed out after 60m => using basic MSI UI ({})", GetLastError()); // Couldn't install using the completely silent mode in an hour, use basic UI. TerminateProcess(processHandle, 0); MsiSetInternalUI(INSTALLUILEVEL_BASIC, nullptr); } } } // Try killing PowerToys and prevent future processes launch by acquiring app mutex for (auto& handle : getProcessHandlesByName(L"PowerToys.exe", PROCESS_TERMINATE)) { TerminateProcess(handle.get(), 0); } auto powerToysMutex = createAppMutex(POWERTOYS_MSI_MUTEX_NAME); auto instanceMutex = createAppMutex(POWERTOYS_BOOTSTRAPPER_MUTEX_NAME); if (!instanceMutex) { spdlog::error("Couldn't acquire PowerToys global mutex. Setup couldn't terminate PowerToys.exe process"); return 1; } spdlog::debug("Extracting embedded MSI installer"); const auto installerPath = ExtractEmbeddedInstaller(fs::temp_directory_path()); if (!installerPath) { ShowMessageBoxError(IDS_INSTALLER_EXTRACT_ERROR); spdlog::error("Couldn't install the MSI installer ({})", GetLastError()); return 1; } auto removeExtractedInstaller = wil::scope_exit([&] { std::error_code _; fs::remove(*installerPath, _); }); spdlog::debug("Acquiring existing MSI package path if exists"); const auto package_path = GetMsiPackagePath(); if (!package_path.empty()) { spdlog::debug(L"Existing MSI package path found: {}", package_path); } else { spdlog::debug("Existing MSI package path not found"); } if (!package_path.empty() && !uninstall_msi_version(package_path)) { spdlog::error("Couldn't install the existing MSI package ({})", GetLastError()); ShowMessageBoxError(IDS_UNINSTALL_PREVIOUS_VERSION_ERROR); return 1; } const bool installDotnet = !skipDotnetInstall; if (!g_Silent) { OpenProgressBarDialog(hInstance, 0, GET_RESOURCE_STRING(IDS_BOOTSTRAPPER_PROGRESS_TITLE).c_str(), GET_RESOURCE_STRING(IDS_DOWNLOADING_DOTNET).c_str()); } try { if (installDotnet) { auto dotnet3Info = std::make_tuple(VersionHelper{ 3, 1, 15 }, L"https://download.visualstudio.microsoft.com/download/pr/d30352fe-d4f3-4203-91b9-01a3b66a802e/bb416e6573fa278fec92113abefc58b3/windowsdesktop-runtime-3.1.15-win-x64.exe"); auto dotnet5Info = std::make_tuple(VersionHelper{ 5, 0, 7 }, L"https://download.visualstudio.microsoft.com/download/pr/2b83d30e-5c86-4d37-a1a6-582e22ac07b2/c7b1b7e21761bbfb7b9951f5b258806e/windowsdesktop-runtime-5.0.7-win-x64.exe"); const std::array dotnetsToInstall = { std::move(dotnet3Info), std::move(dotnet5Info) }; for (const auto& [ver, downloadLink] : dotnetsToInstall) { const auto& [major, minor, minimalRequiredPatch] = ver; spdlog::debug("Detecting if dotnet {} is installed", ver.toString()); const bool dotnetInstalled = updating::dotnet_is_installed(major, minor, minimalRequiredPatch); if (dotnetInstalled) { spdlog::debug("Dotnet {} is already installed: {}", ver.toString(), dotnetInstalled); continue; } bool installedSuccessfully = false; if (const auto dotnetInstallerPath = updating::download_dotnet(downloadLink)) { // Dotnet installer has its own progress bar CloseProgressBarDialog(); installedSuccessfully = updating::install_dotnet(*dotnetInstallerPath, g_Silent); if (!installedSuccessfully) { spdlog::error("Couldn't install dotnet {}", ver.toString()); } } else { spdlog::error("Couldn't download dotnet {}", ver.toString()); } if (!installedSuccessfully) { ShowMessageBoxError(IDS_DOTNET_INSTALL_ERROR); } } } } catch (...) { spdlog::error("Unknown exception during dotnet installation"); ShowMessageBoxError(IDS_DOTNET_INSTALL_ERROR); } // At this point, there's no reason to show progress bar window, since MSI installers have their own CloseProgressBarDialog(); const std::wstring msiProps = installFolderProp; spdlog::debug("Launching MSI installation for new package {}", installerPath->string()); const bool installationDone = MsiInstallProductW(installerPath->c_str(), msiProps.c_str()) == ERROR_SUCCESS; if (!installationDone) { spdlog::error("Couldn't install new MSI package ({})", GetLastError()); return 1; } spdlog::debug("Installation completed"); if ((!noStartPT && !g_Silent) || startPT) { spdlog::debug("Starting the newly installed PowerToys.exe"); auto newPTPath = GetMsiPackageInstalledPath(); if (!newPTPath) { spdlog::error("Couldn't determine new MSI package install location ({})", GetLastError()); return 1; } *newPTPath += L"\\PowerToys.exe"; SHELLEXECUTEINFOW sei{ sizeof(sei) }; sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC | SEE_MASK_NO_CONSOLE }; sei.lpFile = newPTPath->c_str(); sei.nShow = SW_SHOWNORMAL; ShellExecuteExW(&sei); } return 0; } int WINAPI WinMain(HINSTANCE hi, HINSTANCE, LPSTR, int) { try { return Bootstrapper(hi); } catch (const std::exception& ex) { std::string messageA{ "Unhandled std exception encountered\n" }; messageA.append(ex.what()); spdlog::error(messageA.c_str()); std::wstring messageW{}; std::copy(messageA.begin(), messageA.end(), messageW.begin()); ShowMessageBoxError(messageW.c_str()); } catch (winrt::hresult_error const& ex) { std::wstring message{ L"Unhandled winrt exception encountered\n" }; message.append(ex.message().c_str()); spdlog::error(message.c_str()); ShowMessageBoxError(message.c_str()); } catch (...) { auto lastErrorMessage = get_last_error_message(GetLastError()); std::wstring message{ L"Unknown exception encountered\n" }; message.append(lastErrorMessage ? std::move(*lastErrorMessage) : L""); spdlog::error(message.c_str()); ShowMessageBoxError(message.c_str()); } return 0; }