Vidalia  0.2.17
UpdateProcess.cpp
Go to the documentation of this file.
00001 /*
00002 **  This file is part of Vidalia, and is subject to the license terms in the
00003 **  LICENSE file, found in the top level directory of this distribution. If you
00004 **  did not receive the LICENSE file with this file, you may obtain it from the
00005 **  Vidalia source package distributed by the Vidalia Project at
00006 **  http://www.torproject.org/projects/vidalia.html. No part of Vidalia, 
00007 **  including this file, may be copied, modified, propagated, or distributed 
00008 **  except according to the terms described in the LICENSE file.
00009 */
00010 
00011 #include "UpdateProcess.h"
00012 #include "Vidalia.h"
00013 
00014 #include "stringutil.h"
00015 
00016 #include <QDir>
00017 #include <QDomDocument>
00018 #include <QDomElement>
00019 #include <QDomNodeList>
00020 
00021 
00022 UpdateProcess::UpdateProcess(QObject *parent)
00023   : QProcess(parent)
00024 {
00025   _currentCommand = NoCommand;
00026   _socksPort = 0;
00027 
00028   connect(this, SIGNAL(readyReadStandardError()),
00029           this, SLOT(readStandardError()));
00030   connect(this, SIGNAL(readyReadStandardOutput()),
00031           this, SLOT(readStandardOutput()));
00032   connect(this, SIGNAL(finished(int, QProcess::ExitStatus)),
00033           this, SLOT(onFinished(int, QProcess::ExitStatus)));
00034 
00035   setEnvironment(systemEnvironment());
00036 }
00037 
00038 void
00039 UpdateProcess::checkForUpdates(BundleInfo bi)
00040 {
00041   QStringList args;
00042 
00043   args << "update"  << "--force-check"
00044        << " --controller-log-format"
00045        << "--repo=" + updateRepositoryDir()
00046        << "--debug";
00047   if (_socksPort)
00048     args << "--socks-port=" + QString::number(_socksPort);
00049 
00050   args << bundleInfoToString(bi);
00051 
00052   vNotice("updater: launching auto-update executable: %1 %2")
00053                                            .arg(updateExecutable())
00054                                            .arg(args.join(" "));
00055   _currentBundle = bi;
00056   _currentCommand = CheckForUpdates;
00057   start(updateExecutable(), args);
00058 }
00059 
00060 void
00061 UpdateProcess::installUpdates(BundleInfo bi)
00062 {
00063   QStringList args;
00064 
00065   args << "update" << "--controller-log-format"
00066        << "--repo=" + updateRepositoryDir()
00067        << "--install";
00068   if (_socksPort)
00069     args << "--socks-port=" + QString::number(_socksPort);
00070 
00071   args << bundleInfoToString(bi);
00072 
00073   vNotice("updater: launching auto-update executable: %1 %2")
00074                                            .arg(updateExecutable())
00075                                            .arg(args.join(" "));
00076   _currentBundle = bi;
00077   _currentCommand = InstallUpdates;
00078   start(updateExecutable(), args);
00079 }
00080 
00081 void
00082 UpdateProcess::setSocksPort(quint16 port)
00083 {
00084   _socksPort = port;
00085 }
00086 
00087 bool
00088 UpdateProcess::isRunning() const
00089 {
00090   return (state() != QProcess::NotRunning);
00091 }
00092 
00093 void
00094 UpdateProcess::cancel()
00095 {
00096   if (_currentCommand == CheckForUpdates) {
00097 #if defined(Q_OS_WIN32)
00098     kill();
00099 #else
00100     terminate();
00101 #endif
00102   }
00103 }
00104 
00105 void
00106 UpdateProcess::readStandardError()
00107 {
00108   int idx;
00109   bool ok;
00110   QString line, type;
00111   QHash<QString,QString> args;
00112 
00113   setReadChannel(QProcess::StandardError);
00114   while (canReadLine()) {
00115     line = readLine().trimmed();
00116     vInfo("updater (stderr): %1").arg(line);
00117 
00118     idx = line.indexOf(" ");
00119     if (idx < 0 || idx == line.length()-1)
00120       continue;
00121     type = line.mid(0, idx);
00122     line = line.mid(idx + 1);
00123 
00124     args = string_parse_keyvals(line, &ok);
00125     if (! ok)
00126       continue;
00127     else if (line.startsWith("thandy.InstallFailed: ", Qt::CaseInsensitive)) {
00128       /** XXX: This is a fucking kludge. If installation fails, Thandy just
00129        *       dumps a Python traceback that (for obvious reasons) doesn't
00130        *       follow the expected format. There isn't a defined control
00131        *       message type for this yet we'd really like the error, so treat
00132        *       this one specially.
00133        */
00134       emit installUpdatesFailed(line);
00135       continue;
00136     }
00137 
00138     if (! type.compare("CAN_INSTALL", Qt::CaseInsensitive)) {
00139       QString package = args.value("PKG");
00140       if (! package.isEmpty()) {
00141         PackageInfo pkgInfo = packageInfo(package);
00142         if (pkgInfo.isValid())
00143           _packageList << pkgInfo;
00144       }
00145     } else if (_currentCommand == CheckForUpdates
00146                  && ! type.compare("DEBUG") 
00147                  && args.value("msg").startsWith("Got ")) {
00148       /* XXX: This is an even worse fucking kludge. Thandy only reports
00149        *      download progress in a not-so-parser-friendly log message,
00150        *      though, so we must kludge again.
00151        *
00152        *      Here's an example of what we're parsing:
00153        *        "Got 1666048/1666560 bytes from http://updates.torproject.org/thandy/data/win32/tor-0.2.1.9-alpha.msi"
00154        *
00155        *      (Note that the kludge above would even match on "Got milk?".)
00156        */
00157       QStringList parts = args.value("msg").split(" ");
00158       if (parts.size() == 5) {
00159         QStringList progress = parts.at(1).split("/");
00160         if (progress.size() == 2) {
00161           int bytesReceived = progress.at(0).toUInt();
00162           int bytesTotal = progress.at(1).toUInt();
00163           vInfo("updater: Downloaded %1 of %2 bytes of file %3").arg(bytesReceived)
00164                                                                 .arg(bytesTotal)
00165                                                                 .arg(parts.at(4));
00166           emit downloadProgress(parts.at(4), bytesReceived, bytesTotal);
00167         }
00168       }
00169     }
00170   }
00171 }
00172 
00173 void
00174 UpdateProcess::readStandardOutput()
00175 {
00176   QString line;
00177 
00178   setReadChannel(QProcess::StandardOutput);
00179   while (canReadLine()) {
00180     line = readLine().trimmed();
00181     vInfo("updater (stdout): %1").arg(line);
00182   }
00183 }
00184 
00185 void
00186 UpdateProcess::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
00187 {
00188   vInfo("updater: update process finished with exit code %1").arg(exitCode);
00189 
00190   if (_currentCommand == CheckForUpdates) {
00191     if (exitStatus == QProcess::NormalExit) {
00192       emit updatesAvailable(_currentBundle, _packageList);
00193     } else {
00194       emit checkForUpdatesFailed(tr("Vidalia was unable to check for available "
00195                                     "software updates because Tor's update process "
00196                                     "exited unexpectedly."));
00197     }
00198   } else if (_currentCommand == InstallUpdates) {
00199     if (exitStatus == QProcess::NormalExit && exitCode == 0)
00200       emit updatesInstalled(_packageList.size());
00201   }
00202   _packageList.clear();
00203 }
00204 
00205 void
00206 UpdateProcess::onError(QProcess::ProcessError error)
00207 {
00208   if (error == QProcess::FailedToStart) {
00209     vWarn("updater: failed to start");
00210     emit checkForUpdatesFailed(tr("Vidalia was unable to check for available "
00211                                   "software updates because it could not find "
00212                                   "'%1'.").arg(updateExecutable()));
00213   }
00214 }
00215 
00216 int
00217 UpdateProcess::checkForUpdatesInterval()
00218 {
00219   /* XXX: Check twice a day. Why? Because arma said so. */
00220   return 12*60*60;
00221 }
00222 
00223 QDateTime
00224 UpdateProcess::nextCheckForUpdates(const QDateTime &lastCheckedAt)
00225 {
00226   return lastCheckedAt.addSecs(checkForUpdatesInterval()).toUTC();
00227 }
00228 
00229 bool
00230 UpdateProcess::shouldCheckForUpdates(const QDateTime &lastCheckedAt)
00231 {
00232   QDateTime nextCheckAt = nextCheckForUpdates(lastCheckedAt);
00233   return (QDateTime::currentDateTime().toUTC() >= nextCheckAt);
00234 }
00235 
00236 QString
00237 UpdateProcess::updateExecutable()
00238 {
00239   return "thandy.exe";
00240 }
00241 
00242 QString
00243 UpdateProcess::updateRepositoryDir()
00244 {
00245   return QDir::convertSeparators(Vidalia::dataDirectory() + "/updates");
00246 }
00247 
00248 QString
00249 UpdateProcess::bundleInfoToString(BundleInfo bi)
00250 {
00251   switch (bi) {
00252     case TorBundleInfo:
00253       return "/bundleinfo/tor/win32/";
00254     default:
00255       return QString();
00256   };
00257 }
00258 
00259 PackageInfo
00260 UpdateProcess::packageInfo(const QString &package)
00261 {
00262   QProcess proc;
00263   QStringList args;
00264 
00265   args << "json2xml"
00266        << QDir::convertSeparators(updateRepositoryDir() + "/" + package);
00267 
00268   vNotice("updater: launching auto-update executable: %1 %2")
00269                                            .arg(updateExecutable())
00270                                            .arg(args.join(" "));
00271 
00272   proc.setEnvironment(proc.systemEnvironment());
00273   proc.start(updateExecutable(), args);
00274   if (! proc.waitForStarted())
00275     return PackageInfo();
00276   if (! proc.waitForFinished())
00277     return PackageInfo();
00278   return packageInfoFromXml(proc.readAll());
00279 }
00280 
00281 PackageInfo
00282 UpdateProcess::packageInfoFromXml(const QByteArray &xml)
00283 {
00284   QDomDocument doc;
00285   QDomElement dict, elem;
00286   QDomNodeList nodeList;
00287   QString errmsg;
00288   QStringList versionParts;
00289   PackageInfo pkgInfo;
00290 
00291   if (! doc.setContent(xml, false, &errmsg, 0, 0))
00292     goto err;
00293 
00294   /* XXX: Qt 4.4 introduced XPath support, which would make the following
00295    * parsing much easier. Whenever we drop support for Qt < 4.4, this should
00296    * be updated.
00297    */
00298   elem = doc.documentElement().firstChildElement("signed");
00299   if (elem.isNull()) {
00300     errmsg = "Signed element not found";
00301     goto err;
00302   }
00303 
00304   dict = elem.firstChildElement("dict");
00305   if (dict.isNull()) {
00306     errmsg = "no Dict element as a child of Signed";
00307     goto err;
00308   }
00309 
00310   elem = dict.firstChildElement("name");
00311   if (elem.isNull()) {
00312     errmsg = "Name element not found";
00313     goto err;
00314   }
00315   pkgInfo.setName(elem.text());
00316 
00317   elem = dict.firstChildElement("version").firstChildElement("list");
00318   if (elem.isNull()) {
00319     errmsg = "no valid Version element found";
00320     goto err;
00321   }
00322   elem = elem.firstChildElement("item");
00323   for ( ; ! elem.isNull(); elem = elem.nextSiblingElement("item")) {
00324     versionParts << elem.text();
00325   }
00326   pkgInfo.setVersion(versionParts.join("."));
00327 
00328   elem = dict.firstChildElement("shortdesc").firstChildElement("dict");
00329   if (elem.isNull()) {
00330     errmsg = "no valid Shortdesc element found";
00331     goto err;
00332   }
00333   elem = elem.firstChildElement();
00334   for ( ; ! elem.isNull(); elem = elem.nextSiblingElement()) {
00335     pkgInfo.setShortDescription(elem.tagName(), elem.text());
00336   }
00337 
00338   elem = dict.firstChildElement("longdesc").firstChildElement("dict");
00339   if (elem.isNull()) {
00340     errmsg = "no valid Longdesc element found";
00341     goto err;
00342   }
00343   elem = elem.firstChildElement();
00344   for ( ; ! elem.isNull(); elem = elem.nextSiblingElement()) {
00345     pkgInfo.setLongDescription(elem.tagName(), elem.text());
00346   }
00347 
00348   return pkgInfo;
00349 
00350 err:
00351   vWarn("updater: invalid package info XML document: %1").arg(errmsg);
00352   return PackageInfo();
00353 }
00354