File indexing completed on 2025-01-05 04:46:57

0001 /*
0002     SPDX-FileCopyrightText: 2010 Tobias Koenig <tokoe@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "dbconfigmysql.h"
0008 #include "akonadiserver_debug.h"
0009 #include "utils.h"
0010 
0011 #include "private/standarddirs_p.h"
0012 
0013 #include <QCoreApplication>
0014 #include <QDir>
0015 #include <QRegularExpression>
0016 #include <QSqlDriver>
0017 #include <QSqlError>
0018 #include <QSqlQuery>
0019 #include <QStandardPaths>
0020 #include <QThread>
0021 
0022 using namespace Akonadi;
0023 using namespace Akonadi::Server;
0024 
0025 #define MYSQL_MIN_MAJOR 5
0026 #define MYSQL_MIN_MINOR 1
0027 
0028 #define MYSQL_VERSION_CHECK(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch))
0029 
0030 static const QString s_mysqlSocketFileName = QStringLiteral("mysql.socket");
0031 static const QString s_initConnection = QStringLiteral("initConnectionMysql");
0032 
0033 DbConfigMysql::DbConfigMysql(const QString &configFile)
0034     : DbConfig(configFile)
0035 {
0036 }
0037 
0038 DbConfigMysql::~DbConfigMysql() = default;
0039 
0040 QString DbConfigMysql::driverName() const
0041 {
0042     return QStringLiteral("QMYSQL");
0043 }
0044 
0045 QString DbConfigMysql::databaseName() const
0046 {
0047     return mDatabaseName;
0048 }
0049 
0050 QString DbConfigMysql::databasePath() const
0051 {
0052     return mDataDir;
0053 }
0054 
0055 void DbConfigMysql::setDatabasePath(const QString &path, QSettings &settings)
0056 {
0057     mDataDir = path;
0058     settings.beginGroup(driverName());
0059     settings.setValue(QStringLiteral("DataDir"), mDataDir);
0060     settings.endGroup();
0061     settings.sync();
0062 }
0063 
0064 static QString findExecutable(const QString &bin)
0065 {
0066     static const QStringList mysqldSearchPath = {
0067 #ifdef MYSQLD_SCRIPTS_PATH
0068         QStringLiteral(MYSQLD_SCRIPTS_PATH),
0069 #endif
0070         QStringLiteral("/usr/bin"),
0071         QStringLiteral("/usr/sbin"),
0072         QStringLiteral("/usr/local/sbin"),
0073         QStringLiteral("/usr/local/libexec"),
0074         QStringLiteral("/usr/libexec"),
0075         QStringLiteral("/opt/mysql/libexec"),
0076         QStringLiteral("/opt/local/lib/mysql5/bin"),
0077         QStringLiteral("/opt/mysql/sbin"),
0078     };
0079     QString path = QStandardPaths::findExecutable(bin);
0080     if (path.isEmpty()) { // No results in PATH; fall back to hardcoded list.
0081         path = QStandardPaths::findExecutable(bin, mysqldSearchPath);
0082     }
0083     return path;
0084 }
0085 
0086 bool DbConfigMysql::init(QSettings &settings, bool storeSettings, const QString &dbPathOverride)
0087 {
0088     // determine default settings depending on the driver
0089     QString defaultHostName;
0090     QString defaultOptions;
0091     QString defaultServerPath;
0092     QString defaultCleanShutdownCommand;
0093 
0094 #ifndef Q_OS_WIN
0095     const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length());
0096 #endif
0097 
0098     const bool defaultInternalServer = true;
0099 #ifdef MYSQLD_EXECUTABLE
0100     if (QFile::exists(QStringLiteral(MYSQLD_EXECUTABLE))) {
0101         defaultServerPath = QStringLiteral(MYSQLD_EXECUTABLE);
0102     }
0103 #endif
0104     if (defaultServerPath.isEmpty()) {
0105         defaultServerPath = findExecutable(QStringLiteral("mysqld"));
0106     }
0107 
0108     const QString mysqladminPath = findExecutable(QStringLiteral("mysqladmin"));
0109     if (!mysqladminPath.isEmpty()) {
0110 #ifndef Q_OS_WIN
0111         defaultCleanShutdownCommand = QStringLiteral("%1 --defaults-file=%2/mysql.conf --socket=%3/%4 shutdown")
0112                                           .arg(mysqladminPath, StandardDirs::saveDir("data"), socketDirectory, s_mysqlSocketFileName);
0113 #else
0114         defaultCleanShutdownCommand = QString::fromLatin1("%1 shutdown --shared-memory").arg(mysqladminPath);
0115 #endif
0116     }
0117 
0118     const QString defaultDataDir = dbPathOverride.isEmpty() ? StandardDirs::saveDir("data", QStringLiteral("db_data")) : dbPathOverride;
0119 
0120     mMysqlInstallDbPath = findExecutable(QStringLiteral("mysql_install_db"));
0121     qCDebug(AKONADISERVER_LOG) << "Found mysql_install_db: " << mMysqlInstallDbPath;
0122 
0123     mMysqlCheckPath = findExecutable(QStringLiteral("mysqlcheck"));
0124     qCDebug(AKONADISERVER_LOG) << "Found mysqlcheck: " << mMysqlCheckPath;
0125 
0126     mMysqlUpgradePath = findExecutable(QStringLiteral("mysql_upgrade"));
0127     qCDebug(AKONADISERVER_LOG) << "Found mysql_upgrade: " << mMysqlUpgradePath;
0128 
0129     mInternalServer = settings.value(QStringLiteral("QMYSQL/StartServer"), defaultInternalServer).toBool();
0130 #ifndef Q_OS_WIN
0131     if (mInternalServer) {
0132         defaultOptions = QStringLiteral("UNIX_SOCKET=%1/%2").arg(socketDirectory, s_mysqlSocketFileName);
0133     }
0134 #endif
0135 
0136     // read settings for current driver
0137     settings.beginGroup(driverName());
0138     mDatabaseName = settings.value(QStringLiteral("Name"), defaultDatabaseName()).toString();
0139     mHostName = settings.value(QStringLiteral("Host"), defaultHostName).toString();
0140     mUserName = settings.value(QStringLiteral("User")).toString();
0141     mPassword = settings.value(QStringLiteral("Password")).toString();
0142     mConnectionOptions = settings.value(QStringLiteral("Options"), defaultOptions).toString();
0143     mDataDir = settings.value(QStringLiteral("DataDir"), defaultDataDir).toString();
0144     mMysqldPath = settings.value(QStringLiteral("ServerPath"), defaultServerPath).toString();
0145     mCleanServerShutdownCommand = settings.value(QStringLiteral("CleanServerShutdownCommand"), defaultCleanShutdownCommand).toString();
0146     settings.endGroup();
0147 
0148     // verify settings and apply permanent changes (written out below)
0149     if (mInternalServer) {
0150         mConnectionOptions = defaultOptions;
0151         // intentionally not namespaced as we are the only one in this db instance when using internal mode
0152         mDatabaseName = QStringLiteral("akonadi");
0153     }
0154     if (mInternalServer && (mMysqldPath.isEmpty() || !QFile::exists(mMysqldPath))) {
0155         mMysqldPath = defaultServerPath;
0156     }
0157 
0158     qCDebug(AKONADISERVER_LOG) << "Using mysqld:" << mMysqldPath;
0159 
0160     if (storeSettings) {
0161         // store back the default values
0162         settings.beginGroup(driverName());
0163         settings.setValue(QStringLiteral("Name"), mDatabaseName);
0164         settings.setValue(QStringLiteral("Host"), mHostName);
0165         settings.setValue(QStringLiteral("Options"), mConnectionOptions);
0166         if (!mMysqldPath.isEmpty()) {
0167             settings.setValue(QStringLiteral("ServerPath"), mMysqldPath);
0168         }
0169         settings.setValue(QStringLiteral("StartServer"), mInternalServer);
0170         settings.setValue(QStringLiteral("DataDir"), mDataDir);
0171         settings.endGroup();
0172         settings.sync();
0173     }
0174 
0175     // apply temporary changes to the settings
0176     if (mInternalServer) {
0177         mHostName.clear();
0178         mUserName.clear();
0179         mPassword.clear();
0180     }
0181 
0182     return true;
0183 }
0184 
0185 bool DbConfigMysql::isAvailable(QSettings &settings)
0186 {
0187     if (!QSqlDatabase::drivers().contains(driverName())) {
0188         return false;
0189     }
0190 
0191     if (!init(settings, false)) {
0192         return false;
0193     }
0194 
0195     if (mInternalServer && (mMysqldPath.isEmpty() || !QFile::exists(mMysqldPath))) {
0196         return false;
0197     }
0198 
0199     return true;
0200 }
0201 
0202 void DbConfigMysql::apply(QSqlDatabase &database)
0203 {
0204     if (!mDatabaseName.isEmpty()) {
0205         database.setDatabaseName(mDatabaseName);
0206     }
0207     if (!mHostName.isEmpty()) {
0208         database.setHostName(mHostName);
0209     }
0210     if (!mUserName.isEmpty()) {
0211         database.setUserName(mUserName);
0212     }
0213     if (!mPassword.isEmpty()) {
0214         database.setPassword(mPassword);
0215     }
0216 
0217     database.setConnectOptions(mConnectionOptions);
0218 
0219     // can we check that during init() already?
0220     Q_ASSERT(database.driver()->hasFeature(QSqlDriver::LastInsertId));
0221 }
0222 
0223 bool DbConfigMysql::useInternalServer() const
0224 {
0225     return mInternalServer;
0226 }
0227 
0228 bool DbConfigMysql::startInternalServer()
0229 {
0230     bool success = true;
0231 
0232     const QString akDir = StandardDirs::saveDir("data");
0233 #ifndef Q_OS_WIN
0234     const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length());
0235     const QString socketFile = QStringLiteral("%1/%2").arg(socketDirectory, s_mysqlSocketFileName);
0236     const QString pidFileName = QStringLiteral("%1/mysql.pid").arg(socketDirectory);
0237 #endif
0238 
0239     // generate config file
0240     const QString globalConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf"));
0241     const QString localConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-local.conf"));
0242     const QString actualConfig = StandardDirs::saveDir("data") + QLatin1StringView("/mysql.conf");
0243     qCDebug(AKONADISERVER_LOG) << " globalConfig : " << globalConfig << " localConfig : " << localConfig << " actualConfig : " << actualConfig;
0244     if (globalConfig.isEmpty()) {
0245         qCCritical(AKONADISERVER_LOG) << "Did not find MySQL server default configuration (mysql-global.conf)";
0246         return false;
0247     }
0248 
0249 #ifdef Q_OS_LINUX
0250     // It is recommended to disable CoW feature when running on Btrfs to improve
0251     // database performance. Disabling CoW only has effect on empty directory (since
0252     // it affects only new files), so we check whether MySQL has not yet been initialized.
0253     QDir dir(mDataDir + QDir::separator() + QLatin1StringView("mysql"));
0254     if (!dir.exists()) {
0255         if (Utils::getDirectoryFileSystem(mDataDir) == QLatin1StringView("btrfs")) {
0256             Utils::disableCoW(mDataDir);
0257         }
0258     }
0259 #endif
0260 
0261     if (mMysqldPath.isEmpty()) {
0262         qCCritical(AKONADISERVER_LOG) << "mysqld not found. Please verify your installation";
0263         return false;
0264     }
0265 
0266     // Get the version of the mysqld server that we'll be using.
0267     // MySQL (but not MariaDB) deprecates and removes command line options in
0268     // patch version releases, so we need to adjust the command line options accordingly
0269     // when running the helper utilities or starting the server
0270     const unsigned int localVersion = parseCommandLineToolsVersion();
0271     if (localVersion == 0x000000) {
0272         qCCritical(AKONADISERVER_LOG) << "Failed to detect mysqld version!";
0273     }
0274     // TODO: Parse "MariaDB" or "MySQL" from the version string instead of relying
0275     // on the version numbers
0276     const bool isMariaDB = localVersion >= MYSQL_VERSION_CHECK(10, 0, 0);
0277     qCDebug(AKONADISERVER_LOG).nospace() << "mysqld reports version " << (localVersion >> 16) << "." << ((localVersion >> 8) & 0x0000FF) << "."
0278                                          << (localVersion & 0x0000FF) << " (" << (isMariaDB ? "MariaDB" : "Oracle MySQL") << ")";
0279 
0280     QFile actualFile(actualConfig);
0281     // update conf only if either global (or local) is newer than actual
0282     if ((QFileInfo(globalConfig).lastModified() > QFileInfo(actualFile).lastModified())
0283         || (QFileInfo(localConfig).lastModified() > QFileInfo(actualFile).lastModified())) {
0284         QFile globalFile(globalConfig);
0285         QFile localFile(localConfig);
0286         if (globalFile.open(QFile::ReadOnly) && actualFile.open(QFile::WriteOnly)) {
0287             actualFile.write(globalFile.readAll());
0288             if (!localConfig.isEmpty()) {
0289                 if (localFile.open(QFile::ReadOnly)) {
0290                     actualFile.write(localFile.readAll());
0291                     localFile.close();
0292                 }
0293             }
0294             globalFile.close();
0295             actualFile.close();
0296         } else {
0297             qCCritical(AKONADISERVER_LOG) << "Unable to create MySQL server configuration file.";
0298             qCCritical(AKONADISERVER_LOG) << "This means that either the default configuration file (mysql-global.conf) was not readable";
0299             qCCritical(AKONADISERVER_LOG) << "or the target file (mysql.conf) could not be written.";
0300             return false;
0301         }
0302     }
0303 
0304     // MySQL doesn't like world writeable config files (which makes sense), but
0305     // our config file somehow ends up being world-writable on some systems for no
0306     // apparent reason nevertheless, so fix that
0307     const QFile::Permissions allowedPerms =
0308         actualFile.permissions() & (QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::WriteGroup | QFile::ReadOther);
0309     if (allowedPerms != actualFile.permissions()) {
0310         actualFile.setPermissions(allowedPerms);
0311     }
0312 
0313     if (mDataDir.isEmpty()) {
0314         qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database data directory";
0315         return false;
0316     }
0317 
0318     if (akDir.isEmpty()) {
0319         qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database log directory";
0320         return false;
0321     }
0322 
0323 #ifndef Q_OS_WIN
0324     if (socketDirectory.isEmpty()) {
0325         qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database misc directory";
0326         return false;
0327     }
0328 
0329     // the socket path must not exceed 103 characters, so check for max dir length right away
0330     if (socketDirectory.length() >= 90) {
0331         qCCritical(AKONADISERVER_LOG) << "MySQL cannot deal with a socket path this long. Path was: " << socketDirectory;
0332         return false;
0333     }
0334 
0335     // If mysql socket file exists, check if also the server process is still running,
0336     // else we can safely remove the socket file (cleanup after a system crash, etc.)
0337     QFile pidFile(pidFileName);
0338     if (QFile::exists(socketFile) && pidFile.open(QIODevice::ReadOnly)) {
0339         qCDebug(AKONADISERVER_LOG) << "Found a mysqld pid file, checking whether the server is still running...";
0340         QByteArray pid = pidFile.readLine().trimmed();
0341         QFile proc(QString::fromLatin1("/proc/" + pid + "/stat"));
0342         // Check whether the process with the PID from pidfile still exists and whether
0343         // it's actually still mysqld or, whether the PID has been recycled in the meanwhile.
0344         bool serverIsRunning = false;
0345         if (proc.open(QIODevice::ReadOnly)) {
0346             const QByteArray stat = proc.readAll();
0347             const QList<QByteArray> stats = stat.split(' ');
0348             if (stats.count() > 1) {
0349                 // Make sure the PID actually belongs to mysql process
0350 
0351                 // Linux trims executable name in /proc filesystem to 15 characters
0352                 const QString expectedProcName = QFileInfo(mMysqldPath).fileName().left(15);
0353                 if (QString::fromLatin1(stats[1]) == QStringLiteral("(%1)").arg(expectedProcName)) {
0354                     // Yup, our mysqld is actually running, so pretend we started the server
0355                     // and try to connect to it
0356                     qCWarning(AKONADISERVER_LOG) << "mysqld for Akonadi is already running, trying to connect to it.";
0357                     serverIsRunning = true;
0358                 }
0359             }
0360             proc.close();
0361         }
0362 
0363         if (!serverIsRunning) {
0364             qCDebug(AKONADISERVER_LOG) << "No mysqld process with specified PID is running. Removing the pidfile and starting a new instance...";
0365             pidFile.close();
0366             pidFile.remove();
0367             QFile::remove(socketFile);
0368         }
0369     }
0370 #endif
0371 
0372     // synthesize the mysqld command
0373     QStringList arguments;
0374     arguments << QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir);
0375     arguments << QStringLiteral("--datadir=%1/").arg(mDataDir);
0376 #ifndef Q_OS_WIN
0377     arguments << QStringLiteral("--socket=%1").arg(socketFile);
0378     arguments << QStringLiteral("--pid-file=%1").arg(pidFileName);
0379 #else
0380     arguments << QString::fromLatin1("--shared-memory");
0381 #endif
0382 
0383 #ifndef Q_OS_WIN
0384     // If mysql socket file does not exists, then we must start the server,
0385     // otherwise we reconnect to it
0386     if (!QFile::exists(socketFile)) {
0387         // move mysql error log file out of the way
0388         const QFileInfo errorLog(mDataDir + QDir::separator() + QLatin1StringView("mysql.err"));
0389         if (errorLog.exists()) {
0390             QFile logFile(errorLog.absoluteFilePath());
0391             QFile oldLogFile(mDataDir + QDir::separator() + QLatin1StringView("mysql.err.old"));
0392             if (logFile.open(QFile::ReadOnly) && oldLogFile.open(QFile::WriteOnly)) {
0393                 oldLogFile.write(logFile.readAll());
0394                 oldLogFile.close();
0395                 logFile.close();
0396                 logFile.remove();
0397             } else {
0398                 qCCritical(AKONADISERVER_LOG) << "Failed to open MySQL error log.";
0399             }
0400         }
0401 
0402         // first run, some MySQL versions need a mysql_install_db run for that
0403         const QString confFile = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf"));
0404         if (QDir(mDataDir).entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()) {
0405             if (isMariaDB) {
0406                 initializeMariaDBDatabase(confFile, mDataDir);
0407             } else if (localVersion >= MYSQL_VERSION_CHECK(5, 7, 6)) {
0408                 initializeMySQL5_7_6Database(confFile, mDataDir);
0409             } else {
0410                 initializeMySQLDatabase(confFile, mDataDir);
0411             }
0412         }
0413 
0414         qCDebug(AKONADISERVER_LOG) << "Executing:" << mMysqldPath << arguments.join(QLatin1Char(' '));
0415         mDatabaseProcess = std::make_unique<QProcess>();
0416         mDatabaseProcess->start(mMysqldPath, arguments);
0417         if (!mDatabaseProcess->waitForStarted()) {
0418             qCCritical(AKONADISERVER_LOG) << "Could not start database server!";
0419             qCCritical(AKONADISERVER_LOG) << "executable:" << mMysqldPath;
0420             qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments;
0421             qCCritical(AKONADISERVER_LOG) << "process error:" << mDatabaseProcess->errorString();
0422             return false;
0423         }
0424 
0425         connect(mDatabaseProcess.get(), &QProcess::finished, this, &DbConfigMysql::processFinished);
0426 
0427         // wait until mysqld has created the socket file (workaround for QTBUG-47475 in Qt5.5.0)
0428         int counter = 50; // avoid an endless loop in case mysqld terminated
0429         while ((counter-- > 0) && !QFileInfo::exists(socketFile)) {
0430             QThread::msleep(100);
0431         }
0432     } else {
0433         qCDebug(AKONADISERVER_LOG) << "Found " << qPrintable(s_mysqlSocketFileName) << " file, reconnecting to the database";
0434     }
0435 #endif
0436 
0437     {
0438         QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QMYSQL"), s_initConnection);
0439         apply(db);
0440 
0441         db.setDatabaseName(QString()); // might not exist yet, then connecting to the actual db will fail
0442         if (!db.isValid()) {
0443             qCCritical(AKONADISERVER_LOG) << "Invalid database object during database server startup";
0444             return false;
0445         }
0446 
0447         bool opened = false;
0448         for (int i = 0; i < 120; ++i) {
0449             opened = db.open();
0450             if (opened) {
0451                 break;
0452             }
0453             if (mDatabaseProcess && mDatabaseProcess->waitForFinished(500)) {
0454                 qCCritical(AKONADISERVER_LOG) << "Database process exited unexpectedly during initial connection!";
0455                 qCCritical(AKONADISERVER_LOG) << "executable:" << mMysqldPath;
0456                 qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments;
0457                 qCCritical(AKONADISERVER_LOG) << "stdout:" << mDatabaseProcess->readAllStandardOutput();
0458                 qCCritical(AKONADISERVER_LOG) << "stderr:" << mDatabaseProcess->readAllStandardError();
0459                 qCCritical(AKONADISERVER_LOG) << "exit code:" << mDatabaseProcess->exitCode();
0460                 qCCritical(AKONADISERVER_LOG) << "process error:" << mDatabaseProcess->errorString();
0461                 return false;
0462             }
0463         }
0464 
0465         if (opened) {
0466             if (!mMysqlCheckPath.isEmpty()) {
0467                 execute(mMysqlCheckPath,
0468                         {QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir),
0469                          QStringLiteral("--check-upgrade"),
0470                          QStringLiteral("--auto-repair"),
0471 #ifndef Q_OS_WIN
0472                          QStringLiteral("--socket=%1/%2").arg(socketDirectory, s_mysqlSocketFileName),
0473 #endif
0474                          mDatabaseName});
0475             }
0476 
0477             if (!mMysqlUpgradePath.isEmpty()) {
0478                 execute(mMysqlUpgradePath,
0479                         {QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir)
0480 #ifndef Q_OS_WIN
0481                              ,
0482                          QStringLiteral("--socket=%1/%2").arg(socketDirectory, s_mysqlSocketFileName)
0483 #endif
0484                         });
0485             }
0486 
0487             // Verify MySQL version
0488             {
0489                 QSqlQuery query(db);
0490                 if (!query.exec(QStringLiteral("SELECT VERSION()")) || !query.first()) {
0491                     qCCritical(AKONADISERVER_LOG) << "Failed to verify database server version";
0492                     qCCritical(AKONADISERVER_LOG) << "Query error:" << query.lastError().text();
0493                     qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text();
0494                     return false;
0495                 }
0496 
0497                 const QString version = query.value(0).toString();
0498                 const QStringList versions = version.split(QLatin1Char('.'), Qt::SkipEmptyParts);
0499                 if (versions.count() < 3) {
0500                     qCCritical(AKONADISERVER_LOG) << "Invalid database server version: " << version;
0501                     return false;
0502                 }
0503 
0504                 if (versions[0].toInt() < MYSQL_MIN_MAJOR || (versions[0].toInt() == MYSQL_MIN_MAJOR && versions[1].toInt() < MYSQL_MIN_MINOR)) {
0505                     qCCritical(AKONADISERVER_LOG) << "Unsupported MySQL version:";
0506                     qCCritical(AKONADISERVER_LOG) << "Current version:" << QStringLiteral("%1.%2").arg(versions[0], versions[1]);
0507                     qCCritical(AKONADISERVER_LOG) << "Minimum required version:" << QStringLiteral("%1.%2").arg(MYSQL_MIN_MAJOR).arg(MYSQL_MIN_MINOR);
0508                     qCCritical(AKONADISERVER_LOG) << "Please update your MySQL database server";
0509                     return false;
0510                 } else {
0511                     qCDebug(AKONADISERVER_LOG) << "MySQL version OK"
0512                                                << "(required" << QStringLiteral("%1.%2").arg(MYSQL_MIN_MAJOR).arg(MYSQL_MIN_MINOR) << ", available"
0513                                                << QStringLiteral("%1.%2").arg(versions[0], versions[1]) << ")";
0514                 }
0515             }
0516 
0517             {
0518                 QSqlQuery query(db);
0519                 if (!query.exec(QStringLiteral("USE %1").arg(mDatabaseName))) {
0520                     qCDebug(AKONADISERVER_LOG) << "Failed to use database" << mDatabaseName;
0521                     qCDebug(AKONADISERVER_LOG) << "Query error:" << query.lastError().text();
0522                     qCDebug(AKONADISERVER_LOG) << "Database error:" << db.lastError().text();
0523                     qCDebug(AKONADISERVER_LOG) << "Trying to create database now...";
0524                     if (!query.exec(QStringLiteral("CREATE DATABASE akonadi"))) {
0525                         qCCritical(AKONADISERVER_LOG) << "Failed to create database";
0526                         qCCritical(AKONADISERVER_LOG) << "Query error:" << query.lastError().text();
0527                         qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text();
0528                         success = false;
0529                     }
0530                 }
0531             } // make sure query is destroyed before we close the db
0532             db.close();
0533         } else {
0534             qCCritical(AKONADISERVER_LOG) << "Failed to connect to database!";
0535             qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text();
0536             success = false;
0537         }
0538     }
0539 
0540     return success;
0541 }
0542 
0543 void DbConfigMysql::processFinished(int exitCode, QProcess::ExitStatus exitStatus)
0544 {
0545     Q_UNUSED(exitCode)
0546     Q_UNUSED(exitStatus)
0547 
0548     qCCritical(AKONADISERVER_LOG) << "database server stopped unexpectedly";
0549 
0550 #ifndef Q_OS_WIN
0551     // when the server stopped unexpectedly, make sure to remove the stale socket file since otherwise
0552     // it can not be started again
0553     const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length());
0554     const QString socketFile = QStringLiteral("%1/%2").arg(socketDirectory, s_mysqlSocketFileName);
0555     QFile::remove(socketFile);
0556 #endif
0557 
0558     QCoreApplication::quit();
0559 }
0560 
0561 void DbConfigMysql::stopInternalServer()
0562 {
0563     if (!mDatabaseProcess) {
0564         return;
0565     }
0566 
0567     // closing initConnection this late to work around QTBUG-63108
0568     QSqlDatabase::removeDatabase(s_initConnection);
0569 
0570     disconnect(mDatabaseProcess.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &DbConfigMysql::processFinished);
0571 
0572     // first, try the nicest approach
0573     if (!mCleanServerShutdownCommand.isEmpty()) {
0574         QProcess::execute(mCleanServerShutdownCommand, QStringList());
0575         if (mDatabaseProcess->waitForFinished(3000)) {
0576             return;
0577         }
0578     }
0579 
0580     mDatabaseProcess->terminate();
0581     const bool result = mDatabaseProcess->waitForFinished(3000);
0582     // We've waited nicely for 3 seconds, to no avail, let's be rude.
0583     if (!result) {
0584         mDatabaseProcess->kill();
0585     }
0586 }
0587 
0588 void DbConfigMysql::initSession(const QSqlDatabase &database)
0589 {
0590     QSqlQuery query(database);
0591     query.exec(QStringLiteral("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"));
0592 }
0593 
0594 int DbConfigMysql::parseCommandLineToolsVersion() const
0595 {
0596     QProcess mysqldProcess;
0597     mysqldProcess.start(mMysqldPath, {QStringLiteral("--version")});
0598     mysqldProcess.waitForFinished(10000 /* 10 secs */);
0599 
0600     const QString out = QString::fromLocal8Bit(mysqldProcess.readAllStandardOutput());
0601     QRegularExpression regexp(QStringLiteral("Ver ([0-9]+)\\.([0-9]+)\\.([0-9]+)"));
0602     auto match = regexp.match(out);
0603     if (!match.hasMatch()) {
0604         return 0;
0605     }
0606 
0607     return (match.capturedView(1).toInt() << 16) | (match.capturedView(2).toInt() << 8) | match.capturedView(3).toInt();
0608 }
0609 
0610 bool DbConfigMysql::initializeMariaDBDatabase(const QString &confFile, const QString &dataDir) const
0611 {
0612     // KDE Neon (and possible others) don't ship mysql_install_db, but it seems
0613     // that MariaDB can initialize itself automatically on first start, it only
0614     // needs that the datadir directory exists
0615     if (mMysqlInstallDbPath.isEmpty()) {
0616         return QDir().mkpath(dataDir);
0617     }
0618 
0619     QFileInfo fi(mMysqlInstallDbPath);
0620     QDir dir = fi.dir();
0621     dir.cdUp();
0622     const QString baseDir = dir.absolutePath();
0623     return 0
0624         == execute(mMysqlInstallDbPath,
0625                    {QStringLiteral("--defaults-file=%1").arg(confFile),
0626                     QStringLiteral("--force"),
0627                     QStringLiteral("--basedir=%1").arg(baseDir),
0628                     QStringLiteral("--datadir=%1/").arg(dataDir)});
0629 }
0630 
0631 /**
0632  * As of MySQL 5.7.6 mysql_install_db is deprecated and mysqld --initailize should be used instead
0633  * See MySQL Reference Manual section 2.10.1.1 (Initializing the Data Directory Manually Using mysqld)
0634  */
0635 bool DbConfigMysql::initializeMySQL5_7_6Database(const QString &confFile, const QString &dataDir) const
0636 {
0637     return 0
0638         == execute(mMysqldPath,
0639                    {QStringLiteral("--defaults-file=%1").arg(confFile), QStringLiteral("--initialize"), QStringLiteral("--datadir=%1/").arg(dataDir)});
0640 }
0641 
0642 bool DbConfigMysql::initializeMySQLDatabase(const QString &confFile, const QString &dataDir) const
0643 {
0644     // On FreeBSD MySQL 5.6 is also installed without mysql_install_db, so this
0645     // might do the trick there as well.
0646     if (mMysqlInstallDbPath.isEmpty()) {
0647         return QDir().mkpath(dataDir);
0648     }
0649 
0650     QFileInfo fi(mMysqlInstallDbPath);
0651     QDir dir = fi.dir();
0652     dir.cdUp();
0653     const QString baseDir = dir.absolutePath();
0654 
0655     // Don't use --force, it has been removed in MySQL 5.7.5
0656     return 0
0657         == execute(
0658                mMysqlInstallDbPath,
0659                {QStringLiteral("--defaults-file=%1").arg(confFile), QStringLiteral("--basedir=%1").arg(baseDir), QStringLiteral("--datadir=%1/").arg(dataDir)});
0660 }
0661 
0662 bool DbConfigMysql::disableConstraintChecks(const QSqlDatabase &db)
0663 {
0664     QSqlQuery query(db);
0665     return query.exec(QStringLiteral("SET FOREIGN_KEY_CHECKS=0"));
0666 }
0667 
0668 bool DbConfigMysql::enableConstraintChecks(const QSqlDatabase &db)
0669 {
0670     QSqlQuery query(db);
0671     return query.exec(QStringLiteral("SET FOREIGN_KEY_CHECKS=1"));
0672 }
0673 
0674 #include "moc_dbconfigmysql.cpp"