OSDN Git Service

Bump version.
[mutilities/MUtilities.git] / src / UpdateChecker.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // MuldeR's Utilities for Qt
3 // Copyright (C) 2004-2019 LoRd_MuldeR <MuldeR2@GMX.de>
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  02110-1301  USA
18 //
19 // http://www.gnu.org/licenses/lgpl-2.1.txt
20 //////////////////////////////////////////////////////////////////////////////////
21
22 #include <MUtils/Global.h>
23 #include <MUtils/UpdateChecker.h>
24 #include <MUtils/OSSupport.h>
25 #include <MUtils/Exception.h>
26
27 #include <QStringList>
28 #include <QFile>
29 #include <QFileInfo>
30 #include <QDir>
31 #include <QProcess>
32 #include <QUrl>
33 #include <QEventLoop>
34 #include <QTimer>
35 #include <QElapsedTimer>
36 #include <QSet>
37 #include <QHash>
38 #include <QQueue>
39
40 #include "Mirrors.h"
41
42 ///////////////////////////////////////////////////////////////////////////////
43 // CONSTANTS
44 ///////////////////////////////////////////////////////////////////////////////
45
46 static const char *GLOBALHEADER_ID = "!Update";
47
48 static const char *MIRROR_URL_POSTFIX[] = 
49 {
50         "update.ver",
51         "update_beta.ver",
52         NULL
53 };
54
55 static const int MIN_CONNSCORE = 5;
56 static const int QUICK_MIRRORS = 3;
57 static const int MAX_CONN_TIMEOUT = 16000;
58 static const int DOWNLOAD_TIMEOUT = 30000;
59
60 static const int VERSION_INFO_EXPIRES_MONTHS = 6;
61 static char *USER_AGENT_STR = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0"; /*use something innocuous*/
62
63 ////////////////////////////////////////////////////////////
64 // Utility Macros
65 ////////////////////////////////////////////////////////////
66
67 #define CHECK_CANCELLED() do \
68 { \
69         if(MUTILS_BOOLIFY(m_cancelled)) \
70         { \
71                 m_success.fetchAndStoreOrdered(0); \
72                 log("", "Update check has been cancelled by user!"); \
73                 setProgress(m_maxProgress); \
74                 setStatus(UpdateStatus_CancelledByUser); \
75                 return; \
76         } \
77 } \
78 while(0)
79
80 #define LOG_MESSAGE_HELPER(X) do \
81 { \
82         if (!(X).isNull()) \
83         { \
84                 emit messageLogged((X)); \
85         } \
86 } \
87 while(0)
88
89 #define STRICMP(X,Y) ((X).compare((Y), Qt::CaseInsensitive) == 0)
90
91 ////////////////////////////////////////////////////////////
92 // Helper Functions
93 ////////////////////////////////////////////////////////////
94
95 static QQueue<QString> buildRandomList(const char *const *values)
96 {
97         QQueue<QString> list;
98         while(*values)
99         {
100                 list.insert(MUtils::next_rand_u32(list.size() + 1), QString::fromLatin1(*(values++)));
101         }
102         return list;
103 }
104
105 static const QHash<QString, QString> *initEnvVars(void)
106 {
107         const QString tempfolder = QDir::toNativeSeparators(MUtils::temp_folder());
108         QHash<QString, QString> *const environment = new QHash<QString, QString>();
109         environment->insert(QLatin1String("CURL_HOME"), tempfolder);
110         environment->insert(QLatin1String("GNUPGHOME"), tempfolder);
111         return environment;
112 }
113
114 ////////////////////////////////////////////////////////////
115 // Update Info Class
116 ////////////////////////////////////////////////////////////
117
118 MUtils::UpdateCheckerInfo::UpdateCheckerInfo(void)
119 {
120         resetInfo();
121 }
122         
123 void MUtils::UpdateCheckerInfo::resetInfo(void)
124 {
125         m_buildNo = 0;
126         m_buildDate.setDate(1900, 1, 1);
127         m_downloadSite.clear();
128         m_downloadAddress.clear();
129         m_downloadFilename.clear();
130         m_downloadFilecode.clear();
131         m_downloadChecksum.clear();
132 }
133
134 bool MUtils::UpdateCheckerInfo::isComplete(void)
135 {
136         return (this->m_buildNo > 0) &&
137                 (this->m_buildDate.year() >= 2010) &&
138                 (!this->m_downloadSite.isEmpty()) &&
139                 (!this->m_downloadAddress.isEmpty()) &&
140                 (!this->m_downloadFilename.isEmpty()) &&
141                 (!this->m_downloadFilecode.isEmpty()) &&
142                 (!this->m_downloadChecksum.isEmpty());
143 }
144
145 ////////////////////////////////////////////////////////////
146 // Constructor & Destructor
147 ////////////////////////////////////////////////////////////
148
149 MUtils::UpdateChecker::UpdateChecker(const QString &binCurl, const QString &binGnuPG, const QString &binKeys, const QString &applicationId, const quint32 &installedBuildNo, const bool betaUpdates, const bool testMode)
150 :
151         m_updateInfo(new UpdateCheckerInfo()),
152         m_binaryCurl(binCurl),
153         m_binaryGnuPG(binGnuPG),
154         m_binaryKeys(binKeys),
155         m_applicationId(applicationId),
156         m_installedBuildNo(installedBuildNo),
157         m_betaUpdates(betaUpdates),
158         m_testMode(testMode),
159         m_maxProgress(MIN_CONNSCORE + 5),
160         m_environment(initEnvVars())
161 {
162         m_status = UpdateStatus_NotStartedYet;
163         m_progress = 0;
164
165         if(m_binaryCurl.isEmpty() || m_binaryGnuPG.isEmpty() || m_binaryKeys.isEmpty())
166         {
167                 MUTILS_THROW("Tools not initialized correctly!");
168         }
169 }
170
171 MUtils::UpdateChecker::~UpdateChecker(void)
172 {
173 }
174
175 ////////////////////////////////////////////////////////////
176 // Public slots
177 ////////////////////////////////////////////////////////////
178
179 void MUtils::UpdateChecker::start(Priority priority)
180 {
181         m_success.fetchAndStoreOrdered(0);
182         m_cancelled.fetchAndStoreOrdered(0);
183         QThread::start(priority);
184 }
185
186 ////////////////////////////////////////////////////////////
187 // Protected functions
188 ////////////////////////////////////////////////////////////
189
190 void MUtils::UpdateChecker::run(void)
191 {
192         qDebug("Update checker thread started!");
193         MUTILS_EXCEPTION_HANDLER(m_testMode ? testMirrorsList() : checkForUpdates());
194         qDebug("Update checker thread completed.");
195 }
196
197 void MUtils::UpdateChecker::checkForUpdates(void)
198 {
199         // ----- Initialization ----- //
200
201         m_updateInfo->resetInfo();
202         setProgress(0);
203
204         // ----- Test Internet Connection ----- //
205
206         log("Checking your Internet connection...", "");
207         setStatus(UpdateStatus_CheckingConnection);
208
209         const int networkStatus = OS::network_status();
210         if(networkStatus == OS::NETWORK_TYPE_NON)
211         {
212                 if (!MUtils::OS::arguments().contains("ignore-network-status"))
213                 {
214                         log("Operating system reports that the computer is currently offline !!!");
215                         setProgress(m_maxProgress);
216                         setStatus(UpdateStatus_ErrorNoConnection);
217                         return;
218                 }
219         }
220         
221         msleep(333);
222         setProgress(1);
223
224         // ----- Test Known Hosts Connectivity ----- //
225
226         int connectionScore = 0;
227         QQueue<QString> mirrorList = buildRandomList(known_hosts);
228
229         for(int connectionTimeout = 1000; connectionTimeout <= MAX_CONN_TIMEOUT; connectionTimeout *= 2)
230         {
231                 QElapsedTimer elapsedTimer;
232                 elapsedTimer.start();
233                 const int globalTimeout = 2 * MIN_CONNSCORE * connectionTimeout, count = mirrorList.count();
234                 for(int i = 0; i < count; ++i)
235                 {
236                         Q_ASSERT(!mirrorList.isEmpty());
237                         const QString hostName = mirrorList.dequeue();
238                         if (tryContactHost(hostName, connectionTimeout))
239                         {
240                                 setProgress(1 + (++connectionScore));
241                                 if (connectionScore >= MIN_CONNSCORE)
242                                 {
243                                         goto endLoop; /*success*/
244                                 }
245                         }
246                         else
247                         {
248                                 mirrorList.enqueue(hostName);
249                                 if(elapsedTimer.hasExpired(globalTimeout))
250                                 {
251                                         break; /*timer expired*/
252                                 }
253                         }
254                         CHECK_CANCELLED();
255                 }
256         }
257
258 endLoop:
259         if(connectionScore < MIN_CONNSCORE)
260         {
261                 log("", "Connectivity test has failed: Internet connection appears to be broken!");
262                 setProgress(m_maxProgress);
263                 setStatus(UpdateStatus_ErrorConnectionTestFailed);
264                 return;
265         }
266
267         // ----- Fetch Update Info From Server ----- //
268
269         log("----", "", "Internet connection is operational, checking for updates online...");
270         setStatus(UpdateStatus_FetchingUpdates);
271
272         int mirrorCount = 0;
273         mirrorList = buildRandomList(update_mirrors);
274
275         while(!mirrorList.isEmpty())
276         {
277                 const QString currentMirror = mirrorList.takeFirst();
278                 const bool isQuick = (mirrorCount++ < QUICK_MIRRORS);
279                 if(tryUpdateMirror(m_updateInfo.data(), currentMirror, isQuick))
280                 {
281                         m_success.ref(); /*success*/
282                         break;
283                 }
284                 if (isQuick)
285                 {
286                         mirrorList.append(currentMirror); /*re-schedule*/
287                 }
288                 CHECK_CANCELLED();
289                 msleep(1);
290         }
291
292         msleep(333);
293         setProgress(MIN_CONNSCORE + 5);
294
295         // ----- Generate final result ----- //
296
297         if(MUTILS_BOOLIFY(m_success))
298         {
299                 if(m_updateInfo->m_buildNo > m_installedBuildNo)
300                 {
301                         setStatus(UpdateStatus_CompletedUpdateAvailable);
302                 }
303                 else if(m_updateInfo->m_buildNo == m_installedBuildNo)
304                 {
305                         setStatus(UpdateStatus_CompletedNoUpdates);
306                 }
307                 else
308                 {
309                         setStatus(UpdateStatus_CompletedNewVersionOlder);
310                 }
311         }
312         else
313         {
314                 setStatus(UpdateStatus_ErrorFetchUpdateInfo);
315         }
316 }
317
318 void MUtils::UpdateChecker::testMirrorsList(void)
319 {
320         QQueue<QString> mirrorList;
321         for(int i = 0; update_mirrors[i]; i++)
322         {
323                 mirrorList.enqueue(QString::fromLatin1(update_mirrors[i]));
324         }
325
326         // ----- Test update mirrors ----- //
327
328         qDebug("\n[Mirror Sites]");
329         log("Testing all known mirror sites...", "", "---");
330
331         UpdateCheckerInfo updateInfo;
332         while (!mirrorList.isEmpty())
333         {
334                 const QString currentMirror = mirrorList.dequeue();
335                 bool success = false;
336                 qDebug("Testing: %s", MUTILS_L1STR(currentMirror));
337                 log("", "Testing mirror:", currentMirror, "");
338                 for (quint8 attempt = 0; attempt < 3; ++attempt)
339                 {
340                         updateInfo.resetInfo();
341                         if (tryUpdateMirror(&updateInfo, currentMirror, (!attempt)))
342                         {
343                                 success = true;
344                                 break;
345                         }
346                 }
347                 if (!success)
348                 {
349                         qWarning("\nUpdate mirror seems to be unavailable:\n%s\n", MUTILS_L1STR(currentMirror));
350                 }
351                 log("", "---");
352         }
353
354         // ----- Test known hosts ----- //
355
356         mirrorList.clear();
357         for (int i = 0; known_hosts[i]; i++)
358         {
359                 mirrorList.enqueue(QString::fromLatin1(known_hosts[i]));
360         }
361
362         qDebug("\n[Known Hosts]");
363         log("Testing all known hosts...", "", "---");
364
365         while(!mirrorList.isEmpty())
366         {
367                 const QString currentHost = mirrorList.dequeue();
368                 qDebug("Testing: %s", MUTILS_L1STR(currentHost));
369                 log(QLatin1String(""), "Testing host:", currentHost, "");
370                 if (!tryContactHost(currentHost, DOWNLOAD_TIMEOUT))
371                 {
372                         qWarning("\nConnectivity test FAILED on the following host:\n%s\n", MUTILS_L1STR(currentHost));
373                 }
374                 log("---");
375         }
376 }
377
378 ////////////////////////////////////////////////////////////
379 // PRIVATE FUNCTIONS
380 ////////////////////////////////////////////////////////////
381
382 void MUtils::UpdateChecker::setStatus(const int status)
383 {
384         if(m_status != status)
385         {
386                 m_status = status;
387                 emit statusChanged(status);
388         }
389 }
390
391 void MUtils::UpdateChecker::setProgress(const int progress)
392 {
393         const int value = qBound(0, progress, m_maxProgress);
394         if(m_progress != value)
395         {
396                 emit progressChanged(m_progress = value);
397         }
398 }
399
400 void MUtils::UpdateChecker::log(const QString &str1, const QString &str2, const QString &str3, const QString &str4)
401 {
402         LOG_MESSAGE_HELPER(str1);
403         LOG_MESSAGE_HELPER(str2);
404         LOG_MESSAGE_HELPER(str3);
405         LOG_MESSAGE_HELPER(str4);
406 }
407
408 bool MUtils::UpdateChecker::tryUpdateMirror(UpdateCheckerInfo *updateInfo, const QString &url, const bool &quick)
409 {
410         bool success = false;
411         log("", "Trying update mirror:", url, "");
412
413         if (quick)
414         {
415                 setProgress(MIN_CONNSCORE + 1);
416                 if (!tryContactHost(QUrl(url).host(), (MAX_CONN_TIMEOUT / 8)))
417                 {
418                         log("", "Mirror is too slow, skipping!");
419                         return false;
420                 }
421         }
422
423         const QString randPart = next_rand_str();
424         const QString outFileVers = QString("%1/%2.ver").arg(temp_folder(), randPart);
425         const QString outFileSign = QString("%1/%2.sig").arg(temp_folder(), randPart);
426
427         if (!getUpdateInfo(url, outFileVers, outFileSign))
428         {
429                 log("", "Oops: Download of update information has failed!");
430                 goto cleanUp;
431         }
432
433         log("Download completed, verifying signature:", "");
434         setProgress(MIN_CONNSCORE + 4);
435         if (!checkSignature(outFileVers, outFileSign))
436         {
437                 log("", "Bad signature detected, take care !!!");
438                 goto cleanUp;
439         }
440
441         log("", "Signature is valid, parsing update information:", "");
442         success = parseVersionInfo(outFileVers, updateInfo);
443
444 cleanUp:
445         QFile::remove(outFileVers);
446         QFile::remove(outFileSign);
447         return success;
448 }
449
450 bool MUtils::UpdateChecker::getUpdateInfo(const QString &url, const QString &outFileVers, const QString &outFileSign)
451 {
452         log("Downloading update information:", "");
453         setProgress(MIN_CONNSCORE + 2);
454         if(getFile(QUrl(QString("%1%2").arg(url, MIRROR_URL_POSTFIX[m_betaUpdates ? 1 : 0])), outFileVers))
455         {
456                 if (!m_cancelled)
457                 {
458                         log( "Downloading signature file:", "");
459                         setProgress(MIN_CONNSCORE + 3);
460                         if (getFile(QUrl(QString("%1%2.sig2").arg(url, MIRROR_URL_POSTFIX[m_betaUpdates ? 1 : 0])), outFileSign))
461                         {
462                                 return true; /*completed*/
463                         }
464                 }
465         }
466         return false;
467 }
468
469 //----------------------------------------------------------
470 // PARSE UPDATE INFO
471 //----------------------------------------------------------
472
473 #define _CHECK_HEADER(ID,NAME) \
474         if (STRICMP(name, (NAME))) \
475         { \
476                 sectionId = (ID); \
477                 continue; \
478         }
479
480 #define _PARSE_TEXT(OUT,KEY) \
481         if (STRICMP(key, (KEY))) \
482         { \
483                 (OUT) = val; \
484                 break; \
485         }
486
487 #define _PARSE_UINT(OUT,KEY) \
488         if (STRICMP(key, (KEY))) \
489         { \
490                 bool _ok = false; \
491                 const unsigned int _tmp = val.toUInt(&_ok); \
492                 if (_ok) \
493                 { \
494                         (OUT) = _tmp; \
495                         break; \
496                 } \
497         }
498
499 #define _PARSE_DATE(OUT,KEY) \
500         if (STRICMP(key, (KEY))) \
501         { \
502                 const QDate _tmp = QDate::fromString(val, Qt::ISODate); \
503                 if (_tmp.isValid()) \
504                 { \
505                         (OUT) = _tmp; \
506                         break; \
507                 } \
508         }
509
510 bool MUtils::UpdateChecker::parseVersionInfo(const QString &file, UpdateCheckerInfo *const updateInfo)
511 {
512         updateInfo->resetInfo();
513
514         QFile data(file);
515         if(!data.open(QIODevice::ReadOnly))
516         {
517                 qWarning("Cannot open update info file for reading!");
518                 return false;
519         }
520
521         QDate updateInfoDate;
522         int sectionId = 0;
523         QRegExp regex_sec("^\\[(.+)\\]$"), regex_val("^([^=]+)=(.+)$");
524
525         while(!data.atEnd())
526         {
527                 QString line = QString::fromLatin1(data.readLine()).simplified();
528                 if (regex_sec.indexIn(line) >= 0)
529                 {
530                         sectionId = 0; /*unknown section*/
531                         const QString name = regex_sec.cap(1).trimmed();
532                         log(QString("Sec: [%1]").arg(name));
533                         _CHECK_HEADER(1, GLOBALHEADER_ID)
534                         _CHECK_HEADER(2, m_applicationId)
535                         continue;
536                 }
537                 if (regex_val.indexIn(line) >= 0)
538                 {
539                         const QString key = regex_val.cap(1).trimmed();
540                         const QString val = regex_val.cap(2).trimmed();
541                         log(QString("Val: \"%1\" = \"%2\"").arg(key, val));
542                         switch (sectionId)
543                         {
544                         case 1:
545                                 _PARSE_DATE(updateInfoDate, "TimestampCreated")
546                                 break;
547                         case 2:
548                                 _PARSE_UINT(updateInfo->m_buildNo,          "BuildNo")
549                                 _PARSE_DATE(updateInfo->m_buildDate,        "BuildDate")
550                                 _PARSE_TEXT(updateInfo->m_downloadSite,     "DownloadSite")
551                                 _PARSE_TEXT(updateInfo->m_downloadAddress,  "DownloadAddress")
552                                 _PARSE_TEXT(updateInfo->m_downloadFilename, "DownloadFilename")
553                                 _PARSE_TEXT(updateInfo->m_downloadFilecode, "DownloadFilecode")
554                                 _PARSE_TEXT(updateInfo->m_downloadChecksum, "DownloadChecksum")
555                                 break;
556                         }
557                 }
558         }
559
560         if (!updateInfo->isComplete())
561         {
562                 log("", "WARNING: Update information is incomplete!");
563                 goto failure;
564         }
565
566         if(updateInfoDate.isValid())
567         {
568                 const QDate expiredDate = updateInfoDate.addMonths(VERSION_INFO_EXPIRES_MONTHS);
569                 if (expiredDate < OS::current_date())
570                 {
571                         log("", QString("WARNING: Update information has expired at %1!").arg(expiredDate.toString(Qt::ISODate)));
572                         goto failure;
573                 }
574         }
575         else
576         {
577                 log("", "WARNING: Timestamp is missing from update information header!");
578                 goto failure;
579         }
580
581         log("", "Success: Update information is complete.");
582         return true; /*success*/
583
584 failure:
585         updateInfo->resetInfo();
586         return false;
587 }
588
589 //----------------------------------------------------------
590 // EXTERNAL TOOLS
591 //----------------------------------------------------------
592
593 bool MUtils::UpdateChecker::getFile(const QUrl &url, const QString &outFile, const unsigned int maxRedir)
594 {
595         QFileInfo output(outFile);
596         output.setCaching(false);
597
598         if (output.exists())
599         {
600                 QFile::remove(output.canonicalFilePath());
601                 if (output.exists())
602                 {
603                         qWarning("Existing output file could not be found!");
604                         return false;
605                 }
606         }
607         
608         QStringList args(QLatin1String("-vsSNqkfL"));
609         args << "-m" << QString::number(DOWNLOAD_TIMEOUT / 1000);
610         args << "--max-redirs" << QString::number(maxRedir);
611         args << "-A" << USER_AGENT_STR;
612         args << "-e" << QString("%1://%2/;auto").arg(url.scheme(), url.host());
613         args << "-o" << output.fileName() << url.toString();
614
615         return execCurl(args, output.absolutePath(), DOWNLOAD_TIMEOUT);
616 }
617
618 bool MUtils::UpdateChecker::tryContactHost(const QString &hostname, const int &timeoutMsec)
619 {
620         log(QString("Connecting to host: %1").arg(hostname), "");
621
622         QStringList args(QLatin1String("-vsSNqkI"));
623         args << "-m" << QString::number(qMax(1, timeoutMsec / 1000));
624         args << "-A" << USER_AGENT_STR;
625         args << "-o" << OS::null_device() << QString("http://%1/").arg(hostname);
626         
627         return execCurl(args, temp_folder(), timeoutMsec);
628 }
629
630 bool MUtils::UpdateChecker::checkSignature(const QString &file, const QString &signature)
631 {
632         if (QFileInfo(file).absolutePath().compare(QFileInfo(signature).absolutePath(), Qt::CaseInsensitive) != 0)
633         {
634                 qWarning("CheckSignature: File and signature should be in same folder!");
635                 return false;
636         }
637
638         QString keyRingPath(m_binaryKeys);
639         bool removeKeyring = false;
640         if (QFileInfo(file).absolutePath().compare(QFileInfo(m_binaryKeys).absolutePath(), Qt::CaseInsensitive) != 0)
641         {
642                 keyRingPath = make_temp_file(QFileInfo(file).absolutePath(), "gpg");
643                 removeKeyring = true;
644                 if (!QFile::copy(m_binaryKeys, keyRingPath))
645                 {
646                         qWarning("CheckSignature: Failed to copy the key-ring file!");
647                         return false;
648                 }
649         }
650
651         QStringList args;
652         args << QStringList() << "--homedir" << ".";
653         args << "--keyring" << QFileInfo(keyRingPath).fileName();
654         args << QFileInfo(signature).fileName();
655         args << QFileInfo(file).fileName();
656
657         const int exitCode = execProcess(m_binaryGnuPG, args, QFileInfo(file).absolutePath(), DOWNLOAD_TIMEOUT);
658         if (exitCode != INT_MAX)
659         {
660                 log(QString().sprintf("Exited with code %d", exitCode));
661         }
662
663         if (removeKeyring)
664         {
665                 remove_file(keyRingPath);
666         }
667
668         return (exitCode == 0); /*completed*/
669 }
670
671 bool MUtils::UpdateChecker::execCurl(const QStringList &args, const QString &workingDir, const int timeout)
672 {
673         const int exitCode = execProcess(m_binaryCurl, args, workingDir, timeout + (timeout / 2));
674         if (exitCode != INT_MAX)
675         {
676                 switch (exitCode)
677                 {
678                         case  0: log(QLatin1String("DONE: Transfer completed successfully."), "");                     break;
679                         case  6: log(QLatin1String("ERROR: Remote host could not be resolved!"), "");                  break;
680                         case  7: log(QLatin1String("ERROR: Connection to remote host could not be established!"), ""); break;
681                         case 22: log(QLatin1String("ERROR: Requested URL was not found or returned an error!"), "");   break;
682                         case 28: log(QLatin1String("ERROR: Operation timed out !!!"), "");                             break;
683                         default: log(QString().sprintf("ERROR: Terminated with unknown code %d", exitCode), "");       break;
684                 }
685         }
686
687         return (exitCode == 0); /*completed*/
688 }
689
690 int MUtils::UpdateChecker::execProcess(const QString &programFile, const QStringList &args, const QString &workingDir, const int timeout)
691 {
692         QProcess process;
693         init_process(process, workingDir, true, NULL, m_environment.data());
694
695         QEventLoop loop;
696         connect(&process, SIGNAL(error(QProcess::ProcessError)), &loop, SLOT(quit()));
697         connect(&process, SIGNAL(finished(int, QProcess::ExitStatus)), &loop, SLOT(quit()));
698         connect(&process, SIGNAL(readyRead()), &loop, SLOT(quit()));
699
700         QTimer timer;
701         timer.setSingleShot(true);
702         connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
703
704         process.start(programFile, args);
705         if (!process.waitForStarted())
706         {
707                 log("PROCESS FAILED TO START !!!", "");
708                 qWarning("WARNING: %s process could not be created!", MUTILS_UTF8(QFileInfo(programFile).fileName()));
709                 return INT_MAX; /*failed to start*/
710         }
711
712         bool bAborted = false;
713         timer.start(qMax(timeout, 1500));
714
715         while (process.state() != QProcess::NotRunning)
716         {
717                 loop.exec();
718                 while (process.canReadLine())
719                 {
720                         const QString line = QString::fromLatin1(process.readLine()).simplified();
721                         if (line.length() > 1)
722                         {
723                                 log(line);
724                         }
725                 }
726                 const bool bCancelled = MUTILS_BOOLIFY(m_cancelled);
727                 if (bAborted = (bCancelled || ((!timer.isActive()) && (!process.waitForFinished(125)))))
728                 {
729                         log(bCancelled ? "CANCELLED BY USER !!!" : "PROCESS TIMEOUT !!!", "");
730                         qWarning("WARNING: %s process %s!", MUTILS_UTF8(QFileInfo(programFile).fileName()), bCancelled ? "cancelled" : "timed out");
731                         break; /*abort process*/
732                 }
733         }
734
735         timer.stop();
736         timer.disconnect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
737
738         if (bAborted)
739         {
740                 process.kill();
741                 process.waitForFinished(-1);
742         }
743
744         while (process.canReadLine())
745         {
746                 const QString line = QString::fromLatin1(process.readLine()).simplified();
747                 if (line.length() > 1)
748                 {
749                         log(line);
750                 }
751         }
752
753         return bAborted ? INT_MAX : process.exitCode();
754 }
755
756 ////////////////////////////////////////////////////////////
757 // SLOTS
758 ////////////////////////////////////////////////////////////
759
760 /*NONE*/
761
762 ////////////////////////////////////////////////////////////
763 // EVENTS
764 ////////////////////////////////////////////////////////////
765
766 /*NONE*/