7#include "dbconfigpostgresql.h"
8#include "akonadiserver_debug.h"
11#include "private/standarddirs_p.h"
12#include "shared/akranges.h"
15#include <QDirIterator>
17#include <QRegularExpression>
18#include <QRegularExpressionMatch>
22#include <QStandardPaths>
24#include <config-akonadi.h>
31using namespace std::chrono_literals;
34using namespace Akonadi::Server;
35using namespace AkRanges;
40const QString s_initConnection = QStringLiteral(
"initConnectionPsql");
44DbConfigPostgresql::DbConfigPostgresql(
const QString &configFile)
49QString DbConfigPostgresql::driverName()
const
51 return QStringLiteral(
"QPSQL");
54QString DbConfigPostgresql::databaseName()
const
59QString DbConfigPostgresql::databasePath()
const
64void DbConfigPostgresql::setDatabasePath(
const QString &path, QSettings &settings)
68 settings.
setValue(QStringLiteral(
"PgData"), mPgData);
75struct VersionCompare {
76 bool operator()(
const QFileInfo &lhsFi,
const QFileInfo &rhsFi)
const
78 const auto lhs = parseVersion(lhsFi.
fileName());
79 if (!lhs.has_value()) {
82 const auto rhs = parseVersion(rhsFi.
fileName());
83 if (!rhs.has_value()) {
87 return std::tie(lhs->major, lhs->minor) < std::tie(rhs->major, rhs->minor);
95 std::optional<Version> parseVersion(
const QString &name)
const
102 const auto major = QStringView(name).left(dotIdx).toInt(&ok);
106 const auto minor = QStringView(name).mid(dotIdx + 1).toInt(&ok);
116QStringList DbConfigPostgresql::postgresSearchPaths(
const QString &versionedPath)
const
121 const QString
dir(QStringLiteral(POSTGRES_PATH));
122 if (QDir(dir).exists()) {
123 paths.
push_back(QStringLiteral(POSTGRES_PATH));
126 paths << QStringLiteral(
"/usr/bin") << QStringLiteral(
"/usr/sbin") << QStringLiteral(
"/usr/local/sbin");
130 QDir versionedDir(versionedPath);
131 if (versionedDir.exists()) {
133 qDebug() << versionedDirs;
134 std::sort(versionedDirs.begin(), versionedDirs.end(), VersionCompare());
135 std::reverse(versionedDirs.begin(), versionedDirs.end());
136 paths += versionedDirs | Views::transform([](
const auto &dir) -> QString {
137 return dir.absoluteFilePath() + QStringLiteral(
"/bin");
145bool DbConfigPostgresql::init(QSettings &settings,
bool storeSettings,
const QString &dbPathOverride)
148 QString defaultHostName;
149 QString defaultOptions;
150 QString defaultServerPath;
151 QString defaultInitDbPath;
152 QString defaultPgUpgradePath;
153 QString defaultPgData;
156 const bool defaultInternalServer =
true;
158 const bool defaultInternalServer =
false;
161 mInternalServer = settings.
value(QStringLiteral(
"QPSQL/StartServer"), defaultInternalServer).
toBool();
162 if (mInternalServer) {
163 const auto paths = postgresSearchPaths(QStringLiteral(
"/usr/lib/postgresql"));
167 defaultHostName = Utils::preferredSocketDirectory(StandardDirs::saveDir(
"data", QStringLiteral(
"db_misc")));
169 defaultPgData = dbPathOverride.
isEmpty() ? StandardDirs::saveDir(
"data", QStringLiteral(
"db_data")) : dbPathOverride;
175 if (mDatabaseName.isEmpty()) {
178 mHostName = settings.
value(QStringLiteral(
"Host"), defaultHostName).
toString();
179 if (mHostName.isEmpty()) {
180 mHostName = defaultHostName;
182 mHostPort = settings.
value(QStringLiteral(
"Port")).
toInt();
184 mUserName = settings.
value(QStringLiteral(
"User")).
toString();
185 mPassword = settings.
value(QStringLiteral(
"Password")).
toString();
186 mConnectionOptions = settings.
value(QStringLiteral(
"Options"), defaultOptions).
toString();
187 mServerPath = settings.
value(QStringLiteral(
"ServerPath"), defaultServerPath).
toString();
188 if (mInternalServer && mServerPath.isEmpty()) {
189 mServerPath = defaultServerPath;
191 qCDebug(AKONADISERVER_LOG) <<
"Found pg_ctl:" << mServerPath;
192 mInitDbPath = settings.
value(QStringLiteral(
"InitDbPath"), defaultInitDbPath).
toString();
193 if (mInternalServer && mInitDbPath.isEmpty()) {
194 mInitDbPath = defaultInitDbPath;
196 qCDebug(AKONADISERVER_LOG) <<
"Found initdb:" << mServerPath;
197 mPgUpgradePath = settings.
value(QStringLiteral(
"UpgradePath"), defaultPgUpgradePath).
toString();
198 if (mInternalServer && mPgUpgradePath.isEmpty()) {
199 mPgUpgradePath = defaultPgUpgradePath;
201 qCDebug(AKONADISERVER_LOG) <<
"Found pg_upgrade:" << mPgUpgradePath;
202 mPgData = settings.
value(QStringLiteral(
"PgData"), defaultPgData).
toString();
203 if (mPgData.isEmpty()) {
204 mPgData = defaultPgData;
211 settings.
setValue(QStringLiteral(
"Name"), mDatabaseName);
212 settings.
setValue(QStringLiteral(
"Host"), mHostName);
214 settings.
setValue(QStringLiteral(
"Port"), mHostPort);
216 settings.
setValue(QStringLiteral(
"Options"), mConnectionOptions);
217 settings.
setValue(QStringLiteral(
"ServerPath"), mServerPath);
218 settings.
setValue(QStringLiteral(
"InitDbPath"), mInitDbPath);
219 settings.
setValue(QStringLiteral(
"StartServer"), mInternalServer);
220 settings.
setValue(QStringLiteral(
"PgData"), mPgData);
228bool DbConfigPostgresql::isAvailable(QSettings &settings)
234 if (!init(settings,
false)) {
238 if (mInternalServer) {
251void DbConfigPostgresql::apply(QSqlDatabase &database)
253 if (!mDatabaseName.isEmpty()) {
256 if (!mHostName.isEmpty()) {
259 if (mHostPort > 0 && mHostPort < 65535) {
262 if (!mUserName.isEmpty()) {
265 if (!mPassword.isEmpty()) {
275bool DbConfigPostgresql::useInternalServer()
const
277 return mInternalServer;
280std::optional<DbConfigPostgresql::Versions> DbConfigPostgresql::checkPgVersion()
const
283 QFile pgVersionFile(QStringLiteral(
"%1/PG_VERSION").arg(mPgData));
287 const auto clusterVersion = pgVersionFile.readAll().toInt();
298 QRegularExpression re(QStringLiteral(
"\\(PostgreSQL\\) ([0-9]+).[0-9]+"));
299 const auto match = re.match(output);
300 if (!
match.hasMatch()) {
303 const auto serverVersion =
match.captured(1).toInt();
305 qDebug(AKONADISERVER_LOG) <<
"Detected psql versions - cluster:" << clusterVersion <<
", server:" << serverVersion;
306 return {{clusterVersion, serverVersion}};
309bool DbConfigPostgresql::runInitDb(
const QString &newDbPath)
312 if (!QDir(newDbPath).exists()) {
313 if (!QDir().
mkpath(newDbPath)) {
322 if (Utils::getDirectoryFileSystem(newDbPath) == QLatin1StringView(
"btrfs")) {
323 Utils::disableCoW(newDbPath);
328 return execute(mInitDbPath, {QStringLiteral(
"--pgdata=%1").arg(newDbPath), QStringLiteral(
"--encoding=UTF8"), QStringLiteral(
"--no-locale")}) == 0;
333std::optional<QString> findBinPathForVersion(
int version)
336 const auto oldBinSearchPaths = {
337 QStringLiteral(
"/usr/lib64/pgsql/postgresql-%1/bin").arg(version),
338 QStringLiteral(
"/usr/lib/pgsql/postgresql-%1/bin").arg(version),
339 QStringLiteral(
"/usr/lib/postgresql/%1/bin").arg(version),
340 QStringLiteral(
"/opt/pgsql-%1/bin").arg(version),
344 for (
const auto &path : oldBinSearchPaths) {
345 if (QDir(path).exists()) {
353bool checkAndRemoveTmpCluster(
const QDir &baseDir,
const QString &clusterName)
355 if (baseDir.
exists(clusterName)) {
356 qCInfo(AKONADISERVER_LOG) <<
"Postgres cluster update:" << clusterName <<
"cluster already exists, trying to remove it first";
358 qCWarning(AKONADISERVER_LOG) <<
"Postgres cluster update: failed to remove" << clusterName
359 <<
"cluster from some previous run, not performing auto-upgrade";
366bool runPgUpgrade(
const QString &pgUpgrade,
368 const QString &oldBinPath,
369 const QString &newBinPath,
370 const QString &oldDbData,
371 const QString &newDbData)
374 const QStringList args = {QString(QStringLiteral(
"--old-bindir=%1").arg(oldBinPath)),
375 QString(QStringLiteral(
"--new-bindir=%1").arg(newBinPath)),
376 QString(QStringLiteral(
"--old-datadir=%1").arg(oldDbData)),
377 QString(QStringLiteral(
"--new-datadir=%1").arg(newDbData))};
378 qCInfo(AKONADISERVER_LOG) <<
"Postgres cluster update: starting pg_upgrade to upgrade your Akonadi DB cluster";
379 qCDebug(AKONADISERVER_LOG) <<
"Executing pg_upgrade" << QStringList(args);
381 process.
start(pgUpgrade, args);
384 qCWarning(AKONADISERVER_LOG) <<
"Postgres cluster update: pg_upgrade finished with exit code" << process.
exitCode()
385 <<
", please run migration manually.";
389 qCDebug(AKONADISERVER_LOG) <<
"Postgres cluster update: pg_upgrade finished successfully.";
393bool swapClusters(QDir &baseDir,
const QString &oldDbDataCluster,
const QString &newDbDataCluster)
396 if (!baseDir.
rename(QStringLiteral(
"db_data"), oldDbDataCluster)) {
397 qCWarning(AKONADISERVER_LOG) <<
"Postgres cluster update: failed to rename old db_data to" << oldDbDataCluster;
400 if (!baseDir.
rename(newDbDataCluster, QStringLiteral(
"db_data"))) {
401 qCWarning(AKONADISERVER_LOG) <<
"Postgres cluster update: failed to rename" << newDbDataCluster <<
"to db_data, rolling back";
402 if (!baseDir.
rename(oldDbDataCluster, QStringLiteral(
"db_data"))) {
403 qCWarning(AKONADISERVER_LOG) <<
"Postgres cluster update: failed to roll back from" << oldDbDataCluster <<
"to db_data.";
406 qCDebug(AKONADISERVER_LOG) <<
"Postgres cluster update: rollback successful.";
415bool DbConfigPostgresql::upgradeCluster(
int clusterVersion)
417 const auto oldDbDataCluster = QStringLiteral(
"old_db_data");
418 const auto newDbDataCluster = QStringLiteral(
"new_db_data");
420 QDir baseDir(mPgData);
423 const auto oldBinPath = findBinPathForVersion(clusterVersion);
424 if (!oldBinPath.has_value()) {
425 qCDebug(AKONADISERVER_LOG) <<
"Postgres cluster update: failed to find Postgres server for version" << clusterVersion;
428 const auto newBinPath = QFileInfo(mServerPath).path();
430 if (!checkAndRemoveTmpCluster(baseDir, oldDbDataCluster)) {
433 if (!checkAndRemoveTmpCluster(baseDir, newDbDataCluster)) {
439 qCInfo(AKONADISERVER_LOG) <<
"Postgres cluster upgrade: creating a new cluster for current Postgres server";
440 if (!runInitDb(newDbData)) {
441 qCWarning(AKONADISERVER_LOG) <<
"Postgres cluster update: failed to initialize new db cluster";
446 if (!runPgUpgrade(mPgUpgradePath, baseDir, *oldBinPath, newBinPath, mPgData, newDbData)) {
450 if (!swapClusters(baseDir, oldDbDataCluster, newDbDataCluster)) {
456 qCInfo(AKONADISERVER_LOG) <<
"Postgres cluster update: failed to remove" << oldDbDataCluster <<
"cluster (not an issue, continuing)";
462bool DbConfigPostgresql::startInternalServer()
465 const QString socketDir = mHostName;
470 QDir().mkpath(socketDir);
479 QFile postmaster(QStringLiteral(
"%1/postmaster.pid").arg(mPgData));
481 qCDebug(AKONADISERVER_LOG) <<
"Found a postmaster.pid pidfile, checking whether the server is still running...";
482 QByteArray pid = postmaster.readLine();
490 const QByteArray
stat = proc.readAll();
491 const QList<QByteArray> stats =
stat.split(
' ');
492 if (stats.
count() > 1) {
494 if (stats[1] ==
"(postgres)") {
497 qCWarning(AKONADISERVER_LOG) <<
"PostgreSQL for Akonadi is already running, trying to connect to it.";
504 qCDebug(AKONADISERVER_LOG) <<
"No postgres process with specified PID is running. Removing the pidfile and starting a new Postgres instance...";
511 if (!
QFile::exists(QStringLiteral(
"%1/PG_VERSION").arg(mPgData))) {
516 if (Utils::getDirectoryFileSystem(mPgData) == QLatin1StringView(
"btrfs")) {
517 Utils::disableCoW(mPgData);
521 execute(mInitDbPath, {QStringLiteral(
"--pgdata=%1").arg(mPgData), QStringLiteral(
"--encoding=UTF8"), QStringLiteral(
"--no-locale")});
523 const auto versions = checkPgVersion();
524 if (versions.has_value() && (versions->clusterVersion < versions->pgServerVersion)) {
525 qCInfo(AKONADISERVER_LOG) <<
"Cluster PG_VERSION is" << versions->clusterVersion <<
", PostgreSQL server is version " << versions->pgServerVersion
526 <<
", will attempt to upgrade the cluster";
527 if (upgradeCluster(versions->clusterVersion)) {
528 qCInfo(AKONADISERVER_LOG) <<
"Successfully upgraded db cluster from Postgres" << versions->clusterVersion <<
"to" << versions->pgServerVersion;
530 qCWarning(AKONADISERVER_LOG) <<
"Postgres db cluster upgrade failed, Akonadi will fail to start. Sorry.";
536 QStringList arguments;
537 arguments << QStringLiteral(
"start") << QStringLiteral(
"-w") << QStringLiteral(
"--timeout=10")
538 << QStringLiteral(
"--pgdata=%1").arg(mPgData)
542 << QStringLiteral(
"-o \"-k%1\" -h ''").arg(socketDir);
544 qCDebug(AKONADISERVER_LOG) <<
"Executing:" << mServerPath << arguments.
join(QLatin1Char(
' '));
546 pgCtl.
start(mServerPath, arguments);
548 qCCritical(AKONADISERVER_LOG) <<
"Could not start database server!";
549 qCCritical(AKONADISERVER_LOG) <<
"executable:" << mServerPath;
550 qCCritical(AKONADISERVER_LOG) <<
"arguments:" << arguments;
551 qCCritical(AKONADISERVER_LOG) <<
"process error:" << pgCtl.
errorString();
563 qCCritical(AKONADISERVER_LOG) <<
"Invalid database object during database server startup";
568 for (
int i = 0; i < 120; ++i) {
575 qCCritical(AKONADISERVER_LOG) <<
"Database process exited unexpectedly during initial connection!";
576 qCCritical(AKONADISERVER_LOG) <<
"executable:" << mServerPath;
577 qCCritical(AKONADISERVER_LOG) <<
"arguments:" << arguments;
580 qCCritical(AKONADISERVER_LOG) <<
"exit code:" << pgCtl.
exitCode();
581 qCCritical(AKONADISERVER_LOG) <<
"process error:" << pgCtl.
errorString();
591 query.exec(QStringLiteral(
"SELECT 1 FROM pg_catalog.pg_database WHERE datname = '%1'").arg(mDatabaseName));
595 if (!
query.exec(QStringLiteral(
"CREATE DATABASE %1").arg(mDatabaseName))) {
596 qCCritical(AKONADISERVER_LOG) <<
"Failed to create database";
597 qCCritical(AKONADISERVER_LOG) <<
"Query error:" <<
query.lastError().text();
598 qCCritical(AKONADISERVER_LOG) <<
"Database error:" << db.
lastError().
text();
613void DbConfigPostgresql::stopInternalServer()
615 if (!checkServerIsRunning()) {
616 qCDebug(AKONADISERVER_LOG) <<
"Database is no longer running";
621 execute(mServerPath, {QStringLiteral(
"stop"), QStringLiteral(
"--pgdata=%1").arg(mPgData), QStringLiteral(
"--mode=fast")});
622 if (!checkServerIsRunning()) {
627 execute(mServerPath, {QStringLiteral(
"stop"), QStringLiteral(
"--pgdata=%1").arg(mPgData), QStringLiteral(
"--mode=immediate")});
628 if (!checkServerIsRunning()) {
636 const QString pidFileName = QStringLiteral(
"%1/postmaster.pid").arg(mPgData);
637 QFile pidFile(pidFileName);
640 qCCritical(AKONADISERVER_LOG) <<
"The postmaster is still running. Killing it.";
642 execute(mServerPath, {QStringLiteral(
"kill"), QStringLiteral(
"ABRT"), postmasterPid});
646bool DbConfigPostgresql::checkServerIsRunning()
648 const QString command = mServerPath;
649 QStringList arguments;
650 arguments << QStringLiteral(
"status") << QStringLiteral(
"--pgdata=%1").arg(mPgData);
663bool DbConfigPostgresql::disableConstraintChecks(
const QSqlDatabase &db)
665 for (
const auto &table : db.
tables()) {
666 qCDebug(AKONADISERVER_LOG) <<
"Disabling triggers on table" << table;
668 if (!
query.exec(QStringLiteral(
"ALTER TABLE %1 DISABLE TRIGGER ALL").arg(table))) {
669 qCWarning(AKONADISERVER_LOG) <<
"Failed to disable triggers on table" << table <<
":" <<
query.lastError().databaseText();
670 enableConstraintChecks(db);
678bool DbConfigPostgresql::enableConstraintChecks(
const QSqlDatabase &db)
680 for (
const auto &table : db.
tables()) {
681 qCDebug(AKONADISERVER_LOG) <<
"Enabling triggers on table" << table;
683 if (!
query.exec(QStringLiteral(
"ALTER TABLE %1 ENABLE TRIGGER ALL").arg(table))) {
684 qCWarning(AKONADISERVER_LOG) <<
"Failed to enable triggers on table" << table <<
":" <<
query.lastError().databaseText();
A base class that provides an unique access layer to configuration and initialization of different da...
static QString defaultDatabaseName()
Returns the suggested default database name, if none is specified in the configuration already.
int execute(const QString &cmd, const QStringList &args) const
Calls QProcess::execute() and also prints the command and arguments via qCDebug()
Helper integration between Akonadi and Qt.
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT StatJob * stat(const QUrl &url, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT MkpathJob * mkpath(const QUrl &url, const QUrl &baseUrl=QUrl(), JobFlags flags=DefaultFlags)
QString path(const QString &relativePath)
KIOCORE_EXPORT QString dir(const QString &fileClass)
QString name(StandardAction id)
bool exists() const const
QString path() const const
bool rename(const QString &oldName, const QString &newName)
bool exists() const const
QString fileName() const const
QString errorString() const const
qsizetype count() const const
void push_back(parameter_type value)
int exitCode() const const
QByteArray readAllStandardError()
QByteArray readAllStandardOutput()
void setWorkingDirectory(const QString &dir)
void start(OpenMode mode)
bool waitForFinished(int msecs)
bool waitForStarted(int msecs)
void beginGroup(QAnyStringView prefix)
void setValue(QAnyStringView key, const QVariant &value)
QVariant value(QAnyStringView key) const const
QSqlDatabase addDatabase(QSqlDriver *driver, const QString &connectionName)
QSqlDriver * driver() const const
bool isValid() const const
QSqlError lastError() const const
void removeDatabase(const QString &connectionName)
void setConnectOptions(const QString &options)
void setDatabaseName(const QString &name)
void setHostName(const QString &host)
void setPassword(const QString &password)
void setUserName(const QString &name)
QStringList tables(QSql::TableType type) const const
virtual bool hasFeature(DriverFeature feature) const const=0
QString text() const const
QString findExecutable(const QString &executableName, const QStringList &paths)
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString join(QChar separator) const const
bool toBool() const const
int toInt(bool *ok) const const
QString toString() const const