1// Aseprite
2// Copyright (C) 2020-2021 Igara Studio S.A.
3// Copyright (C) 2001-2017 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#ifdef ENABLE_UPDATER
13
14#include "app/check_update.h"
15
16#include "app/check_update_delegate.h"
17#include "app/pref/preferences.h"
18#include "base/convert_to.h"
19#include "base/launcher.h"
20#include "base/replace_string.h"
21#include "base/version.h"
22#include "ver/info.h"
23
24#if ENABLE_SENTRY
25 #include "app/sentry_wrapper.h"
26#endif
27
28#include <ctime>
29#include <sstream>
30
31static const int kMonitoringPeriod = 100;
32
33namespace app {
34
35class CheckUpdateBackgroundJob : public updater::CheckUpdateDelegate {
36public:
37 CheckUpdateBackgroundJob()
38 : m_received(false) { }
39
40 void abort() {
41 m_checker.abort();
42 }
43
44 bool isReceived() const {
45 return m_received;
46 }
47
48 void sendRequest(const updater::Uuid& uuid, const std::string& extraParams) {
49 m_checker.checkNewVersion(uuid, extraParams, this);
50 }
51
52 const updater::CheckUpdateResponse& getResponse() const {
53 return m_response;
54 }
55
56private:
57 void onResponse(updater::CheckUpdateResponse& data) override {
58 m_response = data;
59 m_received = true;
60 }
61
62 bool m_received;
63 updater::CheckUpdate m_checker;
64 updater::CheckUpdateResponse m_response;
65};
66
67CheckUpdateThreadLauncher::CheckUpdateThreadLauncher(CheckUpdateDelegate* delegate)
68 : m_delegate(delegate)
69 , m_preferences(Preferences::instance())
70 , m_doCheck(true)
71 , m_received(false)
72 , m_inits(m_preferences.updater.inits())
73 , m_exits(m_preferences.updater.exits())
74#ifdef _DEBUG
75 , m_isDeveloper(true)
76#else
77 , m_isDeveloper(m_preferences.updater.isDeveloper())
78#endif
79 , m_timer(kMonitoringPeriod, NULL)
80{
81 // Get how many days we have to wait for the next "check for update"
82 double waitDays = m_preferences.updater.waitDays();
83 if (waitDays > 0.0) {
84 // Get the date of the last "check for updates"
85 time_t lastCheck = (time_t)m_preferences.updater.lastCheck();
86 time_t now = std::time(NULL);
87
88 // Verify if we are in the "WaitDays" period...
89 if (now < lastCheck+int(double(60*60*24*waitDays)) &&
90 now > lastCheck) { // <- Avoid broken clocks
91 // So we do not check for updates.
92 m_doCheck = false;
93 }
94 }
95
96 // Minimal stats: number of initializations
97 m_preferences.updater.inits(m_inits+1);
98 m_preferences.save();
99}
100
101CheckUpdateThreadLauncher::~CheckUpdateThreadLauncher()
102{
103 if (m_timer.isRunning())
104 m_timer.stop();
105
106 if (m_thread) {
107 if (m_bgJob)
108 m_bgJob->abort();
109
110 m_thread->join();
111 }
112
113 // Minimal stats: number of exits
114 m_preferences.updater.exits(m_exits+1);
115 m_preferences.save();
116}
117
118void CheckUpdateThreadLauncher::launch()
119{
120 if (m_uuid.empty())
121 m_uuid = m_preferences.updater.uuid();
122
123#if ENABLE_SENTRY
124 if (!m_uuid.empty())
125 Sentry::setUserID(m_uuid);
126#endif
127
128 // In this case we are in the "wait days" period, so we don't check
129 // for updates.
130 if (!m_doCheck) {
131 showUI();
132 return;
133 }
134
135 m_delegate->onCheckingUpdates();
136
137 m_bgJob.reset(new CheckUpdateBackgroundJob);
138 m_thread.reset(new base::thread([this]{ checkForUpdates(); }));
139
140 // Start a timer to monitoring the progress of the background job
141 // executed in "m_thread". The "onMonitoringTick" method will be
142 // called periodically by the GUI main thread.
143 m_timer.Tick.connect(&CheckUpdateThreadLauncher::onMonitoringTick, this);
144 m_timer.start();
145}
146
147bool CheckUpdateThreadLauncher::isReceived() const
148{
149 return m_received;
150}
151
152void CheckUpdateThreadLauncher::onMonitoringTick()
153{
154 // If we do not receive a response yet...
155 if (!m_received)
156 return; // Skip and wait the next call.
157
158 // Depending on the type of update received
159 switch (m_response.getUpdateType()) {
160
161 case updater::CheckUpdateResponse::NoUpdate:
162 // Clear
163 m_preferences.updater.newVersion("");
164 m_preferences.updater.newUrl("");
165 break;
166
167 case updater::CheckUpdateResponse::Critical:
168 case updater::CheckUpdateResponse::Major:
169 m_preferences.updater.newVersion(m_response.getLatestVersion());
170 m_preferences.updater.newUrl(m_response.getUrl());
171 break;
172 }
173
174 showUI();
175
176 // Save the new UUID
177 if (!m_response.getUuid().empty()) {
178 m_uuid = m_response.getUuid();
179 m_preferences.updater.uuid(m_uuid);
180
181#if ENABLE_SENTRY
182 if (!m_uuid.empty())
183 Sentry::setUserID(m_uuid);
184#endif
185 }
186
187 // Set the date of the last "check for updates" and the "WaitDays" parameter.
188 m_preferences.updater.lastCheck((int)std::time(NULL));
189 m_preferences.updater.waitDays(m_response.getWaitDays());
190
191 // Save the config file right now
192 m_preferences.save();
193
194 // Stop the monitoring timer.
195 m_timer.stop();
196}
197
198// This method is executed in a special thread to send the HTTP request.
199void CheckUpdateThreadLauncher::checkForUpdates()
200{
201 // Add mini-stats in the request
202 std::stringstream extraParams;
203 extraParams << "inits=" << m_inits
204 << "&exits=" << m_exits;
205
206 if (m_isDeveloper)
207 extraParams << "&dev=1";
208
209 // Send the HTTP request to check for updates.
210 m_bgJob->sendRequest(m_uuid, extraParams.str());
211
212 if (m_bgJob->isReceived()) {
213 m_received = true;
214 m_response = m_bgJob->getResponse();
215 }
216}
217
218void CheckUpdateThreadLauncher::showUI()
219{
220 std::string localVersionStr = get_app_version();
221 base::replace_string(localVersionStr, "-x64", "");
222 bool newVer = false;
223
224 if (!m_preferences.updater.newVersion().empty()) {
225 base::Version serverVersion(m_preferences.updater.newVersion());
226 base::Version localVersion(localVersionStr);
227 newVer = (localVersion < serverVersion);
228 }
229
230 if (newVer) {
231 m_delegate->onNewUpdate(m_preferences.updater.newUrl(),
232 m_preferences.updater.newVersion());
233 }
234 else {
235 // If the program was updated, reset the "exits" counter
236 if (m_preferences.updater.currentVersion() != localVersionStr) {
237 m_preferences.updater.currentVersion(localVersionStr);
238 m_exits = m_inits;
239 }
240
241 m_delegate->onUpToDate();
242 }
243}
244
245}
246
247#endif // ENABLE_UPDATER
248