bootstrapper: implement logging support

- add cxxopts and spdlog libraries
- implement cmd flag for severity
- add debug and error messages
This commit is contained in:
yuyoyuppe
2020-10-14 17:36:03 +03:00
committed by Andrey Nekrasov
parent 36bcbe9d95
commit c34c963121
12 changed files with 473 additions and 58 deletions

View File

@@ -10,16 +10,22 @@
#include <common/appMutex.h>
#include <common/processApi.h>
#include <runner/action_runner_utils.h>
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<fs::path> 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<std::wstring_view, CmdArgs> 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<CmdArgs> parseCmdArgs(const int nCmdArgs, LPWSTR* argList)
{
std::unordered_set<CmdArgs> result;
for (size_t i = 1; i < nCmdArgs; ++i)
try
{
if (auto it = knownArgs.find(argList[i]); it != end(knownArgs))
std::shared_ptr<spdlog::logger> 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<std::string>()->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<const char**>(__argv));
}
catch (...)
{
}
const bool showHelp = cmdArgs["help"].as<bool>();
const bool noFullUI = cmdArgs["no_full_ui"].as<bool>();
const bool silent = cmdArgs["silent"].as<bool>();
const bool skipDotnetInstall = cmdArgs["skip_dotnet_install"].as<bool>();
const bool noStartPT = cmdArgs["no_start_pt"].as<bool>();
const auto logLevel = cmdArgs["log_level"].as<std::string>();
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";