File indexing completed on 2024-04-14 15:49:37

0001 // SPDX-FileCopyrightText: 2020 Simon Persson <simon.persson@mykolab.com>
0002 //
0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "kupdaemon.h"
0006 #include "kupsettings.h"
0007 #include "backupplan.h"
0008 #include "edexecutor.h"
0009 #include "fsexecutor.h"
0010 
0011 #include <QApplication>
0012 #include <QDBusConnection>
0013 #include <QFileInfo>
0014 #include <QJsonArray>
0015 #include <QJsonDocument>
0016 #include <QJsonObject>
0017 #include <QLocalServer>
0018 #include <QLocalSocket>
0019 #include <QMessageBox>
0020 #include <QPushButton>
0021 #include <QSessionManager>
0022 #include <QTimer>
0023 
0024 #include <KIdleTime>
0025 #include <KLocalizedString>
0026 #include <KUiServerJobTracker>
0027 
0028 KupDaemon::KupDaemon()
0029     : mConfig(KSharedConfig::openConfig(QStringLiteral("kuprc"))),
0030       mSettings(new KupSettings(mConfig, this)),
0031       mUsageAccTimer(new QTimer(this)),
0032       mStatusUpdateTimer(new QTimer(this)),
0033       mWaitingToReloadConfig(false),
0034       mJobTracker(new KUiServerJobTracker(this)),
0035       mLocalServer(new QLocalServer(this))
0036 {}
0037 
0038 KupDaemon::~KupDaemon() {
0039     while(!mExecutors.isEmpty()) {
0040         delete mExecutors.takeFirst();
0041     }
0042     KIdleTime::instance()->removeAllIdleTimeouts();
0043 }
0044 
0045 bool KupDaemon::shouldStart() {
0046     return mSettings->mBackupsEnabled;
0047 }
0048 
0049 void KupDaemon::setupGuiStuff() {
0050     // timer to update logged time and also trigger warning if too long
0051     // time has now passed since last backup
0052     mUsageAccTimer->setInterval(KUP_USAGE_MONITOR_INTERVAL_S * 1000);
0053     mUsageAccTimer->start();
0054     KIdleTime *lIdleTime = KIdleTime::instance();
0055     lIdleTime->addIdleTimeout(KUP_IDLE_TIMEOUT_S * 1000);
0056     connect(lIdleTime, qOverload<int, int>(&KIdleTime::timeoutReached), mUsageAccTimer, &QTimer::stop);
0057     connect(lIdleTime, qOverload<int, int>(&KIdleTime::timeoutReached), lIdleTime, &KIdleTime::catchNextResumeEvent);
0058     connect(lIdleTime, &KIdleTime::resumingFromIdle, mUsageAccTimer, qOverload<>(&QTimer::start));
0059 
0060     // delay status update to avoid sending a status to plasma applet
0061     // that will be changed again just a microsecond later anyway
0062     mStatusUpdateTimer->setInterval(500);
0063     mStatusUpdateTimer->setSingleShot(true);
0064     connect(mStatusUpdateTimer, &QTimer::timeout, this, [this]{
0065         foreach(QLocalSocket *lSocket, mSockets) {
0066             sendStatus(lSocket);
0067         }
0068 
0069         if(mWaitingToReloadConfig) {
0070             // quite likely the config can be reloaded now, give it a try.
0071             QTimer::singleShot(0, this, SLOT(reloadConfig()));
0072         }
0073     });
0074 
0075     QDBusConnection lDBus = QDBusConnection::sessionBus();
0076     if(lDBus.isConnected()) {
0077         if(lDBus.registerService(KUP_DBUS_SERVICE_NAME)) {
0078             lDBus.registerObject(KUP_DBUS_OBJECT_PATH, this, QDBusConnection::ExportAllSlots);
0079         }
0080     }
0081     QString lSocketName = QStringLiteral("kup-daemon-");
0082     lSocketName += QString::fromLocal8Bit(qgetenv("USER"));
0083 
0084     connect(mLocalServer, &QLocalServer::newConnection, this, [this]{
0085         QLocalSocket *lSocket = mLocalServer->nextPendingConnection();
0086         if(lSocket == nullptr) {
0087             return;
0088         }
0089         sendStatus(lSocket);
0090         mSockets.append(lSocket);
0091         connect(lSocket, &QLocalSocket::readyRead, this, [this,lSocket]{handleRequests(lSocket);});
0092         connect(lSocket, &QLocalSocket::disconnected, this, [this,lSocket]{
0093             mSockets.removeAll(lSocket);
0094             lSocket->deleteLater();
0095         });
0096     });
0097     // remove old socket first in case it's still there, otherwise listen() fails.
0098     QLocalServer::removeServer(lSocketName);
0099     mLocalServer->listen(lSocketName);
0100 
0101     reloadConfig();
0102 }
0103 
0104 void KupDaemon::reloadConfig() {
0105     foreach(PlanExecutor *lExecutor, mExecutors) {
0106         if(lExecutor->busy()) {
0107             mWaitingToReloadConfig = true;
0108             return;
0109         }
0110     }
0111     mWaitingToReloadConfig = false;
0112 
0113     mSettings->load();
0114     while(!mExecutors.isEmpty()) {
0115         delete mExecutors.takeFirst();
0116     }
0117     if(!mSettings->mBackupsEnabled)
0118         qApp->quit();
0119 
0120     setupExecutors();
0121     // Juuuust in case all those executors for some reason never
0122     // triggered an updated status... Doesn't hurt anyway.
0123     mStatusUpdateTimer->start();
0124 }
0125 
0126 // This method is exposed over DBus so that filedigger can call it
0127 void KupDaemon::runIntegrityCheck(const QString& pPath) {
0128     foreach(PlanExecutor *lExecutor, mExecutors) {
0129         // if caller passes in an empty path, startsWith will return true and we will try to check
0130         // all backup plans.
0131         if(lExecutor->mDestinationPath.startsWith(pPath)) {
0132             lExecutor->startIntegrityCheck();
0133         }
0134     }
0135 }
0136 
0137 // This method is exposed over DBus so that user scripts can call it
0138 void KupDaemon::saveNewBackup(int pPlanNumber) {
0139     if(pPlanNumber > 0 && pPlanNumber <= mExecutors.count()) {
0140         mExecutors[pPlanNumber - 1]->startBackupSaveJob();
0141     }
0142 }
0143 
0144 void KupDaemon::registerJob(KJob *pJob) {
0145     mJobTracker->registerJob(pJob);
0146 }
0147 
0148 void KupDaemon::unregisterJob(KJob *pJob) {
0149     mJobTracker->unregisterJob(pJob);
0150 }
0151 
0152 void KupDaemon::slotShutdownRequest(QSessionManager &pManager) {
0153     // this will make session management not try (and fail because of KDBusService starting only
0154     // one instance) to start this daemon. We have autostart for the purpose of launching this
0155     // daemon instead.
0156     pManager.setRestartHint(QSessionManager::RestartNever);
0157 
0158 
0159     foreach(PlanExecutor *lExecutor, mExecutors) {
0160         if(lExecutor->busy() && pManager.allowsErrorInteraction()) {
0161             QMessageBox lMessageBox;
0162             QPushButton *lContinueButton = lMessageBox.addButton(i18n("Continue"),
0163                                                                  QMessageBox::RejectRole);
0164             lMessageBox.addButton(i18n("Stop"), QMessageBox::AcceptRole);
0165             lMessageBox.setText(i18nc("%1 is a text explaining the current activity",
0166                                       "Currently busy: %1", lExecutor->currentActivityTitle()));
0167             lMessageBox.setInformativeText(i18n("Do you really want to stop?"));
0168             lMessageBox.setIcon(QMessageBox::Warning);
0169             lMessageBox.setWindowIcon(QIcon::fromTheme(QStringLiteral("kup")));
0170             lMessageBox.setWindowTitle(i18n("User Backups"));
0171             lMessageBox.exec();
0172             if(lMessageBox.clickedButton() == lContinueButton) {
0173                 pManager.cancel();
0174             }
0175             return; //only ask for one active executor.
0176         }
0177     }
0178 }
0179 
0180 void KupDaemon::setupExecutors() {
0181     for(int i = 0; i < mSettings->mNumberOfPlans; ++i) {
0182         PlanExecutor *lExecutor;
0183         auto *lPlan = new BackupPlan(i+1, mConfig, this);
0184         if(lPlan->mPathsIncluded.isEmpty()) {
0185             delete lPlan;
0186             continue;
0187         }
0188         if(lPlan->mDestinationType == 0) {
0189             lExecutor = new FSExecutor(lPlan, this);
0190         } else if(lPlan->mDestinationType == 1) {
0191             lExecutor = new EDExecutor(lPlan, this);
0192         } else {
0193             delete lPlan;
0194             continue;
0195         }
0196         connect(lExecutor, &PlanExecutor::stateChanged, this, [this]{mStatusUpdateTimer->start();});
0197         connect(lExecutor, &PlanExecutor::backupStatusChanged, this, [this]{mStatusUpdateTimer->start();});
0198         connect(mUsageAccTimer, &QTimer::timeout,
0199                 lExecutor, &PlanExecutor::updateAccumulatedUsageTime);
0200         lExecutor->checkStatus();
0201         mExecutors.append(lExecutor);
0202     }
0203 }
0204 
0205 void KupDaemon::handleRequests(QLocalSocket *pSocket) {
0206     if(pSocket->bytesAvailable() <= 0) {
0207         return;
0208     }
0209     QJsonDocument lDoc = QJsonDocument::fromJson(pSocket->readAll());
0210     if(!lDoc.isObject()) {
0211         return;
0212     }
0213     QJsonObject lCommand = lDoc.object();
0214     QString lOperation = lCommand["operation name"].toString();
0215     if(lOperation == QStringLiteral("get status")) {
0216         sendStatus(pSocket);
0217         return;
0218     }
0219     if(lOperation == QStringLiteral("reload")) {
0220         reloadConfig();
0221         return;
0222     }
0223 
0224     int lPlanNumber = lCommand["plan number"].toInt(-1);
0225     if(lPlanNumber < 0 || lPlanNumber >= mExecutors.count()) {
0226         return;
0227     }
0228     if(lOperation == QStringLiteral("save backup")) {
0229         mExecutors.at(lPlanNumber)->startBackupSaveJob();
0230     }
0231     if(lOperation == QStringLiteral("remove backups")) {
0232         mExecutors.at(lPlanNumber)->showBackupPurger();
0233     }
0234     if(lOperation == QStringLiteral("show log file")) {
0235         mExecutors.at(lPlanNumber)->showLog();
0236     }
0237     if(lOperation == QStringLiteral("show backup files")) {
0238         mExecutors.at(lPlanNumber)->showBackupFiles();
0239     }
0240 }
0241 
0242 void KupDaemon::sendStatus(QLocalSocket *pSocket) {
0243     bool lTrayIconActive = false;
0244     bool lAnyPlanBusy = false;
0245     // If all backup plans have status == NO_STATUS then tooltip title will be empty
0246     QString lToolTipTitle;
0247     QString lToolTipSubTitle = i18nc("status in tooltip", "Backup destination not available");
0248     QString lToolTipIconName = QStringLiteral("kup");
0249 
0250     if(mExecutors.isEmpty()) {
0251         lToolTipTitle = i18n("No backup plans configured");
0252         lToolTipSubTitle.clear();
0253     }
0254 
0255     foreach(PlanExecutor *lExec, mExecutors) {
0256         if(lExec->destinationAvailable()) {
0257             lToolTipSubTitle = i18nc("status in tooltip", "Backup destination available");
0258             if(lExec->scheduleType() == BackupPlan::MANUAL) {
0259                 lTrayIconActive = true;
0260             }
0261         }
0262     }
0263 
0264     foreach(PlanExecutor *lExec, mExecutors) {
0265         if(lExec->mPlan->backupStatus() == BackupPlan::GOOD) {
0266             lToolTipIconName = BackupPlan::iconName(BackupPlan::GOOD);
0267             lToolTipTitle = i18nc("status in tooltip", "Backup status OK");
0268         }
0269     }
0270 
0271     foreach(PlanExecutor *lExec, mExecutors) {
0272         if(lExec->mPlan->backupStatus() == BackupPlan::MEDIUM) {
0273             lToolTipIconName = BackupPlan::iconName(BackupPlan::MEDIUM);
0274             lToolTipTitle = i18nc("status in tooltip", "New backup suggested");
0275         }
0276     }
0277 
0278     foreach(PlanExecutor *lExec, mExecutors) {
0279         if(lExec->mPlan->backupStatus() == BackupPlan::BAD) {
0280             lToolTipIconName = BackupPlan::iconName(BackupPlan::BAD);
0281             lToolTipTitle = i18nc("status in tooltip", "New backup needed");
0282             lTrayIconActive = true;
0283         }
0284     }
0285 
0286     foreach(PlanExecutor *lExecutor, mExecutors) {
0287         if(lExecutor->busy()) {
0288             lToolTipIconName = QStringLiteral("kup");
0289             lToolTipTitle = lExecutor->currentActivityTitle();
0290             lToolTipSubTitle = lExecutor->mPlan->mDescription;
0291             lAnyPlanBusy = true;
0292         }
0293     }
0294 
0295     if(lToolTipTitle.isEmpty() && !lToolTipSubTitle.isEmpty()) {
0296         lToolTipTitle = lToolTipSubTitle;
0297         lToolTipSubTitle.clear();
0298     }
0299 
0300     QJsonObject lStatus;
0301     lStatus["event"] = QStringLiteral("status update");
0302     lStatus["tray icon active"] = lTrayIconActive;
0303     lStatus["tooltip icon name"] = lToolTipIconName;
0304     lStatus["tooltip title"] = lToolTipTitle;
0305     lStatus["tooltip subtitle"] = lToolTipSubTitle;
0306     lStatus["any plan busy"] = lAnyPlanBusy;
0307     lStatus["no plan reason"] = mExecutors.isEmpty()
0308           ? i18n("No backup plans configured")
0309            : QString();
0310     QJsonArray lPlans;
0311     foreach(PlanExecutor *lExecutor, mExecutors) {
0312         QJsonObject lPlan;
0313         lPlan[QStringLiteral("description")] = lExecutor->mPlan->mDescription;
0314         lPlan[QStringLiteral("destination available")] = lExecutor->destinationAvailable();
0315         lPlan[QStringLiteral("status heading")] = lExecutor->currentActivityTitle();
0316         lPlan[QStringLiteral("status details")] = lExecutor->mPlan->statusText();
0317         lPlan[QStringLiteral("icon name")] = BackupPlan::iconName(lExecutor->mPlan->backupStatus());
0318         lPlan[QStringLiteral("log file exists")] = QFileInfo::exists(lExecutor->mLogFilePath);
0319         lPlan[QStringLiteral("busy")] = lExecutor->busy();
0320         lPlan[QStringLiteral("bup type")] = lExecutor->mPlan->mBackupType == BackupPlan::BupType;
0321         lPlans.append(lPlan);
0322     }
0323     lStatus["plans"] = lPlans;
0324     QJsonDocument lDoc(lStatus);
0325     pSocket->write(lDoc.toJson());
0326 }