Akonadi

resourcescheduler.cpp
1/*
2 SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "resourcescheduler_p.h"
8
9#include "recursivemover_p.h"
10#include <QDBusConnection>
11
12#include "akonadiagentbase_debug.h"
13#include "private/instance_p.h"
14#include <KLocalizedString>
15
16#include <QDBusInterface>
17#include <QTimer>
18
19using namespace Akonadi;
20using namespace std::chrono_literals;
21qint64 ResourceScheduler::Task::latestSerial = 0;
22static QDBusAbstractInterface *s_resourcetracker = nullptr;
23
24/// @cond PRIVATE
25
26ResourceScheduler::ResourceScheduler(QObject *parent)
27 : QObject(parent)
28{
29}
30
31void ResourceScheduler::scheduleFullSync()
32{
33 Task t;
34 t.type = SyncAll;
35 TaskList &queue = queueForTaskType(t.type);
36 if (queue.contains(t) || mCurrentTask == t) {
37 return;
38 }
39 queue << t;
40 signalTaskToTracker(t, "SyncAll");
41 scheduleNext();
42}
43
44void ResourceScheduler::scheduleCollectionTreeSync()
45{
46 Task t;
47 t.type = SyncCollectionTree;
48 TaskList &queue = queueForTaskType(t.type);
49 if (queue.contains(t) || mCurrentTask == t) {
50 return;
51 }
52 queue << t;
53 signalTaskToTracker(t, "SyncCollectionTree");
54 scheduleNext();
55}
56
57void ResourceScheduler::scheduleTagSync()
58{
59 Task t;
60 t.type = SyncTags;
61 TaskList &queue = queueForTaskType(t.type);
62 if (queue.contains(t) || mCurrentTask == t) {
63 return;
64 }
65 queue << t;
66 signalTaskToTracker(t, "SyncTags");
67 scheduleNext();
68}
69
70void ResourceScheduler::scheduleSync(const Collection &col)
71{
72 Task t;
73 t.type = SyncCollection;
74 t.collection = col;
75 TaskList &queue = queueForTaskType(t.type);
76 if (queue.contains(t) || mCurrentTask == t) {
77 return;
78 }
79 queue << t;
80 signalTaskToTracker(t, "SyncCollection", QString::number(col.id()));
81 scheduleNext();
82}
83
84void ResourceScheduler::scheduleAttributesSync(const Collection &collection)
85{
86 Task t;
87 t.type = SyncCollectionAttributes;
88 t.collection = collection;
89
90 TaskList &queue = queueForTaskType(t.type);
91 if (queue.contains(t) || mCurrentTask == t) {
92 return;
93 }
94 queue << t;
95 signalTaskToTracker(t, "SyncCollectionAttributes", QString::number(collection.id()));
96 scheduleNext();
97}
98
99void ResourceScheduler::scheduleItemFetch(const Akonadi::Item &item, const QSet<QByteArray> &parts, const QList<QDBusMessage> &msgs, qint64 parentId)
100
101{
102 Task t;
103 t.type = FetchItem;
104 t.items << item;
105 t.itemParts = parts;
106 t.dbusMsgs = msgs;
107 t.argument = parentId;
108
109 TaskList &queue = queueForTaskType(t.type);
110 queue << t;
111
112 signalTaskToTracker(t, "FetchItem", QString::number(item.id()));
113 scheduleNext();
114}
115
116void ResourceScheduler::scheduleItemsFetch(const Item::List &items, const QSet<QByteArray> &parts, const QDBusMessage &msg)
117{
118 Task t;
119 t.type = FetchItems;
120 t.items = items;
121 t.itemParts = parts;
122
123 // if the current task does already fetch the requested item, break here but
124 // keep the dbus message, so we can send the reply later on
125 if (mCurrentTask == t) {
126 mCurrentTask.dbusMsgs << msg;
127 return;
128 }
129
130 // If this task is already in the queue, merge with it.
131 TaskList &queue = queueForTaskType(t.type);
132 const int idx = queue.indexOf(t);
133 if (idx != -1) {
134 queue[idx].dbusMsgs << msg;
135 return;
136 }
137
138 t.dbusMsgs << msg;
139 queue << t;
140
141 QStringList ids;
142 ids.reserve(items.size());
143 for (const auto &item : items) {
144 ids.push_back(QString::number(item.id()));
145 }
146 signalTaskToTracker(t, "FetchItems", ids.join(QLatin1StringView(", ")));
147 scheduleNext();
148}
149
150void ResourceScheduler::scheduleResourceCollectionDeletion()
151{
152 Task t;
153 t.type = DeleteResourceCollection;
154 TaskList &queue = queueForTaskType(t.type);
155 if (queue.contains(t) || mCurrentTask == t) {
156 return;
157 }
158 queue << t;
159 signalTaskToTracker(t, "DeleteResourceCollection");
160 scheduleNext();
161}
162
163void ResourceScheduler::scheduleCacheInvalidation(const Collection &collection)
164{
165 Task t;
166 t.type = InvalideCacheForCollection;
167 t.collection = collection;
168 TaskList &queue = queueForTaskType(t.type);
169 if (queue.contains(t) || mCurrentTask == t) {
170 return;
171 }
172 queue << t;
173 signalTaskToTracker(t, "InvalideCacheForCollection", QString::number(collection.id()));
174 scheduleNext();
175}
176
177void ResourceScheduler::scheduleChangeReplay()
178{
179 Task t;
180 t.type = ChangeReplay;
181 TaskList &queue = queueForTaskType(t.type);
182 // see ResourceBase::changeProcessed() for why we do not check for mCurrentTask == t here like in the other tasks
183 if (queue.contains(t)) {
184 return;
185 }
186 queue << t;
187 signalTaskToTracker(t, "ChangeReplay");
188 scheduleNext();
189}
190
191void ResourceScheduler::scheduleMoveReplay(const Collection &movedCollection, RecursiveMover *mover)
192{
193 Task t;
194 t.type = RecursiveMoveReplay;
195 t.collection = movedCollection;
196 t.argument = QVariant::fromValue(mover);
197 TaskList &queue = queueForTaskType(t.type);
198
199 if (queue.contains(t) || mCurrentTask == t) {
200 return;
201 }
202
203 queue << t;
204 signalTaskToTracker(t, "RecursiveMoveReplay", QString::number(t.collection.id()));
205 scheduleNext();
206}
207
208void Akonadi::ResourceScheduler::scheduleFullSyncCompletion()
209{
210 Task t;
211 t.type = SyncAllDone;
212 TaskList &queue = queueForTaskType(t.type);
213 // no compression here, all this does is emitting a D-Bus signal anyway, and compression can trigger races on the receiver side with the signal being lost
214 queue << t;
215 signalTaskToTracker(t, "SyncAllDone");
216 scheduleNext();
217}
218
219void Akonadi::ResourceScheduler::scheduleCollectionTreeSyncCompletion()
220{
221 Task t;
222 t.type = SyncCollectionTreeDone;
223 TaskList &queue = queueForTaskType(t.type);
224 // no compression here, all this does is emitting a D-Bus signal anyway, and compression can trigger races on the receiver side with the signal being lost
225 queue << t;
226 signalTaskToTracker(t, "SyncCollectionTreeDone");
227 scheduleNext();
228}
229
230void Akonadi::ResourceScheduler::scheduleCustomTask(QObject *receiver,
231 const char *methodName,
232 const QVariant &argument,
234{
235 Task t;
236 t.type = Custom;
237 t.receiver = receiver;
238 t.methodName = methodName;
239 t.argument = argument;
240 QueueType queueType = GenericTaskQueue;
241 if (priority == ResourceBase::AfterChangeReplay) {
242 queueType = AfterChangeReplayQueue;
243 } else if (priority == ResourceBase::Prepend) {
244 queueType = PrependTaskQueue;
245 }
246 TaskList &queue = mTaskList[queueType];
247
248 if (queue.contains(t)) {
249 return;
250 }
251
252 switch (priority) {
254 queue.prepend(t);
255 break;
256 default:
257 queue.append(t);
258 break;
259 }
260
261 signalTaskToTracker(t, "Custom-" + t.methodName);
262 scheduleNext();
263}
264
265void ResourceScheduler::taskDone()
266{
267 if (isEmpty()) {
268 Q_EMIT status(AgentBase::Idle, i18nc("@info:status Application ready for work", "Ready"));
269 }
270
271 if (s_resourcetracker) {
272 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), QString()};
273 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
274 }
275
276 mCurrentTask = Task();
277 mCurrentTasksQueue = -1;
278 scheduleNext();
279}
280
281void ResourceScheduler::itemFetchDone(const QString &msg)
282{
283 Q_ASSERT(mCurrentTask.type == FetchItem);
284
285 TaskList &queue = queueForTaskType(mCurrentTask.type);
286
287 const qint64 parentId = mCurrentTask.argument.toLongLong();
288 // msg is empty, there was no error
289 if (msg.isEmpty() && !queue.isEmpty()) {
290 Task &nextTask = queue[0];
291 // If the next task is FetchItem too...
292 if (nextTask.type != mCurrentTask.type || nextTask.argument.toLongLong() != parentId) {
293 // If the next task is not FetchItem or the next FetchItem task has
294 // different parentId then this was the last task in the series, so
295 // send the DBus replies.
296 mCurrentTask.sendDBusReplies(msg);
297 }
298 } else {
299 // msg was not empty, there was an error.
300 // remove all subsequent FetchItem tasks with the same parentId
301 auto iter = queue.begin();
302 while (iter != queue.end()) {
303 if (iter->type != mCurrentTask.type || iter->argument.toLongLong() == parentId) {
304 iter = queue.erase(iter);
305 continue;
306 } else {
307 break;
308 }
309 }
310
311 // ... and send DBus reply with the error message
312 mCurrentTask.sendDBusReplies(msg);
313 }
314
315 taskDone();
316}
317
318void ResourceScheduler::deferTask()
319{
320 if (mCurrentTask.type == Invalid) {
321 return;
322 }
323
324 if (s_resourcetracker) {
325 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), QString()};
326 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
327 }
328
329 Task t = mCurrentTask;
330 mCurrentTask = Task();
331
332 Q_ASSERT(mCurrentTasksQueue >= 0 && mCurrentTasksQueue < NQueueCount);
333 mTaskList[mCurrentTasksQueue].prepend(t);
334 mCurrentTasksQueue = -1;
335
336 signalTaskToTracker(t, "DeferedTask");
337
338 scheduleNext();
339}
340
341bool ResourceScheduler::isEmpty()
342{
343 for (int i = 0; i < NQueueCount; ++i) {
344 if (!mTaskList[i].isEmpty()) {
345 return false;
346 }
347 }
348 return true;
349}
350
351void ResourceScheduler::scheduleNext()
352{
353 if (mCurrentTask.type != Invalid || isEmpty() || !mOnline) {
354 return;
355 }
356 QTimer::singleShot(0s, this, &ResourceScheduler::executeNext);
357}
358
359void ResourceScheduler::executeNext()
360{
361 if (mCurrentTask.type != Invalid || isEmpty()) {
362 return;
363 }
364
365 for (int i = 0; i < NQueueCount; ++i) {
366 if (!mTaskList[i].isEmpty()) {
367 mCurrentTask = mTaskList[i].takeFirst();
368 mCurrentTasksQueue = i;
369 break;
370 }
371 }
372
373 if (s_resourcetracker) {
374 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial)};
375 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobStarted"), argumentList);
376 }
377
378 switch (mCurrentTask.type) {
379 case SyncAll:
380 Q_EMIT executeFullSync();
381 break;
382 case SyncCollectionTree:
383 Q_EMIT executeCollectionTreeSync();
384 break;
385 case SyncCollection:
386 Q_EMIT executeCollectionSync(mCurrentTask.collection);
387 break;
388 case SyncCollectionAttributes:
389 Q_EMIT executeCollectionAttributesSync(mCurrentTask.collection);
390 break;
391 case SyncTags:
392 Q_EMIT executeTagSync();
393 break;
394 case FetchItem:
395 Q_EMIT executeItemFetch(mCurrentTask.items.at(0), mCurrentTask.itemParts);
396 break;
397 case FetchItems:
398 Q_EMIT executeItemsFetch(mCurrentTask.items, mCurrentTask.itemParts);
399 break;
400 case DeleteResourceCollection:
401 Q_EMIT executeResourceCollectionDeletion();
402 break;
403 case InvalideCacheForCollection:
404 Q_EMIT executeCacheInvalidation(mCurrentTask.collection);
405 break;
406 case ChangeReplay:
407 Q_EMIT executeChangeReplay();
408 break;
409 case RecursiveMoveReplay:
410 Q_EMIT executeRecursiveMoveReplay(mCurrentTask.argument.value<RecursiveMover *>());
411 break;
412 case SyncAllDone:
413 Q_EMIT fullSyncComplete();
414 break;
415 case SyncCollectionTreeDone:
416 Q_EMIT collectionTreeSyncComplete();
417 break;
418 case Custom: {
419 const QByteArray methodSig = mCurrentTask.methodName + QByteArray("(QVariant)");
420 const bool hasSlotWithVariant = mCurrentTask.receiver->metaObject()->indexOfMethod(methodSig.constData()) != -1;
421 bool success = false;
422 if (hasSlotWithVariant) {
423 success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData(), Q_ARG(QVariant, mCurrentTask.argument));
424 Q_ASSERT_X(success || !mCurrentTask.argument.isValid(),
425 "ResourceScheduler::executeNext",
426 "Valid argument was provided but the method wasn't found");
427 }
428 if (!success) {
429 success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData());
430 }
431
432 if (!success) {
433 qCCritical(AKONADIAGENTBASE_LOG) << "Could not invoke slot" << mCurrentTask.methodName << "on" << mCurrentTask.receiver << "with argument"
434 << mCurrentTask.argument;
435 }
436 break;
437 }
438 default: {
439 qCCritical(AKONADIAGENTBASE_LOG) << "Unhandled task type" << mCurrentTask.type;
440 dump();
441 Q_ASSERT(false);
442 }
443 }
444}
445
446ResourceScheduler::Task ResourceScheduler::currentTask() const
447{
448 return mCurrentTask;
449}
450
451ResourceScheduler::Task &ResourceScheduler::currentTask()
452{
453 return mCurrentTask;
454}
455
456void ResourceScheduler::setOnline(bool state)
457{
458 if (mOnline == state) {
459 return;
460 }
461 mOnline = state;
462 if (mOnline) {
463 scheduleNext();
464 } else {
465 if (mCurrentTask.type != Invalid) {
466 // abort running task
467 queueForTaskType(mCurrentTask.type).prepend(mCurrentTask);
468 mCurrentTask = Task();
469 mCurrentTasksQueue = -1;
470 }
471 // abort pending synchronous tasks, might take longer until the resource goes online again
472 TaskList &itemFetchQueue = queueForTaskType(FetchItem);
473 qint64 parentId = -1;
474 Task lastTask;
475 for (QList<Task>::iterator it = itemFetchQueue.begin(); it != itemFetchQueue.end();) {
476 if ((*it).type == FetchItem) {
477 qint64 idx = it->argument.toLongLong();
478 if (parentId == -1) {
479 parentId = idx;
480 }
481 if (idx != parentId) {
482 // Only emit the DBus reply once we reach the last taskwith the
483 // same "idx"
484 lastTask.sendDBusReplies(i18nc("@info", "Job canceled."));
485 parentId = idx;
486 }
487 lastTask = (*it);
488 it = itemFetchQueue.erase(it);
489 if (s_resourcetracker) {
490 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), i18nc("@info", "Job canceled.")};
491 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
492 }
493 } else {
494 ++it;
495 }
496 }
497 }
498}
499
500void ResourceScheduler::signalTaskToTracker(const Task &task, const QByteArray &taskType, const QString &debugString)
501{
502 // if there's a job tracer running, tell it about the new job
503 if (!s_resourcetracker) {
504 const QString suffix = Akonadi::Instance::identifier().isEmpty() ? QString() : QLatin1Char('-') + Akonadi::Instance::identifier();
505 if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.akonadiconsole") + suffix)) {
506 s_resourcetracker = new QDBusInterface(QLatin1StringView("org.kde.akonadiconsole") + suffix,
507 QStringLiteral("/resourcesJobtracker"),
508 QStringLiteral("org.freedesktop.Akonadi.JobTracker"),
510 nullptr);
511 }
512 }
513
514 if (s_resourcetracker) {
515 const QList<QVariant> argumentList = QList<QVariant>() << static_cast<AgentBase *>(parent())->identifier() // "session" (in our case resource)
516 << QString::number(task.serial) // "job"
517 << QString() // "parent job"
518 << QString::fromLatin1(taskType) // "job type"
519 << debugString // "job debugging string"
520 ;
521 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobCreated"), argumentList);
522 }
523}
524
525void ResourceScheduler::collectionRemoved(const Akonadi::Collection &collection)
526{
527 if (!collection.isValid()) { // should not happen, but you never know...
528 return;
529 }
530 TaskList &queue = queueForTaskType(SyncCollection);
531 for (QList<Task>::iterator it = queue.begin(); it != queue.end();) {
532 if ((*it).type == SyncCollection && (*it).collection == collection) {
533 it = queue.erase(it);
534 qCDebug(AKONADIAGENTBASE_LOG) << " erasing";
535 } else {
536 ++it;
537 }
538 }
539}
540
541void ResourceScheduler::Task::sendDBusReplies(const QString &errorMsg)
542{
543 for (const QDBusMessage &msg : std::as_const(dbusMsgs)) {
544 qCDebug(AKONADIAGENTBASE_LOG) << "Sending dbus reply for method" << methodName << "with error" << errorMsg;
545 QDBusMessage reply;
546 if (!errorMsg.isEmpty()) {
547 reply = msg.createErrorReply(QDBusError::Failed, errorMsg);
548 } else if (msg.member() == QLatin1StringView("requestItemDelivery")) {
549 reply = msg.createReply();
550 } else if (msg.member().isEmpty()) {
551 continue; // unittest calls scheduleItemFetch with empty QDBusMessage
552 } else {
553 qCCritical(AKONADIAGENTBASE_LOG) << "ResourceScheduler: got unexpected method name :" << msg.member();
554 }
556 }
557}
558
559ResourceScheduler::QueueType ResourceScheduler::queueTypeForTaskType(TaskType type)
560{
561 switch (type) {
562 case ChangeReplay:
563 case RecursiveMoveReplay:
564 return ChangeReplayQueue;
565 case FetchItem:
566 case FetchItems:
567 case SyncCollectionAttributes:
568 return UserActionQueue;
569 default:
570 return GenericTaskQueue;
571 }
572}
573
574ResourceScheduler::TaskList &ResourceScheduler::queueForTaskType(TaskType type)
575{
576 const QueueType qt = queueTypeForTaskType(type);
577 return mTaskList[qt];
578}
579
580void ResourceScheduler::dump() const
581{
582 qCDebug(AKONADIAGENTBASE_LOG) << dumpToString();
583}
584
585QString ResourceScheduler::dumpToString() const
586{
587 QString ret;
588 QTextStream str(&ret);
589 str << "ResourceScheduler: " << (mOnline ? "Online" : "Offline") << '\n';
590 str << " current task: " << mCurrentTask << '\n';
591 for (int i = 0; i < NQueueCount; ++i) {
592 const TaskList &queue = mTaskList[i];
593 if (queue.isEmpty()) {
594 str << " queue " << i << " is empty" << '\n';
595 } else {
596 str << " queue " << i << " " << queue.size() << " tasks:\n";
597 const QList<Task>::const_iterator queueEnd(queue.constEnd());
598 for (QList<Task>::const_iterator it = queue.constBegin(); it != queueEnd; ++it) {
599 str << " " << (*it) << '\n';
600 }
601 }
602 }
603 str.flush();
604 return ret;
605}
606
607void ResourceScheduler::clear()
608{
609 qCDebug(AKONADIAGENTBASE_LOG) << "Clearing ResourceScheduler queues:";
610 for (int i = 0; i < NQueueCount; ++i) {
611 TaskList &queue = mTaskList[i];
612 queue.clear();
613 }
614 mCurrentTask = Task();
615 mCurrentTasksQueue = -1;
616}
617
618void Akonadi::ResourceScheduler::cancelQueues()
619{
620 for (int i = 0; i < NQueueCount; ++i) {
621 TaskList &queue = mTaskList[i];
622 if (s_resourcetracker) {
623 for (const Task &t : queue) {
624 QList<QVariant> argumentList{QString::number(t.serial), QString()};
625 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
626 }
627 }
628 queue.clear();
629 }
630}
631
632static const char s_taskTypes[][27] = {"Invalid (no task)",
633 "SyncAll",
634 "SyncCollectionTree",
635 "SyncCollection",
636 "SyncCollectionAttributes",
637 "SyncTags",
638 "FetchItem",
639 "FetchItems",
640 "ChangeReplay",
641 "RecursiveMoveReplay",
642 "DeleteResourceCollection",
643 "InvalideCacheForCollection",
644 "SyncAllDone",
645 "SyncCollectionTreeDone",
646 "Custom"};
647
648QTextStream &Akonadi::operator<<(QTextStream &d, const ResourceScheduler::Task &task)
649{
650 d << task.serial << " " << s_taskTypes[task.type] << " ";
651 if (task.type != ResourceScheduler::Invalid) {
652 if (task.collection.isValid()) {
653 d << "collection " << task.collection.id() << " ";
654 }
655 if (!task.items.isEmpty()) {
656 QStringList ids;
657 ids.reserve(task.items.size());
658 for (const auto &item : std::as_const(task.items)) {
659 ids.push_back(QString::number(item.id()));
660 }
661 d << "items " << ids.join(QLatin1StringView(", ")) << " ";
662 }
663 if (!task.methodName.isEmpty()) {
664 d << task.methodName << " " << task.argument.toString();
665 }
666 }
667 return d;
668}
669
670QDebug Akonadi::operator<<(QDebug d, const ResourceScheduler::Task &task)
671{
672 QString s;
673 QTextStream str(&s);
674 str << task;
675 d << s;
676 return d;
677}
678
679/// @endcond
680
681#include "moc_resourcescheduler_p.cpp"
The base class for all Akonadi agents and resources.
Definition agentbase.h:73
@ Idle
The agent does currently nothing.
Definition agentbase.h:397
Represents a collection of PIM items.
Definition collection.h:62
Represents a PIM item stored in Akonadi storage.
Definition item.h:100
Id id() const
Returns the unique identifier of the item.
Definition item.cpp:63
SchedulePriority
Describes the scheduling priority of a task that has been queued for execution.
@ Prepend
The task will be executed as soon as the current task has finished.
@ AfterChangeReplay
The task is scheduled after the last ChangeReplay task in the queue.
Q_SCRIPTABLE CaptureState status()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
Helper integration between Akonadi and Qt.
const char * constData() const const
QDBusPendingCall asyncCallWithArgumentList(const QString &method, const QList< QVariant > &args)
bool send(const QDBusMessage &message) const const
QDBusConnection sessionBus()
void push_back(parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QString join(QChar separator) const const
QTaskBuilder< Task > task(Task &&task)
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:11:38 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.