1 /*
2  This file is part of Akonadi.
3 
4  SPDX-FileCopyrightText: 2009 KDAB
5  SPDX-FileContributor: Till Adam <adam@kde.org>
6 
7  SPDX-License-Identifier: GPL-2.0-or-later
8  */
9 
10 #include "jobtracker.h"
11 #include "akonadiconsole_debug.h"
12 #include "jobtrackeradaptor.h"
13 #include <QString>
14 #include <QStringList>
15 #include <akonadi/private/instance_p.h>
16 
17 #include <cassert>
18 #include <chrono>
19 
20 using namespace std::chrono_literals;
stateAsString() const21 QString JobInfo::stateAsString() const
22 {
23     switch (state) {
24     case Initial:
25         return QStringLiteral("Waiting");
26     case Running:
27         return QStringLiteral("Running");
28     case Ended:
29         return QStringLiteral("Ended");
30     case Failed:
31         return QStringLiteral("Failed: %1").arg(error);
32     default:
33         return QStringLiteral("Unknown state!");
34     }
35 }
36 
37 class JobTrackerPrivate
38 {
39 public:
JobTrackerPrivate(JobTracker * _q)40     explicit JobTrackerPrivate(JobTracker *_q)
41         : lastId(42)
42         , timer(_q)
43         , disabled(false)
44         , q(_q)
45     {
46         timer.setSingleShot(true);
47         timer.setInterval(200ms);
48         QObject::connect(&timer, &QTimer::timeout, q, &JobTracker::signalUpdates);
49     }
50 
isSession(int id) const51     bool isSession(int id) const
52     {
53         return id < -1;
54     }
55 
startUpdatedSignalTimer()56     void startUpdatedSignalTimer()
57     {
58         if (!timer.isActive() && !disabled) {
59             timer.start();
60         }
61     }
62 
63     QStringList sessions;
64     QHash<QString, int> nameToId;
65     QHash<int, QVector<int>> childJobs;
66     QHash<int, JobInfo> infoList;
67     int lastId;
68     QTimer timer;
69     bool disabled;
70     QList<QPair<int, int>> unpublishedUpdates;
71 
72 private:
73     JobTracker *const q;
74 };
75 
JobTracker(const char * name,QObject * parent)76 JobTracker::JobTracker(const char *name, QObject *parent)
77     : QObject(parent)
78     , d(new JobTrackerPrivate(this))
79 {
80     new JobTrackerAdaptor(this);
81     const QString suffix = Akonadi::Instance::identifier().isEmpty() ? QString() : QLatin1Char('-') + Akonadi::Instance::identifier();
82     QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.akonadiconsole") + suffix);
83     QDBusConnection::sessionBus().registerObject(QLatin1Char('/') + QLatin1String(name), this, QDBusConnection::ExportAdaptors);
84 }
85 
86 JobTracker::~JobTracker() = default;
87 
jobCreated(const QString & session,const QString & jobName,const QString & parent,const QString & jobType,const QString & debugString)88 void JobTracker::jobCreated(const QString &session, const QString &jobName, const QString &parent, const QString &jobType, const QString &debugString)
89 {
90     if (d->disabled || session.isEmpty() || jobName.isEmpty()) {
91         return;
92     }
93 
94     int parentId = parent.isEmpty() ? -1 /*for now*/ : idForJob(parent);
95 
96     if (!parent.isEmpty() && parentId == -1) {
97         qCWarning(AKONADICONSOLE_LOG) << "JobTracker: Job" << jobName << "arrived before its parent" << parent << " jobType=" << jobType
98                                       << "! Fix the library!";
99         jobCreated(session, parent, QString(), QStringLiteral("dummy job type"), QString());
100         parentId = idForJob(parent);
101         assert(parentId != -1);
102     }
103     int sessionId = idForSession(session);
104     // check if it's a new session, if so, add it
105     if (sessionId == -1) {
106         Q_EMIT aboutToAdd(d->sessions.count(), -1);
107         d->sessions.append(session);
108         Q_EMIT added();
109         sessionId = idForSession(session);
110     }
111     if (parent.isEmpty()) {
112         parentId = sessionId;
113     }
114 
115     // deal with the job
116     const int existingId = idForJob(jobName);
117     if (existingId != -1) {
118         if (d->infoList.value(existingId).state == JobInfo::Running) {
119             qCDebug(AKONADICONSOLE_LOG) << "Job was already known and still running:" << jobName << "from"
120                                         << d->infoList.value(existingId).timestamp.secsTo(QDateTime::currentDateTime()) << "s ago";
121         }
122         // otherwise it just means the pointer got reused... insert duplicate
123     }
124 
125     assert(parentId != -1);
126     QVector<int> &kids = d->childJobs[parentId];
127     const int pos = kids.size();
128 
129     Q_EMIT aboutToAdd(pos, parentId);
130 
131     const int id = d->lastId++;
132 
133     JobInfo info;
134     info.name = jobName;
135     info.parent = parentId;
136     info.state = JobInfo::Initial;
137     info.timestamp = QDateTime::currentDateTime();
138     info.type = jobType;
139     info.debugString = debugString;
140     d->infoList.insert(id, info);
141     d->nameToId.insert(jobName, id); // this replaces any previous entry for jobName, which is exactly what we want
142     kids << id;
143 
144     Q_EMIT added();
145 }
146 
jobEnded(const QString & jobName,const QString & error)147 void JobTracker::jobEnded(const QString &jobName, const QString &error)
148 {
149     if (d->disabled) {
150         return;
151     }
152     // this is called from dbus, so better be defensive
153     const int jobId = idForJob(jobName);
154     if (jobId == -1 || !d->infoList.contains(jobId)) {
155         return;
156     }
157 
158     JobInfo &info = d->infoList[jobId];
159     if (error.isEmpty()) {
160         info.state = JobInfo::Ended;
161     } else {
162         info.state = JobInfo::Failed;
163         info.error = error;
164     }
165     info.endedTimestamp = QDateTime::currentDateTime();
166 
167     d->unpublishedUpdates << QPair<int, int>(d->childJobs[info.parent].size() - 1, info.parent);
168     d->startUpdatedSignalTimer();
169 }
170 
jobStarted(const QString & jobName)171 void JobTracker::jobStarted(const QString &jobName)
172 {
173     if (d->disabled) {
174         return;
175     }
176     // this is called from dbus, so better be defensive
177     const int jobId = idForJob(jobName);
178     if (jobId == -1 || !d->infoList.contains(jobId)) {
179         return;
180     }
181 
182     JobInfo &info = d->infoList[jobId];
183     info.state = JobInfo::Running;
184     info.startedTimestamp = QDateTime::currentDateTime();
185 
186     d->unpublishedUpdates << QPair<int, int>(d->childJobs[info.parent].size() - 1, info.parent);
187     d->startUpdatedSignalTimer();
188 }
189 
sessions() const190 QStringList JobTracker::sessions() const
191 {
192     return d->sessions;
193 }
194 
childJobs(int parentId) const195 QVector<int> JobTracker::childJobs(int parentId) const
196 {
197     return d->childJobs.value(parentId);
198 }
199 
jobCount(int parentId) const200 int JobTracker::jobCount(int parentId) const
201 {
202     return d->childJobs.value(parentId).count();
203 }
204 
jobIdAt(int childPos,int parentId) const205 int JobTracker::jobIdAt(int childPos, int parentId) const
206 {
207     return d->childJobs.value(parentId).at(childPos);
208 }
209 
210 // only works on jobs
idForJob(const QString & job) const211 int JobTracker::idForJob(const QString &job) const
212 {
213     return d->nameToId.value(job, -1);
214 }
215 
216 // To find a session, we take the offset in the list of sessions
217 // in order of appearance, add one, and make it negative. That
218 // way we can discern session ids from job ids and use -1 for invalid
idForSession(const QString & session) const219 int JobTracker::idForSession(const QString &session) const
220 {
221     return (d->sessions.indexOf(session) + 2) * -1;
222 }
223 
sessionForId(int _id) const224 QString JobTracker::sessionForId(int _id) const
225 {
226     const int id = (-_id) - 2;
227     assert(d->sessions.size() > id);
228     if (!d->sessions.isEmpty()) {
229         return d->sessions.at(id);
230     } else {
231         return QString();
232     }
233 }
234 
parentId(int id) const235 int JobTracker::parentId(int id) const
236 {
237     if (d->isSession(id)) {
238         return -1;
239     } else {
240         return d->infoList.value(id).parent;
241     }
242 }
243 
rowForJob(int id,int parentId) const244 int JobTracker::rowForJob(int id, int parentId) const
245 {
246     const QVector<int> children = childJobs(parentId);
247     // Simple version:
248     // return children.indexOf(id);
249     // But we can do faster since the vector is sorted
250     return std::lower_bound(children.constBegin(), children.constEnd(), id) - children.constBegin();
251 }
252 
info(int id) const253 JobInfo JobTracker::info(int id) const
254 {
255     assert(d->infoList.contains(id));
256     return d->infoList.value(id);
257 }
258 
clear()259 void JobTracker::clear()
260 {
261     d->sessions.clear();
262     d->nameToId.clear();
263     d->childJobs.clear();
264     d->infoList.clear();
265     d->unpublishedUpdates.clear();
266 }
267 
setEnabled(bool on)268 void JobTracker::setEnabled(bool on)
269 {
270     d->disabled = !on;
271 }
272 
isEnabled() const273 bool JobTracker::isEnabled() const
274 {
275     return !d->disabled;
276 }
277 
signalUpdates()278 void JobTracker::signalUpdates()
279 {
280     if (!d->unpublishedUpdates.isEmpty()) {
281         Q_EMIT updated(d->unpublishedUpdates);
282         d->unpublishedUpdates.clear();
283     }
284 }
285