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