about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2025-03-07 00:28:08 -0500
committerStar Rauchenberger <fefferburbia@gmail.com>2025-03-07 00:28:08 -0500
commit9b7099d98b87f3a3cf5448c4ee9015e7e2a27684 (patch)
treecb3339f837adac14f70c70286158828fa066539e /src
parentdf10fba47b605e489100045015164fbd31f0ce18 (diff)
downloadlingo-ap-tracker-9b7099d98b87f3a3cf5448c4ee9015e7e2a27684.tar.gz
lingo-ap-tracker-9b7099d98b87f3a3cf5448c4ee9015e7e2a27684.tar.bz2
lingo-ap-tracker-9b7099d98b87f3a3cf5448c4ee9015e7e2a27684.zip
Added auto-updater
Diffstat (limited to 'src')
-rw-r--r--src/tracker_frame.cpp59
-rw-r--r--src/tracker_frame.h8
-rw-r--r--src/updater.cpp302
-rw-r--r--src/updater.h47
4 files changed, 360 insertions, 56 deletions
diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index 9649c43..abc413d 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp
@@ -63,6 +63,9 @@ TrackerFrame::TrackerFrame()
63 AP_SetTrackerFrame(this); 63 AP_SetTrackerFrame(this);
64 IPC_SetTrackerFrame(this); 64 IPC_SetTrackerFrame(this);
65 65
66 updater_ = std::make_unique<Updater>(this);
67 updater_->Cleanup();
68
66 wxMenu *menuFile = new wxMenu(); 69 wxMenu *menuFile = new wxMenu();
67 menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago"); 70 menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago");
68 menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo"); 71 menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo");
@@ -143,7 +146,7 @@ TrackerFrame::TrackerFrame()
143 } 146 }
144 147
145 if (GetTrackerConfig().should_check_for_updates) { 148 if (GetTrackerConfig().should_check_for_updates) {
146 CheckForUpdates(/*manual=*/false); 149 updater_->CheckForUpdates(/*invisible=*/true);
147 } 150 }
148 151
149 SetStatusText(GetStatusMessage()); 152 SetStatusText(GetStatusMessage());
@@ -245,7 +248,7 @@ void TrackerFrame::OnSettings(wxCommandEvent &event) {
245} 248}
246 249
247void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { 250void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) {
248 CheckForUpdates(/*manual=*/true); 251 updater_->CheckForUpdates(/*invisible=*/false);
249} 252}
250 253
251void TrackerFrame::OnZoomIn(wxCommandEvent &event) { 254void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
@@ -345,55 +348,3 @@ void TrackerFrame::OnRedrawPosition(wxCommandEvent &event) {
345void TrackerFrame::OnConnectToAp(ApConnectEvent &event) { 348void TrackerFrame::OnConnectToAp(ApConnectEvent &event) {
346 AP_Connect(event.GetServer(), event.GetUser(), event.GetPass()); 349 AP_Connect(event.GetServer(), event.GetUser(), event.GetPass());
347} 350}
348
349void TrackerFrame::CheckForUpdates(bool manual) {
350 wxWebRequest request = wxWebSession::GetDefault().CreateRequest(
351 this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION");
352
353 if (!request.IsOk()) {
354 if (manual) {
355 wxMessageBox("Could not check for updates.", "Error",
356 wxOK | wxICON_ERROR);
357 } else {
358 SetStatusText("Could not check for updates.");
359 }
360
361 return;
362 }
363
364 Bind(wxEVT_WEBREQUEST_STATE, [this, manual](wxWebRequestEvent &evt) {
365 if (evt.GetState() == wxWebRequest::State_Completed) {
366 std::string response = evt.GetResponse().AsString().ToStdString();
367
368 Version latest_version(response);
369 if (kTrackerVersion < latest_version) {
370 std::ostringstream message_text;
371 message_text << "There is a newer version of Lingo AP Tracker "
372 "available. You have "
373 << kTrackerVersion.ToString()
374 << ", and the latest version is "
375 << latest_version.ToString()
376 << ". Would you like to update?";
377
378 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) ==
379 wxYES) {
380 wxLaunchDefaultBrowser(
381 "https://code.fourisland.com/lingo-ap-tracker/about/"
382 "CHANGELOG.md");
383 }
384 } else if (manual) {
385 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker",
386 wxOK);
387 }
388 } else if (evt.GetState() == wxWebRequest::State_Failed) {
389 if (manual) {
390 wxMessageBox("Could not check for updates.", "Error",
391 wxOK | wxICON_ERROR);
392 } else {
393 SetStatusText("Could not check for updates.");
394 }
395 }
396 });
397
398 request.Start();
399}
diff --git a/src/tracker_frame.h b/src/tracker_frame.h index 29745b4..76c071f 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h
@@ -7,6 +7,10 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <memory>
11
12#include "updater.h"
13
10class AchievementsPane; 14class AchievementsPane;
11class SubwayMap; 15class SubwayMap;
12class TrackerPanel; 16class TrackerPanel;
@@ -78,8 +82,8 @@ class TrackerFrame : public wxFrame {
78 void OnStatusChanged(wxCommandEvent &event); 82 void OnStatusChanged(wxCommandEvent &event);
79 void OnRedrawPosition(wxCommandEvent &event); 83 void OnRedrawPosition(wxCommandEvent &event);
80 void OnConnectToAp(ApConnectEvent &event); 84 void OnConnectToAp(ApConnectEvent &event);
81 85
82 void CheckForUpdates(bool manual); 86 std::unique_ptr<Updater> updater_;
83 87
84 wxSplitterWindow *splitter_window_; 88 wxSplitterWindow *splitter_window_;
85 wxNotebook *notebook_; 89 wxNotebook *notebook_;
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}
diff --git a/src/updater.h b/src/updater.h new file mode 100644 index 0000000..2d2f746 --- /dev/null +++ b/src/updater.h
@@ -0,0 +1,47 @@
1#ifndef UPDATER_H_809E7381
2#define UPDATER_H_809E7381
3
4#include <filesystem>
5#include <set>
6#include <string>
7
8#include <wx/wxprec.h>
9
10#ifndef WX_PRECOMP
11#include <wx/wx.h>
12#endif
13
14class wxWebRequest;
15class wxWebRequestEvent;
16
17class Updater : public wxEvtHandler {
18 public:
19 explicit Updater(wxFrame* parent);
20
21 void Cleanup();
22
23 void CheckForUpdates(bool invisible);
24
25 private:
26 enum class UpdateState {
27 GetVersionInvisible,
28 GetVersionManual,
29 GetPackage,
30 };
31
32 void OnWebRequestState(wxWebRequestEvent& event);
33
34 void ProcessVersionFile(std::string data);
35
36 void InstallUpdate(std::string url, std::string checksum,
37 std::vector<std::filesystem::path> files,
38 std::vector<std::filesystem::path> deletedFiles);
39
40 bool DownloadWithProgress(wxWebRequest& request);
41
42 wxFrame* parent_;
43 UpdateState update_state_ = UpdateState::GetVersionInvisible;
44 std::string package_path_;
45};
46
47#endif /* end of include guard: UPDATER_H_809E7381 */