impr: Better updater experience on macOS

(cherry picked from commit 428fbddbbb)
This commit is contained in:
WerWolv
2025-12-21 11:47:21 +01:00
parent 6405c75242
commit 96232c2d80
3 changed files with 133 additions and 16 deletions

View File

@@ -98,6 +98,7 @@ namespace hex {
void startProgram(const std::vector<std::string> &command);
int executeCommand(const std::string &command);
std::optional<std::string> executeCommandWithOutput(const std::string &command);
void openWebpage(std::string url);
extern "C" void registerFont(const char *fontName, const char *fontPath);

View File

@@ -309,6 +309,50 @@ namespace hex {
return ::system(command.c_str());
}
std::optional<std::string> executeCommandWithOutput(const std::string &command) {
std::array<char, 256> 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;

View File

@@ -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<bool(const std::fs::path &updatePath)>;
struct UpdateHandler {
std::string ending;
std::string command;
UpdaterFunction func;
};
const static auto UpdateHandlers = {
UpdateHandler { ".msi", "msiexec /i \"{}\" /qb" },
UpdateHandler { ".dmg", "hdiutil attach -autoopen \"{}\"" },
UpdateHandler { ".deb", "zenity --password | sudo -S apt install -y --fix-broken \"{}\"" },
UpdateHandler { ".rpm", "zenity --password | sudo -S rpm -i \"{}\"" },
UpdateHandler { ".pkg.tar.zst", "zenity --password | sudo -S pacman -Syy && sudo pacman -U --noconfirm \"{}\"" },
UpdateHandler { ".AppImage", fmt::format("zenity --password | sudo -S cp \"{{}}\" \"{}\"", hex::getEnvironmentVariable("APPIMAGE").value_or("")) },
UpdateHandler { ".flatpak", "zenity --password | sudo -S flatpak install -y --reinstall \"{}\"" },
UpdateHandler { ".snap", "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;