Vidalia
0.2.17
|
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