diff --git a/src/private/standarddirs.cpp b/src/private/standarddirs.cpp index 45af611aa..84434856f 100644 --- a/src/private/standarddirs.cpp +++ b/src/private/standarddirs.cpp @@ -1,220 +1,222 @@ /* Copyright (c) 2011 Volker Krause Copyright (c) 2018 Daniel Vrátil 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 "standarddirs_p.h" #include "instance_p.h" #include "akonadi-prefix.h" #include "akonadiprivate_debug.h" #include #include #include #include #include #include using namespace Akonadi; namespace { QString buildFullRelPath(const char *resource, const QString &relPath) { QString fullRelPath = QStringLiteral("/akonadi"); #ifdef Q_OS_WIN // On Windows all Generic*Location fall into ~/AppData/Local so we need to disambiguate // inside the "akonadi" folder whether it's data or config. fullRelPath += QLatin1Char('/') + QString::fromLocal8Bit(resource); #else Q_UNUSED(resource); #endif if (Akonadi::Instance::hasIdentifier()) { fullRelPath += QStringLiteral("/instance/") + Akonadi::Instance::identifier(); } if (!relPath.isEmpty()) { fullRelPath += QLatin1Char('/') + relPath; } return fullRelPath; } } QString StandardDirs::configFile(const QString &configFile, FileAccessMode openMode) { const QString savePath = StandardDirs::saveDir("config") + QLatin1Char('/') + configFile; if (openMode == WriteOnly) { return savePath; } auto path = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QStringLiteral("akonadi/") + configFile); // HACK: when using instance namespaces, ignore the non-namespaced file if (Akonadi::Instance::hasIdentifier() && path.startsWith(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation))) { path.clear(); } if (path.isEmpty()) { return savePath; } else if (openMode == ReadOnly || path == savePath) { return path; } // file found in system paths and mode is ReadWrite, thus // we copy to the home path location and return this path QFile::copy(path, savePath); return savePath; } QString StandardDirs::serverConfigFile(FileAccessMode openMode) { return configFile(QStringLiteral("akonadiserverrc"), openMode); } QString StandardDirs::connectionConfigFile(FileAccessMode openMode) { return configFile(QStringLiteral("akonadiconnectionrc"), openMode); } QString StandardDirs::agentsConfigFile(FileAccessMode openMode) { return configFile(QStringLiteral("agentsrc"), openMode); } QString StandardDirs::agentConfigFile(const QString &identifier, FileAccessMode openMode) { return configFile(QStringLiteral("agent_config_") + identifier, openMode); } QString StandardDirs::saveDir(const char *resource, const QString &relPath) { const QString fullRelPath = buildFullRelPath(resource, relPath); QString fullPath; if (qstrncmp(resource, "config", 6) == 0) { fullPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + fullRelPath; } else if (qstrncmp(resource, "data", 4) == 0) { fullPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + fullRelPath; + } else if (qstrncmp(resource, "runtime", 7) == 0) { + fullPath = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + fullRelPath; } else { qt_assert_x(__FUNCTION__, "Invalid resource type", __FILE__, __LINE__); return {}; } // ensure directory exists or is created QFileInfo fileInfo(fullPath); if (fileInfo.exists()) { if (fileInfo.isDir()) { return fullPath; } else { qCWarning(AKONADIPRIVATE_LOG) << "StandardDirs::saveDir: '" << fileInfo.absoluteFilePath() << "' exists but is not a directory"; } } else { if (!QDir::home().mkpath(fileInfo.absoluteFilePath())) { qCWarning(AKONADIPRIVATE_LOG) << "StandardDirs::saveDir: failed to create directory '" << fileInfo.absoluteFilePath() << "'"; } else { return fullPath; } } return {}; } QString StandardDirs::locateResourceFile(const char *resource, const QString &relPath) { const QString fullRelPath = buildFullRelPath(resource, relPath); QVector userLocations; QStandardPaths::StandardLocation genericLocation; QString fallback; if (qstrncmp(resource, "config", 6) == 0) { userLocations = { QStandardPaths::AppConfigLocation, QStandardPaths::ConfigLocation }; genericLocation = QStandardPaths::GenericConfigLocation; fallback = QDir::toNativeSeparators(QStringLiteral(AKONADIPREFIX "/" AKONADICONFIG)); } else if (qstrncmp(resource, "data", 4) == 0) { userLocations = { QStandardPaths::AppLocalDataLocation, QStandardPaths::AppDataLocation }; genericLocation = QStandardPaths::GenericDataLocation; fallback = QDir::toNativeSeparators(QStringLiteral(AKONADIPREFIX "/" AKONADIDATA)); } else { qt_assert_x(__FUNCTION__, "Invalid resource type", __FILE__, __LINE__); return {}; } const auto locateFile = [](QStandardPaths::StandardLocation location, const QString &relPath) -> QString { const auto path = QStandardPaths::locate(location, relPath); if (!path.isEmpty()) { QFileInfo file(path); if (file.exists() && file.isFile() && file.isReadable()) { return path; } } return {}; }; // Always honor instance in user-specific locations for (const auto location : qAsConst(userLocations)) { const auto path = locateFile(location, fullRelPath); if (!path.isEmpty()) { return path; } } // First try instance-specific path in generic locations auto path = locateFile(genericLocation, fullRelPath); if (!path.isEmpty()) { return path; } // Fallback to global instance path in generic locations path = locateFile(genericLocation, QStringLiteral("/akonadi/") + relPath); if (!path.isEmpty()) { return path; } QFile f(fallback + QStringLiteral("/akonadi/") + relPath); if (f.exists()) { return f.fileName(); } return {}; } QStringList StandardDirs::locateAllResourceDirs(const QString &relPath) { auto dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, relPath, QStandardPaths::LocateDirectory); const auto fallback = QDir::toNativeSeparators(QStringLiteral(AKONADIPREFIX "/" AKONADIDATA "/") + relPath); if (!dirs.contains(fallback)) { if (QDir::root().exists(fallback)) { dirs.push_back(fallback); } } return dirs; } QString StandardDirs::findExecutable(const QString &executableName) { QString executable = QStandardPaths::findExecutable(executableName, {qApp->applicationDirPath()}); if (executable.isEmpty()) { executable = QStandardPaths::findExecutable(executableName); } return executable; } diff --git a/src/server/akonadi.cpp b/src/server/akonadi.cpp index b758763bb..4f71a6a53 100644 --- a/src/server/akonadi.cpp +++ b/src/server/akonadi.cpp @@ -1,422 +1,425 @@ /*************************************************************************** * Copyright (C) 2006 by Till Adam * * * * This program 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 program 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 General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "akonadi.h" #include "handler.h" #include "connection.h" #include "serveradaptor.h" #include "akonadiserver_debug.h" #include "cachecleaner.h" #include "intervalcheck.h" #include "storagejanitor.h" #include "storage/dbconfig.h" #include "storage/datastore.h" #include "notificationmanager.h" #include "resourcemanager.h" #include "tracer.h" #include "utils.h" #include "debuginterface.h" #include "storage/itemretrievalmanager.h" #include "storage/collectionstatistics.h" #include "preprocessormanager.h" #include "search/searchmanager.h" #include "search/searchtaskmanager.h" #include "aklocalserver.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace Akonadi::Server; AkonadiServer *AkonadiServer::s_instance = nullptr; AkonadiServer::AkonadiServer(QObject *parent) : QObject(parent) { // Register bunch of useful types qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType("quintptr"); } bool AkonadiServer::init() { qCInfo(AKONADISERVER_LOG) << "Starting up the Akonadi Server..."; const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); QSettings settings(serverConfigFile, QSettings::IniFormat); // Restrict permission to 600, as the file might contain database password in plaintext QFile::setPermissions(serverConfigFile, QFile::ReadOwner | QFile::WriteOwner); if (!DbConfig::configuredDatabase()) { quit(); return false; } if (DbConfig::configuredDatabase()->useInternalServer()) { if (!startDatabaseProcess()) { quit(); return false; } } else { if (!createDatabase()) { quit(); return false; } } DbConfig::configuredDatabase()->setup(); s_instance = this; const QString connectionSettingsFile = StandardDirs::connectionConfigFile(StandardDirs::WriteOnly); QSettings connectionSettings(connectionSettingsFile, QSettings::IniFormat); mCmdServer = new AkLocalServer(this); connect(mCmdServer, QOverload::of(&AkLocalServer::newConnection), this, &AkonadiServer::newCmdConnection); mNotificationManager = new NotificationManager(); mNtfServer = new AkLocalServer(this); // Note: this is a queued connection, as NotificationManager lives in its // own thread connect(mNtfServer, QOverload::of(&AkLocalServer::newConnection), mNotificationManager, &NotificationManager::registerConnection); // TODO: share socket setup with client #ifdef Q_OS_WIN // use the installation prefix as uid QString suffix; if (Instance::hasIdentifier()) { suffix = QStringLiteral("%1-").arg(Instance::identifier()); } suffix += QString::fromUtf8(QUrl::toPercentEncoding(qApp->applicationDirPath())); const QString defaultCmdPipe = QStringLiteral("Akonadi-Cmd-") % suffix; const QString cmdPipe = settings.value(QStringLiteral("Connection/NamedPipe"), defaultCmdPipe).toString(); if (!mCmdServer->listen(cmdPipe)) { qCCritical(AKONADISERVER_LOG) << "Unable to listen on Named Pipe" << cmdPipe << ":" << mCmdServer->errorString(); quit(); return false; } const QString defaultNtfPipe = QStringLiteral("Akonadi-Ntf-") % suffix; const QString ntfPipe = settings.value(QStringLiteral("Connection/NtfNamedPipe"), defaultNtfPipe).toString(); if (!mNtfServer->listen(ntfPipe)) { qCCritical(AKONADISERVER_LOG) << "Unable to listen on Named Pipe" << ntfPipe << ":" << mNtfServer->errorString(); quit(); return false; } connectionSettings.setValue(QStringLiteral("Data/Method"), QStringLiteral("NamedPipe")); connectionSettings.setValue(QStringLiteral("Data/NamedPipe"), cmdPipe); connectionSettings.setValue(QStringLiteral("Notifications/Method"), QStringLiteral("NamedPipe")); connectionSettings.setValue(QStringLiteral("Notifications/NamedPipe"), ntfPipe); #else - const QString socketDir = Utils::preferredSocketDirectory(StandardDirs::saveDir("data")); - const QString cmdSocketFile = socketDir % QStringLiteral("/akonadiserver-cmd.socket"); + const QString cmdSocketName = QStringLiteral("akonadiserver-cmd.socket"); + const QString ntfSocketName = QStringLiteral("akonadiserver-ntf.socket"); + const QString socketDir = Utils::preferredSocketDirectory(StandardDirs::saveDir("data"), + qMax(cmdSocketName.length(), ntfSocketName.length())); + const QString cmdSocketFile = socketDir % QLatin1Char('/') % cmdSocketName; QFile::remove(cmdSocketFile); if (!mCmdServer->listen(cmdSocketFile)) { qCCritical(AKONADISERVER_LOG) << "Unable to listen on Unix socket" << cmdSocketFile << ":" << mCmdServer->errorString(); quit(); return false; } - const QString ntfSocketFile = socketDir % QStringLiteral("/akonadiserver-ntf.socket"); + const QString ntfSocketFile = socketDir % QLatin1Char('/') % ntfSocketName; QFile::remove(ntfSocketFile); if (!mNtfServer->listen(ntfSocketFile)) { qCCritical(AKONADISERVER_LOG) << "Unable to listen on Unix socket" << ntfSocketFile << ":" << mNtfServer->errorString(); quit(); return false; } connectionSettings.setValue(QStringLiteral("Data/Method"), QStringLiteral("UnixPath")); connectionSettings.setValue(QStringLiteral("Data/UnixPath"), cmdSocketFile); connectionSettings.setValue(QStringLiteral("Notifications/Method"), QStringLiteral("UnixPath")); connectionSettings.setValue(QStringLiteral("Notifications/UnixPath"), ntfSocketFile); #endif // initialize the database DataStore *db = DataStore::self(); if (!db->database().isOpen()) { qCCritical(AKONADISERVER_LOG) << "Unable to open database" << db->database().lastError().text(); quit(); return false; } if (!db->init()) { qCCritical(AKONADISERVER_LOG) << "Unable to initialize database."; quit(); return false; } Tracer::self(); new DebugInterface(this); ResourceManager::self(); CollectionStatistics::self(); // Initialize the preprocessor manager PreprocessorManager::init(); // Forcibly disable it if configuration says so if (settings.value(QStringLiteral("General/DisablePreprocessing"), false).toBool()) { PreprocessorManager::instance()->setEnabled(false); } if (settings.value(QStringLiteral("Cache/EnableCleaner"), true).toBool()) { mCacheCleaner = new CacheCleaner(); } mIntervalCheck = new IntervalCheck(); mStorageJanitor = new StorageJanitor(); mItemRetrieval = new ItemRetrievalManager(); mAgentSearchManager = new SearchTaskManager(); const QStringList searchManagers = settings.value(QStringLiteral("Search/Manager"), QStringList() << QStringLiteral("Agent")).toStringList(); mSearchManager = new SearchManager(searchManagers); new ServerAdaptor(this); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Server"), this); const QByteArray dbusAddress = qgetenv("DBUS_SESSION_BUS_ADDRESS"); if (!dbusAddress.isEmpty()) { connectionSettings.setValue(QStringLiteral("DBUS/Address"), QLatin1String(dbusAddress)); } QDBusServiceWatcher *watcher = new QDBusServiceWatcher(DBus::serviceName(DBus::Control), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this); connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &AkonadiServer::serviceOwnerChanged); // Unhide all the items that are actually hidden. // The hidden flag was probably left out after an (abrupt) // server quit. We don't attempt to resume preprocessing // for the items as we don't actually know at which stage the // operation was interrupted... db->unhideAllPimItems(); // We are ready, now register org.freedesktop.Akonadi service to DBus and // the fun can begin if (!QDBusConnection::sessionBus().registerService(DBus::serviceName(DBus::Server))) { qCCritical(AKONADISERVER_LOG) << "Unable to connect to dbus service: " << QDBusConnection::sessionBus().lastError().message(); quit(); return false; } return true; } AkonadiServer::~AkonadiServer() { } template static void quitThread(T &thread) { if (thread) { thread->quit(); thread->wait(); delete thread; thread = nullptr; } } bool AkonadiServer::quit() { if (mAlreadyShutdown) { return true; } mAlreadyShutdown = true; qCDebug(AKONADISERVER_LOG) << "terminating connection threads"; qDeleteAll(mConnections); mConnections.clear(); qCDebug(AKONADISERVER_LOG) << "terminating service threads"; delete mCacheCleaner; delete mIntervalCheck; delete mStorageJanitor; delete mItemRetrieval; delete mAgentSearchManager; delete mSearchManager; delete mNotificationManager; // Terminate the preprocessor manager before the database but after all connections are gone PreprocessorManager::done(); CollectionStatistics::destroy(); if (DbConfig::isConfigured()) { if (DataStore::hasDataStore()) { DataStore::self()->close(); } qCDebug(AKONADISERVER_LOG) << "stopping db process"; stopDatabaseProcess(); } //QSettings settings(StandardDirs::serverConfigFile(), QSettings::IniFormat); const QString connectionSettingsFile = StandardDirs::connectionConfigFile(StandardDirs::WriteOnly); if (!QDir::home().remove(connectionSettingsFile)) { qCCritical(AKONADISERVER_LOG) << "Failed to remove runtime connection config file"; } QTimer::singleShot(0, this, &AkonadiServer::doQuit); return true; } void AkonadiServer::doQuit() { QCoreApplication::exit(); } void AkonadiServer::newCmdConnection(quintptr socketDescriptor) { if (mAlreadyShutdown) { return; } Connection *connection = new Connection(socketDescriptor); connect(connection, &Connection::disconnected, this, &AkonadiServer::connectionDisconnected); mConnections.append(connection); } void AkonadiServer::connectionDisconnected() { auto conn = qobject_cast(sender()); mConnections.removeOne(conn); delete conn; } AkonadiServer *AkonadiServer::instance() { if (!s_instance) { s_instance = new AkonadiServer(); } return s_instance; } bool AkonadiServer::startDatabaseProcess() { if (!DbConfig::configuredDatabase()->useInternalServer()) { qCCritical(AKONADISERVER_LOG) << "Trying to start external database!"; } // create the database directories if they don't exists StandardDirs::saveDir("data"); StandardDirs::saveDir("data", QStringLiteral("file_db_data")); return DbConfig::configuredDatabase()->startInternalServer(); } bool AkonadiServer::createDatabase() { bool success = true; const QLatin1String initCon("initConnection"); QSqlDatabase db = QSqlDatabase::addDatabase(DbConfig::configuredDatabase()->driverName(), initCon); DbConfig::configuredDatabase()->apply(db); db.setDatabaseName(DbConfig::configuredDatabase()->databaseName()); if (!db.isValid()) { qCCritical(AKONADISERVER_LOG) << "Invalid database object during initial database connection"; return false; } if (db.open()) { db.close(); } else { qCCritical(AKONADISERVER_LOG) << "Failed to use database" << DbConfig::configuredDatabase()->databaseName(); qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); qCDebug(AKONADISERVER_LOG) << "Trying to create database now..."; db.close(); db.setDatabaseName(QString()); if (db.open()) { { QSqlQuery query(db); if (!query.exec(QStringLiteral("CREATE DATABASE %1").arg(DbConfig::configuredDatabase()->databaseName()))) { 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 AkonadiServer::stopDatabaseProcess() { if (!DbConfig::configuredDatabase()->useInternalServer()) { // closing initConnection this late to work around QTBUG-63108 QSqlDatabase::removeDatabase(QStringLiteral("initConnection")); return; } DbConfig::configuredDatabase()->stopInternalServer(); } void AkonadiServer::serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) { Q_UNUSED(name); Q_UNUSED(oldOwner); if (newOwner.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "Control process died, committing suicide!"; quit(); } } CacheCleaner *AkonadiServer::cacheCleaner() { return mCacheCleaner; } IntervalCheck *AkonadiServer::intervalChecker() { return mIntervalCheck; } NotificationManager *AkonadiServer::notificationManager() { return mNotificationManager; } QString AkonadiServer::serverPath() const { return StandardDirs::saveDir("config"); } diff --git a/src/server/storage/dbconfigmysql.cpp b/src/server/storage/dbconfigmysql.cpp index dfff6fc29..49edad301 100644 --- a/src/server/storage/dbconfigmysql.cpp +++ b/src/server/storage/dbconfigmysql.cpp @@ -1,628 +1,633 @@ /* 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 #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"))); + 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/mysql.socket shutdown") - .arg(mysqladminPath, StandardDirs::saveDir("data"), socketDirectory); + 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/mysql.socket").arg(socketDirectory); + 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"))); - const QString socketFile = QStringLiteral("%1/mysql.socket").arg(socketDirectory); + 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, + // 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 if (stats[1] == "(mysqld)") { // 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, + // 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")); 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 mysql.socket file, reconnecting to the database"; + 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/mysql.socket").arg(socketDirectory), + 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"))); - const QString socketFile = QStringLiteral("%1/mysql.socket").arg(socketDirectory); + 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); 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/server/utils.cpp b/src/server/utils.cpp index 08623fc00..715408823 100644 --- a/src/server/utils.cpp +++ b/src/server/utils.cpp @@ -1,251 +1,242 @@ /* * Copyright (C) 2010 Tobias Koenig * Copyright (C) 2014 Daniel Vrátil * * 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 "utils.h" #include "akonadiserver_debug.h" +#include "instance_p.h" #include #include #include #include #include #if !defined(Q_OS_WIN) #include #include +#include #include -#include #include static QString akonadiSocketDirectory(); static bool checkSocketDirectory(const QString &path); -static bool createSocketDirectory(const QString &link, const QString &tmpl); +static bool createSocketDirectory(const QString &link, const QString &identifier); #endif #ifdef Q_OS_LINUX #include #include #include #include #endif using namespace Akonadi; using namespace Akonadi::Server; -QString Utils::preferredSocketDirectory(const QString &defaultDirectory) +QString Utils::preferredSocketDirectory(const QString &defaultDirectory, int fnLengthHint) { const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); const QSettings serverSettings(serverConfigFile, QSettings::IniFormat); #if defined(Q_OS_WIN) const QString socketDir = serverSettings.value(QLatin1String("Connection/SocketDirectory"), defaultDirectory).toString(); #else QString socketDir = defaultDirectory; if (!serverSettings.contains(QStringLiteral("Connection/SocketDirectory"))) { // if no socket directory is defined, use the symlinked from /tmp socketDir = akonadiSocketDirectory(); if (socketDir.isEmpty()) { // if that does not work, fall back on default socketDir = defaultDirectory; } } else { socketDir = serverSettings.value(QStringLiteral("Connection/SocketDirectory"), defaultDirectory).toString(); } if (socketDir.contains(QLatin1String("$USER"))) { const QString userName = QString::fromLocal8Bit(qgetenv("USER")); if (!userName.isEmpty()) { socketDir.replace(QLatin1String("$USER"), userName); } } if (socketDir[0] != QLatin1Char('/')) { QDir::home().mkdir(socketDir); socketDir = QDir::homePath() + QLatin1Char('/') + socketDir; } QFileInfo dirInfo(socketDir); if (!dirInfo.exists()) { QDir::home().mkpath(dirInfo.absoluteFilePath()); } + + if (socketDir.length() + 1 + fnLengthHint >= static_cast(sizeof(sockaddr_un::sun_path))) { + qCCritical(AKONADISERVER_LOG) << "akonadiSocketDirectory() length is too long to be used by the system."; + } #endif return socketDir; } #if !defined(Q_OS_WIN) QString akonadiSocketDirectory() { const QString hostname = QHostInfo::localHostName(); if (hostname.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "QHostInfo::localHostName() failed"; return QString(); } - const uid_t uid = getuid(); - const struct passwd *pw_ent = getpwuid(uid); - if (!pw_ent) { - qCCritical(AKONADISERVER_LOG) << "Could not get passwd entry for user id" << uid; - return QString(); - } - - const QString link = StandardDirs::saveDir("data") + QLatin1Char('/') + QLatin1String("socket-") + hostname; - QString tmpl = QLatin1String("akonadi-") + QString::fromLocal8Bit(pw_ent->pw_name) + QLatin1String(".XXXXXX"); - - // Workaround for QLocalServer encoding bug - // basically replace non-latin characters - tmpl = QString::fromLatin1(tmpl.toLatin1()); + const QString identifier = Instance::hasIdentifier() ? Instance::identifier() : QLatin1String("default"); + const QString link = StandardDirs::saveDir("data") + QStringLiteral("/socket-%1-%2").arg(hostname, identifier); if (checkSocketDirectory(link)) { return QFileInfo(link).symLinkTarget(); } - if (createSocketDirectory(link, tmpl)) { + if (createSocketDirectory(link, identifier)) { return QFileInfo(link).symLinkTarget(); } qCCritical(AKONADISERVER_LOG) << "Could not create socket directory for Akonadi."; return QString(); } static bool checkSocketDirectory(const QString &path) { QFileInfo info(path); if (!info.exists()) { return false; } if (info.isSymLink()) { info = QFileInfo(info.symLinkTarget()); } if (!info.isDir()) { return false; } if (info.ownerId() != getuid()) { return false; } return true; } -static bool createSocketDirectory(const QString &link, const QString &tmpl) +static bool createSocketDirectory(const QString &link, const QString &identifier) { - QString directory = QStringLiteral("%1%2%3").arg(QDir::tempPath()).arg(QDir::separator()).arg(tmpl); + const QString directory = QStringLiteral("%1/%2").arg(StandardDirs::saveDir("runtime"), identifier); - QByteArray directoryString = directory.toLocal8Bit(); - - if (!mkdtemp(directoryString.data())) { - qCCritical(AKONADISERVER_LOG) << "Creating socket directory with template" << directoryString << "failed:" << strerror(errno); + if (!QDir().mkpath(directory)) { + qCCritical(AKONADISERVER_LOG) << "Creating socket directory with name" << directory << "failed:" << strerror(errno); return false; } - directory = QString::fromLocal8Bit(directoryString); - QFile::remove(link); if (!QFile::link(directory, link)) { qCCritical(AKONADISERVER_LOG) << "Creating symlink from" << directory << "to" << link << "failed"; return false; } return true; } #endif QString Utils::getDirectoryFileSystem(const QString &directory) { #ifndef Q_OS_LINUX + Q_UNUSED(directory); return QString(); #else QString bestMatchPath; QString bestMatchFS; FILE *mtab = setmntent("/etc/mtab", "r"); if (!mtab) { return QString(); } while (mntent *mnt = getmntent(mtab)) { if (qstrcmp(mnt->mnt_type, MNTTYPE_IGNORE) == 0) { continue; } const QString dir = QString::fromLocal8Bit(mnt->mnt_dir); if (!directory.startsWith(dir) || dir.length() < bestMatchPath.length()) { continue; } bestMatchPath = dir; bestMatchFS = QString::fromLocal8Bit(mnt->mnt_type); } endmntent(mtab); return bestMatchFS; #endif } void Utils::disableCoW(const QString &path) { #ifndef Q_OS_LINUX Q_UNUSED(path); #else qCDebug(AKONADISERVER_LOG) << "Detected Btrfs, disabling copy-on-write on database files"; // from linux/fs.h, so that Akonadi does not depend on Linux header files #ifndef FS_IOC_GETFLAGS #define FS_IOC_GETFLAGS _IOR('f', 1, long) #endif #ifndef FS_IOC_SETFLAGS #define FS_IOC_SETFLAGS _IOW('f', 2, long) #endif // Disable COW on file #ifndef FS_NOCOW_FL #define FS_NOCOW_FL 0x00800000 #endif ulong flags = 0; const int fd = open(qPrintable(path), O_RDONLY); if (fd == -1) { qCWarning(AKONADISERVER_LOG) << "Failed to open" << path << "to modify flags (" << errno << ")"; return; } if (ioctl(fd, FS_IOC_GETFLAGS, &flags) == -1) { qCWarning(AKONADISERVER_LOG) << "ioctl error: failed to get file flags (" << errno << ")"; close(fd); return; } if (!(flags & FS_NOCOW_FL)) { flags |= FS_NOCOW_FL; if (ioctl(fd, FS_IOC_SETFLAGS, &flags) == -1) { qCWarning(AKONADISERVER_LOG) << "ioctl error: failed to set file flags (" << errno << ")"; close(fd); return; } } close(fd); #endif } diff --git a/src/server/utils.h b/src/server/utils.h index fe268b0d3..0c6256a1f 100644 --- a/src/server/utils.h +++ b/src/server/utils.h @@ -1,125 +1,127 @@ /* * 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. * */ #ifndef UTILS_H #define UTILS_H #include #include #include #include #include "storage/datastore.h" #include "storage/dbtype.h" namespace Akonadi { namespace Server { namespace Utils { /** * Converts a QVariant to a QString depending on its internal type. */ static inline QString variantToString(const QVariant &variant) { if (variant.type() == QVariant::String) { return variant.toString(); } else if (variant.type() == QVariant::ByteArray) { return QString::fromUtf8(variant.toByteArray()); } else { qWarning("Unable to convert variant of type %s to QString", variant.typeName()); Q_ASSERT(false); return QString(); } } /** * Converts a QVariant to a QByteArray depending on its internal type. */ static inline QByteArray variantToByteArray(const QVariant &variant) { if (variant.type() == QVariant::String) { return variant.toString().toUtf8(); } else if (variant.type() == QVariant::ByteArray) { return variant.toByteArray(); } else { qWarning("Unable to convert variant of type %s to QByteArray", variant.typeName()); Q_ASSERT(false); return QByteArray(); } } static inline QDateTime variantToDateTime(const QVariant &variant) { if (variant.canConvert(QVariant::DateTime)) { // MySQL and SQLite backends read the datetime from the database and // assume it's local time. We stored it as UTC though, so we just need // to change the interpretation in QDateTime. // PostgreSQL on the other hand reads the datetime and assumes it's // UTC(?) and converts it to local time via QDateTime::toLocalTime(), // so we need to convert it back to UTC manually. switch (DbType::type(DataStore::self()->database())) { case DbType::MySQL: case DbType::Sqlite: { QDateTime dt = variant.toDateTime(); dt.setTimeSpec(Qt::UTC); return dt; } case DbType::PostgreSQL: return variant.toDateTime().toUTC(); default: Q_UNREACHABLE(); } } else { qWarning("Unable to convert variant of type %s to QDateTime", variant.typeName()); Q_ASSERT(false); return QDateTime(); } } /** * Returns the socket @p directory that is passed to this method or the one * the user has overwritten via the config file. + * The passed @p fnLengthHint will also ensure the absolute file path length of the + * directory + separator + hint would not overflow the system limitation. */ -QString preferredSocketDirectory(const QString &directory); +QString preferredSocketDirectory(const QString &directory, int fnLengthHint = -1); /** * Returns name of filesystem that @p directory is stored on. This * only works on Linux and returns empty string on other platforms or when it's * unable to detect the filesystem. */ QString getDirectoryFileSystem(const QString &directory); /** * Disables filesystem copy-on-write feature on given file or directory. * Only works on Linux and does nothing on other platforms. * * It was tested only with Btrfs but in theory can be called on any FS that * supports NOCOW. */ void disableCoW(const QString &path); } // namespace Utils } // namespace Server } // namespace Akonadi #endif