about summary refs log tree commit diff stats
path: root/src/updater.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/updater.cpp')
-rw-r--r--src/updater.cpp302
1 files changed, 302 insertions, 0 deletions
diff --git a/src/updater.cpp b/src/updater.cpp new file mode 100644 index 0000000..67d5f31 --- /dev/null +++ b/src/updater.cpp
@@ -0,0 +1,302 @@
1#include "updater.h"
2
3#include <fmt/core.h>
4#include <openssl/evp.h>
5#include <openssl/sha.h>
6#include <wx/evtloop.h>
7#include <wx/progdlg.h>
8#include <wx/webrequest.h>
9#include <wx/wfstream.h>
10#include <wx/zipstrm.h>
11#include <yaml-cpp/yaml.h>
12
13#include <cstdio>
14#include <deque>
15#include <filesystem>
16#include <fstream>
17
18#include "global.h"
19#include "logger.h"
20#include "version.h"
21
22constexpr const char* kVersionFileUrl =
23 "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml";
24constexpr const char* kChangelogUrl =
25 "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md";
26
27namespace {
28
29std::string CalculateStringSha256(const wxString& data) {
30 unsigned char hash[SHA256_DIGEST_LENGTH];
31 EVP_MD_CTX* sha256 = EVP_MD_CTX_new();
32 EVP_DigestInit(sha256, EVP_sha256());
33 EVP_DigestUpdate(sha256, data.c_str(), data.length());
34 EVP_DigestFinal_ex(sha256, hash, nullptr);
35 EVP_MD_CTX_free(sha256);
36
37 char output[65] = {0};
38 for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
39 sprintf(output + (i * 2), "%02x", hash[i]);
40 }
41
42 return std::string(output);
43}
44
45} // namespace
46
47Updater::Updater(wxFrame* parent) : parent_(parent) {
48 Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this);
49}
50
51void Updater::Cleanup() {
52 std::filesystem::path oldDir = GetExecutableDirectory() / "old";
53 if (std::filesystem::is_directory(oldDir)) {
54 std::filesystem::remove_all(oldDir);
55 }
56}
57
58void Updater::CheckForUpdates(bool invisible) {
59 wxWebRequest versionRequest =
60 wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl);
61
62 if (invisible) {
63 update_state_ = UpdateState::GetVersionInvisible;
64
65 versionRequest.Start();
66 } else {
67 update_state_ = UpdateState::GetVersionManual;
68
69 if (DownloadWithProgress(versionRequest)) {
70 if (versionRequest.GetState() == wxWebRequest::State_Failed) {
71 wxMessageBox("Could not check for updates.", "Error",
72 wxOK | wxICON_ERROR);
73 } else if (versionRequest.GetState() == wxWebRequest::State_Completed) {
74 ProcessVersionFile(
75 versionRequest.GetResponse().AsString().utf8_string());
76 }
77 }
78 }
79}
80
81void Updater::OnWebRequestState(wxWebRequestEvent& evt) {
82 if (update_state_ == UpdateState::GetVersionInvisible) {
83 if (evt.GetState() == wxWebRequest::State_Completed) {
84 ProcessVersionFile(evt.GetResponse().AsString().utf8_string());
85 } else if (evt.GetState() == wxWebRequest::State_Failed) {
86 parent_->SetStatusText("Could not check for updates.");
87 }
88 }
89}
90
91void Updater::ProcessVersionFile(std::string data) {
92 try {
93 YAML::Node versionInfo = YAML::Load(data);
94 Version latestVersion(versionInfo["version"].as<std::string>());
95
96 if (kTrackerVersion < latestVersion) {
97 if (versionInfo["packages"]) {
98 std::string platformIdentifier;
99
100 if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) {
101 platformIdentifier = "win64";
102 }
103
104 if (!platformIdentifier.empty() &&
105 versionInfo["packages"][platformIdentifier]) {
106 wxMessageDialog dialog(
107 nullptr,
108 fmt::format("There is a newer version of Lingo AP Tracker "
109 "available. You have {}, and the latest version is "
110 "{}. Would you like to update?",
111 kTrackerVersion.ToString(), latestVersion.ToString()),
112 "Update available", wxYES_NO | wxCANCEL);
113 dialog.SetYesNoLabels("Install update", "Open changelog");
114
115 int dlgResult = dialog.ShowModal();
116 if (dlgResult == wxID_YES) {
117 const YAML::Node& packageInfo =
118 versionInfo["packages"][platformIdentifier];
119 std::string packageUrl = packageInfo["url"].as<std::string>();
120 std::string packageChecksum =
121 packageInfo["checksum"].as<std::string>();
122
123 std::vector<std::filesystem::path> packageFiles;
124 if (packageInfo["files"]) {
125 for (const YAML::Node& filename : packageInfo["files"]) {
126 packageFiles.push_back(filename.as<std::string>());
127 }
128 }
129
130 std::vector<std::filesystem::path> deletedFiles;
131 if (packageInfo["deleted_files"]) {
132 for (const YAML::Node& filename : packageInfo["deleted_files"]) {
133 deletedFiles.push_back(filename.as<std::string>());
134 }
135 }
136
137 InstallUpdate(packageUrl, packageChecksum, packageFiles,
138 deletedFiles);
139 } else if (dlgResult == wxID_NO) {
140 wxLaunchDefaultBrowser(kChangelogUrl);
141 }
142
143 return;
144 }
145 }
146
147 if (wxMessageBox(
148 fmt::format("There is a newer version of Lingo AP Tracker "
149 "available. You have {}, and the latest version is "
150 "{}. Would you like to update?",
151 kTrackerVersion.ToString(), latestVersion.ToString()),
152 "Update available", wxYES_NO) == wxYES) {
153 wxLaunchDefaultBrowser(kChangelogUrl);
154 }
155 } else if (update_state_ == UpdateState::GetVersionManual) {
156 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK);
157 }
158 } catch (const std::exception& ex) {
159 wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR);
160 }
161}
162
163void Updater::InstallUpdate(std::string url, std::string checksum,
164 std::vector<std::filesystem::path> files,
165 std::vector<std::filesystem::path> deletedFiles) {
166 update_state_ = UpdateState::GetPackage;
167
168 wxWebRequest packageRequest =
169 wxWebSession::GetDefault().CreateRequest(this, url);
170
171 if (!DownloadWithProgress(packageRequest)) {
172 return;
173 }
174
175 package_path_.clear();
176 package_path_.resize(L_tmpnam + 1);
177 tmpnam_s(package_path_.data(), L_tmpnam);
178
179 {
180 wxFileOutputStream writeOut(package_path_);
181 wxString fileData = packageRequest.GetResponse().AsString();
182 writeOut.WriteAll(fileData.c_str(), fileData.length());
183
184 std::string downloadedChecksum = CalculateStringSha256(fileData);
185 if (downloadedChecksum != checksum) {
186 if (wxMessageBox("There was an issue downloading the update. Would you "
187 "like to manually download it instead?",
188 "Error", wxYES_NO | wxICON_ERROR) == wxID_YES) {
189 wxLaunchDefaultBrowser(kChangelogUrl);
190 }
191 return;
192 }
193 }
194
195 std::filesystem::path newArea = GetExecutableDirectory();
196 std::filesystem::path oldArea = newArea / "old";
197 std::set<std::filesystem::path> folders;
198 std::set<std::filesystem::path> filesToMove;
199 for (const std::filesystem::path& existingFile : files) {
200 std::filesystem::path movedPath = oldArea / existingFile;
201 std::filesystem::path movedDir = movedPath;
202 movedDir.remove_filename();
203 folders.insert(movedDir);
204 filesToMove.insert(existingFile);
205 }
206 for (const std::filesystem::path& existingFile : deletedFiles) {
207 std::filesystem::path movedPath = oldArea / existingFile;
208 std::filesystem::path movedDir = movedPath;
209 movedDir.remove_filename();
210 folders.insert(movedDir);
211 }
212
213 for (const std::filesystem::path& newFolder : folders) {
214 TrackerLog(fmt::format("Creating directory {}", newFolder.string()));
215
216 std::filesystem::create_directories(newFolder);
217 }
218
219 for (const std::filesystem::path& existingFile : files) {
220 std::filesystem::path existingPath = newArea / existingFile;
221
222 if (std::filesystem::is_regular_file(existingPath)) {
223 std::filesystem::path movedPath = oldArea / existingFile;
224
225 TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
226 movedPath.string()));
227
228 std::filesystem::rename(existingPath, movedPath);
229 }
230 }
231 for (const std::filesystem::path& existingFile : deletedFiles) {
232 std::filesystem::path existingPath = newArea / existingFile;
233
234 if (std::filesystem::is_regular_file(existingPath)) {
235 std::filesystem::path movedPath = oldArea / existingFile;
236
237 TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
238 movedPath.string()));
239
240 std::filesystem::rename(existingPath, movedPath);
241 }
242 }
243
244 wxFileInputStream fileInputStream(package_path_);
245 wxZipInputStream zipStream(fileInputStream);
246 std::unique_ptr<wxZipEntry> zipEntry;
247 while ((zipEntry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) !=
248 nullptr) {
249 if (zipEntry->IsDir()) {
250 continue;
251 }
252
253 std::filesystem::path archivePath = zipEntry->GetName().utf8_string();
254
255 TrackerLog(fmt::format("Found {} in archive", archivePath.string()));
256
257 // Cut off the root folder name
258 std::filesystem::path subPath;
259 for (auto it = std::next(archivePath.begin()); it != archivePath.end();
260 it++) {
261 subPath /= *it;
262 }
263
264 std::filesystem::path pastePath = newArea / subPath;
265
266 wxFileOutputStream fileOutput(pastePath.string());
267 zipStream.Read(fileOutput);
268 }
269
270 if (wxMessageBox(
271 "Update installed! The tracker must be restarted for the changes to take "
272 "effect. Do you want to close the tracker?",
273 "Update installed", wxYES_NO) == wxYES) {
274 wxExit();
275 }
276}
277
278bool Updater::DownloadWithProgress(wxWebRequest& request) {
279 request.Start();
280
281 wxProgressDialog dialog("Checking for updates...", "Checking for updates...",
282 100, nullptr,
283 wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT |
284 wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME);
285 while (request.GetState() != wxWebRequest::State_Completed &&
286 request.GetState() != wxWebRequest::State_Failed) {
287 if (request.GetBytesExpectedToReceive() == -1) {
288 if (!dialog.Pulse()) {
289 request.Cancel();
290 return false;
291 }
292 } else {
293 dialog.SetRange(request.GetBytesExpectedToReceive());
294 if (!dialog.Update(request.GetBytesReceived())) {
295 request.Cancel();
296 return false;
297 }
298 }
299 }
300
301 return true;
302}