diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..5a6991232 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,7 @@ +[submodule "deps/spdlog"] + path = deps/spdlog + url = https://github.com/gabime/spdlog.git + +[submodule "deps/cxxopts"] + path = deps/cxxopts + url = https://github.com/jarro2783/cxxopts.git diff --git a/PowerToys.sln b/PowerToys.sln index ce3c63635..c44829d47 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -269,6 +269,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Calculator EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Folder.UnitTests", "src\modules\launcher\Plugins\Microsoft.Plugin.Folder.UnitTests\Microsoft.Plugin.Folder.UnitTests.csproj", "{4FA206A5-F69F-4193-BF8F-F6EEB496734C}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "logging", "src\logging\logging.vcxproj", "{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -543,6 +545,10 @@ Global {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Debug|x64.Build.0 = Debug|x64 {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|x64.ActiveCfg = Release|x64 {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|x64.Build.0 = Release|x64 + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.ActiveCfg = Debug|x64 + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.Build.0 = Debug|x64 + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.ActiveCfg = Release|x64 + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -621,6 +627,7 @@ Global {0F85E674-34AE-443D-954C-8321EB8B93B1} = {C3081D9A-1586-441A-B5F4-ED815B3719C1} {632BBE62-5421-49EA-835A-7FFA4F499BD6} = {4AFC9975-2456-4C70-94A4-84073C1CED93} {4FA206A5-F69F-4193-BF8F-F6EEB496734C} = {4AFC9975-2456-4C70-94A4-84073C1CED93} + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F} = {1AFB6476-670D-4E80-A464-657E01DFF482} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/deps/cxxopts b/deps/cxxopts new file mode 160000 index 000000000..12e496da3 --- /dev/null +++ b/deps/cxxopts @@ -0,0 +1 @@ +Subproject commit 12e496da3d486b87fa9df43edea65232ed852510 diff --git a/deps/cxxopts.props b/deps/cxxopts.props new file mode 100644 index 000000000..25cc72b19 --- /dev/null +++ b/deps/cxxopts.props @@ -0,0 +1,7 @@ + + + + $(MSBuildThisFileDirectory)cxxopts\include;%(AdditionalIncludeDirectories) + + + diff --git a/deps/spdlog b/deps/spdlog new file mode 160000 index 000000000..cbe944865 --- /dev/null +++ b/deps/spdlog @@ -0,0 +1 @@ +Subproject commit cbe9448650176797739dbab13961ef4c07f4290f diff --git a/deps/spdlog.props b/deps/spdlog.props new file mode 100644 index 000000000..2689f2423 --- /dev/null +++ b/deps/spdlog.props @@ -0,0 +1,8 @@ + + + + $(MSBuildThisFileDirectory)spdlog\include;%(AdditionalIncludeDirectories) + SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_COMPILED_LIB;%(PreprocessorDefinitions) + + + diff --git a/installer/PowerToysBootstrapper/PowerToysBootstrapper.sln b/installer/PowerToysBootstrapper/PowerToysBootstrapper.sln index f935a035e..119aa75d0 100644 --- a/installer/PowerToysBootstrapper/PowerToysBootstrapper.sln +++ b/installer/PowerToysBootstrapper/PowerToysBootstrapper.sln @@ -9,6 +9,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "common", "..\..\src\common\ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "bootstrapper", "bootstrapper\bootstrapper.vcxproj", "{D194E3AA-F824-4CA9-9A58-034DD6B7D022}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "logging", "..\..\src\logging\logging.vcxproj", "{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -27,6 +29,10 @@ Global {D194E3AA-F824-4CA9-9A58-034DD6B7D022}.Debug|x64.Build.0 = Debug|x64 {D194E3AA-F824-4CA9-9A58-034DD6B7D022}.Release|x64.ActiveCfg = Release|x64 {D194E3AA-F824-4CA9-9A58-034DD6B7D022}.Release|x64.Build.0 = Release|x64 + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.ActiveCfg = Debug|x64 + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.Build.0 = Debug|x64 + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.ActiveCfg = Release|x64 + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/installer/PowerToysBootstrapper/bootstrapper/bootstrapper.cpp b/installer/PowerToysBootstrapper/bootstrapper/bootstrapper.cpp index cc22f38b3..2cbc5184a 100644 --- a/installer/PowerToysBootstrapper/bootstrapper/bootstrapper.cpp +++ b/installer/PowerToysBootstrapper/bootstrapper/bootstrapper.cpp @@ -10,16 +10,22 @@ #include #include - #include extern "C" IMAGE_DOS_HEADER __ImageBase; +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) namespace { const wchar_t APPLICATION_ID[] = L"PowerToysInstaller"; const wchar_t TOAST_TAG[] = L"PowerToysInstallerProgress"; + const char LOG_FILENAME[] = "powertoys-bootstrapper-" 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 localized_strings { @@ -59,63 +65,89 @@ std::optional extractIcon() return iconRes->saveAsFile(icoPath) ? std::make_optional(std::move(icoPath)) : std::nullopt; } -enum class CmdArgs +void setup_log(const spdlog::level::level_enum severity) { - silent, - noFullUI, - noStartPT, - skipDotnetInstall, - showHelp -}; - -namespace -{ - const std::unordered_map knownArgs = { - { L"--help", CmdArgs::showHelp }, - { L"--no_full_ui", CmdArgs::noFullUI }, - { L"--silent", CmdArgs::silent }, - { L"--no_start_pt", CmdArgs::noStartPT }, - { L"--skip_dotnet_install", CmdArgs::skipDotnetInstall } - }; -} - -std::unordered_set parseCmdArgs(const int nCmdArgs, LPWSTR* argList) -{ - std::unordered_set result; - for (size_t i = 1; i < nCmdArgs; ++i) + try { - if (auto it = knownArgs.find(argList[i]); it != end(knownArgs)) + std::shared_ptr logger; + if (severity != spdlog::level::off) { - result.emplace(it->second); + logger = spdlog::basic_logger_mt("file", LOG_FILENAME); + + std::error_code _; + const DWORD msiSev = severity == spdlog::level::debug ? INSTALLLOGMODE_VERBOSE : INSTALLLOGMODE_ERROR; + const auto msiLogPath = fs::current_path(_) / MSI_LOG_FILENAME; + MsiEnableLogW(msiSev, msiLogPath.c_str(), INSTALLLOGATTRIBUTES_APPEND); } + else + { + logger = spdlog::null_logger_mt("null"); + } + 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 (...) + { } - return result; } int bootstrapper() { using namespace localized_strings; winrt::init_apartment(); - - int nCmdArgs = 0; - LPWSTR* argList = CommandLineToArgvW(GetCommandLineW(), &nCmdArgs); - const auto cmdArgs = parseCmdArgs(nCmdArgs, argList); - std::wostringstream oss; - if (cmdArgs.contains(CmdArgs::showHelp)) + 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 MSI UI and notifications") + ("no_start_pt", "Do not 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")); + // clang-format on + cxxopts::ParseResult cmdArgs; + options.allow_unrecognised_options(); + try { - oss << "Supported arguments:\n\n"; - for (auto [arg, _] : knownArgs) - { - oss << arg << '\n'; - } - MessageBoxW(nullptr, oss.str().c_str(), L"Help", MB_OK | MB_ICONINFORMATION); + cmdArgs = options.parse(__argc, const_cast(__argv)); + } + catch (...) + { + } + const bool showHelp = cmdArgs["help"].as(); + const bool noFullUI = cmdArgs["no_full_ui"].as(); + const bool silent = cmdArgs["silent"].as(); + const bool skipDotnetInstall = cmdArgs["skip_dotnet_install"].as(); + const bool noStartPT = cmdArgs["no_start_pt"].as(); + const auto logLevel = cmdArgs["log_level"].as(); + spdlog::level::level_enum severity = spdlog::level::off; + + if (logLevel == "debug") + { + severity = spdlog::level::debug; + } + else if (logLevel == "error") + { + severity = spdlog::level::err; + } + setup_log(severity); + if (showHelp) + { + std::ostringstream helpMsg; + helpMsg << options.help(); + MessageBoxA(nullptr, helpMsg.str().c_str(), "Help", MB_OK | MB_ICONINFORMATION); return 0; } - if (!cmdArgs.contains(CmdArgs::noFullUI)) + spdlog::debug("PowerToys Bootstrapper is launched!\nnoFullUI: {}\nsilent: {}\nno_start_pt: {}\nskip_dotnet_install: {}\nlog_level: {}", noFullUI, silent, noStartPT, skipDotnetInstall, logLevel); + + if (!noFullUI) { MsiSetInternalUI(INSTALLUILEVEL_FULL, nullptr); } - if (cmdArgs.contains(CmdArgs::silent)) + if (silent) { if (is_process_elevated()) { @@ -123,8 +155,12 @@ int bootstrapper() } else { - // MSI fails to run in silent mode due to a suppressed UAC w/o elevation, so we restart elevated + 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) { params += argList[i]; @@ -136,6 +172,7 @@ int bootstrapper() 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) @@ -146,6 +183,7 @@ int bootstrapper() } 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); @@ -163,16 +201,17 @@ int bootstrapper() auto instanceMutex = createAppMutex(POWERTOYS_BOOTSTRAPPER_MUTEX_NAME); if (!instanceMutex) { + spdlog::error("Couldn't acquire PowerToys global mutex. That means setup couldn't kill PowerToys.exe process"); return 1; } notifications::override_application_id(APPLICATION_ID); - + spdlog::debug("Extracting icon for toast notifications"); fs::path iconPath{ L"C:\\" }; if (auto extractedIcon = extractIcon()) { iconPath = std::move(*extractedIcon); } - + spdlog::debug("Registering app id for toast notifications"); notifications::register_application_id(TOAST_TITLE, iconPath.c_str()); auto removeShortcut = wil::scope_exit([&] { @@ -186,6 +225,7 @@ int bootstrapper() auto msi_path = updating::get_msi_package_path(); if (!msi_path.empty()) { + spdlog::error(L"Detected a newer {} version => launching its installer", installedVersion->toWstring()); MsiInstallProductW(msi_path.c_str(), nullptr); return 0; } @@ -196,19 +236,22 @@ int bootstrapper() progressParams.progress = 0.0f; progressParams.progress_title = EXTRACTING_INSTALLER; notifications::toast_params params{ TOAST_TAG, false, std::move(progressParams) }; - if (!cmdArgs.contains(CmdArgs::silent)) + if (!silent) { + spdlog::debug("Launching progress toast notification"); notifications::show_toast_with_activations({}, TOAST_TITLE, {}, {}, std::move(params)); } auto processToasts = wil::scope_exit([&] { + spdlog::debug("Processing HWND messages for 2s so toast have time to show up"); run_message_loop(true, 2); }); - if (!cmdArgs.contains(CmdArgs::silent)) + if (!silent) { // Worker thread to periodically increase progress and keep the progress toast from losing focus std::thread{ [&] { + spdlog::debug("Started worker thread for progress bar update"); for (;; Sleep(3000)) { std::scoped_lock lock{ progressLock }; @@ -223,7 +266,7 @@ int bootstrapper() } auto updateProgressBar = [&](const float value, const wchar_t* title) { - if (cmdArgs.contains(CmdArgs::silent)) + if (silent) { return; } @@ -233,13 +276,15 @@ int bootstrapper() notifications::update_progress_bar_toast(TOAST_TAG, progressParams); }; + spdlog::debug("Extracting embedded MSI installer"); const auto installerPath = extractEmbeddedInstaller(); if (!installerPath) { - if (!cmdArgs.contains(CmdArgs::silent)) + if (!silent) { notifications::show_toast(INSTALLER_EXTRACT_ERROR, TOAST_TITLE); } + spdlog::error("Couldn't install the MSI installer ({})", GetLastError()); return 1; } auto removeExtractedInstaller = wil::scope_exit([&] { @@ -248,12 +293,22 @@ int bootstrapper() }); updateProgressBar(.25f, UNINSTALLING_PREVIOUS_VERSION); + spdlog::debug("Acquiring existing MSI package path"); const auto package_path = updating::get_msi_package_path(); - if (!package_path.empty() && !updating::uninstall_msi_version(package_path) && !cmdArgs.contains(CmdArgs::silent)) + if (!package_path.empty()) { + spdlog::debug(L"Existing MSI package path: {}", package_path); + } + else + { + spdlog::debug("Existing MSI package path not found"); + } + if (!package_path.empty() && !updating::uninstall_msi_version(package_path) && !silent) + { + spdlog::error("Couldn't install the existing MSI package ({})", GetLastError()); notifications::show_toast(UNINSTALL_PREVIOUS_VERSION_ERROR, TOAST_TITLE); } - const bool installDotnet = !cmdArgs.contains(CmdArgs::skipDotnetInstall); + const bool installDotnet = !skipDotnetInstall; if (installDotnet) { updateProgressBar(.5f, INSTALLING_DOTNET); @@ -261,16 +316,22 @@ int bootstrapper() try { - if (installDotnet && - !updating::dotnet_is_installed() && - !updating::install_dotnet(cmdArgs.contains(CmdArgs::silent)) && - !cmdArgs.contains(CmdArgs::silent)) + if (installDotnet) { - notifications::show_toast(DOTNET_INSTALL_ERROR, TOAST_TITLE); + spdlog::debug("Detecting if dotnet is installed"); + const bool dotnetInstalled = updating::dotnet_is_installed(); + spdlog::debug("Dotnet is installed: {}", dotnetInstalled); + if (!dotnetInstalled && + !updating::install_dotnet(silent) && + !silent) + { + notifications::show_toast(DOTNET_INSTALL_ERROR, TOAST_TITLE); + } } } catch (...) { + spdlog::error("Unknown exception during dotnet installation"); MessageBoxW(nullptr, L".NET Core installation", L"Unknown exception encountered!", MB_OK | MB_ICONERROR); } @@ -278,18 +339,23 @@ int bootstrapper() // Always skip dotnet install, because we should've installed it from here earlier std::wstring msiProps = L"SKIPDOTNETINSTALL=1 "; + spdlog::debug("Launching MSI installation for new package {}", installerPath->string()); const bool installationDone = MsiInstallProductW(installerPath->c_str(), msiProps.c_str()) == ERROR_SUCCESS; updateProgressBar(1.f, installationDone ? NEW_VERSION_INSTALLATION_DONE : NEW_VERSION_INSTALLATION_ERROR); if (!installationDone) { + spdlog::error("Couldn't install new MSI package ({})", GetLastError()); return 1; } + spdlog::debug("Installation completed"); - if (!cmdArgs.contains(CmdArgs::noStartPT) && !cmdArgs.contains(CmdArgs::silent)) + if (!noStartPT && !silent) { + spdlog::debug("Starting the newly installed PowerToys.exe"); auto newPTPath = updating::get_msi_package_installed_path(); if (!newPTPath) { + spdlog::error("Couldn't determine new MSI package install location ({})", GetLastError()); return 1; } *newPTPath += L"\\PowerToys.exe"; diff --git a/installer/PowerToysBootstrapper/bootstrapper/bootstrapper.vcxproj b/installer/PowerToysBootstrapper/bootstrapper/bootstrapper.vcxproj index 3966dac23..469100100 100644 --- a/installer/PowerToysBootstrapper/bootstrapper/bootstrapper.vcxproj +++ b/installer/PowerToysBootstrapper/bootstrapper/bootstrapper.vcxproj @@ -28,6 +28,8 @@ bootstrapper + + Application true @@ -78,7 +80,7 @@ true MultiThreaded true - ../../../src/ + ../../../src/;%(AdditionalIncludeDirectories) Use pch.h @@ -99,7 +101,7 @@ true MultiThreadedDebug true - ../../../src/ + ../../../src/;%(AdditionalIncludeDirectories) Use pch.h @@ -137,6 +139,9 @@ {17da04df-e393-4397-9cf0-84dabe11032e} + + {7e1e3f13-2bd6-3f75-a6a7-873a2b55c60f} + diff --git a/installer/PowerToysBootstrapper/bootstrapper/pch.h b/installer/PowerToysBootstrapper/bootstrapper/pch.h index 11dc82b4f..b9367228c 100644 --- a/installer/PowerToysBootstrapper/bootstrapper/pch.h +++ b/installer/PowerToysBootstrapper/bootstrapper/pch.h @@ -14,3 +14,10 @@ #include #include #include + +#include +#include +#include +#include + +#include diff --git a/src/logging/logging.vcxproj b/src/logging/logging.vcxproj new file mode 100644 index 000000000..6649b4e2b --- /dev/null +++ b/src/logging/logging.vcxproj @@ -0,0 +1,178 @@ + + + + x64 + + + + Debug + x64 + + + Release + x64 + + + + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F} + 10.0.19041.0 + Win32Proj + x64 + logging + NoUpgrade + + + + + StaticLibrary + MultiByte + v142 + + + StaticLibrary + MultiByte + v142 + + + + + + + + + + true + $(SolutionDir)$(Platform)\$(Configuration)\obj\$(ProjectName)\ + + + + MultiThreadedDebug + + + + + MaxSpeed + + + + + $(IntDir) + Default + CompileAsCpp + None + Sync + AnySuitable + NotUsing + true + false + Level4 + WIN32;_WINDOWS;SPDLOG_COMPILED_LIB;%(PreprocessorDefinitions) + $(IntDir) + true + true + true + true + MultiThreaded + + + %(AdditionalOptions) /machine:x64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/logging/logging.vcxproj.filters b/src/logging/logging.vcxproj.filters new file mode 100644 index 000000000..937fae1a2 --- /dev/null +++ b/src/logging/logging.vcxproj.filters @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {CDF4BA23-560C-3A6F-8D1C-2F5ACA434329} + + + {EFFE8123-D806-3145-8ABC-B48562A6C8F2} + + + {C546A431-88F1-390F-B0F0-D9CAC274B7F5} + + + {08320F28-6D0D-3217-B0B3-A98758C02C97} + + + {C856528D-4506-3A62-B279-CBB4558CB61D} + + + {A5EE33C4-AB64-38F0-BF4A-CCD02FFAB715} + + + {8B480F42-A230-3344-A387-2D050CFF7D9C} + + + \ No newline at end of file