diff --git a/src/server/storage/dbconfigmysql.cpp b/src/server/storage/dbconfigmysql.cpp index cac40f5ca..846bee3d7 100644 --- a/src/server/storage/dbconfigmysql.cpp +++ b/src/server/storage/dbconfigmysql.cpp @@ -1,635 +1,635 @@ /* Copyright (c) 2010 Tobias Koenig This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dbconfigmysql.h" #include "utils.h" #include "akonadiserver_debug.h" #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace Akonadi::Server; #define MYSQL_MIN_MAJOR 5 #define MYSQL_MIN_MINOR 1 #define MYSQL_VERSION_CHECK(major, minor, patch) ((major << 16) | (minor << 8) | patch) static const QString s_mysqlSocketFileName = QStringLiteral("mysql.socket"); DbConfigMysql::DbConfigMysql() : mInternalServer(true) , mDatabaseProcess(nullptr) { } QString DbConfigMysql::driverName() const { return QStringLiteral("QMYSQL"); } QString DbConfigMysql::databaseName() const { return mDatabaseName; } static QString findExecutable(const QString &bin) { static const QStringList mysqldSearchPath = { QStringLiteral("/usr/bin"), QStringLiteral("/usr/sbin"), QStringLiteral("/usr/local/sbin"), QStringLiteral("/usr/local/libexec"), QStringLiteral("/usr/libexec"), QStringLiteral("/opt/mysql/libexec"), QStringLiteral("/opt/local/lib/mysql5/bin"), QStringLiteral("/opt/mysql/sbin"), }; QString path = QStandardPaths::findExecutable(bin); if (path.isEmpty()) { // No results in PATH; fall back to hardcoded list. path = QStandardPaths::findExecutable(bin, mysqldSearchPath); } return path; } bool DbConfigMysql::init(QSettings &settings) { // determine default settings depending on the driver QString defaultHostName; QString defaultOptions; QString defaultServerPath; QString defaultCleanShutdownCommand; #ifndef Q_OS_WIN const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length()); #endif const bool defaultInternalServer = true; #ifdef MYSQLD_EXECUTABLE if (QFile::exists(QStringLiteral(MYSQLD_EXECUTABLE))) { defaultServerPath = QStringLiteral(MYSQLD_EXECUTABLE); } #endif if (defaultServerPath.isEmpty()) { defaultServerPath = findExecutable(QStringLiteral("mysqld")); } const QString mysqladminPath = findExecutable(QStringLiteral("mysqladmin")); if (!mysqladminPath.isEmpty()) { #ifndef Q_OS_WIN defaultCleanShutdownCommand = QStringLiteral("%1 --defaults-file=%2/mysql.conf --socket=%3/%4 shutdown") .arg(mysqladminPath, StandardDirs::saveDir("data"), socketDirectory, s_mysqlSocketFileName); #else defaultCleanShutdownCommand = QString::fromLatin1("%1 shutdown --shared-memory").arg(mysqladminPath); #endif } mMysqlInstallDbPath = findExecutable(QStringLiteral("mysql_install_db")); qCDebug(AKONADISERVER_LOG) << "Found mysql_install_db: " << mMysqlInstallDbPath; mMysqlCheckPath = findExecutable(QStringLiteral("mysqlcheck")); qCDebug(AKONADISERVER_LOG) << "Found mysqlcheck: " << mMysqlCheckPath; mInternalServer = settings.value(QStringLiteral("QMYSQL/StartServer"), defaultInternalServer).toBool(); #ifndef Q_OS_WIN if (mInternalServer) { defaultOptions = QStringLiteral("UNIX_SOCKET=%1/%2").arg(socketDirectory, s_mysqlSocketFileName); } #endif // read settings for current driver settings.beginGroup(driverName()); mDatabaseName = settings.value(QStringLiteral("Name"), defaultDatabaseName()).toString(); mHostName = settings.value(QStringLiteral("Host"), defaultHostName).toString(); mUserName = settings.value(QStringLiteral("User")).toString(); mPassword = settings.value(QStringLiteral("Password")).toString(); mConnectionOptions = settings.value(QStringLiteral("Options"), defaultOptions).toString(); mMysqldPath = settings.value(QStringLiteral("ServerPath"), defaultServerPath).toString(); mCleanServerShutdownCommand = settings.value(QStringLiteral("CleanServerShutdownCommand"), defaultCleanShutdownCommand).toString(); settings.endGroup(); // verify settings and apply permanent changes (written out below) if (mInternalServer) { mConnectionOptions = defaultOptions; // intentionally not namespaced as we are the only one in this db instance when using internal mode mDatabaseName = QStringLiteral("akonadi"); } if (mInternalServer && (mMysqldPath.isEmpty() || !QFile::exists(mMysqldPath))) { mMysqldPath = defaultServerPath; } qCDebug(AKONADISERVER_LOG) << "Using mysqld:" << mMysqldPath; // store back the default values settings.beginGroup(driverName()); settings.setValue(QStringLiteral("Name"), mDatabaseName); settings.setValue(QStringLiteral("Host"), mHostName); settings.setValue(QStringLiteral("Options"), mConnectionOptions); if (!mMysqldPath.isEmpty()) { settings.setValue(QStringLiteral("ServerPath"), mMysqldPath); } settings.setValue(QStringLiteral("StartServer"), mInternalServer); settings.endGroup(); settings.sync(); // apply temporary changes to the settings if (mInternalServer) { mHostName.clear(); mUserName.clear(); mPassword.clear(); } return true; } void DbConfigMysql::apply(QSqlDatabase &database) { if (!mDatabaseName.isEmpty()) { database.setDatabaseName(mDatabaseName); } if (!mHostName.isEmpty()) { database.setHostName(mHostName); } if (!mUserName.isEmpty()) { database.setUserName(mUserName); } if (!mPassword.isEmpty()) { database.setPassword(mPassword); } database.setConnectOptions(mConnectionOptions); // can we check that during init() already? Q_ASSERT(database.driver()->hasFeature(QSqlDriver::LastInsertId)); } bool DbConfigMysql::useInternalServer() const { return mInternalServer; } bool DbConfigMysql::startInternalServer() { bool success = true; const QString akDir = StandardDirs::saveDir("data"); const QString dataDir = StandardDirs::saveDir("data", QStringLiteral("db_data")); #ifndef Q_OS_WIN const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length()); const QString socketFile = QStringLiteral("%1/%2").arg(socketDirectory, s_mysqlSocketFileName); const QString pidFileName = QStringLiteral("%1/mysql.pid").arg(socketDirectory); #endif // generate config file const QString globalConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf")); const QString localConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-local.conf")); const QString actualConfig = StandardDirs::saveDir("data") + QLatin1String("/mysql.conf"); if (globalConfig.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "Did not find MySQL server default configuration (mysql-global.conf)"; return false; } #ifdef Q_OS_LINUX // It is recommended to disable CoW feature when running on Btrfs to improve // database performance. Disabling CoW only has effect on empty directory (since // it affects only new files), so we check whether MySQL has not yet been initialized. QDir dir(dataDir + QDir::separator() + QLatin1String("mysql")); if (!dir.exists()) { if (Utils::getDirectoryFileSystem(dataDir) == QLatin1String("btrfs")) { Utils::disableCoW(dataDir); } } #endif if (mMysqldPath.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "mysqld not found. Please verify your installation"; return false; } // Get the version of the mysqld server that we'll be using. // MySQL (but not MariaDB) deprecates and removes command line options in // patch version releases, so we need to adjust the command line options accordingly // when running the helper utilities or starting the server const unsigned int localVersion = parseCommandLineToolsVersion(); if (localVersion == 0x000000) { qCCritical(AKONADISERVER_LOG) << "Failed to detect mysqld version!"; } // TODO: Parse "MariaDB" or "MySQL" from the version string instead of relying // on the version numbers const bool isMariaDB = localVersion >= MYSQL_VERSION_CHECK(10, 0, 0); qCDebug(AKONADISERVER_LOG).nospace() << "mysqld reports version " << (localVersion >> 16) << "." << ((localVersion >> 8) & 0x0000FF) << "." << (localVersion & 0x0000FF) << " (" << (isMariaDB ? "MariaDB" : "Oracle MySQL") << ")"; bool confUpdate = false; QFile actualFile(actualConfig); // update conf only if either global (or local) is newer than actual if ((QFileInfo(globalConfig).lastModified() > QFileInfo(actualFile).lastModified()) || (QFileInfo(localConfig).lastModified() > QFileInfo(actualFile).lastModified())) { QFile globalFile(globalConfig); QFile localFile(localConfig); if (globalFile.open(QFile::ReadOnly) && actualFile.open(QFile::WriteOnly)) { actualFile.write(globalFile.readAll()); if (!localConfig.isEmpty()) { if (localFile.open(QFile::ReadOnly)) { actualFile.write(localFile.readAll()); localFile.close(); } } globalFile.close(); actualFile.close(); confUpdate = true; } else { qCCritical(AKONADISERVER_LOG) << "Unable to create MySQL server configuration file."; qCCritical(AKONADISERVER_LOG) << "This means that either the default configuration file (mysql-global.conf) was not readable"; qCCritical(AKONADISERVER_LOG) << "or the target file (mysql.conf) could not be written."; return false; } } // MySQL doesn't like world writeable config files (which makes sense), but // our config file somehow ends up being world-writable on some systems for no // apparent reason nevertheless, so fix that const QFile::Permissions allowedPerms = actualFile.permissions() & (QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::WriteGroup | QFile::ReadOther); if (allowedPerms != actualFile.permissions()) { actualFile.setPermissions(allowedPerms); } if (dataDir.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database data directory"; return false; } if (akDir.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database log directory"; return false; } #ifndef Q_OS_WIN if (socketDirectory.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database misc directory"; return false; } // the socket path must not exceed 103 characters, so check for max dir length right away if (socketDirectory.length() >= 90) { qCCritical(AKONADISERVER_LOG) << "MySQL cannot deal with a socket path this long. Path was: " << socketDirectory; return false; } // If mysql socket file exists, check if also the server process is still running, // else we can safely remove the socket file (cleanup after a system crash, etc.) QFile pidFile(pidFileName); if (QFile::exists(socketFile) && pidFile.open(QIODevice::ReadOnly)) { qCDebug(AKONADISERVER_LOG) << "Found a mysqld pid file, checking whether the server is still running..."; QByteArray pid = pidFile.readLine().trimmed(); QFile proc(QString::fromLatin1("/proc/" + pid + "/stat")); // Check whether the process with the PID from pidfile still exists and whether // it's actually still mysqld or, whether the PID has been recycled in the meanwhile. bool serverIsRunning = false; if (proc.open(QIODevice::ReadOnly)) { const QByteArray stat = proc.readAll(); const QList stats = stat.split(' '); if (stats.count() > 1) { // Make sure the PID actually belongs to mysql process // Linux trims executable name in /proc filesystem to 15 characters const QString expectedProcName = QFileInfo(mMysqldPath).fileName().left(15); if (QString::fromLatin1(stats[1]) == QString::fromLatin1("(%1)").arg(expectedProcName)) { // Yup, our mysqld is actually running, so pretend we started the server // and try to connect to it qCWarning(AKONADISERVER_LOG) << "mysqld for Akonadi is already running, trying to connect to it."; serverIsRunning = true; } } proc.close(); } if (!serverIsRunning) { qCDebug(AKONADISERVER_LOG) << "No mysqld process with specified PID is running. Removing the pidfile and starting a new instance..."; pidFile.close(); pidFile.remove(); QFile::remove(socketFile); } } #endif // synthesize the mysqld command QStringList arguments; arguments << QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir); arguments << QStringLiteral("--datadir=%1/").arg(dataDir); #ifndef Q_OS_WIN arguments << QStringLiteral("--socket=%1").arg(socketFile); arguments << QStringLiteral("--pid-file=%1").arg(pidFileName); #else arguments << QString::fromLatin1("--shared-memory"); #endif #ifndef Q_OS_WIN // If mysql socket file does not exists, then we must start the server, // otherwise we reconnect to it if (!QFile::exists(socketFile)) { // move mysql error log file out of the way const QFileInfo errorLog(dataDir + QDir::separator() + QLatin1String("mysql.err")); if (errorLog.exists()) { QFile logFile(errorLog.absoluteFilePath()); QFile oldLogFile(dataDir + QDir::separator() + QLatin1String("mysql.err.old")); if (logFile.open(QFile::ReadOnly) && oldLogFile.open(QFile::Append)) { oldLogFile.write(logFile.readAll()); oldLogFile.close(); logFile.close(); logFile.remove(); } else { qCCritical(AKONADISERVER_LOG) << "Failed to open MySQL error log."; } } // first run, some MySQL versions need a mysql_install_db run for that - const QString confFile = StandardDirs::locateResourceFile("config", QStringLiteral("akonadi/mysql-global.conf")); + const QString confFile = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf")); if (QDir(dataDir).entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()) { if (isMariaDB) { initializeMariaDBDatabase(confFile, dataDir); } else if (localVersion >= MYSQL_VERSION_CHECK(5, 7, 6)) { initializeMySQL5_7_6Database(confFile, dataDir); } else { initializeMySQLDatabase(confFile, dataDir); } } // clear mysql ib_logfile's in case innodb_log_file_size option changed in last confUpdate if (confUpdate) { QFile(dataDir + QDir::separator() + QLatin1String("ib_logfile0")).remove(); QFile(dataDir + QDir::separator() + QLatin1String("ib_logfile1")).remove(); } qCDebug(AKONADISERVER_LOG) << "Executing:" << mMysqldPath << arguments.join(QLatin1Char(' ')); mDatabaseProcess = new QProcess; mDatabaseProcess->start(mMysqldPath, arguments); if (!mDatabaseProcess->waitForStarted()) { qCCritical(AKONADISERVER_LOG) << "Could not start database server!"; qCCritical(AKONADISERVER_LOG) << "executable:" << mMysqldPath; qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments; qCCritical(AKONADISERVER_LOG) << "process error:" << mDatabaseProcess->errorString(); return false; } connect(mDatabaseProcess, QOverload::of(&QProcess::finished), this, &DbConfigMysql::processFinished); // wait until mysqld has created the socket file (workaround for QTBUG-47475 in Qt5.5.0) int counter = 50; // avoid an endless loop in case mysqld terminated while ((counter-- > 0) && !QFileInfo::exists(socketFile)) { QThread::msleep(100); } } else { qCDebug(AKONADISERVER_LOG) << "Found " << qPrintable(s_mysqlSocketFileName) << " file, reconnecting to the database"; } #endif const QLatin1String initCon("initConnection"); { QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QMYSQL"), initCon); apply(db); db.setDatabaseName(QString()); // might not exist yet, then connecting to the actual db will fail if (!db.isValid()) { qCCritical(AKONADISERVER_LOG) << "Invalid database object during database server startup"; return false; } bool opened = false; for (int i = 0; i < 120; ++i) { opened = db.open(); if (opened) { break; } if (mDatabaseProcess && mDatabaseProcess->waitForFinished(500)) { qCCritical(AKONADISERVER_LOG) << "Database process exited unexpectedly during initial connection!"; qCCritical(AKONADISERVER_LOG) << "executable:" << mMysqldPath; qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments; qCCritical(AKONADISERVER_LOG) << "stdout:" << mDatabaseProcess->readAllStandardOutput(); qCCritical(AKONADISERVER_LOG) << "stderr:" << mDatabaseProcess->readAllStandardError(); qCCritical(AKONADISERVER_LOG) << "exit code:" << mDatabaseProcess->exitCode(); qCCritical(AKONADISERVER_LOG) << "process error:" << mDatabaseProcess->errorString(); return false; } } if (opened) { if (!mMysqlCheckPath.isEmpty()) { execute(mMysqlCheckPath, { QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir), QStringLiteral("--check-upgrade"), QStringLiteral("--auto-repair"), #ifndef Q_OS_WIN QStringLiteral("--socket=%1/%2").arg(socketDirectory, s_mysqlSocketFileName), #endif mDatabaseName }); } // Verify MySQL version { QSqlQuery query(db); if (!query.exec(QStringLiteral("SELECT VERSION()")) || !query.first()) { qCCritical(AKONADISERVER_LOG) << "Failed to verify database server version"; qCCritical(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); return false; } const QString version = query.value(0).toString(); const QStringList versions = version.split(QLatin1Char('.'), QString::SkipEmptyParts); if (versions.count() < 3) { qCCritical(AKONADISERVER_LOG) << "Invalid database server version: " << version; return false; } if (versions[0].toInt() < MYSQL_MIN_MAJOR || (versions[0].toInt() == MYSQL_MIN_MAJOR && versions[1].toInt() < MYSQL_MIN_MINOR)) { qCCritical(AKONADISERVER_LOG) << "Unsupported MySQL version:"; qCCritical(AKONADISERVER_LOG) << "Current version:" << QStringLiteral("%1.%2").arg(versions[0], versions[1]); qCCritical(AKONADISERVER_LOG) << "Minimum required version:" << QStringLiteral("%1.%2").arg(MYSQL_MIN_MAJOR).arg(MYSQL_MIN_MINOR); qCCritical(AKONADISERVER_LOG) << "Please update your MySQL database server"; return false; } else { qCDebug(AKONADISERVER_LOG) << "MySQL version OK" << "(required" << QStringLiteral("%1.%2").arg(MYSQL_MIN_MAJOR).arg(MYSQL_MIN_MINOR) << ", available" << QStringLiteral("%1.%2").arg(versions[0], versions[1]) << ")"; } } { QSqlQuery query(db); if (!query.exec(QStringLiteral("USE %1").arg(mDatabaseName))) { qCDebug(AKONADISERVER_LOG) << "Failed to use database" << mDatabaseName; qCDebug(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); qCDebug(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); qCDebug(AKONADISERVER_LOG) << "Trying to create database now..."; if (!query.exec(QStringLiteral("CREATE DATABASE akonadi"))) { qCCritical(AKONADISERVER_LOG) << "Failed to create database"; qCCritical(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); success = false; } } } // make sure query is destroyed before we close the db db.close(); } else { qCCritical(AKONADISERVER_LOG) << "Failed to connect to database!"; qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); success = false; } } return success; } void DbConfigMysql::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitCode); Q_UNUSED(exitStatus); qCCritical(AKONADISERVER_LOG) << "database server stopped unexpectedly"; #ifndef Q_OS_WIN // when the server stopped unexpectedly, make sure to remove the stale socket file since otherwise // it can not be started again const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length()); const QString socketFile = QStringLiteral("%1/%2").arg(socketDirectory, s_mysqlSocketFileName); QFile::remove(socketFile); #endif QCoreApplication::quit(); } void DbConfigMysql::stopInternalServer() { if (!mDatabaseProcess) { return; } // closing initConnection this late to work around QTBUG-63108 QSqlDatabase::removeDatabase(QStringLiteral("initConnection")); disconnect(mDatabaseProcess, static_cast(&QProcess::finished), this, &DbConfigMysql::processFinished); // first, try the nicest approach if (!mCleanServerShutdownCommand.isEmpty()) { QProcess::execute(mCleanServerShutdownCommand, QStringList()); if (mDatabaseProcess->waitForFinished(3000)) { return; } } mDatabaseProcess->terminate(); const bool result = mDatabaseProcess->waitForFinished(3000); // We've waited nicely for 3 seconds, to no avail, let's be rude. if (!result) { mDatabaseProcess->kill(); } } void DbConfigMysql::initSession(const QSqlDatabase &database) { QSqlQuery query(database); query.exec(QStringLiteral("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")); } int DbConfigMysql::parseCommandLineToolsVersion() const { QProcess mysqldProcess; mysqldProcess.start(mMysqldPath, { QStringLiteral("--version") }); mysqldProcess.waitForFinished(10000 /* 10 secs */); const QString out = QString::fromLocal8Bit(mysqldProcess.readAllStandardOutput()); QRegularExpression regexp(QStringLiteral("Ver ([0-9]+)\\.([0-9]+)\\.([0-9]+)")); auto match = regexp.match(out); if (!match.hasMatch()) { return 0; } return (match.capturedRef(1).toInt() << 16) | (match.capturedRef(2).toInt() << 8) | match.capturedRef(3).toInt(); } bool DbConfigMysql::initializeMariaDBDatabase(const QString &confFile, const QString &dataDir) const { // KDE Neon (and possible others) don't ship mysql_install_db, but it seems // that MariaDB can initialize itself automatically on first start, it only // needs that the datadir directory exists if (mMysqlInstallDbPath.isEmpty()) { return QDir().mkpath(dataDir); } QFileInfo fi(mMysqlInstallDbPath); QDir dir = fi.dir(); dir.cdUp(); const QString baseDir = dir.absolutePath(); return 0 == execute(mMysqlInstallDbPath, { QStringLiteral("--defaults-file=%1").arg(confFile), QStringLiteral("--force"), QStringLiteral("--basedir=%1").arg(baseDir), QStringLiteral("--datadir=%1/").arg(dataDir) }); } /** * As of MySQL 5.7.6 mysql_install_db is deprecated and mysqld --initailize should be used instead * See MySQL Reference Manual section 2.10.1.1 (Initializing the Data Directory Manually Using mysqld) */ bool DbConfigMysql::initializeMySQL5_7_6Database(const QString &confFile, const QString &dataDir) const { return 0 == execute(mMysqldPath, { QStringLiteral("--defaults-file=%1").arg(confFile), QStringLiteral("--initialize"), QStringLiteral("--datadir=%1/").arg(dataDir) }); } bool DbConfigMysql::initializeMySQLDatabase(const QString &confFile, const QString &dataDir) const { // On FreeBSD MySQL 5.6 is also installed without mysql_install_db, so this // might do the trick there as well. if (mMysqlInstallDbPath.isEmpty()) { return QDir().mkpath(dataDir); } QFileInfo fi(mMysqlInstallDbPath); QDir dir = fi.dir(); dir.cdUp(); const QString baseDir = dir.absolutePath(); // Don't use --force, it has been removed in MySQL 5.7.5 return 0 == execute(mMysqlInstallDbPath, { QStringLiteral("--defaults-file=%1").arg(confFile), QStringLiteral("--basedir=%1").arg(baseDir), QStringLiteral("--datadir=%1/").arg(dataDir) }); } diff --git a/src/widgets/selftestdialog.cpp b/src/widgets/selftestdialog.cpp index e20a09b4b..200f90516 100644 --- a/src/widgets/selftestdialog.cpp +++ b/src/widgets/selftestdialog.cpp @@ -1,653 +1,653 @@ /* Copyright (c) 2008 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "selftestdialog.h" #include "agentmanager.h" #include "servermanager.h" #include "servermanager_p.h" #include "private/standarddirs_p.h" #include "private/protocol_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // @cond PRIVATE using namespace Akonadi; static QString makeLink(const QString &file) { return QStringLiteral("%2").arg(file, file); } enum SelfTestRole { ResultTypeRole = Qt::UserRole, FileIncludeRole, ListDirectoryRole, EnvVarRole, SummaryRole, DetailsRole }; SelfTestDialog::SelfTestDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Akonadi Server Self-Test")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, this); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->addWidget(mainWidget); QPushButton *user1Button = new QPushButton(this); buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole); QPushButton *user2Button = new QPushButton(this); buttonBox->addButton(user2Button, QDialogButtonBox::ActionRole); connect(buttonBox, &QDialogButtonBox::rejected, this, &SelfTestDialog::reject); mainLayout->addWidget(buttonBox); user1Button->setText(i18n("Save Report...")); user1Button->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); user2Button->setText(i18n("Copy Report to Clipboard")); user2Button->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); ui.setupUi(mainWidget); mTestModel = new QStandardItemModel(this); ui.testView->setModel(mTestModel); connect(ui.testView->selectionModel(), &QItemSelectionModel::currentChanged, this, &SelfTestDialog::selectionChanged); connect(ui.detailsLabel, &QLabel::linkActivated, this, &SelfTestDialog::linkActivated); connect(user1Button, &QPushButton::clicked, this, &SelfTestDialog::saveReport); connect(user2Button, &QPushButton::clicked, this, &SelfTestDialog::copyReport); connect(ServerManager::self(), &ServerManager::stateChanged, this, &SelfTestDialog::runTests); runTests(); } void SelfTestDialog::hideIntroduction() { ui.introductionLabel->hide(); } QStandardItem *SelfTestDialog::report(ResultType type, const KLocalizedString &summary, const KLocalizedString &details) { QStandardItem *item = new QStandardItem(summary.toString()); switch (type) { case Skip: item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); break; case Success: item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply"))); break; case Warning: item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); break; case Error: item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-error"))); break; } item->setEditable(false); item->setWhatsThis(details.toString()); item->setData(type, ResultTypeRole); item->setData(summary.toString(nullptr), SummaryRole); item->setData(details.toString(nullptr), DetailsRole); mTestModel->appendRow(item); return item; } void SelfTestDialog::selectionChanged(const QModelIndex &index) { if (index.isValid()) { ui.detailsLabel->setText(index.data(Qt::WhatsThisRole).toString()); ui.detailsGroup->setEnabled(true); } else { ui.detailsLabel->setText(QString()); ui.detailsGroup->setEnabled(false); } } void SelfTestDialog::runTests() { mTestModel->clear(); const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString(); testSQLDriver(); if (driver == QLatin1String("QPSQL")) { testPSQLServer(); } else { #ifndef Q_OS_WIN testRootUser(); #endif testMySQLServer(); testMySQLServerLog(); testMySQLServerConfig(); } testAkonadiCtl(); testServerStatus(); testProtocolVersion(); testResources(); testServerLog(); testControlLog(); } QVariant SelfTestDialog::serverSetting(const QString &group, const char *key, const QVariant &def) const { const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadOnly); QSettings settings(serverConfigFile, QSettings::IniFormat); settings.beginGroup(group); return settings.value(QString::fromLatin1(key), def); } bool SelfTestDialog::useStandaloneMysqlServer() const { const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString(); if (driver != QLatin1String("QMYSQL")) { return false; } const bool startServer = serverSetting(driver, "StartServer", true).toBool(); if (!startServer) { return false; } return true; } bool SelfTestDialog::runProcess(const QString &app, const QStringList &args, QString &result) const { QProcess proc; proc.start(app, args); const bool rv = proc.waitForFinished(); result.clear(); result = QString::fromLocal8Bit(proc.readAllStandardError()); result += QString::fromLocal8Bit(proc.readAllStandardOutput()); return rv; } void SelfTestDialog::testSQLDriver() { const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString(); const QStringList availableDrivers = QSqlDatabase::drivers(); const KLocalizedString detailsOk = ki18n("The QtSQL driver '%1' is required by your current Akonadi server configuration and was found on your system.") .subs(driver); const KLocalizedString detailsFail = ki18n("The QtSQL driver '%1' is required by your current Akonadi server configuration.\n" "The following drivers are installed: %2.\n" "Make sure the required driver is installed.") .subs(driver) .subs(availableDrivers.join(QLatin1String(", "))); QStandardItem *item = nullptr; if (availableDrivers.contains(driver)) { item = report(Success, ki18n("Database driver found."), detailsOk); } else { item = report(Error, ki18n("Database driver not found."), detailsFail); } item->setData(StandardDirs::serverConfigFile(StandardDirs::ReadOnly), FileIncludeRole); } void SelfTestDialog::testMySQLServer() { if (!useStandaloneMysqlServer()) { report(Skip, ki18n("MySQL server executable not tested."), ki18n("The current configuration does not require an internal MySQL server.")); return; } const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString(); const QString serverPath = serverSetting(driver, "ServerPath", QString()).toString(); // ### default? const KLocalizedString details = ki18n("You have currently configured Akonadi to use the MySQL server '%1'.\n" "Make sure you have the MySQL server installed, set the correct path and ensure you have the " "necessary read and execution rights on the server executable. The server executable is typically " "called 'mysqld'; its location varies depending on the distribution.").subs(serverPath); QFileInfo info(serverPath); if (!info.exists()) { report(Error, ki18n("MySQL server not found."), details); } else if (!info.isReadable()) { report(Error, ki18n("MySQL server not readable."), details); } else if (!info.isExecutable()) { report(Error, ki18n("MySQL server not executable."), details); } else if (!serverPath.contains(QLatin1String("mysqld"))) { report(Warning, ki18n("MySQL found with unexpected name."), details); } else { report(Success, ki18n("MySQL server found."), details); } // be extra sure and get the server version while we are at it QString result; if (runProcess(serverPath, QStringList() << QStringLiteral("--version"), result)) { const KLocalizedString details = ki18n("MySQL server found: %1").subs(result); report(Success, ki18n("MySQL server is executable."), details); } else { const KLocalizedString details = ki18n("Executing the MySQL server '%1' failed with the following error message: '%2'") .subs(serverPath).subs(result); report(Error, ki18n("Executing the MySQL server failed."), details); } } void SelfTestDialog::testMySQLServerLog() { if (!useStandaloneMysqlServer()) { report(Skip, ki18n("MySQL server error log not tested."), ki18n("The current configuration does not require an internal MySQL server.")); return; } const QString logFileName = StandardDirs::saveDir("data", QStringLiteral("db_data")) + QLatin1String("/mysql.err"); const QFileInfo logFileInfo(logFileName); if (!logFileInfo.exists() || logFileInfo.size() == 0) { report(Success, ki18n("No current MySQL error log found."), ki18n("The MySQL server did not report any errors during this startup. The log can be found in '%1'.").subs(logFileName)); return; } QFile logFile(logFileName); if (!logFile.open(QFile::ReadOnly | QFile::Text)) { report(Error, ki18n("MySQL error log not readable."), ki18n("A MySQL server error log file was found but is not readable: %1").subs(makeLink(logFileName))); return; } bool warningsFound = false; QStandardItem *item = nullptr; while (!logFile.atEnd()) { const QString line = QString::fromUtf8(logFile.readLine()); if (line.contains(QLatin1String("error"), Qt::CaseInsensitive)) { item = report(Error, ki18n("MySQL server log contains errors."), ki18n("The MySQL server error log file '%1' contains errors.").subs(makeLink(logFileName))); item->setData(logFileName, FileIncludeRole); return; } if (!warningsFound && line.contains(QLatin1String("warn"), Qt::CaseInsensitive)) { warningsFound = true; } } if (warningsFound) { item = report(Warning, ki18n("MySQL server log contains warnings."), ki18n("The MySQL server log file '%1' contains warnings.").subs(makeLink(logFileName))); } else { item = report(Success, ki18n("MySQL server log contains no errors."), ki18n("The MySQL server log file '%1' does not contain any errors or warnings.") .subs(makeLink(logFileName))); } item->setData(logFileName, FileIncludeRole); logFile.close(); } void SelfTestDialog::testMySQLServerConfig() { if (!useStandaloneMysqlServer()) { report(Skip, ki18n("MySQL server configuration not tested."), ki18n("The current configuration does not require an internal MySQL server.")); return; } QStandardItem *item = nullptr; - const QString globalConfig = StandardDirs::locateResourceFile("config", QStringLiteral("akonadi/mysql-global.conf")); + const QString globalConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf")); const QFileInfo globalConfigInfo(globalConfig); if (!globalConfig.isEmpty() && globalConfigInfo.exists() && globalConfigInfo.isReadable()) { item = report(Success, ki18n("MySQL server default configuration found."), ki18n("The default configuration for the MySQL server was found and is readable at %1.") .subs(makeLink(globalConfig))); item->setData(globalConfig, FileIncludeRole); } else { report(Error, ki18n("MySQL server default configuration not found."), ki18n("The default configuration for the MySQL server was not found or was not readable. " "Check your Akonadi installation is complete and you have all required access rights.")); } - const QString localConfig = StandardDirs::locateResourceFile("config", QStringLiteral("akonadi/mysql-local.conf")); + const QString localConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-local.conf")); const QFileInfo localConfigInfo(localConfig); if (localConfig.isEmpty() || !localConfigInfo.exists()) { report(Skip, ki18n("MySQL server custom configuration not available."), ki18n("The custom configuration for the MySQL server was not found but is optional.")); } else if (localConfigInfo.exists() && localConfigInfo.isReadable()) { item = report(Success, ki18n("MySQL server custom configuration found."), ki18n("The custom configuration for the MySQL server was found and is readable at %1") .subs(makeLink(localConfig))); item->setData(localConfig, FileIncludeRole); } else { report(Error, ki18n("MySQL server custom configuration not readable."), ki18n("The custom configuration for the MySQL server was found at %1 but is not readable. " "Check your access rights.").subs(makeLink(localConfig))); } const QString actualConfig = StandardDirs::saveDir("data") + QStringLiteral("/mysql.conf"); const QFileInfo actualConfigInfo(actualConfig); if (actualConfig.isEmpty() || !actualConfigInfo.exists() || !actualConfigInfo.isReadable()) { report(Error, ki18n("MySQL server configuration not found or not readable."), ki18n("The MySQL server configuration was not found or is not readable.")); } else { item = report(Success, ki18n("MySQL server configuration is usable."), ki18n("The MySQL server configuration was found at %1 and is readable.").subs(makeLink(actualConfig))); item->setData(actualConfig, FileIncludeRole); } } void SelfTestDialog::testPSQLServer() { const QString dbname = serverSetting(QStringLiteral("QPSQL"), "Name", QStringLiteral("akonadi")).toString(); const QString hostname = serverSetting(QStringLiteral("QPSQL"), "Host", QStringLiteral("localhost")).toString(); const QString username = serverSetting(QStringLiteral("QPSQL"), "User", QString()).toString(); const QString password = serverSetting(QStringLiteral("QPSQL"), "Password", QString()).toString(); const int port = serverSetting(QStringLiteral("QPSQL"), "Port", 5432).toInt(); QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL")); db.setHostName(hostname); db.setDatabaseName(dbname); if (!username.isEmpty()) { db.setUserName(username); } if (!password.isEmpty()) { db.setPassword(password); } db.setPort(port); if (!db.open()) { const KLocalizedString details = ki18n(db.lastError().text().toLatin1().constData()); report(Error, ki18n("Cannot connect to PostgreSQL server."), details); } else { report(Success, ki18n("PostgreSQL server found."), ki18n("The PostgreSQL server was found and connection is working.")); } db.close(); } void SelfTestDialog::testAkonadiCtl() { const QString path = Akonadi::StandardDirs::findExecutable(QStringLiteral("akonadictl")); if (path.isEmpty()) { report(Error, ki18n("akonadictl not found"), ki18n("The program 'akonadictl' needs to be accessible in $PATH. " "Make sure you have the Akonadi server installed.")); return; } QString result; if (runProcess(path, QStringList() << QStringLiteral("--version"), result)) { report(Success, ki18n("akonadictl found and usable"), ki18n("The program '%1' to control the Akonadi server was found " "and could be executed successfully.\nResult:\n%2").subs(path).subs(result)); } else { report(Error, ki18n("akonadictl found but not usable"), ki18n("The program '%1' to control the Akonadi server was found " "but could not be executed successfully.\nResult:\n%2\n" "Make sure the Akonadi server is installed correctly.").subs(path).subs(result)); } } void SelfTestDialog::testServerStatus() { if (QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control))) { report(Success, ki18n("Akonadi control process registered at D-Bus."), ki18n("The Akonadi control process is registered at D-Bus which typically indicates it is operational.")); } else { report(Error, ki18n("Akonadi control process not registered at D-Bus."), ki18n("The Akonadi control process is not registered at D-Bus which typically means it was not started " "or encountered a fatal error during startup.")); } if (QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server))) { report(Success, ki18n("Akonadi server process registered at D-Bus."), ki18n("The Akonadi server process is registered at D-Bus which typically indicates it is operational.")); } else { report(Error, ki18n("Akonadi server process not registered at D-Bus."), ki18n("The Akonadi server process is not registered at D-Bus which typically means it was not started " "or encountered a fatal error during startup.")); } } void SelfTestDialog::testProtocolVersion() { if (Internal::serverProtocolVersion() < 0) { report(Skip, ki18n("Protocol version check not possible."), ki18n("Without a connection to the server it is not possible to check if the protocol version meets the requirements.")); return; } if (Internal::serverProtocolVersion() < Protocol::version()) { report(Error, ki18n("Server protocol version is too old."), ki18n("The server protocol version is %1, but version %2 is required by the client. " "If you recently updated KDE PIM, please make sure to restart both Akonadi and KDE PIM applications.") .subs(Internal::serverProtocolVersion()) .subs(Protocol::version())); } else if (Internal::serverProtocolVersion() > Protocol::version()) { report(Error, ki18n("Server protocol version is too new."), ki18n("The server protocol version is %1, but version %2 is required by the client. " "If you recently updated KDE PIM, please make sure to restart both Akonadi and KDE PIM applications.") .subs(Internal::serverProtocolVersion()) .subs(Protocol::version())); } else { report(Success, ki18n("Server protocol version matches."), ki18n("The current Protocol version is %1.") .subs(Internal::serverProtocolVersion())); } } void SelfTestDialog::testResources() { const AgentType::List agentTypes = AgentManager::self()->types(); bool resourceFound = false; for (const AgentType &type : agentTypes) { if (type.capabilities().contains(QLatin1String("Resource"))) { resourceFound = true; break; } } const auto pathList = StandardDirs::locateAllResourceDirs(QStringLiteral("akonadi/agents")); QStandardItem *item = nullptr; if (resourceFound) { item = report(Success, ki18n("Resource agents found."), ki18n("At least one resource agent has been found.")); } else { item = report(Error, ki18n("No resource agents found."), ki18n("No resource agents have been found, Akonadi is not usable without at least one. " "This usually means that no resource agents are installed or that there is a setup problem. " "The following paths have been searched: '%1'. " "The XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes all paths " "where Akonadi agents are installed.") .subs(pathList.join(QLatin1Char(' '))) .subs(QString::fromLocal8Bit(qgetenv("XDG_DATA_DIRS")))); } item->setData(pathList, ListDirectoryRole); item->setData(QByteArray("XDG_DATA_DIRS"), EnvVarRole); } void SelfTestDialog::testServerLog() { QString serverLog = StandardDirs::saveDir("data") + QLatin1String("/akonadiserver.error"); QFileInfo info(serverLog); if (!info.exists() || info.size() <= 0) { report(Success, ki18n("No current Akonadi server error log found."), ki18n("The Akonadi server did not report any errors during its current startup.")); } else { QStandardItem *item = report(Error, ki18n("Current Akonadi server error log found."), ki18n("The Akonadi server reported errors during its current startup. The log can be found in %1.").subs(makeLink(serverLog))); item->setData(serverLog, FileIncludeRole); } serverLog += QStringLiteral(".old"); info.setFile(serverLog); if (!info.exists() || info.size() <= 0) { report(Success, ki18n("No previous Akonadi server error log found."), ki18n("The Akonadi server did not report any errors during its previous startup.")); } else { QStandardItem *item = report(Error, ki18n("Previous Akonadi server error log found."), ki18n("The Akonadi server reported errors during its previous startup. The log can be found in %1.").subs(makeLink(serverLog))); item->setData(serverLog, FileIncludeRole); } } void SelfTestDialog::testControlLog() { QString controlLog = StandardDirs::saveDir("data") + QLatin1String("/akonadi_control.error"); QFileInfo info(controlLog); if (!info.exists() || info.size() <= 0) { report(Success, ki18n("No current Akonadi control error log found."), ki18n("The Akonadi control process did not report any errors during its current startup.")); } else { QStandardItem *item = report(Error, ki18n("Current Akonadi control error log found."), ki18n("The Akonadi control process reported errors during its current startup. The log can be found in %1.").subs(makeLink(controlLog))); item->setData(controlLog, FileIncludeRole); } controlLog += QStringLiteral(".old"); info.setFile(controlLog); if (!info.exists() || info.size() <= 0) { report(Success, ki18n("No previous Akonadi control error log found."), ki18n("The Akonadi control process did not report any errors during its previous startup.")); } else { QStandardItem *item = report(Error, ki18n("Previous Akonadi control error log found."), ki18n("The Akonadi control process reported errors during its previous startup. The log can be found in %1.").subs(makeLink(controlLog))); item->setData(controlLog, FileIncludeRole); } } void SelfTestDialog::testRootUser() { KUser user; if (user.isSuperUser()) { 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.")); } else { 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.")); } } QString SelfTestDialog::createReport() { QString result; QTextStream s(&result); s << "Akonadi Server Self-Test Report"; s << "==============================="; for (int i = 0; i < mTestModel->rowCount(); ++i) { QStandardItem *item = mTestModel->item(i); s << '\n'; s << "Test " << (i + 1) << ": "; switch (item->data(ResultTypeRole).toInt()) { case Skip: s << "SKIP"; break; case Success: s << "SUCCESS"; break; case Warning: s << "WARNING"; break; case Error: default: s << "ERROR"; break; } s << "\n--------\n"; s << '\n'; s << item->data(SummaryRole).toString() << '\n'; s << "Details: " << item->data(DetailsRole).toString() << '\n'; if (item->data(FileIncludeRole).isValid()) { s << '\n'; const QString fileName = item->data(FileIncludeRole).toString(); QFile f(fileName); if (f.open(QFile::ReadOnly)) { s << "File content of '" << fileName << "':" << '\n'; s << f.readAll() << '\n'; } else { s << "File '" << fileName << "' could not be opened\n"; } } if (item->data(ListDirectoryRole).isValid()) { s << '\n'; const QStringList pathList = item->data(ListDirectoryRole).toStringList(); if (pathList.isEmpty()) { s << "Directory list is empty.\n"; } for (const QString &path : pathList) { s << "Directory listing of '" << path << "':\n"; QDir dir(path); dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); const QStringList listEntries(dir.entryList()); for (const QString &entry : listEntries) { s << entry << '\n'; } } } if (item->data(EnvVarRole).isValid()) { s << '\n'; const QByteArray envVarName = item->data(EnvVarRole).toByteArray(); const QByteArray envVarValue = qgetenv(envVarName.constData()); s << "Environment variable " << envVarName << " is set to '" << envVarValue << "'\n"; } } s << '\n'; s.flush(); return result; } void SelfTestDialog::saveReport() { const QString defaultFileName = QStringLiteral("akonadi-selftest-report-") + QDate::currentDate().toString(QStringLiteral("yyyyMMdd")) + QStringLiteral(".txt"); const QString fileName = QFileDialog::getSaveFileName(this, i18n("Save Test Report"), defaultFileName); if (fileName.isEmpty()) { return; } QFile file(fileName); if (!file.open(QFile::ReadWrite)) { QMessageBox::critical(this, i18n("Error"), i18n("Could not open file '%1'", fileName)); return; } file.write(createReport().toUtf8()); file.close(); } void SelfTestDialog::copyReport() { #ifndef QT_NO_CLIPBOARD QApplication::clipboard()->setText(createReport()); #endif } void SelfTestDialog::linkActivated(const QString &link) { QDesktopServices::openUrl(QUrl::fromLocalFile(link)); } // @endcond