diff --git a/lib/libimhex/include/hex/helpers/utils.hpp b/lib/libimhex/include/hex/helpers/utils.hpp index a82d91b6d..b3baebeed 100644 --- a/lib/libimhex/include/hex/helpers/utils.hpp +++ b/lib/libimhex/include/hex/helpers/utils.hpp @@ -98,6 +98,7 @@ namespace hex { void startProgram(const std::vector &command); int executeCommand(const std::string &command); + std::optional executeCommandWithOutput(const std::string &command); void openWebpage(std::string url); extern "C" void registerFont(const char *fontName, const char *fontPath); diff --git a/lib/libimhex/source/helpers/utils.cpp b/lib/libimhex/source/helpers/utils.cpp index c70fc4748..261dc219e 100644 --- a/lib/libimhex/source/helpers/utils.cpp +++ b/lib/libimhex/source/helpers/utils.cpp @@ -310,6 +310,50 @@ namespace hex { return ::system(command.c_str()); } + std::optional executeCommandWithOutput(const std::string &command) { + std::array buffer = {}; + std::string result; + + #if defined(OS_WINDOWS) + FILE* pipe = _popen(command.c_str(), "r"); + #else + FILE* pipe = popen(command.c_str(), "r"); + #endif + + if (!pipe) { + hex::log::error("Failed to open pipe for command: {}", command); + return std::nullopt; + } + + try { + while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { + result += buffer.data(); + } + } catch (const std::exception &e) { + hex::log::error("Exception while reading command output: {}", e.what()); + + #if defined(OS_WINDOWS) + _pclose(pipe); + #else + pclose(pipe); + #endif + + return std::nullopt; + } + + #if defined(OS_WINDOWS) + int exitCode = _pclose(pipe); + #else + int exitCode = pclose(pipe); + #endif + + if (exitCode != 0) { + hex::log::debug("Command exited with code {}: {}", exitCode, command); + } + + return result; + } + void openWebpage(std::string url) { if (!url.contains("://")) url = "https://" + url; diff --git a/main/updater/source/main.cpp b/main/updater/source/main.cpp index 9e1f03cba..423464a3b 100644 --- a/main/updater/source/main.cpp +++ b/main/updater/source/main.cpp @@ -146,33 +146,103 @@ std::string_view getUpdateArtifactEnding() { return ""; } +auto updateCommand(std::string command) { + return [command](const std::fs::path &updatePath) -> bool { + const auto formattedCommand = fmt::format(fmt::runtime(command), updatePath.string()); + + hex::log::info("Starting update process with command: '{}'", formattedCommand); + return hex::executeCommand(formattedCommand) == 0; + }; +} + +auto updateMacOSBundle(const std::fs::path &updatePath) { + // Mount the DMG + auto mountCmd = fmt::format("hdiutil attach \"{}\" -nobrowse", updatePath.string()); + auto mountOutput = hex::executeCommandWithOutput(mountCmd); + if (!mountOutput.has_value()) { + hex::log::error("Failed to mount DMG"); + return false; + } + + // Extract mount point from output + std::string mountPoint; + for (const auto &line : wolv::util::splitString(*mountOutput, "\n")) { + if (line.contains("/Volumes/")) { + auto parts = wolv::util::splitString(line, "\t"); + mountPoint = parts.back(); + break; + } + } + + if (mountPoint.empty()) { + hex::log::error("Failed to find mount point"); + return false; + } + + ON_SCOPE_EXIT { + hex::executeCommand(fmt::format("hdiutil detach \"{}\"", mountPoint)); + }; + + // Find the .app bundle in the mounted DMG + auto findCmd = fmt::format("find \"{}\" -name '*.app' -maxdepth 1", mountPoint); + auto appPath = hex::executeCommandWithOutput(findCmd); + if (!appPath.has_value()) { + hex::log::error("Failed to find .app in DMG"); + hex::executeCommand(fmt::format("hdiutil detach \"{}\"", mountPoint)); + return false; + } + + appPath = wolv::util::trim(*appPath); + + if (appPath->empty()) { + hex::log::error("Failed to find .app in DMG"); + return false; + } + + // Get the app name + auto appName = std::fs::path(*appPath).filename().string(); + auto installPath = fmt::format("/Applications/{}", appName); + + // Use AppleScript to copy with elevated privileges + auto installScript = fmt::format( + R"(osascript -e 'do shell script "rm -rf \"{}\" && cp -R \"{}\" /Applications/" with administrator privileges')", + installPath, *appPath + ); + + if (hex::executeCommand(installScript) != 0) { + hex::log::error("Failed to install update"); + return false; + } + + // Launch the new version + hex::executeCommand(fmt::format("open \"{}\"", installPath)); + + return true; +} + bool installUpdate(const std::fs::path &updatePath) { + using UpdaterFunction = std::function; struct UpdateHandler { std::string ending; - std::string command; + UpdaterFunction func; }; const static auto UpdateHandlers = { - UpdateHandler { .ending=".msi", .command="msiexec /i \"{}\" /qb" }, - UpdateHandler { .ending=".dmg", .command="hdiutil attach -autoopen \"{}\"" }, - UpdateHandler { .ending=".deb", .command="zenity --password | sudo -S apt install -y --fix-broken \"{}\"" }, - UpdateHandler { .ending=".rpm", .command="zenity --password | sudo -S rpm -i \"{}\"" }, - UpdateHandler { .ending=".pkg.tar.zst", .command="zenity --password | sudo -S pacman -Syy && sudo pacman -U --noconfirm \"{}\"" }, - UpdateHandler { .ending=".AppImage", .command=fmt::format(R"(zenity --password | sudo -S cp "{{}}" "{}")", hex::getEnvironmentVariable("APPIMAGE").value_or("")) }, - UpdateHandler { .ending=".flatpak", .command="zenity --password | sudo -S flatpak install -y --reinstall \"{}\"" }, - UpdateHandler { .ending=".snap", .command="zenity --password | sudo -S snap install --dangerous \"{}\"" }, + UpdateHandler { .ending=".msi", .func=updateCommand("msiexec /i \"{}\" /qb") }, + UpdateHandler { .ending=".dmg", .func=updateMacOSBundle }, + UpdateHandler { .ending=".deb", .func=updateCommand("zenity --password | sudo -S apt install -y --fix-broken \"{}\"") }, + UpdateHandler { .ending=".rpm", .func=updateCommand("zenity --password | sudo -S rpm -i \"{}\"") }, + UpdateHandler { .ending=".pkg.tar.zst", .func=updateCommand("zenity --password | sudo -S pacman -Syy && sudo pacman -U --noconfirm \"{}\"") }, + UpdateHandler { .ending=".AppImage", .func=updateCommand(fmt::format(R"(zenity --password | sudo -S cp "{{}}" "{}")", hex::getEnvironmentVariable("APPIMAGE").value_or(""))) }, + UpdateHandler { .ending=".flatpak", .func=updateCommand("zenity --password | sudo -S flatpak install -y --reinstall \"{}\"") }, + UpdateHandler { .ending=".snap", .func=updateCommand("zenity --password | sudo -S snap install --dangerous \"{}\"") }, }; const auto updateFileName = wolv::util::toUTF8String(updatePath.filename()); for (const auto &handler : UpdateHandlers) { if (updateFileName.ends_with(handler.ending)) { // Install the update using the correct command - const auto command = fmt::format(fmt::runtime(handler.command), updatePath.string()); - - hex::log::info("Starting update process with command: '{}'", command); - hex::executeCommand(command); - - return true; + return handler.func(updatePath); } } @@ -200,9 +270,11 @@ int main(int argc, char **argv) { return EXIT_FAILURE; } + hex::log::info("Updating ImHex..."); + // Read the version type from the arguments const std::string_view versionTypeString = argv[1]; - hex::log::info("Updater started with version type: {}", versionTypeString); + hex::log::info("Installing '{}' version of ImHex", versionTypeString); // Convert the version type string to the enum value hex::ImHexApi::System::UpdateType updateType;