Improved progress indicator + refactored update info parsing code.
[MUtilities.git] / src / UpdateChecker.cpp
blobbff5814dd52dec592f2583da71250a78e60763bc
1 ///////////////////////////////////////////////////////////////////////////////
2 // MuldeR's Utilities for Qt
3 // Copyright (C) 2004-2018 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.
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
19 // http://www.gnu.org/licenses/lgpl-2.1.txt
20 //////////////////////////////////////////////////////////////////////////////////
22 #include <MUtils/Global.h>
23 #include <MUtils/UpdateChecker.h>
24 #include <MUtils/OSSupport.h>
25 #include <MUtils/Exception.h>
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>
39 #include "Mirrors.h"
41 ///////////////////////////////////////////////////////////////////////////////
42 // CONSTANTS
43 ///////////////////////////////////////////////////////////////////////////////
45 static const char *HEADER_ID = "!Update";
47 static const char *MIRROR_URL_POSTFIX[] =
49 "update.ver",
50 "update_beta.ver",
51 NULL
54 static const int MIN_CONNSCORE = 5;
55 static const int QUICK_MIRRORS = 3;
56 static const int MAX_CONN_TIMEOUT = 16000;
57 static const int DOWNLOAD_TIMEOUT = 30000;
59 static const int VERSION_INFO_EXPIRES_MONTHS = 6;
60 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 #define CHECK_CANCELLED() do \
63 { \
64 if(MUTILS_BOOLIFY(m_cancelled)) \
65 { \
66 m_success.fetchAndStoreOrdered(0); \
67 log("", "Update check has been cancelled by user!"); \
68 setProgress(m_maxProgress); \
69 setStatus(UpdateStatus_CancelledByUser); \
70 return; \
71 } \
72 } \
73 while(0)
75 #define LOG_MESSAGE_HELPER(X) do \
76 { \
77 if (!(X).isNull()) \
78 { \
79 emit messageLogged((X)); \
80 } \
81 } \
82 while(0)
84 #define STRICMP(X,Y) ((X).compare((Y), Qt::CaseInsensitive) == 0)
86 ////////////////////////////////////////////////////////////
87 // Helper Functions
88 ////////////////////////////////////////////////////////////
90 static QStringList buildRandomList(const char *const values[])
92 QStringList list;
93 for (int index = 0; values[index]; index++)
95 const int pos = MUtils::next_rand_u32() % (index + 1);
96 list.insert(pos, QString::fromLatin1(values[index]));
98 return list;
101 static const QHash<QString, QString> *initEnvVars(void)
103 QHash<QString, QString> *const environment = new QHash<QString, QString>();
104 environment->insert(QLatin1String("CURL_HOME"), QDir::toNativeSeparators(MUtils::temp_folder()));
105 return environment;
108 ////////////////////////////////////////////////////////////
109 // Update Info Class
110 ////////////////////////////////////////////////////////////
112 MUtils::UpdateCheckerInfo::UpdateCheckerInfo(void)
114 resetInfo();
117 void MUtils::UpdateCheckerInfo::resetInfo(void)
119 m_buildNo = 0;
120 m_buildDate.setDate(1900, 1, 1);
121 m_downloadSite.clear();
122 m_downloadAddress.clear();
123 m_downloadFilename.clear();
124 m_downloadFilecode.clear();
125 m_downloadChecksum.clear();
128 bool MUtils::UpdateCheckerInfo::isComplete(void)
130 return (this->m_buildNo > 0) &&
131 (this->m_buildDate.year() >= 2010) &&
132 (!this->m_downloadSite.isEmpty()) &&
133 (!this->m_downloadAddress.isEmpty()) &&
134 (!this->m_downloadFilename.isEmpty()) &&
135 (!this->m_downloadFilecode.isEmpty()) &&
136 (!this->m_downloadChecksum.isEmpty());
139 ////////////////////////////////////////////////////////////
140 // Constructor & Destructor
141 ////////////////////////////////////////////////////////////
143 MUtils::UpdateChecker::UpdateChecker(const QString &binCurl, const QString &binGnuPG, const QString &binKeys, const QString &applicationId, const quint32 &installedBuildNo, const bool betaUpdates, const bool testMode)
145 m_updateInfo(new UpdateCheckerInfo()),
146 m_binaryCurl(binCurl),
147 m_binaryGnuPG(binGnuPG),
148 m_binaryKeys(binKeys),
149 m_applicationId(applicationId),
150 m_installedBuildNo(installedBuildNo),
151 m_betaUpdates(betaUpdates),
152 m_testMode(testMode),
153 m_maxProgress(MIN_CONNSCORE + 5),
154 m_environment(initEnvVars())
156 m_status = UpdateStatus_NotStartedYet;
157 m_progress = 0;
159 if(m_binaryCurl.isEmpty() || m_binaryGnuPG.isEmpty() || m_binaryKeys.isEmpty())
161 MUTILS_THROW("Tools not initialized correctly!");
165 MUtils::UpdateChecker::~UpdateChecker(void)
169 ////////////////////////////////////////////////////////////
170 // Public slots
171 ////////////////////////////////////////////////////////////
173 void MUtils::UpdateChecker::start(Priority priority)
175 m_success.fetchAndStoreOrdered(0);
176 m_cancelled.fetchAndStoreOrdered(0);
177 QThread::start(priority);
180 ////////////////////////////////////////////////////////////
181 // Protected functions
182 ////////////////////////////////////////////////////////////
184 void MUtils::UpdateChecker::run(void)
186 qDebug("Update checker thread started!");
187 MUTILS_EXCEPTION_HANDLER(m_testMode ? testMirrorsList() : checkForUpdates());
188 qDebug("Update checker thread completed.");
191 void MUtils::UpdateChecker::checkForUpdates(void)
193 // ----- Initialization ----- //
195 m_updateInfo->resetInfo();
196 setProgress(0);
198 // ----- Test Internet Connection ----- //
200 log("Checking your Internet connection...", "");
201 setStatus(UpdateStatus_CheckingConnection);
203 const int networkStatus = OS::network_status();
204 if(networkStatus == OS::NETWORK_TYPE_NON)
206 if (!MUtils::OS::arguments().contains("ignore-network-status"))
208 log("Operating system reports that the computer is currently offline !!!");
209 setProgress(m_maxProgress);
210 setStatus(UpdateStatus_ErrorNoConnection);
211 return;
215 msleep(333);
216 setProgress(1);
218 // ----- Test Known Hosts Connectivity ----- //
220 int connectionScore = 0;
221 QStringList mirrorList = buildRandomList(known_hosts);
223 for(int connectionTimout = 1000; connectionTimout <= MAX_CONN_TIMEOUT; connectionTimout *= 2)
225 QElapsedTimer elapsedTimer;
226 elapsedTimer.start();
227 const int globalTimout = 2 * MIN_CONNSCORE * connectionTimout;
228 while (!elapsedTimer.hasExpired(globalTimout))
230 const QString hostName = mirrorList.takeFirst();
231 if (tryContactHost(hostName, connectionTimout))
233 setProgress(1 + (connectionScore += 1));
234 elapsedTimer.restart();
235 if (connectionScore >= MIN_CONNSCORE)
237 goto endLoop; /*success*/
240 else
242 mirrorList.append(hostName); /*re-schedule*/
244 CHECK_CANCELLED();
245 msleep(1);
249 endLoop:
250 if(connectionScore < MIN_CONNSCORE)
252 log("", "Connectivity test has failed: Internet connection appears to be broken!");
253 setProgress(m_maxProgress);
254 setStatus(UpdateStatus_ErrorConnectionTestFailed);
255 return;
258 // ----- Fetch Update Info From Server ----- //
260 log("----", "", "Internet connection is operational, checking for updates online...");
261 setStatus(UpdateStatus_FetchingUpdates);
263 int mirrorCount = 0;
264 mirrorList = buildRandomList(update_mirrors);
266 while(!mirrorList.isEmpty())
268 const QString currentMirror = mirrorList.takeFirst();
269 const bool isQuick = (mirrorCount++ < QUICK_MIRRORS);
270 if(tryUpdateMirror(m_updateInfo.data(), currentMirror, isQuick))
272 m_success.ref(); /*success*/
273 break;
275 if (isQuick)
277 mirrorList.append(currentMirror); /*re-schedule*/
279 CHECK_CANCELLED();
280 msleep(1);
283 msleep(333);
284 setProgress(MIN_CONNSCORE + 5);
286 // ----- Generate final result ----- //
288 if(MUTILS_BOOLIFY(m_success))
290 if(m_updateInfo->m_buildNo > m_installedBuildNo)
292 setStatus(UpdateStatus_CompletedUpdateAvailable);
294 else if(m_updateInfo->m_buildNo == m_installedBuildNo)
296 setStatus(UpdateStatus_CompletedNoUpdates);
298 else
300 setStatus(UpdateStatus_CompletedNewVersionOlder);
303 else
305 setStatus(UpdateStatus_ErrorFetchUpdateInfo);
309 void MUtils::UpdateChecker::testMirrorsList(void)
311 QStringList mirrorList;
312 for(int i = 0; update_mirrors[i]; i++)
314 mirrorList << QString::fromLatin1(update_mirrors[i]);
317 // ----- Test update mirrors ----- //
319 qDebug("\n[Mirror Sites]");
320 log("Testing all known mirror sites...", "", "---");
322 UpdateCheckerInfo updateInfo;
323 while (!mirrorList.isEmpty())
325 const QString currentMirror = mirrorList.takeFirst();
326 bool success = false;
327 qDebug("Testing: %s", MUTILS_L1STR(currentMirror));
328 log("", "Testing:", currentMirror, "");
329 for (quint8 attempt = 0; attempt < 3; ++attempt)
331 updateInfo.resetInfo();
332 if (tryUpdateMirror(&updateInfo, currentMirror, (!attempt)))
334 success = true;
335 break;
338 if (!success)
340 qWarning("\nUpdate mirror seems to be unavailable:\n%s\n", MUTILS_L1STR(currentMirror));
342 log("", "---");
345 // ----- Test known hosts ----- //
347 QStringList knownHostList;
348 for (int i = 0; known_hosts[i]; i++)
350 knownHostList << QString::fromLatin1(known_hosts[i]);
353 qDebug("\n[Known Hosts]");
354 log("Testing all known hosts...", "", "---");
356 while(!knownHostList.isEmpty())
358 const QString currentHost = knownHostList.takeFirst();
359 qDebug("Testing: %s", MUTILS_L1STR(currentHost));
360 log(QLatin1String(""), "Testing:", currentHost, "");
361 if (!tryContactHost(currentHost, DOWNLOAD_TIMEOUT))
363 qWarning("\nConnectivity test FAILED on the following host:\n%s\n", MUTILS_L1STR(currentHost));
365 log("---");
369 ////////////////////////////////////////////////////////////
370 // PRIVATE FUNCTIONS
371 ////////////////////////////////////////////////////////////
373 void MUtils::UpdateChecker::setStatus(const int status)
375 if(m_status != status)
377 m_status = status;
378 emit statusChanged(status);
382 void MUtils::UpdateChecker::setProgress(const int progress)
384 const int value = qBound(0, progress, m_maxProgress);
385 if(m_progress != value)
387 emit progressChanged(m_progress = value);
391 void MUtils::UpdateChecker::log(const QString &str1, const QString &str2, const QString &str3, const QString &str4)
393 LOG_MESSAGE_HELPER(str1);
394 LOG_MESSAGE_HELPER(str2);
395 LOG_MESSAGE_HELPER(str3);
396 LOG_MESSAGE_HELPER(str4);
399 bool MUtils::UpdateChecker::tryUpdateMirror(UpdateCheckerInfo *updateInfo, const QString &url, const bool &quick)
401 bool success = false;
402 log("", "Trying update mirror:", url, "");
404 if (quick)
406 setProgress(MIN_CONNSCORE + 1);
407 if (!tryContactHost(QUrl(url).host(), (MAX_CONN_TIMEOUT / 8)))
409 log("", "Mirror is too slow, skipping!");
410 return false;
414 const QString randPart = next_rand_str();
415 const QString outFileVers = QString("%1/%2.ver").arg(temp_folder(), randPart);
416 const QString outFileSign = QString("%1/%2.sig").arg(temp_folder(), randPart);
418 if (!getUpdateInfo(url, outFileVers, outFileSign))
420 log("", "Oops: Download of update information has failed!");
421 goto cleanUp;
424 log("Download completed, verifying signature:", "");
425 setProgress(MIN_CONNSCORE + 4);
426 if (!checkSignature(outFileVers, outFileSign))
428 log("", "Bad signature detected, take care !!!");
429 goto cleanUp;
432 log("", "Signature is valid, parsing update information:", "");
433 success = parseVersionInfo(outFileVers, updateInfo);
435 cleanUp:
436 QFile::remove(outFileVers);
437 QFile::remove(outFileSign);
438 return success;
441 bool MUtils::UpdateChecker::getUpdateInfo(const QString &url, const QString &outFileVers, const QString &outFileSign)
443 log("Downloading update information:", "");
444 setProgress(MIN_CONNSCORE + 2);
445 if(getFile(QUrl(QString("%1%2").arg(url, MIRROR_URL_POSTFIX[m_betaUpdates ? 1 : 0])), outFileVers))
447 if (!m_cancelled)
449 log( "Downloading signature file:", "");
450 setProgress(MIN_CONNSCORE + 3);
451 if (getFile(QUrl(QString("%1%2.sig2").arg(url, MIRROR_URL_POSTFIX[m_betaUpdates ? 1 : 0])), outFileSign))
453 return true; /*completed*/
457 return false;
460 //----------------------------------------------------------
461 // PARSE UPDATE INFO
462 //----------------------------------------------------------
464 bool MUtils::UpdateChecker::parseVersionInfo(const QString &file, UpdateCheckerInfo *const updateInfo)
466 updateInfo->resetInfo();
468 QFile data(file);
469 if(!data.open(QIODevice::ReadOnly))
471 qWarning("Cannot open update info file for reading!");
472 return false;
475 QDate updateInfoDate;
476 int sectionId = 0;
477 QRegExp regex_sec("^\\[(.+)\\]$"), regex_val("^([^=]+)=(.+)$");
479 while(!data.atEnd())
481 QString line = QString::fromLatin1(data.readLine()).trimmed();
482 if (regex_sec.indexIn(line) >= 0)
484 sectionId = parseSectionHeaderStr(regex_sec.cap(1).trimmed());
485 continue;
487 if (regex_val.indexIn(line) >= 0)
489 const QString key = regex_val.cap(1).trimmed();
490 const QString val = regex_val.cap(2).trimmed();
491 switch (sectionId)
493 case 1:
494 parseHeaderValue(key, val, updateInfoDate);
495 break;
496 case 2:
497 parseUpdateInfoValue(key, val, updateInfo);
498 break;
503 if(updateInfoDate.isValid())
505 const QDate expiredDate = updateInfoDate.addMonths(VERSION_INFO_EXPIRES_MONTHS);
506 if (expiredDate < OS::current_date())
508 log(QString("WARNING: Update information has expired at %1!").arg(expiredDate.toString(Qt::ISODate)));
509 goto cleanUp;
512 else
514 log("WARNING: Timestamp is missing from update information header!");
515 goto cleanUp;
518 if(!updateInfo->isComplete())
520 log("WARNING: Update information is incomplete!");
521 goto cleanUp;
524 log("", "Success: Update information is complete.");
525 return true; /*success*/
527 cleanUp:
528 updateInfo->resetInfo();
529 return false;
532 int MUtils::UpdateChecker::parseSectionHeaderStr(const QString &name)
534 log(QString("Sec: [%1]").arg(name));
536 if (STRICMP(name, HEADER_ID))
538 return 1;
540 if (STRICMP(name, m_applicationId))
542 return 2;
545 //Unknonw section encountered!
546 return 0;
549 void MUtils::UpdateChecker::parseHeaderValue(const QString &key, const QString &val, QDate &updateInfoDate)
551 log(QString("Hdr: \"%1\"=\"%2\"").arg(key, val));
553 if (STRICMP(key, "TimestampCreated"))
555 const QDate temp = QDate::fromString(val, Qt::ISODate);
556 if (temp.isValid())
558 updateInfoDate = temp;
560 return;
563 //Unknown entry encountered!
564 qWarning("Unknown header value: %s", MUTILS_L1STR(key));
567 void MUtils::UpdateChecker::parseUpdateInfoValue(const QString &key, const QString &val, UpdateCheckerInfo *const updateInfo)
569 log(QString("Val: \"%1\"=\"%2\"").arg(key, val));
571 if (STRICMP(key, "BuildNo"))
573 bool ok = false;
574 const unsigned int temp = val.toUInt(&ok);
575 if (ok)
577 updateInfo->m_buildNo = temp;
579 return;
581 if (STRICMP(key, "BuildDate"))
583 const QDate temp = QDate::fromString(val, Qt::ISODate);
584 if (temp.isValid())
586 updateInfo->m_buildDate = temp;
588 return;
590 if (STRICMP(key, "DownloadSite"))
592 updateInfo->m_downloadSite = val;
593 return;
595 if (STRICMP(key, "DownloadAddress"))
597 updateInfo->m_downloadAddress = val;
598 return;
600 if (STRICMP(key, "DownloadFilename"))
602 updateInfo->m_downloadFilename = val;
603 return;
605 if (STRICMP(key, "DownloadFilecode"))
607 updateInfo->m_downloadFilecode = val;
608 return;
610 if (STRICMP(key, "DownloadChecksum"))
612 updateInfo->m_downloadChecksum = val;
613 return;
616 //Unknown entry encountered!
617 qWarning("Unknown update value: %s", MUTILS_L1STR(key));
620 //----------------------------------------------------------
621 // EXTERNAL TOOLS
622 //----------------------------------------------------------
624 bool MUtils::UpdateChecker::getFile(const QUrl &url, const QString &outFile, const unsigned int maxRedir)
626 QFileInfo output(outFile);
627 output.setCaching(false);
629 if (output.exists())
631 QFile::remove(output.canonicalFilePath());
632 if (output.exists())
634 qWarning("Existing output file could not be found!");
635 return false;
639 QStringList args(QLatin1String("-vsSNqkfL"));
640 args << "-m" << QString::number(DOWNLOAD_TIMEOUT / 1000);
641 args << "--max-redirs" << QString::number(maxRedir);
642 args << "-A" << USER_AGENT_STR;
643 args << "-e" << QString("%1://%2/;auto").arg(url.scheme(), url.host());
644 args << "-o" << output.fileName() << url.toString();
646 return execCurl(args, output.absolutePath(), DOWNLOAD_TIMEOUT);
649 bool MUtils::UpdateChecker::tryContactHost(const QString &hostname, const int &timeoutMsec)
651 log(QString("Connecting to host: %1").arg(hostname), "");
653 QStringList args(QLatin1String("-vsSNqkI"));
654 args << "-m" << QString::number(qMax(1, timeoutMsec / 1000));
655 args << "-A" << USER_AGENT_STR;
656 args << "-o" << OS::null_device() << QString("http://%1/").arg(hostname);
658 return execCurl(args, temp_folder(), timeoutMsec);
661 bool MUtils::UpdateChecker::checkSignature(const QString &file, const QString &signature)
663 if (QFileInfo(file).absolutePath().compare(QFileInfo(signature).absolutePath(), Qt::CaseInsensitive) != 0)
665 qWarning("CheckSignature: File and signature should be in same folder!");
666 return false;
669 QString keyRingPath(m_binaryKeys);
670 bool removeKeyring = false;
671 if (QFileInfo(file).absolutePath().compare(QFileInfo(m_binaryKeys).absolutePath(), Qt::CaseInsensitive) != 0)
673 keyRingPath = make_temp_file(QFileInfo(file).absolutePath(), "gpg");
674 removeKeyring = true;
675 if (!QFile::copy(m_binaryKeys, keyRingPath))
677 qWarning("CheckSignature: Failed to copy the key-ring file!");
678 return false;
682 QStringList args;
683 args << QStringList() << "--homedir" << ".";
684 args << "--keyring" << QFileInfo(keyRingPath).fileName();
685 args << QFileInfo(signature).fileName();
686 args << QFileInfo(file).fileName();
688 const int exitCode = execProcess(m_binaryGnuPG, args, QFileInfo(file).absolutePath(), DOWNLOAD_TIMEOUT);
689 if (exitCode != INT_MAX)
691 log(QString().sprintf("Exited with code %d", exitCode));
694 if (removeKeyring)
696 remove_file(keyRingPath);
699 return (exitCode == 0); /*completed*/
702 bool MUtils::UpdateChecker::execCurl(const QStringList &args, const QString &workingDir, const int timeout)
704 const int exitCode = execProcess(m_binaryCurl, args, workingDir, timeout + (timeout / 2));
705 if (exitCode != INT_MAX)
707 switch (exitCode)
709 case -1:
710 case 0: log(QLatin1String("DONE: Transfer completed successfully."), ""); break;
711 case 6: log(QLatin1String("ERROR: Remote host could not be resolved!"), ""); break;
712 case 7: log(QLatin1String("ERROR: Connection to remote host could not be established!"), ""); break;
713 case 22: log(QLatin1String("ERROR: Requested URL was not found or returned an error!"), ""); break;
714 case 28: log(QLatin1String("ERROR: Operation timed out !!!"), ""); break;
715 default: log(QString().sprintf("ERROR: Terminated with unknown code %d", exitCode), ""); break;
719 return (exitCode == 0); /*completed*/
722 int MUtils::UpdateChecker::execProcess(const QString &programFile, const QStringList &args, const QString &workingDir, const int timeout)
724 QProcess process;
725 init_process(process, workingDir, true, NULL, m_environment.data());
727 QEventLoop loop;
728 connect(&process, SIGNAL(error(QProcess::ProcessError)), &loop, SLOT(quit()));
729 connect(&process, SIGNAL(finished(int, QProcess::ExitStatus)), &loop, SLOT(quit()));
730 connect(&process, SIGNAL(readyRead()), &loop, SLOT(quit()));
732 QTimer timer;
733 timer.setSingleShot(true);
734 connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
736 process.start(programFile, args);
737 if (!process.waitForStarted())
739 log("PROCESS FAILED TO START !!!", "");
740 qWarning("WARNING: %s process could not be created!", MUTILS_UTF8(QFileInfo(programFile).fileName()));
741 return INT_MAX; /*failed to start*/
744 bool bAborted = false;
745 timer.start(qMax(timeout, 1500));
747 while (process.state() != QProcess::NotRunning)
749 loop.exec();
750 while (process.canReadLine())
752 const QString line = QString::fromLatin1(process.readLine()).simplified();
753 if (line.length() > 1)
755 log(line);
758 const bool bCancelled = MUTILS_BOOLIFY(m_cancelled);
759 if (bAborted = (bCancelled || ((!timer.isActive()) && (!process.waitForFinished(125)))))
761 log(bCancelled ? "CANCELLED BY USER !!!" : "PROCESS TIMEOUT !!!", "");
762 qWarning("WARNING: %s process %s!", MUTILS_UTF8(QFileInfo(programFile).fileName()), bCancelled ? "cancelled" : "timed out");
763 break; /*abort process*/
767 timer.stop();
768 timer.disconnect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
770 if (bAborted)
772 process.kill();
773 process.waitForFinished(-1);
776 while (process.canReadLine())
778 const QString line = QString::fromLatin1(process.readLine()).simplified();
779 if (line.length() > 1)
781 log(line);
785 return bAborted ? INT_MAX : process.exitCode();
788 ////////////////////////////////////////////////////////////
789 // SLOTS
790 ////////////////////////////////////////////////////////////
792 /*NONE*/
794 ////////////////////////////////////////////////////////////
795 // EVENTS
796 ////////////////////////////////////////////////////////////
798 /*NONE*/