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

0001 /***************************************************************************
0002  *   SPDX-FileCopyrightText: 2006 Tobias Koenig <tokoe@kde.org>            *
0003  *                                                                         *
0004  *   SPDX-License-Identifier: LGPL-2.0-or-later                            *
0005  ***************************************************************************/
0006 
0007 #include "processcontrol.h"
0008 #include "akonadicontrol_debug.h"
0009 
0010 #include "shared/akapplication.h"
0011 
0012 #include "private/instance_p.h"
0013 #include "private/standarddirs_p.h"
0014 
0015 #include <QTimer>
0016 
0017 #ifdef Q_OS_UNIX
0018 #include <signal.h>
0019 #include <sys/types.h>
0020 #endif
0021 
0022 using namespace Akonadi;
0023 using namespace std::chrono_literals;
0024 
0025 static const int s_maxCrashCount = 2;
0026 
0027 ProcessControl::ProcessControl(QObject *parent)
0028     : QObject(parent)
0029     , mShutdownTimeout(1s)
0030 {
0031     connect(&mProcess, &QProcess::errorOccurred, this, &ProcessControl::slotError);
0032     connect(&mProcess, &QProcess::finished, this, &ProcessControl::slotFinished);
0033     mProcess.setProcessChannelMode(QProcess::ForwardedChannels);
0034 
0035     if (Akonadi::Instance::hasIdentifier()) {
0036         QProcessEnvironment env = mProcess.processEnvironment();
0037         if (env.isEmpty()) {
0038             env = QProcessEnvironment::systemEnvironment();
0039         }
0040         env.insert(QStringLiteral("AKONADI_INSTANCE"), Akonadi::Instance::identifier());
0041         mProcess.setProcessEnvironment(env);
0042     }
0043 }
0044 
0045 ProcessControl::~ProcessControl()
0046 {
0047     stop();
0048 }
0049 
0050 void ProcessControl::start(const QString &application, const QStringList &arguments, CrashPolicy policy)
0051 {
0052     mFailedToStart = false;
0053 
0054     mApplication = application;
0055     mArguments = arguments;
0056     mPolicy = policy;
0057 
0058     start();
0059 }
0060 
0061 void ProcessControl::setCrashPolicy(CrashPolicy policy)
0062 {
0063     mPolicy = policy;
0064 }
0065 
0066 void ProcessControl::stop()
0067 {
0068     if (mProcess.state() != QProcess::NotRunning) {
0069         mProcess.waitForFinished(mShutdownTimeout.count());
0070         mProcess.terminate();
0071         mProcess.waitForFinished(std::chrono::milliseconds{10000}.count());
0072         mProcess.kill();
0073     }
0074 }
0075 
0076 void ProcessControl::slotError(QProcess::ProcessError error)
0077 {
0078     switch (error) {
0079     case QProcess::Crashed:
0080         mCrashCount++;
0081         // do nothing, we'll respawn in slotFinished
0082         break;
0083     case QProcess::FailedToStart:
0084     default:
0085         mFailedToStart = true;
0086         break;
0087     }
0088 
0089     qCWarning(AKONADICONTROL_LOG) << "ProcessControl: Application" << mApplication << "stopped unexpectedly (" << mProcess.errorString() << ")";
0090 }
0091 
0092 void ProcessControl::slotFinished(int exitCode, QProcess::ExitStatus exitStatus)
0093 {
0094     if (exitStatus == QProcess::CrashExit) {
0095         if (mPolicy == RestartOnCrash) {
0096             // don't try to start an unstartable application
0097             if (!mFailedToStart && mCrashCount <= s_maxCrashCount) {
0098                 qCWarning(AKONADICONTROL_LOG, "Application '%s' crashed! %d restarts left.", qPrintable(mApplication), s_maxCrashCount - mCrashCount);
0099                 start();
0100                 Q_EMIT restarted();
0101             } else {
0102                 if (mFailedToStart) {
0103                     qCCritical(AKONADICONTROL_LOG, "Application '%s' failed to start!", qPrintable(mApplication));
0104                 } else {
0105                     qCCritical(AKONADICONTROL_LOG, "Application '%s' crashed too often. Giving up!", qPrintable(mApplication));
0106                 }
0107                 mPolicy = StopOnCrash;
0108                 Q_EMIT unableToStart();
0109                 return;
0110             }
0111         } else {
0112             qCCritical(AKONADICONTROL_LOG, "Application '%s' crashed. No restart!", qPrintable(mApplication));
0113         }
0114     } else {
0115         if (exitCode != 0) {
0116             qCWarning(AKONADICONTROL_LOG,
0117                       "ProcessControl: Application '%s' returned with exit code %d (%s)",
0118                       qPrintable(mApplication),
0119                       exitCode,
0120                       qPrintable(mProcess.errorString()));
0121             if (mPolicy == RestartOnCrash) {
0122                 if (mCrashCount > s_maxCrashCount) {
0123                     qCCritical(AKONADICONTROL_LOG) << mApplication << "crashed too often and will not be restarted!";
0124                     mPolicy = StopOnCrash;
0125                     Q_EMIT unableToStart();
0126                     return;
0127                 }
0128                 ++mCrashCount;
0129                 QTimer::singleShot(std::chrono::seconds{60}, this, &ProcessControl::resetCrashCount);
0130                 if (!mFailedToStart) { // don't try to start an unstartable application
0131                     start();
0132                     Q_EMIT restarted();
0133                 }
0134             }
0135         } else {
0136             if (mRestartOnceOnExit) {
0137                 mRestartOnceOnExit = false;
0138                 qCInfo(AKONADICONTROL_LOG, "Restarting application '%s'.", qPrintable(mApplication));
0139                 start();
0140             } else {
0141                 qCInfo(AKONADICONTROL_LOG, "Application '%s' exited normally...", qPrintable(mApplication));
0142                 Q_EMIT unableToStart();
0143             }
0144         }
0145     }
0146 }
0147 
0148 static bool listContains(const QStringList &list, const QString &pattern)
0149 {
0150     for (const QString &s : list) {
0151         if (s.contains(pattern)) {
0152             return true;
0153         }
0154     }
0155     return false;
0156 }
0157 
0158 void ProcessControl::start()
0159 {
0160     // Prefer akonadiserver from the builddir
0161     mApplication = StandardDirs::findExecutable(mApplication);
0162 
0163 #ifdef Q_OS_UNIX
0164     QString agentValgrind = akGetEnv("AKONADI_VALGRIND");
0165     if (!agentValgrind.isEmpty() && (mApplication.contains(agentValgrind) || listContains(mArguments, agentValgrind))) {
0166         mArguments.prepend(mApplication);
0167         const QString originalArguments = mArguments.join(QString::fromLocal8Bit(" "));
0168         mApplication = QString::fromLocal8Bit("valgrind");
0169 
0170         const QString valgrindSkin = akGetEnv("AKONADI_VALGRIND_SKIN", QString::fromLocal8Bit("memcheck"));
0171         mArguments.prepend(QLatin1StringView("--tool=") + valgrindSkin);
0172 
0173         const QString valgrindOptions = akGetEnv("AKONADI_VALGRIND_OPTIONS");
0174         if (!valgrindOptions.isEmpty()) {
0175             mArguments = valgrindOptions.split(QLatin1Char(' '), Qt::SkipEmptyParts) << mArguments;
0176         }
0177 
0178         qCDebug(AKONADICONTROL_LOG);
0179         qCDebug(AKONADICONTROL_LOG) << "============================================================";
0180         qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Valgrinding process" << originalArguments;
0181         if (!valgrindSkin.isEmpty()) {
0182             qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Valgrind skin:" << valgrindSkin;
0183         }
0184         if (!valgrindOptions.isEmpty()) {
0185             qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Additional Valgrind options:" << valgrindOptions;
0186         }
0187         qCDebug(AKONADICONTROL_LOG) << "============================================================";
0188         qCDebug(AKONADICONTROL_LOG);
0189     }
0190 
0191     const QString agentHeaptrack = akGetEnv("AKONADI_HEAPTRACK");
0192     if (!agentHeaptrack.isEmpty() && (mApplication.contains(agentHeaptrack) || listContains(mArguments, agentHeaptrack))) {
0193         mArguments.prepend(mApplication);
0194         const QString originalArguments = mArguments.join(QLatin1Char(' '));
0195         mApplication = QStringLiteral("heaptrack");
0196 
0197         qCDebug(AKONADICONTROL_LOG);
0198         qCDebug(AKONADICONTROL_LOG) << "============================================================";
0199         qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Heaptracking process" << originalArguments;
0200         qCDebug(AKONADICONTROL_LOG) << "============================================================";
0201         qCDebug(AKONADICONTROL_LOG);
0202     }
0203 
0204     const QString agentPerf = akGetEnv("AKONADI_PERF");
0205     if (!agentPerf.isEmpty() && (mApplication.contains(agentPerf) || listContains(mArguments, agentPerf))) {
0206         mArguments.prepend(mApplication);
0207         const QString originalArguments = mArguments.join(QLatin1Char(' '));
0208         mApplication = QStringLiteral("perf");
0209 
0210         mArguments = QStringList{QStringLiteral("record"), QStringLiteral("--call-graph"), QStringLiteral("dwarf"), QStringLiteral("--")} + mArguments;
0211 
0212         qCDebug(AKONADICONTROL_LOG);
0213         qCDebug(AKONADICONTROL_LOG) << "============================================================";
0214         qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Perf-recording process" << originalArguments;
0215         qCDebug(AKONADICONTROL_LOG) << "============================================================";
0216         qCDebug(AKONADICONTROL_LOG);
0217     }
0218 #endif
0219 
0220     mProcess.start(mApplication, mArguments);
0221     if (!mProcess.waitForStarted()) {
0222         qCWarning(AKONADICONTROL_LOG, "ProcessControl: Unable to start application '%s' (%s)", qPrintable(mApplication), qPrintable(mProcess.errorString()));
0223         Q_EMIT unableToStart();
0224         return;
0225     } else {
0226         QString agentDebug = QString::fromLocal8Bit(qgetenv("AKONADI_DEBUG_WAIT"));
0227         auto pid = mProcess.processId();
0228         if (!agentDebug.isEmpty() && mApplication.contains(agentDebug)) {
0229             qCDebug(AKONADICONTROL_LOG);
0230             qCDebug(AKONADICONTROL_LOG) << "============================================================";
0231             qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Suspending process" << mApplication;
0232 #ifdef Q_OS_UNIX
0233             qCDebug(AKONADICONTROL_LOG) << "'gdb --pid" << pid << "' to debug";
0234             qCDebug(AKONADICONTROL_LOG) << "'kill -SIGCONT" << pid << "' to continue";
0235             kill(pid, SIGSTOP);
0236 #endif
0237 #ifdef Q_OS_WIN
0238             qCDebug(AKONADICONTROL_LOG) << "PID:" << pid;
0239             qCDebug(AKONADICONTROL_LOG) << "Process is waiting for a debugger...";
0240             // the agent process will wait for a debugger to be attached in AgentBase::debugAgent()
0241 #endif
0242             qCDebug(AKONADICONTROL_LOG) << "============================================================";
0243             qCDebug(AKONADICONTROL_LOG);
0244         }
0245     }
0246 }
0247 
0248 void ProcessControl::resetCrashCount()
0249 {
0250     mCrashCount = 0;
0251 }
0252 
0253 bool ProcessControl::isRunning() const
0254 {
0255     return mProcess.state() != QProcess::NotRunning;
0256 }
0257 
0258 void ProcessControl::setShutdownTimeout(std::chrono::milliseconds timeout)
0259 {
0260     mShutdownTimeout = timeout;
0261 }
0262 
0263 #include "moc_processcontrol.cpp"