1 | /*************************************************************************** |
2 | * Copyright (C) 2005-2014 by the Quassel Project * |
3 | * devel@quassel-irc.org * |
4 | * * |
5 | * This program is free software; you can redistribute it and/or modify * |
6 | * it under the terms of the GNU General Public License as published by * |
7 | * the Free Software Foundation; either version 2 of the License, or * |
8 | * (at your option) version 3. * |
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 * |
17 | * Free Software Foundation, Inc., * |
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * |
19 | ***************************************************************************/ |
20 | |
21 | #include "quassel.h" |
22 | |
23 | #include <iostream> |
24 | #include <signal.h> |
25 | #if !defined Q_OS_WIN && !defined Q_OS_MAC |
26 | # include <sys/types.h> |
27 | # include <sys/time.h> |
28 | # include <sys/resource.h> |
29 | #endif |
30 | |
31 | #include <QCoreApplication> |
32 | #include <QDateTime> |
33 | #include <QFileInfo> |
34 | #include <QHostAddress> |
35 | #include <QLibraryInfo> |
36 | #include <QSettings> |
37 | #include <QTranslator> |
38 | #include <QUuid> |
39 | |
40 | #include "bufferinfo.h" |
41 | #include "identity.h" |
42 | #include "logger.h" |
43 | #include "message.h" |
44 | #include "network.h" |
45 | #include "protocol.h" |
46 | #include "syncableobject.h" |
47 | #include "types.h" |
48 | |
49 | #include "../../version.h" |
50 | |
51 | Quassel::BuildInfo Quassel::_buildInfo; |
52 | AbstractCliParser *Quassel::_cliParser = 0; |
53 | Quassel::RunMode Quassel::_runMode; |
54 | QString Quassel::_configDirPath; |
55 | QString Quassel::_translationDirPath; |
56 | QStringList Quassel::_dataDirPaths; |
57 | bool Quassel::_initialized = false; |
58 | bool Quassel::DEBUG = false; |
59 | QString Quassel::_coreDumpFileName; |
60 | Quassel *Quassel::_instance = 0; |
61 | bool Quassel::_handleCrashes = true; |
62 | Quassel::LogLevel Quassel::_logLevel = InfoLevel; |
63 | QFile *Quassel::_logFile = 0; |
64 | bool Quassel::_logToSyslog = false; |
65 | |
66 | Quassel::Quassel() |
67 | { |
68 | Q_ASSERT(!_instance); |
69 | _instance = this; |
70 | |
71 | // We catch SIGTERM and SIGINT (caused by Ctrl+C) to graceful shutdown Quassel. |
72 | signal(SIGTERM, handleSignal); |
73 | signal(SIGINT, handleSignal); |
74 | } |
75 | |
76 | |
77 | Quassel::~Quassel() |
78 | { |
79 | if (logFile()) { |
80 | logFile()->close(); |
81 | logFile()->deleteLater(); |
82 | } |
83 | delete _cliParser; |
84 | } |
85 | |
86 | |
87 | bool Quassel::init() |
88 | { |
89 | if (_initialized) |
90 | return true; // allow multiple invocations because of MonolithicApplication |
91 | |
92 | if (_handleCrashes) { |
93 | // we have crashhandler for win32 and unix (based on execinfo). |
94 | #if defined(Q_OS_WIN) || defined(HAVE_EXECINFO) |
95 | # ifndef Q_OS_WIN |
96 | // we only handle crashes ourselves if coredumps are disabled |
97 | struct rlimit *limit = (rlimit *)malloc(sizeof(struct rlimit)); |
98 | int rc = getrlimit(RLIMIT_CORE, limit); |
99 | |
100 | if (rc == -1 || !((long)limit->rlim_cur > 0 || limit->rlim_cur == RLIM_INFINITY)) { |
101 | # endif /* Q_OS_WIN */ |
102 | signal(SIGABRT, handleSignal); |
103 | signal(SIGSEGV, handleSignal); |
104 | # ifndef Q_OS_WIN |
105 | signal(SIGBUS, handleSignal); |
106 | } |
107 | free(limit); |
108 | # endif /* Q_OS_WIN */ |
109 | #endif /* Q_OS_WIN || HAVE_EXECINFO */ |
110 | } |
111 | |
112 | _initialized = true; |
113 | qsrand(QTime(0, 0, 0).secsTo(QTime::currentTime())); |
114 | |
115 | registerMetaTypes(); |
116 | |
117 | Network::setDefaultCodecForServer("ISO-8859-1" ); |
118 | Network::setDefaultCodecForEncoding("UTF-8" ); |
119 | Network::setDefaultCodecForDecoding("ISO-8859-15" ); |
120 | |
121 | if (isOptionSet("help" )) { |
122 | cliParser()->usage(); |
123 | return false; |
124 | } |
125 | |
126 | if (isOptionSet("version" )) { |
127 | std::cout << qPrintable("Quassel IRC: " + Quassel::buildInfo().plainVersionString) << std::endl; |
128 | return false; |
129 | } |
130 | |
131 | DEBUG = isOptionSet("debug" ); |
132 | |
133 | // set up logging |
134 | if (Quassel::runMode() != Quassel::ClientOnly) { |
135 | if (isOptionSet("loglevel" )) { |
136 | QString level = optionValue("loglevel" ); |
137 | |
138 | if (level == "Debug" ) _logLevel = DebugLevel; |
139 | else if (level == "Info" ) _logLevel = InfoLevel; |
140 | else if (level == "Warning" ) _logLevel = WarningLevel; |
141 | else if (level == "Error" ) _logLevel = ErrorLevel; |
142 | } |
143 | |
144 | QString logfilename = optionValue("logfile" ); |
145 | if (!logfilename.isEmpty()) { |
146 | _logFile = new QFile(logfilename); |
147 | if (!_logFile->open(QIODevice::Append | QIODevice::Text)) { |
148 | qWarning() << "Could not open log file" << logfilename << ":" << _logFile->errorString(); |
149 | _logFile->deleteLater(); |
150 | _logFile = 0; |
151 | } |
152 | } |
153 | #ifdef HAVE_SYSLOG |
154 | _logToSyslog = isOptionSet("syslog" ); |
155 | #endif |
156 | } |
157 | |
158 | return true; |
159 | } |
160 | |
161 | |
162 | void Quassel::quit() |
163 | { |
164 | QCoreApplication::quit(); |
165 | } |
166 | |
167 | |
168 | //! Register our custom types with Qt's Meta Object System. |
169 | /** This makes them available for QVariant and in signals/slots, among other things. |
170 | * |
171 | */ |
172 | void Quassel::registerMetaTypes() |
173 | { |
174 | // Complex types |
175 | qRegisterMetaType<Message>("Message" ); |
176 | qRegisterMetaType<BufferInfo>("BufferInfo" ); |
177 | qRegisterMetaType<NetworkInfo>("NetworkInfo" ); |
178 | qRegisterMetaType<Network::Server>("Network::Server" ); |
179 | qRegisterMetaType<Identity>("Identity" ); |
180 | |
181 | qRegisterMetaTypeStreamOperators<Message>("Message" ); |
182 | qRegisterMetaTypeStreamOperators<BufferInfo>("BufferInfo" ); |
183 | qRegisterMetaTypeStreamOperators<NetworkInfo>("NetworkInfo" ); |
184 | qRegisterMetaTypeStreamOperators<Network::Server>("Network::Server" ); |
185 | qRegisterMetaTypeStreamOperators<Identity>("Identity" ); |
186 | |
187 | qRegisterMetaType<IdentityId>("IdentityId" ); |
188 | qRegisterMetaType<BufferId>("BufferId" ); |
189 | qRegisterMetaType<NetworkId>("NetworkId" ); |
190 | qRegisterMetaType<UserId>("UserId" ); |
191 | qRegisterMetaType<AccountId>("AccountId" ); |
192 | qRegisterMetaType<MsgId>("MsgId" ); |
193 | |
194 | qRegisterMetaType<QHostAddress>("QHostAddress" ); |
195 | qRegisterMetaType<QUuid>("QUuid" ); |
196 | |
197 | qRegisterMetaTypeStreamOperators<IdentityId>("IdentityId" ); |
198 | qRegisterMetaTypeStreamOperators<BufferId>("BufferId" ); |
199 | qRegisterMetaTypeStreamOperators<NetworkId>("NetworkId" ); |
200 | qRegisterMetaTypeStreamOperators<UserId>("UserId" ); |
201 | qRegisterMetaTypeStreamOperators<AccountId>("AccountId" ); |
202 | qRegisterMetaTypeStreamOperators<MsgId>("MsgId" ); |
203 | |
204 | qRegisterMetaType<Protocol::SessionState>("Protocol::SessionState" ); |
205 | |
206 | // Versions of Qt prior to 4.7 didn't define QVariant as a meta type |
207 | if (!QMetaType::type("QVariant" )) { |
208 | qRegisterMetaType<QVariant>("QVariant" ); |
209 | qRegisterMetaTypeStreamOperators<QVariant>("QVariant" ); |
210 | } |
211 | } |
212 | |
213 | |
214 | void Quassel::setupBuildInfo() |
215 | { |
216 | _buildInfo.applicationName = "Quassel IRC" ; |
217 | _buildInfo.coreApplicationName = "quasselcore" ; |
218 | _buildInfo.clientApplicationName = "quasselclient" ; |
219 | _buildInfo.organizationName = "Quassel Project" ; |
220 | _buildInfo.organizationDomain = "quassel-irc.org" ; |
221 | |
222 | _buildInfo.protocolVersion = 10; // FIXME: deprecated, will be removed |
223 | |
224 | _buildInfo.baseVersion = QUASSEL_VERSION_STRING; |
225 | _buildInfo.generatedVersion = GIT_DESCRIBE; |
226 | |
227 | // This will be imprecise for incremental builds not touching this file, but we really don't want to always recompile |
228 | _buildInfo.buildDate = QString("%1 %2" ).arg(__DATE__, __TIME__); |
229 | |
230 | // Check if we got a commit hash |
231 | if (!QString(GIT_HEAD).isEmpty()) |
232 | _buildInfo.commitHash = GIT_HEAD; |
233 | else if (!QString(DIST_HASH).contains("Format" )) { |
234 | _buildInfo.commitHash = DIST_HASH; |
235 | _buildInfo.commitDate = QString(DIST_DATE).toUInt(); |
236 | } |
237 | |
238 | // create a nice version string |
239 | if (_buildInfo.generatedVersion.isEmpty()) { |
240 | if (!_buildInfo.commitHash.isEmpty()) { |
241 | // dist version |
242 | _buildInfo.plainVersionString = QString("v%1 (dist-%2)" ) |
243 | .arg(_buildInfo.baseVersion) |
244 | .arg(_buildInfo.commitHash.left(7)); |
245 | _buildInfo.fancyVersionString = QString("v%1 (dist-<a href=\"http://git.quassel-irc.org/?p=quassel.git;a=commit;h=%3\">%2</a>)" ) |
246 | .arg(_buildInfo.baseVersion) |
247 | .arg(_buildInfo.commitHash.left(7)) |
248 | .arg(_buildInfo.commitHash); |
249 | } |
250 | else { |
251 | // we only have a base version :( |
252 | _buildInfo.plainVersionString = QString("v%1 (unknown revision)" ).arg(_buildInfo.baseVersion); |
253 | } |
254 | } |
255 | else { |
256 | // analyze what we got from git-describe |
257 | QRegExp rx("(.*)-(\\d+)-g([0-9a-f]+)(-dirty)?$" ); |
258 | if (rx.exactMatch(_buildInfo.generatedVersion)) { |
259 | QString distance = rx.cap(2) == "0" ? QString() : QString("%1+%2 " ).arg(rx.cap(1), rx.cap(2)); |
260 | _buildInfo.plainVersionString = QString("v%1 (%2git-%3%4)" ) |
261 | .arg(_buildInfo.baseVersion, distance, rx.cap(3), rx.cap(4)); |
262 | if (!_buildInfo.commitHash.isEmpty()) { |
263 | _buildInfo.fancyVersionString = QString("v%1 (%2git-<a href=\"http://git.quassel-irc.org/?p=quassel.git;a=commit;h=%5\">%3</a>%4)" ) |
264 | .arg(_buildInfo.baseVersion, distance, rx.cap(3), rx.cap(4), _buildInfo.commitHash); |
265 | } |
266 | } |
267 | else { |
268 | _buildInfo.plainVersionString = QString("v%1 (invalid revision)" ).arg(_buildInfo.baseVersion); |
269 | } |
270 | } |
271 | if (_buildInfo.fancyVersionString.isEmpty()) |
272 | _buildInfo.fancyVersionString = _buildInfo.plainVersionString; |
273 | } |
274 | |
275 | |
276 | //! Signal handler for graceful shutdown. |
277 | void Quassel::handleSignal(int sig) |
278 | { |
279 | switch (sig) { |
280 | case SIGTERM: |
281 | case SIGINT: |
282 | qWarning("%s" , qPrintable(QString("Caught signal %1 - exiting." ).arg(sig))); |
283 | if (_instance) |
284 | _instance->quit(); |
285 | else |
286 | QCoreApplication::quit(); |
287 | break; |
288 | case SIGABRT: |
289 | case SIGSEGV: |
290 | #ifndef Q_OS_WIN |
291 | case SIGBUS: |
292 | #endif |
293 | logBacktrace(coreDumpFileName()); |
294 | exit(EXIT_FAILURE); |
295 | break; |
296 | default: |
297 | break; |
298 | } |
299 | } |
300 | |
301 | |
302 | void Quassel::logFatalMessage(const char *msg) |
303 | { |
304 | #ifdef Q_OS_MAC |
305 | Q_UNUSED(msg) |
306 | #else |
307 | QFile dumpFile(coreDumpFileName()); |
308 | dumpFile.open(QIODevice::Append); |
309 | QTextStream dumpStream(&dumpFile); |
310 | |
311 | dumpStream << "Fatal: " << msg << '\n'; |
312 | dumpStream.flush(); |
313 | dumpFile.close(); |
314 | #endif |
315 | } |
316 | |
317 | |
318 | Quassel::Features Quassel::features() |
319 | { |
320 | Features feats = 0; |
321 | for (int i = 1; i <= NumFeatures; i <<= 1) |
322 | feats |= (Feature)i; |
323 | |
324 | return feats; |
325 | } |
326 | |
327 | |
328 | const QString &Quassel::coreDumpFileName() |
329 | { |
330 | if (_coreDumpFileName.isEmpty()) { |
331 | QDir configDir(configDirPath()); |
332 | _coreDumpFileName = configDir.absoluteFilePath(QString("Quassel-Crash-%1.log" ).arg(QDateTime::currentDateTime().toString("yyyyMMdd-hhmm" ))); |
333 | QFile dumpFile(_coreDumpFileName); |
334 | dumpFile.open(QIODevice::Append); |
335 | QTextStream dumpStream(&dumpFile); |
336 | dumpStream << "Quassel IRC: " << _buildInfo.baseVersion << ' ' << _buildInfo.commitHash << '\n'; |
337 | qDebug() << "Quassel IRC: " << _buildInfo.baseVersion << ' ' << _buildInfo.commitHash; |
338 | dumpStream.flush(); |
339 | dumpFile.close(); |
340 | } |
341 | return _coreDumpFileName; |
342 | } |
343 | |
344 | |
345 | QString Quassel::configDirPath() |
346 | { |
347 | if (!_configDirPath.isEmpty()) |
348 | return _configDirPath; |
349 | |
350 | if (Quassel::isOptionSet("datadir" )) { |
351 | qWarning() << "Obsolete option --datadir used!" ; |
352 | _configDirPath = Quassel::optionValue("datadir" ); |
353 | } |
354 | else if (Quassel::isOptionSet("configdir" )) { |
355 | _configDirPath = Quassel::optionValue("configdir" ); |
356 | } |
357 | else { |
358 | #ifdef Q_OS_MAC |
359 | // On Mac, the path is always the same |
360 | _configDirPath = QDir::homePath() + "/Library/Application Support/Quassel/" ; |
361 | #else |
362 | // We abuse QSettings to find us a sensible path on the other platforms |
363 | # ifdef Q_OS_WIN |
364 | // don't use the registry |
365 | QSettings::Format format = QSettings::IniFormat; |
366 | # else |
367 | QSettings::Format format = QSettings::NativeFormat; |
368 | # endif |
369 | QSettings s(format, QSettings::UserScope, QCoreApplication::organizationDomain(), buildInfo().applicationName); |
370 | QFileInfo fileInfo(s.fileName()); |
371 | _configDirPath = fileInfo.dir().absolutePath(); |
372 | #endif /* Q_OS_MAC */ |
373 | } |
374 | |
375 | if (!_configDirPath.endsWith(QDir::separator()) && !_configDirPath.endsWith('/')) |
376 | _configDirPath += QDir::separator(); |
377 | |
378 | QDir qDir(_configDirPath); |
379 | if (!qDir.exists(_configDirPath)) { |
380 | if (!qDir.mkpath(_configDirPath)) { |
381 | qCritical() << "Unable to create Quassel config directory:" << qPrintable(qDir.absolutePath()); |
382 | return QString(); |
383 | } |
384 | } |
385 | |
386 | return _configDirPath; |
387 | } |
388 | |
389 | |
390 | QStringList Quassel::dataDirPaths() |
391 | { |
392 | return _dataDirPaths; |
393 | } |
394 | |
395 | |
396 | QStringList Quassel::findDataDirPaths() const |
397 | { |
398 | QStringList dataDirNames = QString(qgetenv("XDG_DATA_DIRS" )).split(':', QString::SkipEmptyParts); |
399 | |
400 | if (!dataDirNames.isEmpty()) { |
401 | for (int i = 0; i < dataDirNames.count(); i++) |
402 | dataDirNames[i].append("/apps/quassel/" ); |
403 | } |
404 | else { |
405 | // Provide a fallback |
406 | #ifdef Q_OS_WIN |
407 | dataDirNames << qgetenv("APPDATA" ) + QCoreApplication::organizationDomain() + "/share/apps/quassel/" |
408 | << qgetenv("APPDATA" ) + QCoreApplication::organizationDomain() |
409 | << QCoreApplication::applicationDirPath(); |
410 | } |
411 | #elif defined Q_OS_MAC |
412 | dataDirNames << QDir::homePath() + "/Library/Application Support/Quassel/" |
413 | << QCoreApplication::applicationDirPath(); |
414 | } |
415 | #else |
416 | dataDirNames.append("/usr/share/apps/quassel/" ); |
417 | } |
418 | // on UNIX, we always check our install prefix |
419 | QString appDir = QCoreApplication::applicationDirPath(); |
420 | int binpos = appDir.lastIndexOf("/bin" ); |
421 | if (binpos >= 0) { |
422 | appDir.replace(binpos, 4, "/share" ); |
423 | appDir.append("/apps/quassel/" ); |
424 | if (!dataDirNames.contains(appDir)) |
425 | dataDirNames.append(appDir); |
426 | } |
427 | #endif |
428 | |
429 | // add resource path and workdir just in case |
430 | dataDirNames << QCoreApplication::applicationDirPath() + "/data/" |
431 | << ":/data/" ; |
432 | |
433 | // append trailing '/' and check for existence |
434 | QStringList::Iterator iter = dataDirNames.begin(); |
435 | while (iter != dataDirNames.end()) { |
436 | if (!iter->endsWith(QDir::separator()) && !iter->endsWith('/')) |
437 | iter->append(QDir::separator()); |
438 | if (!QFile::exists(*iter)) |
439 | iter = dataDirNames.erase(iter); |
440 | else |
441 | ++iter; |
442 | } |
443 | |
444 | return dataDirNames; |
445 | } |
446 | |
447 | |
448 | QString Quassel::findDataFilePath(const QString &fileName) |
449 | { |
450 | QStringList dataDirs = dataDirPaths(); |
451 | foreach(QString dataDir, dataDirs) { |
452 | QString path = dataDir + fileName; |
453 | if (QFile::exists(path)) |
454 | return path; |
455 | } |
456 | return QString(); |
457 | } |
458 | |
459 | |
460 | QStringList Quassel::scriptDirPaths() |
461 | { |
462 | QStringList res(configDirPath() + "scripts/" ); |
463 | foreach(QString path, dataDirPaths()) |
464 | res << path + "scripts/" ; |
465 | return res; |
466 | } |
467 | |
468 | |
469 | QString Quassel::translationDirPath() |
470 | { |
471 | if (_translationDirPath.isEmpty()) { |
472 | // We support only one translation dir; fallback mechanisms wouldn't work else. |
473 | // This means that if we have a $data/translations dir, the internal :/i18n resource won't be considered. |
474 | foreach(const QString &dir, dataDirPaths()) { |
475 | if (QFile::exists(dir + "translations/" )) { |
476 | _translationDirPath = dir + "translations/" ; |
477 | break; |
478 | } |
479 | } |
480 | if (_translationDirPath.isEmpty()) |
481 | _translationDirPath = ":/i18n/" ; |
482 | } |
483 | return _translationDirPath; |
484 | } |
485 | |
486 | |
487 | void Quassel::loadTranslation(const QLocale &locale) |
488 | { |
489 | QTranslator *qtTranslator = QCoreApplication::instance()->findChild<QTranslator *>("QtTr" ); |
490 | QTranslator *quasselTranslator = QCoreApplication::instance()->findChild<QTranslator *>("QuasselTr" ); |
491 | |
492 | if (qtTranslator) |
493 | qApp->removeTranslator(qtTranslator); |
494 | if (quasselTranslator) |
495 | qApp->removeTranslator(quasselTranslator); |
496 | |
497 | // We use QLocale::C to indicate that we don't want a translation |
498 | if (locale.language() == QLocale::C) |
499 | return; |
500 | |
501 | qtTranslator = new QTranslator(qApp); |
502 | qtTranslator->setObjectName("QtTr" ); |
503 | qApp->installTranslator(qtTranslator); |
504 | |
505 | quasselTranslator = new QTranslator(qApp); |
506 | quasselTranslator->setObjectName("QuasselTr" ); |
507 | qApp->installTranslator(quasselTranslator); |
508 | |
509 | #if QT_VERSION >= 0x040800 && !defined Q_OS_MAC |
510 | bool success = qtTranslator->load(locale, QString("qt_" ), translationDirPath()); |
511 | if (!success) |
512 | qtTranslator->load(locale, QString("qt_" ), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); |
513 | quasselTranslator->load(locale, QString("" ), translationDirPath()); |
514 | #else |
515 | bool success = qtTranslator->load(QString("qt_%1" ).arg(locale.name()), translationDirPath()); |
516 | if (!success) |
517 | qtTranslator->load(QString("qt_%1" ).arg(locale.name()), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); |
518 | quasselTranslator->load(QString("%1" ).arg(locale.name()), translationDirPath()); |
519 | #endif |
520 | } |
521 | |