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 
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 
540 {
541  QString dirPath = gDefaultDataPaths->localDataLocationPath(DataPaths::UPDATE_PACKAGES_DIR_NAME);
542  QString name = DataPaths::UPDATE_PACKAGE_FILENAME_PREFIX + "-updater-script.xml";
543  return Strings::combinePaths(dirPath, name);
544 }
545 
546 QString AutoUpdater::updateStorageDirPath()
547 {
548  return gDefaultDataPaths->localDataLocationPath(DataPaths::UPDATE_PACKAGES_DIR_NAME);
549 }
QUrl downloadScriptUrl
Updater script download URL.
Definition: updatepackage.h:59
static const QString UPDATER_INFO_URL_BASE
Base URL to the directory where "update-info*" JSON files are contained.
Definition: autoupdater.h:138
static QString combinePaths(QString pathFront, QString pathEnd)
Definition: strings.cpp:147
One of packages has no revision info.
Definition: autoupdater.h:93
int parse(const QByteArray &json)
Parses updater info JSON and sets certain internal properties which can then be accessed through gett...
QNetworkReply::NetworkError lastNetworkError() const
The network error that caused the updater to fail.
QUrl downloadUrl
Package download URL.
Definition: updatepackage.h:51
Failed to create directory for updates storage.
Definition: autoupdater.h:110
static QString updaterScriptPath()
Path to updater script XML file.
No valid UpdateChannel was specified.
Definition: autoupdater.h:76
const UpdateChannel & channel() const
setChannel() .
Failed to download update package.
Definition: autoupdater.h:106
Interface to Mendeley updater .xml script files.
QUrl.isValid() for package download URL returned false or QUrl.isRelative() returned true...
Definition: autoupdater.h:102
QString displayName
Name displayed to the user.
Definition: updatepackage.h:45
Update script can&#39;t be merged and stored on the local filesystem.
Definition: autoupdater.h:119
const QList< UpdatePackage > & newUpdatePackages() const
List of new update packages to install.
static QString userAgent()
WWW User Agent used for HTTP communications.
Definition: version.cpp:66
One of packages has no download URL.
Definition: autoupdater.h:97
void setChannel(const UpdateChannel &updateChannel)
Update channel name.
void setIgnoreRevisions(const QMap< QString, QList< QString > > &packagesRevisions)
Revisions set in this map will not be treated as updates even if they differ from the currently insta...
File was parseable but there was no main program information inside.
Definition: autoupdater.h:89
Deals with program updates/upgrades.
Definition: autoupdater.h:61
const QStringList & downloadedPackagesFilenames() const
Filenames for packages which are ready to install.
void setRequireDownloadAndInstallConfirmation(bool b)
Controls if the download&installation process is automated.
Network error when downloading updater info file.
Definition: autoupdater.h:80
Filters UpdatePackage information basing on what is requested by the program.
void merge(const QDomDocument &otherDoc)
Merges other script with current script.
Updater info file can&#39;t be parsed.
Definition: autoupdater.h:84
Update was aborted by the user or by the program.
Definition: autoupdater.h:72
bool wasAnyUpdatePackageIgnored() const
After filter() flag which says if any package was ignored.
Package file can&#39;t be stored on the local filesystem.
Definition: autoupdater.h:114
void downloadAndInstallConfirmationRequested()
Information on update packages has been received and install confirmation is requested.
QString name
Name of the package (program name or plugin name).
Definition: updatepackage.h:63
void finished()
AutoUpdater has finished its job.