#include "updater.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "global.h" #include "logger.h" #include "version.h" constexpr const char* kVersionFileUrl = "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml"; constexpr const char* kChangelogUrl = "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md"; namespace { std::string CalculateStringSha256(const wxString& data) { unsigned char hash[SHA256_DIGEST_LENGTH]; EVP_MD_CTX* sha256 = EVP_MD_CTX_new(); EVP_DigestInit(sha256, EVP_sha256()); EVP_DigestUpdate(sha256, data.c_str(), data.length()); EVP_DigestFinal_ex(sha256, hash, nullptr); EVP_MD_CTX_free(sha256); char output[65] = {0}; for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { sprintf(output + (i * 2), "%02x", hash[i]); } return std::string(output); } } // namespace Updater::Updater(wxFrame* parent) : parent_(parent) { Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this); } void Updater::Cleanup() { std::filesystem::path oldDir = GetExecutableDirectory() / "old"; if (std::filesystem::is_directory(oldDir)) { std::filesystem::remove_all(oldDir); } } void Updater::CheckForUpdates(bool invisible) { wxWebRequest versionRequest = wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl); if (invisible) { update_state_ = UpdateState::GetVersionInvisible; versionRequest.Start(); } else { update_state_ = UpdateState::GetVersionManual; if (DownloadWithProgress(versionRequest)) { if (versionRequest.GetState() == wxWebRequest::State_Failed) { wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR); } else if (versionRequest.GetState() == wxWebRequest::State_Completed) { ProcessVersionFile( versionRequest.GetResponse().AsString().utf8_string()); } } } } void Updater::OnWebRequestState(wxWebRequestEvent& evt) { if (update_state_ == UpdateState::GetVersionInvisible) { if (evt.GetState() == wxWebRequest::State_Completed) { ProcessVersionFile(evt.GetResponse().AsString().utf8_string()); } else if (evt.GetState() == wxWebRequest::State_Failed) { parent_->SetStatusText("Could not check for updates."); } } } void Updater::ProcessVersionFile(std::string data) { try { YAML::Node versionInfo = YAML::Load(data); Version latestVersion(versionInfo["version"].as()); if (kTrackerVersion < latestVersion) { if (versionInfo["packages"]) { std::string platformIdentifier; if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) { platformIdentifier = "win64"; } if (!platformIdentifier.empty() && versionInfo["packages"][platformIdentifier]) { wxMessageDialog dialog( nullptr, fmt::format("There is a newer version of Lingo AP Tracker " "available. You have {}, and the latest version is " "{}. Would you like to update?", kTrackerVersion.ToString(), latestVersion.ToString()), "Update available", wxYES_NO | wxCANCEL); dialog.SetYesNoLabels("Install update", "Open changelog"); int dlgResult = dialog.ShowModal(); if (dlgResult == wxID_YES) { const YAML::Node& packageInfo = versionInfo["packages"][platformIdentifier]; std::string packageUrl = packageInfo["url"].as(); std::string packageChecksum = packageInfo["checksum"].as(); std::vector packageFiles; if (packageInfo["files"]) { for (const YAML::Node& filename : packageInfo["files"]) { packageFiles.push_back(filename.as()); } } std::vector deletedFiles; if (packageInfo["deleted_files"]) { for (const YAML::Node& filename : packageInfo["deleted_files"]) { deletedFiles.push_back(filename.as()); } } InstallUpdate(packageUrl, packageChecksum, packageFiles, deletedFiles); } else if (dlgResult == wxID_NO) { wxLaunchDefaultBrowser(kChangelogUrl); } return; } } if (wxMessageBox( fmt::format("There is a newer version of Lingo AP Tracker " "available. You have {}, and the latest version is " "{}. Would you like to update?", kTrackerVersion.ToString(), latestVersion.ToString()), "Update available", wxYES_NO) == wxYES) { wxLaunchDefaultBrowser(kChangelogUrl); } } else if (update_state_ == UpdateState::GetVersionManual) { wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK); } } catch (const std::exception& ex) { wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR); } } void Updater::InstallUpdate(std::string url, std::string checksum, std::vector files, std::vector deletedFiles) { update_state_ = UpdateState::GetPackage; wxWebRequest packageRequest = wxWebSession::GetDefault().CreateRequest(this, url); if (!DownloadWithProgress(packageRequest)) { return; } package_path_.clear(); package_path_.resize(L_tmpnam + 1); tmpnam_s(package_path_.data(), L_tmpnam); { wxFileOutputStream writeOut(package_path_); wxString fileData = packageRequest.GetResponse().AsString(); writeOut.WriteAll(fileData.c_str(), fileData.length()); std::string downloadedChecksum = CalculateStringSha256(fileData); if (downloadedChecksum != checksum) { if (wxMessageBox("There was an issue downloading the update. Would you " "like to manually download it instead?", "Error", wxYES_NO | wxICON_ERROR) == wxID_YES) { wxLaunchDefaultBrowser(kChangelogUrl); } return; } } std::filesystem::path newArea = GetExecutableDirectory(); std::filesystem::path oldArea = newArea / "old"; std::set folders; std::set filesToMove; for (const std::filesystem::path& existingFile : files) { std::filesystem::path movedPath = oldArea / existingFile; std::filesystem::path movedDir = movedPath; movedDir.remove_filename(); folders.insert(movedDir); filesToMove.insert(existingFile); } for (const std::filesystem::path& existingFile : deletedFiles) { std::filesystem::path movedPath = oldArea / existingFile; std::filesystem::path movedDir = movedPath; movedDir.remove_filename(); folders.insert(movedDir); } for (const std::filesystem::path& newFolder : folders) { TrackerLog(fmt::format("Creating directory {}", newFolder.string())); std::filesystem::create_directories(newFolder); } for (const std::filesystem::path& existingFile : files) { std::filesystem::path existingPath = newArea / existingFile; if (std::filesystem::is_regular_file(existingPath)) { std::filesystem::path movedPath = oldArea / existingFile; TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(), movedPath.string())); std::filesystem::rename(existingPath, movedPath); } } for (const std::filesystem::path& existingFile : deletedFiles) { std::filesystem::path existingPath = newArea / existingFile; if (std::filesystem::is_regular_file(existingPath)) { std::filesystem::path movedPath = oldArea / existingFile; TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(), movedPath.string())); std::filesystem::rename(existingPath, movedPath); } } wxFileInputStream fileInputStream(package_path_); wxZipInputStream zipStream(fileInputStream); std::unique_ptr zipEntry; while ((zipEntry = std::unique_ptr(zipStream.GetNextEntry())) != nullptr) { if (zipEntry->IsDir()) { continue; } std::filesystem::path archivePath = zipEntry->GetName().utf8_string(); TrackerLog(fmt::format("Found {} in archive", archivePath.string())); // Cut off the root folder name std::filesystem::path subPath; for (auto it = std::next(archivePath.begin()); it != archivePath.end(); it++) { subPath /= *it; } std::filesystem::path pastePath = newArea / subPath; wxFileOutputStream fileOutput(pastePath.string()); zipStream.Read(fileOutput); } if (wxMessageBox( "Update installed! The tracker must be restarted for the changes to take " "effect. Do you want to close the tracker?", "Update installed", wxYES_NO) == wxYES) { wxExit(); } } bool Updater::DownloadWithProgress(wxWebRequest& request) { request.Start(); wxProgressDialog dialog("Checking for updates...", "Checking for updates...", 100, nullptr, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT | wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME); while (request.GetState() != wxWebRequest::State_Completed && request.GetState() != wxWebRequest::State_Failed) { if (request.GetBytesExpectedToReceive() == -1) { if (!dialog.Pulse()) { request.Cancel(); return false; } } else { dialog.SetRange(request.GetBytesExpectedToReceive()); if (!dialog.Update(request.GetBytesReceived())) { request.Cancel(); return false; } } } return true; }