Akonadi

dbconfigpostgresql.cpp
1/*
2 SPDX-FileCopyrightText: 2010 Tobias Koenig <tokoe@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "dbconfigpostgresql.h"
8#include "akonadiserver_debug.h"
9#include "utils.h"
10
11#include "private/standarddirs_p.h"
12#include "shared/akranges.h"
13
14#include <QDir>
15#include <QDirIterator>
16#include <QProcess>
17#include <QRegularExpression>
18#include <QRegularExpressionMatch>
19#include <QSqlDriver>
20#include <QSqlError>
21#include <QSqlQuery>
22#include <QStandardPaths>
23
24#include <config-akonadi.h>
25#if HAVE_UNISTD_H
26#include <unistd.h>
27#endif
28#include <chrono>
29#include <filesystem>
30
31using namespace std::chrono_literals;
32
33using namespace Akonadi;
34using namespace Akonadi::Server;
35using namespace AkRanges;
36
37namespace
38{
39
40const QString s_initConnection = QStringLiteral("initConnectionPsql");
41
42} // namespace
43
44DbConfigPostgresql::DbConfigPostgresql(const QString &configFile)
45 : DbConfig(configFile)
46{
47}
48
49QString DbConfigPostgresql::driverName() const
50{
51 return QStringLiteral("QPSQL");
52}
53
54QString DbConfigPostgresql::databaseName() const
55{
56 return mDatabaseName;
57}
58
59QString DbConfigPostgresql::databasePath() const
60{
61 return mPgData;
62}
63
64void DbConfigPostgresql::setDatabasePath(const QString &path, QSettings &settings)
65{
66 mPgData = path;
67 settings.beginGroup(driverName());
68 settings.setValue(QStringLiteral("PgData"), mPgData);
69 settings.endGroup();
70 settings.sync();
71}
72
73namespace
74{
75struct VersionCompare {
76 bool operator()(const QFileInfo &lhsFi, const QFileInfo &rhsFi) const
77 {
78 const auto lhs = parseVersion(lhsFi.fileName());
79 if (!lhs.has_value()) {
80 return false;
81 }
82 const auto rhs = parseVersion(rhsFi.fileName());
83 if (!rhs.has_value()) {
84 return true;
85 }
86
87 return std::tie(lhs->major, lhs->minor) < std::tie(rhs->major, rhs->minor);
88 }
89
90private:
91 struct Version {
92 int major;
93 int minor;
94 };
95 std::optional<Version> parseVersion(const QString &name) const
96 {
97 const auto dotIdx = name.indexOf(QLatin1Char('.'));
98 if (dotIdx == -1) {
99 return {};
100 }
101 bool ok = false;
102 const auto major = QStringView(name).left(dotIdx).toInt(&ok);
103 if (!ok) {
104 return {};
105 }
106 const auto minor = QStringView(name).mid(dotIdx + 1).toInt(&ok);
107 if (!ok) {
108 return {};
109 }
110 return Version{major, minor};
111 }
112};
113
114} // namespace
115
116QStringList DbConfigPostgresql::postgresSearchPaths(const QString &versionedPath) const
117{
118 QStringList paths;
119
120#ifdef POSTGRES_PATH
121 const QString dir(QStringLiteral(POSTGRES_PATH));
122 if (QDir(dir).exists()) {
123 paths.push_back(QStringLiteral(POSTGRES_PATH));
124 }
125#endif
126 paths << QStringLiteral("/usr/bin") << QStringLiteral("/usr/sbin") << QStringLiteral("/usr/local/sbin");
127
128 // Locate all versions in /usr/lib/postgresql (i.e. /usr/lib/postgresql/X.Y) in reversed
129 // sorted order, so we search from the newest one to the oldest.
130 QDir versionedDir(versionedPath);
131 if (versionedDir.exists()) {
132 auto versionedDirs = versionedDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::NoSort);
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");
138 })
139 | Actions::toQList;
140 }
141
142 return paths;
143}
144
145bool DbConfigPostgresql::init(QSettings &settings, bool storeSettings, const QString &dbPathOverride)
146{
147 // determine default settings depending on the driver
148 QString defaultHostName;
149 QString defaultOptions;
150 QString defaultServerPath;
151 QString defaultInitDbPath;
152 QString defaultPgUpgradePath;
153 QString defaultPgData;
154
155#ifndef Q_OS_WINDOWS // We assume that PostgreSQL is running as service on Windows
156 const bool defaultInternalServer = true;
157#else
158 const bool defaultInternalServer = false;
159#endif
160
161 mInternalServer = settings.value(QStringLiteral("QPSQL/StartServer"), defaultInternalServer).toBool();
162 if (mInternalServer) {
163 const auto paths = postgresSearchPaths(QStringLiteral("/usr/lib/postgresql"));
164
165 defaultServerPath = QStandardPaths::findExecutable(QStringLiteral("pg_ctl"), paths);
166 defaultInitDbPath = QStandardPaths::findExecutable(QStringLiteral("initdb"), paths);
167 defaultHostName = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")));
168 defaultPgUpgradePath = QStandardPaths::findExecutable(QStringLiteral("pg_upgrade"), paths);
169 defaultPgData = dbPathOverride.isEmpty() ? StandardDirs::saveDir("data", QStringLiteral("db_data")) : dbPathOverride;
170 }
171
172 // read settings for current driver
173 settings.beginGroup(driverName());
174 mDatabaseName = settings.value(QStringLiteral("Name"), defaultDatabaseName()).toString();
175 if (mDatabaseName.isEmpty()) {
176 mDatabaseName = defaultDatabaseName();
177 }
178 mHostName = settings.value(QStringLiteral("Host"), defaultHostName).toString();
179 if (mHostName.isEmpty()) {
180 mHostName = defaultHostName;
181 }
182 mHostPort = settings.value(QStringLiteral("Port")).toInt();
183 // User, password and Options can be empty and still valid, so don't override them
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;
190 }
191 qCDebug(AKONADISERVER_LOG) << "Found pg_ctl:" << mServerPath;
192 mInitDbPath = settings.value(QStringLiteral("InitDbPath"), defaultInitDbPath).toString();
193 if (mInternalServer && mInitDbPath.isEmpty()) {
194 mInitDbPath = defaultInitDbPath;
195 }
196 qCDebug(AKONADISERVER_LOG) << "Found initdb:" << mServerPath;
197 mPgUpgradePath = settings.value(QStringLiteral("UpgradePath"), defaultPgUpgradePath).toString();
198 if (mInternalServer && mPgUpgradePath.isEmpty()) {
199 mPgUpgradePath = defaultPgUpgradePath;
200 }
201 qCDebug(AKONADISERVER_LOG) << "Found pg_upgrade:" << mPgUpgradePath;
202 mPgData = settings.value(QStringLiteral("PgData"), defaultPgData).toString();
203 if (mPgData.isEmpty()) {
204 mPgData = defaultPgData;
205 }
206 settings.endGroup();
207
208 if (storeSettings) {
209 // store back the default values
210 settings.beginGroup(driverName());
211 settings.setValue(QStringLiteral("Name"), mDatabaseName);
212 settings.setValue(QStringLiteral("Host"), mHostName);
213 if (mHostPort) {
214 settings.setValue(QStringLiteral("Port"), mHostPort);
215 }
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);
221 settings.endGroup();
222 settings.sync();
223 }
224
225 return true;
226}
227
228bool DbConfigPostgresql::isAvailable(QSettings &settings)
229{
230 if (!QSqlDatabase::drivers().contains(driverName())) {
231 return false;
232 }
233
234 if (!init(settings, false)) {
235 return false;
236 }
237
238 if (mInternalServer) {
239 if (mInitDbPath.isEmpty() || !QFile::exists(mInitDbPath)) {
240 return false;
241 }
242
243 if (mServerPath.isEmpty() || !QFile::exists(mServerPath)) {
244 return false;
245 }
246 }
247
248 return true;
249}
250
251void DbConfigPostgresql::apply(QSqlDatabase &database)
252{
253 if (!mDatabaseName.isEmpty()) {
254 database.setDatabaseName(mDatabaseName);
255 }
256 if (!mHostName.isEmpty()) {
257 database.setHostName(mHostName);
258 }
259 if (mHostPort > 0 && mHostPort < 65535) {
260 database.setPort(mHostPort);
261 }
262 if (!mUserName.isEmpty()) {
263 database.setUserName(mUserName);
264 }
265 if (!mPassword.isEmpty()) {
266 database.setPassword(mPassword);
267 }
268
269 database.setConnectOptions(mConnectionOptions);
270
271 // can we check that during init() already?
272 Q_ASSERT(database.driver()->hasFeature(QSqlDriver::LastInsertId));
273}
274
275bool DbConfigPostgresql::useInternalServer() const
276{
277 return mInternalServer;
278}
279
280std::optional<DbConfigPostgresql::Versions> DbConfigPostgresql::checkPgVersion() const
281{
282 // Contains major version of Postgres that created the cluster
283 QFile pgVersionFile(QStringLiteral("%1/PG_VERSION").arg(mPgData));
284 if (!pgVersionFile.open(QIODevice::ReadOnly)) {
285 return std::nullopt;
286 }
287 const auto clusterVersion = pgVersionFile.readAll().toInt();
288
289 QProcess pgctl;
290 pgctl.start(mServerPath, {QStringLiteral("--version")}, QIODevice::ReadOnly);
291 if (!pgctl.waitForFinished()) {
292 return std::nullopt;
293 }
294 // Looks like "pg_ctl (PostgreSQL) 11.2"
295 const auto output = QString::fromUtf8(pgctl.readAll());
296
297 // Get the major version from major.minor
298 QRegularExpression re(QStringLiteral("\\(PostgreSQL\\) ([0-9]+).[0-9]+"));
299 const auto match = re.match(output);
300 if (!match.hasMatch()) {
301 return std::nullopt;
302 }
303 const auto serverVersion = match.captured(1).toInt();
304
305 qDebug(AKONADISERVER_LOG) << "Detected psql versions - cluster:" << clusterVersion << ", server:" << serverVersion;
306 return {{clusterVersion, serverVersion}};
307}
308
309bool DbConfigPostgresql::runInitDb(const QString &newDbPath)
310{
311 // Make sure the cluster directory exists
312 if (!QDir(newDbPath).exists()) {
313 if (!QDir().mkpath(newDbPath)) {
314 return false;
315 }
316 }
317
318#ifdef Q_OS_LINUX
319 // It is recommended to disable CoW feature when running on Btrfs to improve
320 // database performance. This only has effect when done on empty directory,
321 // so we only call this before calling initdb
322 if (Utils::getDirectoryFileSystem(newDbPath) == QLatin1StringView("btrfs")) {
323 Utils::disableCoW(newDbPath);
324 }
325#endif
326
327 // call 'initdb --pgdata=/home/user/.local/share/akonadi/data_db'
328 return execute(mInitDbPath, {QStringLiteral("--pgdata=%1").arg(newDbPath), QStringLiteral("--encoding=UTF8"), QStringLiteral("--no-locale")}) == 0;
329}
330
331namespace
332{
333std::optional<QString> findBinPathForVersion(int version)
334{
335 // First we need to find where the previous PostgreSQL version binaries are available
336 const auto oldBinSearchPaths = {
337 QStringLiteral("/usr/lib64/pgsql/postgresql-%1/bin").arg(version), // Fedora & friends
338 QStringLiteral("/usr/lib/pgsql/postgresql-%1/bin").arg(version),
339 QStringLiteral("/usr/lib/postgresql/%1/bin").arg(version), // Debian-based
340 QStringLiteral("/opt/pgsql-%1/bin").arg(version), // Arch Linux
341 // TODO: Check other distros as well, they might do things differently.
342 };
343
344 for (const auto &path : oldBinSearchPaths) {
345 if (QDir(path).exists()) {
346 return path;
347 }
348 }
349
350 return std::nullopt;
351}
352
353bool checkAndRemoveTmpCluster(const QDir &baseDir, const QString &clusterName)
354{
355 if (baseDir.exists(clusterName)) {
356 qCInfo(AKONADISERVER_LOG) << "Postgres cluster update:" << clusterName << "cluster already exists, trying to remove it first";
357 if (!QDir(baseDir.path() + QDir::separator() + clusterName).removeRecursively()) {
358 qCWarning(AKONADISERVER_LOG) << "Postgres cluster update: failed to remove" << clusterName
359 << "cluster from some previous run, not performing auto-upgrade";
360 return false;
361 }
362 }
363 return true;
364}
365
366bool runPgUpgrade(const QString &pgUpgrade,
367 const QDir &baseDir,
368 const QString &oldBinPath,
369 const QString &newBinPath,
370 const QString &oldDbData,
371 const QString &newDbData)
372{
373 QProcess process;
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);
380 process.setWorkingDirectory(baseDir.path());
381 process.start(pgUpgrade, args);
382 process.waitForFinished(std::chrono::milliseconds(1h).count());
383 if (process.exitCode() != 0) {
384 qCWarning(AKONADISERVER_LOG) << "Postgres cluster update: pg_upgrade finished with exit code" << process.exitCode()
385 << ", please run migration manually.";
386 return false;
387 }
388
389 qCDebug(AKONADISERVER_LOG) << "Postgres cluster update: pg_upgrade finished successfully.";
390 return true;
391}
392
393bool swapClusters(QDir &baseDir, const QString &oldDbDataCluster, const QString &newDbDataCluster)
394{
395 // If everything went fine, swap the old and new clusters
396 if (!baseDir.rename(QStringLiteral("db_data"), oldDbDataCluster)) {
397 qCWarning(AKONADISERVER_LOG) << "Postgres cluster update: failed to rename old db_data to" << oldDbDataCluster;
398 return false;
399 }
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.";
404 return false;
405 }
406 qCDebug(AKONADISERVER_LOG) << "Postgres cluster update: rollback successful.";
407 return false;
408 }
409
410 return true;
411}
412
413} // namespace
414
415bool DbConfigPostgresql::upgradeCluster(int clusterVersion)
416{
417 const auto oldDbDataCluster = QStringLiteral("old_db_data");
418 const auto newDbDataCluster = QStringLiteral("new_db_data");
419
420 QDir baseDir(mPgData); // db_data
421 baseDir.cdUp(); // move to its parent folder
422
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;
426 return false;
427 }
428 const auto newBinPath = QFileInfo(mServerPath).path();
429
430 if (!checkAndRemoveTmpCluster(baseDir, oldDbDataCluster)) {
431 return false;
432 }
433 if (!checkAndRemoveTmpCluster(baseDir, newDbDataCluster)) {
434 return false;
435 }
436
437 // Next, initialize a new cluster
438 const QString newDbData = baseDir.path() + QDir::separator() + 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";
442 return false;
443 }
444
445 // Now migrate the old cluster from the old version into the new cluster
446 if (!runPgUpgrade(mPgUpgradePath, baseDir, *oldBinPath, newBinPath, mPgData, newDbData)) {
447 return false;
448 }
449
450 if (!swapClusters(baseDir, oldDbDataCluster, newDbDataCluster)) {
451 return false;
452 }
453
454 // Drop the old cluster
455 if (!QDir(baseDir.path() + QDir::separator() + oldDbDataCluster).removeRecursively()) {
456 qCInfo(AKONADISERVER_LOG) << "Postgres cluster update: failed to remove" << oldDbDataCluster << "cluster (not an issue, continuing)";
457 }
458
459 return true;
460}
461
462bool DbConfigPostgresql::startInternalServer()
463{
464 // We defined the mHostName to the socket directory, during init
465 const QString socketDir = mHostName;
466 bool success = true;
467
468 // Make sure the path exists, otherwise pg_ctl fails
469 if (!QFile::exists(socketDir)) {
470 QDir().mkpath(socketDir);
471 }
472
473// TODO Windows support
474#ifndef Q_OS_WINDOWS
475 // If postmaster.pid exists, check whether the postgres process still exists too,
476 // because normally we shouldn't be able to get this far if Akonadi is already
477 // running. If postgres is not running, then the pidfile was left after a system
478 // crash or something similar and we can remove it (otherwise pg_ctl won't start)
479 QFile postmaster(QStringLiteral("%1/postmaster.pid").arg(mPgData));
480 if (postmaster.exists() && postmaster.open(QIODevice::ReadOnly)) {
481 qCDebug(AKONADISERVER_LOG) << "Found a postmaster.pid pidfile, checking whether the server is still running...";
482 QByteArray pid = postmaster.readLine();
483 // Remove newline character
484 pid.chop(1);
485 QFile proc(QString::fromLatin1("/proc/" + pid + "/stat"));
486 // Check whether the process with the PID from pidfile still exists and whether
487 // it's actually still postgres or, whether the PID has been recycled in the
488 // meanwhile.
489 if (proc.open(QIODevice::ReadOnly)) {
490 const QByteArray stat = proc.readAll();
491 const QList<QByteArray> stats = stat.split(' ');
492 if (stats.count() > 1) {
493 // Make sure the PID actually belongs to postgres process
494 if (stats[1] == "(postgres)") {
495 // Yup, our PostgreSQL is actually running, so pretend we started the server
496 // and try to connect to it
497 qCWarning(AKONADISERVER_LOG) << "PostgreSQL for Akonadi is already running, trying to connect to it.";
498 return true;
499 }
500 }
501 proc.close();
502 }
503
504 qCDebug(AKONADISERVER_LOG) << "No postgres process with specified PID is running. Removing the pidfile and starting a new Postgres instance...";
505 postmaster.close();
506 postmaster.remove();
507 }
508#endif
509
510 // postgres data directory not initialized yet, so call initdb on it
511 if (!QFile::exists(QStringLiteral("%1/PG_VERSION").arg(mPgData))) {
512#ifdef Q_OS_LINUX
513 // It is recommended to disable CoW feature when running on Btrfs to improve
514 // database performance. This only has effect when done on an empty directory,
515 // so we call this before calling initdb.
516 if (Utils::getDirectoryFileSystem(mPgData) == QLatin1StringView("btrfs")) {
517 Utils::disableCoW(mPgData);
518 }
519#endif
520 // call 'initdb --pgdata=/home/user/.local/share/akonadi/db_data'
521 execute(mInitDbPath, {QStringLiteral("--pgdata=%1").arg(mPgData), QStringLiteral("--encoding=UTF8"), QStringLiteral("--no-locale")});
522 } else {
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;
529 } else {
530 qCWarning(AKONADISERVER_LOG) << "Postgres db cluster upgrade failed, Akonadi will fail to start. Sorry.";
531 }
532 }
533 }
534
535 // synthesize the postgres command
536 QStringList arguments;
537 arguments << QStringLiteral("start") << QStringLiteral("-w") << QStringLiteral("--timeout=10") // default is 60 seconds.
538 << QStringLiteral("--pgdata=%1").arg(mPgData)
539 // These options are passed to postgres
540 // -k - directory for unix domain socket communication
541 // -h - disable listening for TCP/IP
542 << QStringLiteral("-o \"-k%1\" -h ''").arg(socketDir);
543
544 qCDebug(AKONADISERVER_LOG) << "Executing:" << mServerPath << arguments.join(QLatin1Char(' '));
545 QProcess pgCtl;
546 pgCtl.start(mServerPath, arguments);
547 if (!pgCtl.waitForStarted()) {
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();
552 return false;
553 }
554
555 {
556 QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL"), s_initConnection);
557 apply(db);
558
559 // use the default database that is always available
560 db.setDatabaseName(QStringLiteral("postgres"));
561
562 if (!db.isValid()) {
563 qCCritical(AKONADISERVER_LOG) << "Invalid database object during database server startup";
564 return false;
565 }
566
567 bool opened = false;
568 for (int i = 0; i < 120; ++i) {
569 opened = db.open();
570 if (opened) {
571 break;
572 }
573
574 if (pgCtl.waitForFinished(500) && pgCtl.exitCode()) {
575 qCCritical(AKONADISERVER_LOG) << "Database process exited unexpectedly during initial connection!";
576 qCCritical(AKONADISERVER_LOG) << "executable:" << mServerPath;
577 qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments;
578 qCCritical(AKONADISERVER_LOG) << "stdout:" << pgCtl.readAllStandardOutput();
579 qCCritical(AKONADISERVER_LOG) << "stderr:" << pgCtl.readAllStandardError();
580 qCCritical(AKONADISERVER_LOG) << "exit code:" << pgCtl.exitCode();
581 qCCritical(AKONADISERVER_LOG) << "process error:" << pgCtl.errorString();
582 return false;
583 }
584 }
585
586 if (opened) {
587 {
588 QSqlQuery query(db);
589
590 // check if the 'akonadi' database already exists
591 query.exec(QStringLiteral("SELECT 1 FROM pg_catalog.pg_database WHERE datname = '%1'").arg(mDatabaseName));
592
593 // if not, create it
594 if (!query.first()) {
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();
599 success = false;
600 }
601 }
602 } // make sure query is destroyed before we close the db
603 db.close();
604 }
605 }
606 // Make sure pg_ctl has returned
607 pgCtl.waitForFinished();
608
609 QSqlDatabase::removeDatabase(s_initConnection);
610 return success;
611}
612
613void DbConfigPostgresql::stopInternalServer()
614{
615 if (!checkServerIsRunning()) {
616 qCDebug(AKONADISERVER_LOG) << "Database is no longer running";
617 return;
618 }
619
620 // first, try a FAST shutdown
621 execute(mServerPath, {QStringLiteral("stop"), QStringLiteral("--pgdata=%1").arg(mPgData), QStringLiteral("--mode=fast")});
622 if (!checkServerIsRunning()) {
623 return;
624 }
625
626 // second, try an IMMEDIATE shutdown
627 execute(mServerPath, {QStringLiteral("stop"), QStringLiteral("--pgdata=%1").arg(mPgData), QStringLiteral("--mode=immediate")});
628 if (!checkServerIsRunning()) {
629 return;
630 }
631
632 // third, pg_ctl couldn't terminate all the postgres processes, we have to
633 // kill the master one. We don't want to do that, but we've passed the last
634 // call. pg_ctl is used to send the kill signal (safe when kill is not
635 // supported by OS)
636 const QString pidFileName = QStringLiteral("%1/postmaster.pid").arg(mPgData);
637 QFile pidFile(pidFileName);
638 if (pidFile.open(QIODevice::ReadOnly)) {
639 QString postmasterPid = QString::fromUtf8(pidFile.readLine(0).trimmed());
640 qCCritical(AKONADISERVER_LOG) << "The postmaster is still running. Killing it.";
641
642 execute(mServerPath, {QStringLiteral("kill"), QStringLiteral("ABRT"), postmasterPid});
643 }
644}
645
646bool DbConfigPostgresql::checkServerIsRunning()
647{
648 const QString command = mServerPath;
649 QStringList arguments;
650 arguments << QStringLiteral("status") << QStringLiteral("--pgdata=%1").arg(mPgData);
651
652 QProcess pgCtl;
653 pgCtl.start(command, arguments, QIODevice::ReadOnly);
654 if (!pgCtl.waitForFinished(3000)) {
655 // Error?
656 return false;
657 }
658
659 // "pg_ctl status" exits with 0 when server is running and a non-zero code when not.
660 return pgCtl.exitCode() == 0;
661}
662
663bool DbConfigPostgresql::disableConstraintChecks(const QSqlDatabase &db)
664{
665 for (const auto &table : db.tables()) {
666 qCDebug(AKONADISERVER_LOG) << "Disabling triggers on table" << table;
667 QSqlQuery query(db);
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);
671 return false;
672 }
673 }
674
675 return true;
676}
677
678bool DbConfigPostgresql::enableConstraintChecks(const QSqlDatabase &db)
679{
680 for (const auto &table : db.tables()) {
681 qCDebug(AKONADISERVER_LOG) << "Enabling triggers on table" << table;
682 QSqlQuery query(db);
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();
685 // continue
686 }
687 }
688
689 return true;
690}
A base class that provides an unique access layer to configuration and initialization of different da...
Definition dbconfig.h:21
static QString defaultDatabaseName()
Returns the suggested default database name, if none is specified in the configuration already.
Definition dbconfig.cpp:143
int execute(const QString &cmd, const QStringList &args) const
Calls QProcess::execute() and also prints the command and arguments via qCDebug()
Definition dbconfig.cpp:157
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)
void chop(qsizetype n)
bool cdUp()
bool exists() const const
bool mkpath(const QString &dirPath) const const
QString path() const const
bool removeRecursively()
bool rename(const QString &oldName, const QString &newName)
QChar separator()
bool exists() const const
QString fileName() const const
QString path() const const
QString errorString() const const
QByteArray readAll()
qsizetype count() const const
T & first()
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 endGroup()
void setValue(QAnyStringView key, const QVariant &value)
void sync()
QVariant value(QAnyStringView key) const const
QSqlDatabase addDatabase(QSqlDriver *driver, const QString &connectionName)
QSqlDriver * driver() const const
QStringList drivers()
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 setPort(int port)
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 arg(Args &&... args) const const
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
QStringView left(qsizetype length) const const
QStringView mid(qsizetype start, qsizetype length) const const
int toInt(bool *ok, int base) const const
bool toBool() const const
int toInt(bool *ok) const const
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 27 2024 11:48:58 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.