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 }