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