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