autoupdater.cpp
1 //------------------------------------------------------------------------------
2 // autoupdater.cpp
3 //------------------------------------------------------------------------------
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 // 02110-1301 USA
19 //
20 //------------------------------------------------------------------------------
21 // Copyright (C) 2012 "Zalewa" <zalewapl@gmail.com>
22 //------------------------------------------------------------------------------
23 #include "autoupdater.h"
24 
25 #include "configuration/doomseekerconfig.h"
26 #include "datapaths.h"
27 #include "log.h"
28 #include "strings.hpp"
29 #include "updater/updatechannel.h"
30 #include "updater/updatepackagefilter.h"
31 #include "updater/updaterinfoparser.h"
32 #include "updater/updaterscriptparser.h"
33 #include "version.h"
34 #include <cassert>
35 #include <QByteArray>
36 #include <QDebug>
37 #include <QNetworkAccessManager>
38 #include <QNetworkRequest>
39 #include <QTemporaryFile>
40 
41 DClass<AutoUpdater>
42 {
43 public:
45  QList<QDomDocument> allScripts;
46  bool bDownloadAndInstallRequireConfirmation;
47  bool bIsRunning;
49  bool bPackageDownloadStarted;
50  bool bStarted;
51  UpdateChannel channel;
52  UpdatePackage currentlyDownloadedPackage;
53  QStringList downloadedPackagesFilenames;
54  AutoUpdater::ErrorCode errorCode;
55  QMap<QString, QList<QString> > ignoredPackagesRevisions;
56  QList<UpdatePackage> newUpdatePackages;
57  QList<UpdatePackage> packagesInDownloadQueue;
58  QTemporaryFile *pCurrentPackageFile;
59  QNetworkAccessManager *pNam;
60  QNetworkReply *pNetworkReply;
61 };
62 
63 DPointered(AutoUpdater)
64 
65 
66 
67 const QString AutoUpdater::PLUGIN_PREFIX = "p-";
68 const QString AutoUpdater::MAIN_PROGRAM_PACKAGE_NAME = "doomseeker-core";
69 const QString AutoUpdater::QT_PACKAGE_NAME = "qt";
70 const QString AutoUpdater::WADSEEKER_PACKAGE_NAME = "wadseeker";
71 const QString AutoUpdater::UPDATER_INFO_URL_BASE = "https://doomseeker.drdteam.org/updates/";
72 
73 AutoUpdater::AutoUpdater(QObject *pParent)
74  : QObject(pParent)
75 {
76  d->bDownloadAndInstallRequireConfirmation = false;
77  d->bPackageDownloadStarted = false;
78  d->bIsRunning = false;
79  d->bStarted = false;
80  d->errorCode = EC_Ok;
81  d->pCurrentPackageFile = nullptr;
82  d->pNam = new QNetworkAccessManager();
83  d->pNetworkReply = nullptr;
84 }
85 
86 AutoUpdater::~AutoUpdater()
87 {
88  if (d->pCurrentPackageFile != nullptr)
89  {
90  delete d->pCurrentPackageFile;
91  }
92  if (d->pNetworkReply != nullptr)
93  {
94  d->pNetworkReply->disconnect();
95  d->pNetworkReply->abort();
96  d->pNetworkReply->deleteLater();
97  }
98  d->pNam->disconnect();
99  d->pNam->deleteLater();
100 }
101 
102 void AutoUpdater::abort()
103 {
104  if (d->pNetworkReply != nullptr)
105  {
106  d->pNetworkReply->disconnect();
107  d->pNetworkReply->abort();
108  d->pNetworkReply->deleteLater();
109  d->pNetworkReply = nullptr;
110  }
111  emit finishWithError(EC_Aborted);
112 }
113 
114 QDomDocument AutoUpdater::adjustUpdaterScriptXml(const QByteArray &xmlSource)
115 {
116  QDomDocument xmlDoc;
117  QString xmlError;
118  int xmlErrLine = -1;
119  int xmlErrCol = -1;
120  if (!xmlDoc.setContent(xmlSource, &xmlError, &xmlErrLine, &xmlErrCol))
121  {
122  gLog << tr("Failed to parse updater XML script: %1, l: %2, c: %3")
123  .arg(xmlError).arg(xmlErrLine).arg(xmlErrCol);
124  return QDomDocument();
125  }
126  UpdaterScriptParser scriptParser(xmlDoc);
127  QFileInfo currentPackageFileInfo(d->pCurrentPackageFile->fileName());
128  QString scriptParserErrMsg = scriptParser.setPackageName(
129  currentPackageFileInfo.completeBaseName());
130  if (!scriptParserErrMsg.isNull())
131  {
132  gLog << tr("Failed to modify package name in updater script: %1")
133  .arg(scriptParserErrMsg);
134  return QDomDocument();
135  }
136  return xmlDoc;
137 }
138 
140 {
141  return d->channel;
142 }
143 
144 void AutoUpdater::confirmDownloadAndInstall()
145 {
146  d->packagesInDownloadQueue = d->newUpdatePackages;
147  d->bPackageDownloadStarted = true;
148  startNextPackageDownload();
149 }
150 
152 {
153  return d->downloadedPackagesFilenames;
154 }
155 
156 void AutoUpdater::dumpUpdatePackagesToLog(const QList<UpdatePackage> &packages)
157 {
158  for (const UpdatePackage &pkg : packages)
159  {
160  gLog << tr(R"(Detected update for package "%1" from version "%2" to version "%3".)")
161  .arg(pkg.displayName, pkg.currentlyInstalledDisplayVersion, pkg.displayVersion);
162  }
163 }
164 
165 void AutoUpdater::emitOverallProgress(const QString &message)
166 {
167  int total = 1;
168  int current = 0;
169  if (!d->newUpdatePackages.isEmpty() && d->bPackageDownloadStarted)
170  {
171  total = d->newUpdatePackages.size();
172  // Add 1 because currently downloaded package has already been
173  // removed from the queue.
174  current = total - (d->packagesInDownloadQueue.size() + 1);
175  assert(current >= 0 && "AutoUpdater::emitOverallProgress()");
176  }
177  emit overallProgress(current, total, message);
178 }
179 
180 void AutoUpdater::emitStatusMessage(const QString &message)
181 {
182  emit statusMessage(message);
183 }
184 
185 AutoUpdater::ErrorCode AutoUpdater::errorCode() const
186 {
187  return d->errorCode;
188 }
189 
190 QString AutoUpdater::errorCodeToString(ErrorCode code)
191 {
192  switch (code)
193  {
194  case EC_Ok:
195  return tr("Ok");
196  case EC_Aborted:
197  return tr("Update was aborted.");
199  return tr("Update channel is not configured. Please check your configuration.");
201  return tr("Failed to download updater info file.");
203  return tr("Cannot parse updater info file.");
205  return tr("Main program node is missing from updater info file.");
207  return tr("Revision info on one of the packages is missing from the "
208  "updater info file. Check the log for details.");
210  return tr("Download URL for one of the packages is missing from the "
211  "updater info file. Check the log for details.");
213  return tr("Download URL for one of the packages is invalid. "
214  "Check the log for details.");
216  return tr("Update package download failed. Check the log for details.");
218  return tr("Failed to create directory for updates packages storage.");
220  return tr("Failed to save update package.");
222  return tr("Failed to save update script.");
223  default:
224  return tr("Unknown error.");
225  }
226 }
227 
228 QString AutoUpdater::errorString() const
229 {
230  return errorCodeToString(errorCode());
231 }
232 
233 void AutoUpdater::finishWithError(ErrorCode code)
234 {
235  d->bIsRunning = false;
236  d->errorCode = code;
237  emit finished();
238 }
239 
240 bool AutoUpdater::isRunning() const
241 {
242  return d->bIsRunning;
243 }
244 
245 QNetworkReply::NetworkError AutoUpdater::lastNetworkError() const
246 {
247  if (errorCode() != EC_Ok && d->pNetworkReply != nullptr)
248  {
249  return d->pNetworkReply->error();
250  }
251  return QNetworkReply::NoError;
252 }
253 
254 QUrl AutoUpdater::mkVersionDataFileUrl()
255 {
256  return QUrl(UPDATER_INFO_URL_BASE + d->channel.versionDataFileName());
257 }
258 
259 const QList<UpdatePackage> &AutoUpdater::newUpdatePackages() const
260 {
261  return d->newUpdatePackages;
262 }
263 
264 void AutoUpdater::onPackageDownloadFinish()
265 {
266  if (d->pNetworkReply->error() == QNetworkReply::NoError)
267  {
268  emitStatusMessage(tr("Finished downloading package \"%1\".")
269  .arg(d->currentlyDownloadedPackage.displayName));
270  startPackageScriptDownload(d->currentlyDownloadedPackage);
271  }
272  else
273  {
274  emitStatusMessage(tr("Network error when downloading package \"%1\": [%2] %3")
275  .arg(d->currentlyDownloadedPackage.displayName)
276  .arg(d->pNetworkReply->error())
277  .arg(d->pNetworkReply->errorString()));
278  finishWithError(EC_PackageDownloadProblem);
279  }
280 }
281 
282 void AutoUpdater::onPackageDownloadReadyRead()
283 {
284  const int MAX_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
285  QByteArray data = d->pNetworkReply->read(MAX_CHUNK_SIZE);
286  while (!data.isEmpty())
287  {
288  d->pCurrentPackageFile->write(data);
289  data = d->pNetworkReply->read(MAX_CHUNK_SIZE);
290  }
291 }
292 
293 void AutoUpdater::onPackageScriptDownloadFinish()
294 {
295  if (d->pNetworkReply->error() == QNetworkReply::NoError)
296  {
297  emitStatusMessage(tr("Finished downloading package script \"%1\".")
298  .arg(d->currentlyDownloadedPackage.displayName));
299  QByteArray xmlData = d->pNetworkReply->readAll();
300  QDomDocument xmlDoc = adjustUpdaterScriptXml(xmlData);
301  if (xmlDoc.isNull())
302  {
303  finishWithError(EC_PackageDownloadProblem);
304  return;
305  }
306  d->allScripts.append(xmlDoc);
307 
308  if (!d->packagesInDownloadQueue.isEmpty())
309  {
310  startNextPackageDownload();
311  }
312  else
313  {
314  emitStatusMessage(tr("All packages downloaded. Building updater script."));
315  ErrorCode result = saveUpdaterScript();
316  finishWithError(result);
317  }
318  }
319  else
320  {
321  emitStatusMessage(tr("Network error when downloading package script \"%1\": [%2] %3")
322  .arg(d->currentlyDownloadedPackage.displayName)
323  .arg(d->pNetworkReply->error())
324  .arg(d->pNetworkReply->errorString()));
325  finishWithError(EC_PackageDownloadProblem);
326  }
327 }
328 
329 void AutoUpdater::onUpdaterInfoDownloadFinish()
330 {
331  if (d->pNetworkReply->error() != QNetworkReply::NoError)
332  {
333  finishWithError(EC_UpdaterInfoDownloadProblem);
334  return;
335  }
336  QByteArray json = d->pNetworkReply->readAll();
337  UpdaterInfoParser parser;
338  auto parseResult = (ErrorCode) parser.parse(json);
339  if (parseResult == EC_Ok)
340  {
341  UpdatePackageFilter filter;
342  filter.setIgnoreRevisions(d->ignoredPackagesRevisions);
343  QList<UpdatePackage> packagesList = filter.filter(parser.packages());
344  if (!packagesList.isEmpty())
345  {
346  dumpUpdatePackagesToLog(packagesList);
347  d->newUpdatePackages = packagesList;
348  if (d->bDownloadAndInstallRequireConfirmation)
349  {
350  emitStatusMessage(tr("Requesting update confirmation."));
351  emitOverallProgress(tr("Confirm"));
353  }
354  else
355  {
356  confirmDownloadAndInstall();
357  }
358  }
359  else
360  {
361  // Nothing to update.
362  emitStatusMessage(tr("No new program updates detected."));
363  if (filter.wasAnyUpdatePackageIgnored())
364  {
365  emitStatusMessage(tr("Some update packages were ignored. To install them "
366  "select \"Check for updates\" option from \"Help\" menu."));
367  }
368  finishWithError(EC_Ok);
369  }
370  }
371  else
372  {
373  finishWithError(parseResult);
374  }
375 }
376 
377 AutoUpdater::ErrorCode AutoUpdater::saveUpdaterScript()
378 {
379  QDomDocument xmlDocAllScripts;
380  UpdaterScriptParser scriptParser(xmlDocAllScripts);
381  for (const QDomDocument &doc : d->allScripts)
382  {
383  scriptParser.merge(doc);
384  }
385  QFile f(updaterScriptPath());
386  if (!f.open(QIODevice::WriteOnly))
387  {
388  return EC_ScriptCantBeSaved;
389  }
390  f.write(xmlDocAllScripts.toByteArray());
391  f.close();
392  return EC_Ok;
393 }
394 
395 void AutoUpdater::setChannel(const UpdateChannel &updateChannel)
396 {
397  d->channel = updateChannel;
398 }
399 
400 void AutoUpdater::setIgnoreRevisions(const QMap<QString, QList<QString> > &packagesRevisions)
401 {
402  d->ignoredPackagesRevisions = packagesRevisions;
403 }
404 
406 {
407  d->bDownloadAndInstallRequireConfirmation = b;
408 }
409 
410 void AutoUpdater::start()
411 {
412  if (d->bStarted)
413  {
414  qDebug() << "Cannot start AutoUpdater more than once.";
415  // Always cause assertion failure. Program shouldn't
416  // go into this state.
417  assert(false && "Cannot start AutoUpdater more than once.");
418  return;
419  }
420  d->bStarted = true;
421  d->bPackageDownloadStarted = false;
422  if (d->channel.isNull())
423  {
424  finishWithError(EC_NullUpdateChannel);
425  return;
426  }
427  QDir storageDir(updateStorageDirPath());
428  if (!storageDir.mkpath("."))
429  {
430  gLog << tr("Failed to create directory for updates storage: %1")
431  .arg(storageDir.path());
432  finishWithError(EC_StorageDirCreateFailure);
433  }
434  d->bIsRunning = true;
435  QNetworkRequest request;
436  request.setRawHeader("User-Agent", Version::userAgent().toUtf8());
437  request.setUrl(mkVersionDataFileUrl());
438  QNetworkReply *pReply = d->pNam->get(request);
439  // The updater info file should always be very small and
440  // we can safely store it all in memory.
441  this->connect(pReply,
442  SIGNAL(finished()),
443  SLOT(onUpdaterInfoDownloadFinish()));
444  this->connect(pReply,
445  SIGNAL(downloadProgress(qint64,qint64)),
446  SIGNAL(packageDownloadProgress(qint64,qint64)));
447  d->pNetworkReply = pReply;
448  emitOverallProgress(tr("Update info"));
449 }
450 
451 void AutoUpdater::startNextPackageDownload()
452 {
453  assert(!d->packagesInDownloadQueue.isEmpty() && "AutoUpdater::startNextPackageDownload()");
454  UpdatePackage pkg = d->packagesInDownloadQueue.takeFirst();
455  startPackageDownload(pkg);
456 }
457 
458 void AutoUpdater::startPackageDownload(const UpdatePackage &pkg)
459 {
460  QUrl url = pkg.downloadUrl;
461  if (!url.isValid() || url.isRelative())
462  {
463  // Parser already performs a check for this but let's do this
464  // again to make sure nothing got lost on the way.
465  gLog << tr("Invalid download URL for package \"%1\": %2")
466  .arg(pkg.displayName, pkg.downloadUrl.toString());
467  finishWithError(EC_InvalidDownloadUrl);
468  return;
469  }
470  emitOverallProgress(tr("Package: %1").arg(pkg.displayName));
471  gLog << tr("Downloading package \"%1\" from URL: %2.").arg(pkg.displayName,
472  pkg.downloadUrl.toString());
473 
474  QString fileNameTemplate = QString("%1%2-XXXXXX.zip")
475  .arg(DataPaths::UPDATE_PACKAGE_FILENAME_PREFIX).arg(pkg.name);
476  QString filePathTemplate = Strings::combinePaths(updateStorageDirPath(), fileNameTemplate);
477  qDebug() << "filePathTemplate: " << filePathTemplate;
478  if (d->pCurrentPackageFile != nullptr)
479  {
480  delete d->pCurrentPackageFile;
481  }
482  d->pCurrentPackageFile = new QTemporaryFile(filePathTemplate);
483  d->pCurrentPackageFile->setAutoRemove(false);
484  if (!d->pCurrentPackageFile->open())
485  {
486  gLog << tr("Couldn't save file in path: %1").arg(updateStorageDirPath());
487  delete d->pCurrentPackageFile;
488  d->pCurrentPackageFile = nullptr;
489  finishWithError(EC_PackageCantBeSaved);
490  return;
491  }
492  QFileInfo fileInfo(d->pCurrentPackageFile->fileName());
493  d->downloadedPackagesFilenames << fileInfo.fileName();
494 
495  QNetworkRequest request;
496  request.setRawHeader("User-Agent", Version::userAgent().toUtf8());
497  request.setUrl(url);
498  QNetworkReply *pReply = d->pNam->get(request);
499  d->currentlyDownloadedPackage = pkg;
500  d->pNetworkReply = pReply;
501  this->connect(pReply, SIGNAL(readyRead()),
502  SLOT(onPackageDownloadReadyRead()));
503  this->connect(pReply, SIGNAL(finished()),
504  SLOT(onPackageDownloadFinish()));
505  this->connect(pReply, SIGNAL(downloadProgress(qint64,qint64)),
506  SIGNAL(packageDownloadProgress(qint64,qint64)));
507 }
508 
509 void AutoUpdater::startPackageScriptDownload(const UpdatePackage &pkg)
510 {
511  QUrl url = pkg.downloadScriptUrl;
512  if (!url.isValid() || url.isRelative())
513  {
514  // Parser already performs a check for this but let's do this
515  // again to make sure nothing got lost on the way.
516  gLog << tr("Invalid download URL for package script \"%1\": %2")
517  .arg(pkg.displayName, pkg.downloadScriptUrl.toString());
518  finishWithError(EC_InvalidDownloadUrl);
519  return;
520  }
521  gLog << tr("Downloading package script \"%1\" from URL: %2.").arg(pkg.displayName,
522  pkg.downloadScriptUrl.toString());
523 
524  QNetworkRequest request;
525  request.setRawHeader("User-Agent", Version::userAgent().toUtf8());
526  request.setUrl(url);
527  QNetworkReply *pReply = d->pNam->get(request);
528  d->currentlyDownloadedPackage = pkg;
529  d->pNetworkReply = pReply;
530  // Scripts are small enough that they can be downloaded "in one take",
531  // without saving them continuously to a file.
532  this->connect(pReply, SIGNAL(finished()),
533  SLOT(onPackageScriptDownloadFinish()));
534  this->connect(pReply,
535  SIGNAL(downloadProgress(qint64,qint64)),
536  SIGNAL(packageDownloadProgress(qint64,qint64)));
537 }
538 
539 QString AutoUpdater::updaterLogPath()
540 {
541  return gDefaultDataPaths->localDataLocationPath("updater.log");
542 }
543 
545 {
546  QString dirPath = gDefaultDataPaths->localDataLocationPath(DataPaths::UPDATE_PACKAGES_DIR_NAME);
547  QString name = DataPaths::UPDATE_PACKAGE_FILENAME_PREFIX + "-updater-script.xml";
548  return Strings::combinePaths(dirPath, name);
549 }
550 
551 QString AutoUpdater::updateStorageDirPath()
552 {
553  return gDefaultDataPaths->localDataLocationPath(DataPaths::UPDATE_PACKAGES_DIR_NAME);
554 }