20#include "selftestdialog_p.h"
21#include "agentmanager.h"
22#include "dbusconnectionpool.h"
24#include "servermanager.h"
25#include "servermanager_p.h"
27#include <akonadi/private/xdgbasedirs_p.h>
33#include <KLocalizedString>
36#include <KStandardDirs>
39#include <QtCore/QFileInfo>
40#include <QtCore/QProcess>
41#include <QtCore/QSettings>
42#include <QtCore/QTextStream>
43#include <QtDBus/QtDBus>
44#include <QApplication>
46#include <QStandardItemModel>
47#include <QtSql/QSqlDatabase>
48#include <QtSql/QSqlError>
54static QString makeLink(
const QString &file)
56 return QString::fromLatin1(
"<a href=\"%1\">%2</a>").arg(file, file);
60 ResultTypeRole = Qt::UserRole,
68SelfTestDialog::SelfTestDialog(QWidget *parent)
71 setCaption(i18n(
"Akonadi Server Self-Test"));
72 setButtons(Close | User1 | User2);
73 setButtonText(User1, i18n(
"Save Report..."));
74 setButtonIcon(User1, KIcon(QString::fromLatin1(
"document-save")));
75 setButtonText(User2, i18n(
"Copy Report to Clipboard"));
76 setButtonIcon(User2, KIcon(QString::fromLatin1(
"edit-copy")));
77 showButtonSeparator(
true);
78 ui.setupUi(mainWidget());
80 mTestModel =
new QStandardItemModel(
this);
81 ui.testView->setModel(mTestModel);
82 connect(ui.testView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
83 SLOT(selectionChanged(QModelIndex)));
84 connect(ui.detailsLabel, SIGNAL(linkActivated(QString)), SLOT(linkActivated(QString)));
86 connect(
this, SIGNAL(user1Clicked()), SLOT(saveReport()));
87 connect(
this, SIGNAL(user2Clicked()), SLOT(copyReport()));
93void SelfTestDialog::hideIntroduction()
95 ui.introductionLabel->hide();
98QStandardItem *SelfTestDialog::report(ResultType type,
const KLocalizedString &summary,
const KLocalizedString &details)
100 QStandardItem *item =
new QStandardItem(summary.toString());
103 item->setIcon(KIcon(QString::fromLatin1(
"dialog-ok")));
106 item->setIcon(KIcon(QString::fromLatin1(
"dialog-ok-apply")));
109 item->setIcon(KIcon(QString::fromLatin1(
"dialog-warning")));
113 item->setIcon(KIcon(QString::fromLatin1(
"dialog-error")));
115 item->setEditable(
false);
116 item->setWhatsThis(details.toString());
117 item->setData(type, ResultTypeRole);
118 item->setData(summary.toString(0), SummaryRole);
119 item->setData(details.toString(0), DetailsRole);
120 mTestModel->appendRow(item);
124void SelfTestDialog::selectionChanged(
const QModelIndex &index)
126 if (index.isValid()) {
127 ui.detailsLabel->setText(index.data(Qt::WhatsThisRole).toString());
128 ui.detailsGroup->setEnabled(
true);
130 ui.detailsLabel->setText(QString());
131 ui.detailsGroup->setEnabled(
false);
135void SelfTestDialog::runTests()
139 const QString driver = serverSetting(QLatin1String(
"General"),
"Driver", QLatin1String(
"QMYSQL")).toString();
141 if (driver == QLatin1String(
"QPSQL")) {
148 testMySQLServerLog();
149 testMySQLServerConfig();
153 testProtocolVersion();
159QVariant SelfTestDialog::serverSetting(
const QString &group,
const char *key,
const QVariant &def)
const
161 const QString serverConfigFile = XdgBaseDirs::akonadiServerConfigFile(XdgBaseDirs::ReadWrite);
162 QSettings settings(serverConfigFile, QSettings::IniFormat);
163 settings.beginGroup(group);
164 return settings.value(QString::fromLatin1(key), def);
167bool SelfTestDialog::useStandaloneMysqlServer()
const
169 const QString driver = serverSetting(QLatin1String(
"General"),
"Driver", QLatin1String(
"QMYSQL")).toString();
170 if (driver != QLatin1String(
"QMYSQL")) {
173 const bool startServer = serverSetting(driver,
"StartServer",
true).toBool();
180bool SelfTestDialog::runProcess(
const QString &app,
const QStringList &args, QString &result)
const
183 proc.start(app, args);
184 const bool rv = proc.waitForFinished();
186 result += QString::fromLocal8Bit(proc.readAllStandardError());
187 result += QString::fromLocal8Bit(proc.readAllStandardOutput());
191void SelfTestDialog::testSQLDriver()
193 const QString driver = serverSetting(QLatin1String(
"General"),
"Driver", QLatin1String(
"QMYSQL")).toString();
194 const QStringList availableDrivers = QSqlDatabase::drivers();
195 const KLocalizedString detailsOk = ki18n(
"The QtSQL driver '%1' is required by your current Akonadi server configuration and was found on your system.")
197 const KLocalizedString detailsFail = ki18n(
"The QtSQL driver '%1' is required by your current Akonadi server configuration.\n"
198 "The following drivers are installed: %2.\n"
199 "Make sure the required driver is installed.")
201 .subs(availableDrivers.join(QLatin1String(
", ")));
202 QStandardItem *item = 0;
203 if (availableDrivers.contains(driver)) {
204 item = report(Success, ki18n(
"Database driver found."), detailsOk);
206 item = report(Error, ki18n(
"Database driver not found."), detailsFail);
208 item->setData(XdgBaseDirs::akonadiServerConfigFile(XdgBaseDirs::ReadWrite), FileIncludeRole);
211void SelfTestDialog::testMySQLServer()
213 if (!useStandaloneMysqlServer()) {
214 report(Skip, ki18n(
"MySQL server executable not tested."),
215 ki18n(
"The current configuration does not require an internal MySQL server."));
219 const QString driver = serverSetting(QLatin1String(
"General"),
"Driver", QLatin1String(
"QMYSQL")).toString();
220 const QString serverPath = serverSetting(driver,
"ServerPath", QLatin1String(
"")).toString();
222 const KLocalizedString details = ki18n(
"You have currently configured Akonadi to use the MySQL server '%1'.\n"
223 "Make sure you have the MySQL server installed, set the correct path and ensure you have the "
224 "necessary read and execution rights on the server executable. The server executable is typically "
225 "called 'mysqld'; its location varies depending on the distribution.").subs(serverPath);
227 QFileInfo info(serverPath);
228 if (!info.exists()) {
229 report(Error, ki18n(
"MySQL server not found."), details);
230 }
else if (!info.isReadable()) {
231 report(Error, ki18n(
"MySQL server not readable."), details);
232 }
else if (!info.isExecutable()) {
233 report(Error, ki18n(
"MySQL server not executable."), details);
234 }
else if (!serverPath.contains(QLatin1String(
"mysqld"))) {
235 report(Warning, ki18n(
"MySQL found with unexpected name."), details);
237 report(Success, ki18n(
"MySQL server found."), details);
242 if (runProcess(serverPath, QStringList() << QLatin1String(
"--version"), result)) {
243 const KLocalizedString details = ki18n(
"MySQL server found: %1").subs(result);
244 report(Success, ki18n(
"MySQL server is executable."), details);
246 const KLocalizedString details = ki18n(
"Executing the MySQL server '%1' failed with the following error message: '%2'")
247 .subs(serverPath).subs(result);
248 report(Error, ki18n(
"Executing the MySQL server failed."), details);
252void SelfTestDialog::testMySQLServerLog()
254 if (!useStandaloneMysqlServer()) {
255 report(Skip, ki18n(
"MySQL server error log not tested."),
256 ki18n(
"The current configuration does not require an internal MySQL server."));
260 const QString logFileName = XdgBaseDirs::saveDir(
"data", QLatin1String(
"akonadi/db_data"))
261 + QDir::separator() + QString::fromLatin1(
"mysql.err");
262 const QFileInfo logFileInfo(logFileName);
263 if (!logFileInfo.exists() || logFileInfo.size() == 0) {
264 report(Success, ki18n(
"No current MySQL error log found."),
265 ki18n(
"The MySQL server did not report any errors during this startup. The log can be found in '%1'.").subs(logFileName));
268 QFile logFile(logFileName);
269 if (!logFile.open(QFile::ReadOnly | QFile::Text)) {
270 report(Error, ki18n(
"MySQL error log not readable."),
271 ki18n(
"A MySQL server error log file was found but is not readable: %1").subs(makeLink(logFileName)));
274 bool warningsFound =
false;
275 QStandardItem *item = 0;
276 while (!logFile.atEnd()) {
277 const QString line = QString::fromUtf8(logFile.readLine());
278 if (line.contains(QLatin1String(
"error"), Qt::CaseInsensitive)) {
279 item = report(Error, ki18n(
"MySQL server log contains errors."),
280 ki18n(
"The MySQL server error log file '%1' contains errors.").subs(makeLink(logFileName)));
281 item->setData(logFileName, FileIncludeRole);
284 if (!warningsFound && line.contains(QLatin1String(
"warn"), Qt::CaseInsensitive)) {
285 warningsFound =
true;
289 item = report(Warning, ki18n(
"MySQL server log contains warnings."),
290 ki18n(
"The MySQL server log file '%1' contains warnings.").subs(makeLink(logFileName)));
292 item = report(Success, ki18n(
"MySQL server log contains no errors."),
293 ki18n(
"The MySQL server log file '%1' does not contain any errors or warnings.")
294 .subs(makeLink(logFileName)));
296 item->setData(logFileName, FileIncludeRole);
301void SelfTestDialog::testMySQLServerConfig()
303 if (!useStandaloneMysqlServer()) {
304 report(Skip, ki18n(
"MySQL server configuration not tested."),
305 ki18n(
"The current configuration does not require an internal MySQL server."));
309 QStandardItem *item = 0;
310 const QString globalConfig = XdgBaseDirs::findResourceFile(
"config", QLatin1String(
"akonadi/mysql-global.conf"));
311 const QFileInfo globalConfigInfo(globalConfig);
312 if (!globalConfig.isEmpty() && globalConfigInfo.exists() && globalConfigInfo.isReadable()) {
313 item = report(Success, ki18n(
"MySQL server default configuration found."),
314 ki18n(
"The default configuration for the MySQL server was found and is readable at %1.")
315 .subs(makeLink(globalConfig)));
316 item->setData(globalConfig, FileIncludeRole);
318 report(Error, ki18n(
"MySQL server default configuration not found."),
319 ki18n(
"The default configuration for the MySQL server was not found or was not readable. "
320 "Check your Akonadi installation is complete and you have all required access rights."));
323 const QString localConfig = XdgBaseDirs::findResourceFile(
"config", QLatin1String(
"akonadi/mysql-local.conf"));
324 const QFileInfo localConfigInfo(localConfig);
325 if (localConfig.isEmpty() || !localConfigInfo.exists()) {
326 report(Skip, ki18n(
"MySQL server custom configuration not available."),
327 ki18n(
"The custom configuration for the MySQL server was not found but is optional."));
328 }
else if (localConfigInfo.exists() && localConfigInfo.isReadable()) {
329 item = report(Success, ki18n(
"MySQL server custom configuration found."),
330 ki18n(
"The custom configuration for the MySQL server was found and is readable at %1")
331 .subs(makeLink(localConfig)));
332 item->setData(localConfig, FileIncludeRole);
334 report(Error, ki18n(
"MySQL server custom configuration not readable."),
335 ki18n(
"The custom configuration for the MySQL server was found at %1 but is not readable. "
336 "Check your access rights.").subs(makeLink(localConfig)));
339 const QString actualConfig = XdgBaseDirs::saveDir(
"data", QLatin1String(
"akonadi")) + QLatin1String(
"/mysql.conf");
340 const QFileInfo actualConfigInfo(actualConfig);
341 if (actualConfig.isEmpty() || !actualConfigInfo.exists() || !actualConfigInfo.isReadable()) {
342 report(Error, ki18n(
"MySQL server configuration not found or not readable."),
343 ki18n(
"The MySQL server configuration was not found or is not readable."));
345 item = report(Success, ki18n(
"MySQL server configuration is usable."),
346 ki18n(
"The MySQL server configuration was found at %1 and is readable.").subs(makeLink(actualConfig)));
347 item->setData(actualConfig, FileIncludeRole);
351void SelfTestDialog::testPSQLServer()
353 const QString dbname = serverSetting(QLatin1String(
"QPSQL"),
"Name", QLatin1String(
"akonadi")).toString();
354 const QString hostname = serverSetting(QLatin1String(
"QPSQL"),
"Host", QLatin1String(
"localhost")).toString();
355 const QString username = serverSetting(QLatin1String(
"QPSQL"),
"User", QString()).toString();
356 const QString password = serverSetting(QLatin1String(
"QPSQL"),
"Password", QString()).toString();
357 const int port = serverSetting(QLatin1String(
"QPSQL"),
"Port", 5432).toInt();
359 QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String(
"QPSQL"));
360 db.setHostName(hostname);
361 db.setDatabaseName(dbname);
363 if (!username.isEmpty()) {
364 db.setUserName(username);
367 if (!password.isEmpty()) {
368 db.setPassword(password);
374 const KLocalizedString details = ki18n(db.lastError().text().toLatin1());
375 report(Error, ki18n(
"Cannot connect to PostgreSQL server."), details);
377 report(Success, ki18n(
"PostgreSQL server found."),
378 ki18n(
"The PostgreSQL server was found and connection is working."));
383void SelfTestDialog::testAkonadiCtl()
385 const QString path = KStandardDirs::findExe(QLatin1String(
"akonadictl"));
386 if (path.isEmpty()) {
387 report(Error, ki18n(
"akonadictl not found"),
388 ki18n(
"The program 'akonadictl' needs to be accessible in $PATH. "
389 "Make sure you have the Akonadi server installed."));
393 if (runProcess(path, QStringList() << QLatin1String(
"--version"), result)) {
394 report(Success, ki18n(
"akonadictl found and usable"),
395 ki18n(
"The program '%1' to control the Akonadi server was found "
396 "and could be executed successfully.\nResult:\n%2").subs(path).subs(result));
398 report(Error, ki18n(
"akonadictl found but not usable"),
399 ki18n(
"The program '%1' to control the Akonadi server was found "
400 "but could not be executed successfully.\nResult:\n%2\n"
401 "Make sure the Akonadi server is installed correctly.").subs(path).subs(result));
405void SelfTestDialog::testServerStatus()
407 if (DBusConnectionPool::threadConnection().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control))) {
408 report(Success, ki18n(
"Akonadi control process registered at D-Bus."),
409 ki18n(
"The Akonadi control process is registered at D-Bus which typically indicates it is operational."));
411 report(Error, ki18n(
"Akonadi control process not registered at D-Bus."),
412 ki18n(
"The Akonadi control process is not registered at D-Bus which typically means it was not started "
413 "or encountered a fatal error during startup."));
416 if (DBusConnectionPool::threadConnection().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server))) {
417 report(Success, ki18n(
"Akonadi server process registered at D-Bus."),
418 ki18n(
"The Akonadi server process is registered at D-Bus which typically indicates it is operational."));
420 report(Error, ki18n(
"Akonadi server process not registered at D-Bus."),
421 ki18n(
"The Akonadi server process is not registered at D-Bus which typically means it was not started "
422 "or encountered a fatal error during startup."));
426void SelfTestDialog::testProtocolVersion()
428 if (Internal::serverProtocolVersion() < 0) {
429 report(Skip, ki18n(
"Protocol version check not possible."),
430 ki18n(
"Without a connection to the server it is not possible to check if the protocol version meets the requirements."));
433 if (Internal::serverProtocolVersion() < SessionPrivate::minimumProtocolVersion()) {
434 report(Error, ki18n(
"Server protocol version is too old."),
435 ki18n(
"The server protocol version is %1, but at least version %2 is required. "
436 "Install a newer version of the Akonadi server.")
437 .subs(Internal::serverProtocolVersion())
438 .subs(SessionPrivate::minimumProtocolVersion()));
440 report(Success, ki18n(
"Server protocol version is recent enough."),
441 ki18n(
"The server Protocol version is %1, which equal or newer than the required version %2.")
442 .subs(Internal::serverProtocolVersion())
443 .subs(SessionPrivate::minimumProtocolVersion()));
447void SelfTestDialog::testResources()
450 bool resourceFound =
false;
451 foreach (
const AgentType &type, agentTypes) {
452 if (type.capabilities().contains(QLatin1String(
"Resource"))) {
453 resourceFound =
true;
458 const QStringList pathList = XdgBaseDirs::findAllResourceDirs(
"data", QLatin1String(
"akonadi/agents"));
459 QStandardItem *item = 0;
461 item = report(Success, ki18n(
"Resource agents found."), ki18n(
"At least one resource agent has been found."));
463 item = report(Error, ki18n(
"No resource agents found."),
464 ki18n(
"No resource agents have been found, Akonadi is not usable without at least one. "
465 "This usually means that no resource agents are installed or that there is a setup problem. "
466 "The following paths have been searched: '%1'. "
467 "The XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes all paths "
468 "where Akonadi agents are installed.")
469 .subs(pathList.join(QLatin1String(
" ")))
470 .subs(QString::fromLocal8Bit(qgetenv(
"XDG_DATA_DIRS"))));
472 item->setData(pathList, ListDirectoryRole);
473 item->setData(QByteArray(
"XDG_DATA_DIRS"), EnvVarRole);
476void Akonadi::SelfTestDialog::testServerLog()
478 QString serverLog = XdgBaseDirs::saveDir(
"data", QLatin1String(
"akonadi"))
479 + QDir::separator() + QString::fromLatin1(
"akonadiserver.error");
480 QFileInfo info(serverLog);
481 if (!info.exists() || info.size() <= 0) {
482 report(Success, ki18n(
"No current Akonadi server error log found."),
483 ki18n(
"The Akonadi server did not report any errors during its current startup."));
485 QStandardItem *item = report(Error, ki18n(
"Current Akonadi server error log found."),
486 ki18n(
"The Akonadi server reported errors during its current startup. The log can be found in %1.").subs(makeLink(serverLog)));
487 item->setData(serverLog, FileIncludeRole);
490 serverLog += QLatin1String(
".old");
491 info.setFile(serverLog);
492 if (!info.exists() || info.size() <= 0) {
493 report(Success, ki18n(
"No previous Akonadi server error log found."),
494 ki18n(
"The Akonadi server did not report any errors during its previous startup."));
496 QStandardItem *item = report(Error, ki18n(
"Previous Akonadi server error log found."),
497 ki18n(
"The Akonadi server reported errors during its previous startup. The log can be found in %1.").subs(makeLink(serverLog)));
498 item->setData(serverLog, FileIncludeRole);
502void SelfTestDialog::testControlLog()
504 QString controlLog = XdgBaseDirs::saveDir(
"data", QLatin1String(
"akonadi"))
505 + QDir::separator() + QString::fromLatin1(
"akonadi_control.error");
506 QFileInfo info(controlLog);
507 if (!info.exists() || info.size() <= 0) {
508 report(Success, ki18n(
"No current Akonadi control error log found."),
509 ki18n(
"The Akonadi control process did not report any errors during its current startup."));
511 QStandardItem *item = report(Error, ki18n(
"Current Akonadi control error log found."),
512 ki18n(
"The Akonadi control process reported errors during its current startup. The log can be found in %1.").subs(makeLink(controlLog)));
513 item->setData(controlLog, FileIncludeRole);
516 controlLog += QLatin1String(
".old");
517 info.setFile(controlLog);
518 if (!info.exists() || info.size() <= 0) {
519 report(Success, ki18n(
"No previous Akonadi control error log found."),
520 ki18n(
"The Akonadi control process did not report any errors during its previous startup."));
522 QStandardItem *item = report(Error, ki18n(
"Previous Akonadi control error log found."),
523 ki18n(
"The Akonadi control process reported errors during its previous startup. The log can be found in %1.").subs(makeLink(controlLog)));
524 item->setData(controlLog, FileIncludeRole);
528void SelfTestDialog::testRootUser()
531 if (user.isSuperUser()) {
532 report(Error, ki18n(
"Akonadi was started as root"), ki18n(
"Running Internet-facing applications as root/administrator exposes you to many security risks. MySQL, used by this Akonadi installation, will not allow itself to run as root, to protect you from these risks."));
534 report(Success, ki18n(
"Akonadi is not running as root"), ki18n(
"Akonadi is not running as a root/administrator user, which is the recommended setup for a secure system."));
538QString SelfTestDialog::createReport()
541 QTextStream s(&result);
542 s <<
"Akonadi Server Self-Test Report" << endl;
543 s <<
"===============================" << endl;
545 for (
int i = 0; i < mTestModel->rowCount(); ++i) {
546 QStandardItem *item = mTestModel->item(i);
548 s <<
"Test " << (i + 1) <<
": ";
549 switch (item->data(ResultTypeRole).toInt()) {
564 s << endl <<
"--------" << endl;
566 s << item->data(SummaryRole).toString() << endl;
567 s <<
"Details: " << item->data(DetailsRole).toString() << endl;
568 if (item->data(FileIncludeRole).isValid()) {
570 const QString fileName = item->data(FileIncludeRole).toString();
572 if (f.open(QFile::ReadOnly)) {
573 s <<
"File content of '" << fileName <<
"':" << endl;
574 s << f.readAll() << endl;
576 s <<
"File '" << fileName <<
"' could not be opened" << endl;
579 if (item->data(ListDirectoryRole).isValid()) {
581 const QStringList pathList = item->data(ListDirectoryRole).toStringList();
582 if (pathList.isEmpty()) {
583 s <<
"Directory list is empty." << endl;
585 foreach (
const QString &path, pathList) {
586 s <<
"Directory listing of '" << path <<
"':" << endl;
588 dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
589 foreach (
const QString &entry, dir.entryList()) {
594 if (item->data(EnvVarRole).isValid()) {
596 const QByteArray envVarName = item->data(EnvVarRole).toByteArray();
597 const QByteArray envVarValue = qgetenv(envVarName);
598 s <<
"Environment variable " << envVarName <<
" is set to '" << envVarValue <<
"'" << endl;
607void SelfTestDialog::saveReport()
609 const QString defaultFileName = QLatin1String(
"akonadi-selftest-report-")
610 + QDate::currentDate().toString(QLatin1String(
"yyyyMMdd"))
611 + QLatin1String(
".txt");
612 const QString fileName = KFileDialog::getSaveFileName(QUrl(defaultFileName), QString(),
this,
613 i18n(
"Save Test Report"), KFileDialog::ConfirmOverwrite);
614 if (fileName.isEmpty()) {
618 QFile file(fileName);
619 if (!file.open(QFile::ReadWrite)) {
620 KMessageBox::error(
this, i18n(
"Could not open file '%1'", fileName));
624 file.write(createReport().toUtf8());
628void SelfTestDialog::copyReport()
630#ifndef QT_NO_CLIPBOARD
631 QApplication::clipboard()->setText(createReport());
635void SelfTestDialog::linkActivated(
const QString &link)
637 KRun::runUrl(KUrl::fromPath(link), QLatin1String(
"text/plain"),
this);
642#include "moc_selftestdialog_p.cpp"
A representation of an agent type.
QList< AgentType > List
Describes a list of agent types.
State
Enum for the various states the server can be in.
FreeBusyManager::Singleton.