swift
datacache.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2015 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include "misc/datacache.h"
7 
8 #include <chrono>
9 #include <memory>
10 #include <utility>
11 
12 #include <QByteArray>
13 #include <QDir>
14 #include <QFile>
15 #include <QFlags>
16 #include <QIODevice>
17 #include <QJsonDocument>
18 #include <QJsonValue>
19 #include <QMutexLocker>
20 #include <QStandardPaths>
21 #include <QTimer>
22 #include <Qt>
23 
24 #include "misc/atomicfile.h"
25 #include "misc/identifier.h"
26 #include "misc/logmessage.h"
27 #include "misc/processinfo.h"
28 
29 namespace swift::misc
30 {
31 
32  using private_ns::CValuePage;
33  using private_ns::CDataPageQueue;
34 
35  class CDataCacheRevision::LockGuard
36  {
37  public:
38  LockGuard(const LockGuard &) = delete;
39  LockGuard &operator=(const LockGuard &) = delete;
40  LockGuard(LockGuard &&other) noexcept : m_movedFrom(true) { *this = std::move(other); }
41  LockGuard &operator=(LockGuard &&other) noexcept
42  {
43  auto tuple = std::tie(other.m_movedFrom, other.m_keepPromises, other.m_rev);
44  std::tie(m_movedFrom, m_keepPromises, m_rev).swap(tuple);
45  return *this;
46  }
47 
48  ~LockGuard()
49  {
50  if (!m_movedFrom) { m_rev->finishUpdate(m_keepPromises); }
51  }
52 
53  operator bool() const { return !m_movedFrom; }
54 
55  private:
56  LockGuard() : m_movedFrom(true) {}
57  LockGuard(CDataCacheRevision *rev) : m_movedFrom(!rev), m_rev(rev) {}
58  LockGuard &keepPromises()
59  {
60  m_keepPromises = true;
61  return *this;
62  }
63  friend class CDataCacheRevision;
64 
65  bool m_movedFrom = false;
66  bool m_keepPromises = false;
67  CDataCacheRevision *m_rev = nullptr;
68  };
69 
70  CDataCache::CDataCache() : CValueCache(1), m_serializer(new CDataCacheSerializer { this, revisionFileName() })
71  {
72  if (!QDir::root().mkpath(persistentStore()))
73  {
74  CLogMessage(this).error(u"Failed to create directory '%1'") << persistentStore();
75  }
76 
77  connect(this, &CValueCache::valuesChangedByLocal, this, &CDataCache::saveToStoreAsync);
78  connect(this, &CValueCache::valuesChangedByLocal, this, [=](CValueCachePacket values) {
79  values.setSaved();
80  changeValuesFromRemote(values, CIdentifier());
81  });
82  connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &CDataCache::loadFromStoreAsync);
83  connect(m_serializer, &CDataCacheSerializer::valuesLoadedFromStore, this, &CDataCache::changeValuesFromRemote,
84  Qt::DirectConnection);
85 
86  if (!QFile::exists(revisionFileName())) { QFile(revisionFileName()).open(QFile::WriteOnly); }
87  m_serializer->loadFromStore({}, false, true); // load pinned values
88  singleShot(0, this,
89  [this] // only start the serializer if the main thread event loop runs
90  {
91  m_serializer->start();
92  m_watcher.addPath(revisionFileName());
93  loadFromStoreAsync();
94  });
95  }
96 
97  CDataCache::~CDataCache() { m_serializer->quitAndWait(); }
98 
99  CDataCache *CDataCache::instance()
100  {
101  static std::unique_ptr<CDataCache> cache(new CDataCache);
102  static auto dummy = (connect(qApp, &QObject::destroyed, cache.get(), [] { cache.reset(); }), nullptr);
103  Q_UNUSED(dummy) // declared as static to get thread-safe initialization
104  return cache.get();
105  }
106 
107  const QString &CDataCache::persistentStore()
108  {
109  static const QString dir = CFileUtils::appendFilePaths(getCacheRootDirectory(), relativeFilePath());
110  return dir;
111  }
112 
113  const QString &CDataCache::revisionFileName()
114  {
115  static const QString rev = CFileUtils::appendFilePaths(persistentStore(), ".rev");
116  return rev;
117  }
118 
119  QString CDataCache::filenameForKey(const QString &key)
120  {
121  return CFileUtils::appendFilePaths(persistentStore(), instance()->CValueCache::filenameForKey(key));
122  }
123 
124  QStringList CDataCache::enumerateStore() const { return enumerateFiles(persistentStore()); }
125 
126  bool CDataCache::synchronize(const QString &key)
127  {
128  constexpr auto timeout = std::chrono::seconds(1);
129  constexpr auto ready = std::future_status::ready;
130  constexpr auto zero = std::chrono::seconds::zero();
131 
132  std::future<void> future = m_revision.promiseLoadedValue(key, getTimestampSync(key));
133  if (future.valid())
134  {
135  std::future_status s {};
136  do {
137  s = future.wait_for(timeout);
138  }
139  while (s != ready && m_revision.isNewerValueAvailable(key, getTimestampSync(key)));
140  if (s != ready) { s = future.wait_for(zero); }
141  if (s != ready) { return false; }
142 
145  // maybe this happens if a cache is written and this takes a while, maybe we can
146  // use a write in prgress flag or such?
147  try
148  {
149  future.get();
150  }
151  catch (const std::future_error &)
152  {
153  return false;
154  } // broken promise
155  return true;
156  }
157  return false;
158  }
159 
160  void CDataCache::setTimeToLive(const QString &key, int ttl)
161  {
162  singleShot(0, m_serializer, [this, key, ttl] { m_revision.setTimeToLive(key, ttl); });
163  }
164 
165  void CDataCache::renewTimestamp(const QString &key, qint64 timestamp)
166  {
167  singleShot(0, m_serializer, [this, key, timestamp] { m_revision.overrideTimestamp(key, timestamp); });
168  }
169 
170  qint64 CDataCache::getTimestampOnDisk(const QString &key) { return m_revision.getTimestampOnDisk(key); }
171 
172  void CDataCache::pinValue(const QString &key)
173  {
174  singleShot(0, m_serializer, [this, key] { m_revision.pinValue(key); });
175  }
176 
177  void CDataCache::deferValue(const QString &key)
178  {
179  singleShot(0, m_serializer, [this, key] { m_revision.deferValue(key); });
180  }
181 
182  void CDataCache::admitValue(const QString &key, bool triggerLoad)
183  {
184  m_revision.admitValue(key);
185  if (triggerLoad) { loadFromStoreAsync(); }
186  }
187 
188  void CDataCache::sessionValue(const QString &key)
189  {
190  singleShot(0, m_serializer, [this, key] { m_revision.sessionValue(key); });
191  }
192 
193  const QString &CDataCache::relativeFilePath()
194  {
195  static const QString p("/data/cache/core");
196  return p;
197  }
198 
199  void CDataCache::saveToStoreAsync(const swift::misc::CValueCachePacket &values)
200  {
201  singleShot(0, m_serializer,
202  [this, values] { m_serializer->saveToStore(values.toVariantMap(), getAllValuesWithTimestamps()); });
203  }
204 
205  void CDataCache::loadFromStoreAsync()
206  {
207  singleShot(0, m_serializer, [this] { m_serializer->loadFromStore(getAllValuesWithTimestamps()); });
208  }
209 
210  void CDataCache::connectPage(CValuePage *page)
211  {
212  auto *queue = new CDataPageQueue(page);
213  connect(page, &CValuePage::valuesWantToCache, this, &CDataCache::changeValues);
214  connect(this, &CDataCache::valuesChanged, queue, &CDataPageQueue::queueValuesFromCache, Qt::DirectConnection);
215  }
216 
217  void CDataPageQueue::queueValuesFromCache(const CValueCachePacket &values, QObject *changedBy)
218  {
219  QMutexLocker lock(&m_mutex);
220  if (m_queue.isEmpty())
221  {
222  singleShot(0, this, [this] { setQueuedValuesFromCache(); });
223  }
224  m_queue.push_back(std::make_pair(values, changedBy));
225  }
226 
227  void CDataPageQueue::setQueuedValuesFromCache()
228  {
229  QMutexLocker lock(&m_mutex);
230  decltype(m_queue) queue;
231  std::swap(m_queue, queue);
232  lock.unlock();
233 
234  for (const auto &pair : std::as_const(queue)) { m_page->setValuesFromCache(pair.first, pair.second); }
235  }
236 
237  void CDataPageQueue::setQueuedValueFromCache(const QString &key)
238  {
239  QMutexLocker lock(&m_mutex);
240 
241  decltype(m_queue) filtered;
242  for (auto &pair : m_queue)
243  {
244  if (pair.first.contains(key)) { filtered.push_back({ pair.first.takeByKey(key), pair.second }); }
245  }
246  lock.unlock();
247  for (const auto &pair : filtered) { m_page->setValuesFromCache(pair.first, pair.second); }
248  }
249 
250  const QStringList &CDataCacheSerializer::getLogCategories()
251  {
252  static const QStringList cats { swift::misc::CLogCategories::cache() };
253  return cats;
254  }
255 
256  CDataCacheSerializer::CDataCacheSerializer(CDataCache *owner, const QString &revisionFileName)
257  : CContinuousWorker(owner, QStringLiteral("CDataCacheSerializer '%1'").arg(revisionFileName)), m_cache(owner),
258  m_revisionFileName(revisionFileName)
259  {}
260 
261  const QString &CDataCacheSerializer::persistentStore() const { return m_cache->persistentStore(); }
262 
263  void CDataCacheSerializer::saveToStore(const swift::misc::CVariantMap &values,
264  const swift::misc::CValueCachePacket &baseline)
265  {
266  m_cache->m_revision.notifyPendingWrite();
267  auto lock =
268  loadFromStore(baseline, true); // last-minute check for remote changes before clobbering the revision file
269  for (const auto &key : values.keys())
270  {
271  m_deferredChanges.remove(key);
272  } // ignore changes that we are about to overwrite
273 
274  if (!lock) { return; }
275  m_cache->m_revision.writeNewRevision(baseline.toTimestampMap());
276 
277  auto msg = m_cache->saveToFiles(persistentStore(), values, baseline.toTimestampMapString(values.keys()));
278  msg.setCategories(this);
279  CLogMessage::preformatted(msg);
280 
281  applyDeferredChanges(); // apply changes which we grabbed at the last minute above
282  }
283 
284  CDataCacheRevision::LockGuard CDataCacheSerializer::loadFromStore(const CValueCachePacket &baseline, bool defer,
285  bool pinsOnly)
286  {
287  auto lock = m_cache->m_revision.beginUpdate(baseline.toTimestampMap(), !pinsOnly, pinsOnly);
288  if (lock && m_cache->m_revision.isPendingRead())
289  {
290  CValueCachePacket newValues;
291  if (!m_cache->m_revision.isFound())
292  {
293  m_cache->loadFromFiles(persistentStore(), {}, {}, newValues, {}, true);
294  m_cache->m_revision.regenerate(newValues);
295  newValues.clear();
296  }
297  auto msg =
298  m_cache->loadFromFiles(persistentStore(), m_cache->m_revision.keysWithNewerTimestamps(),
299  baseline.toVariantMap(), newValues, m_cache->m_revision.timestampsAsString());
300  newValues.setTimestamps(m_cache->m_revision.newerTimestamps());
301 
302  auto missingKeys = m_cache->m_revision.keysWithNewerTimestamps() - newValues.keys();
303  if (!missingKeys.isEmpty()) { m_cache->m_revision.writeNewRevision({}, missingKeys); }
304 
305  msg.setCategories(this);
306  CLogMessage::preformatted(msg);
307  m_deferredChanges.insert(newValues);
308  }
309 
310  if (!defer) { applyDeferredChanges(); }
311  return lock;
312  }
313 
314  void CDataCacheSerializer::applyDeferredChanges()
315  {
316  if (!m_deferredChanges.isEmpty())
317  {
318  m_deferredChanges.setSaved();
319  emit valuesLoadedFromStore(m_deferredChanges, CIdentifier::null());
320  deliverPromises(m_cache->m_revision.loadedValuePromises());
321  m_deferredChanges.clear();
322  }
323  }
324 
325  void CDataCacheSerializer::deliverPromises(std::vector<std::promise<void>> i_promises)
326  {
327  QTimer::singleShot(0, Qt::PreciseTimer, this,
328  [promises = std::make_shared<decltype(i_promises)>(std::move(i_promises))]() {
329  for (auto &promise : *promises) { promise.set_value(); }
330  });
331  }
332 
333  class SWIFT_MISC_EXPORT CDataCacheRevision::Session
334  {
335  public:
336  // cppcheck-suppress missingReturn
337  Session(const QString &filename) : m_filename(filename) {}
338  void updateSession();
339  const QUuid &uuid() const { return m_uuid; }
340 
341  private:
342  const QString m_filename;
343  QUuid m_uuid;
344  };
345 
346  CDataCacheRevision::CDataCacheRevision(const QString &basename)
347  : m_basename(basename), m_session(std::make_unique<Session>(m_basename + "/.session"))
348  {}
349 
350  CDataCacheRevision::~CDataCacheRevision() = default;
351 
352  CDataCacheRevision::LockGuard CDataCacheRevision::beginUpdate(const QMap<QString, qint64> &timestamps,
353  bool updateUuid, bool pinsOnly)
354  {
355  QMutexLocker lock(&m_mutex);
356 
357  Q_ASSERT(!m_updateInProgress);
358  Q_ASSERT(!m_lockFile.isLocked());
359 
360  if (!m_lockFile.lock())
361  {
362  CLogMessage(this).error(u"Failed to lock %1: %2") << m_basename << CFileUtils::lockFileError(m_lockFile);
363  return {};
364  }
365  m_updateInProgress = true;
366  LockGuard guard(this);
367 
368  m_timestamps.clear();
369  m_originalTimestamps.clear();
370 
371  QFile revisionFile(CFileUtils::appendFilePaths(m_basename, ".rev"));
372  if ((m_found = revisionFile.exists()))
373  {
374  if (!revisionFile.open(QFile::ReadOnly | QFile::Text))
375  {
376  CLogMessage(this).error(u"Failed to open %1: %2")
377  << revisionFile.fileName() << revisionFile.errorString();
378  return {};
379  }
380 
381  auto json = QJsonDocument::fromJson(revisionFile.readAll()).object();
382  if (json.contains("uuid") && json.contains("timestamps"))
383  {
384  m_originalTimestamps = fromJson(json.value("timestamps").toObject());
385 
386  QUuid id(json.value("uuid").toString());
387  if (id == m_uuid && m_admittedQueue.isEmpty())
388  {
389  if (m_pendingWrite) { return guard; }
390  return {};
391  }
392  if (updateUuid) { m_uuid = id; }
393 
394  auto timesToLive = fromJson(json.value("ttl").toObject());
395  for (auto it = m_originalTimestamps.cbegin(); it != m_originalTimestamps.cend(); ++it)
396  {
397  auto current = timestamps.value(it.key(), -1);
398  auto ttl = timesToLive.value(it.key(), -1);
399  if (current < it.value() && (ttl < 0 || QDateTime::currentMSecsSinceEpoch() < it.value() + ttl))
400  {
401  m_timestamps.insert(it.key(), it.value());
402  }
403  }
404  if (m_timestamps.isEmpty())
405  {
406  if (m_pendingWrite) { return guard; }
407  return {};
408  }
409 
410  if (pinsOnly)
411  {
412  auto pins = fromJson(json.value("pins").toArray());
413  for (const auto &key : m_timestamps.keys()) // clazy:exclude=container-anti-pattern,range-loop
414  {
415  if (!pins.contains(key)) { m_timestamps.remove(key); }
416  }
417  }
418 
419  auto deferrals = fromJson(json.value("deferrals").toArray());
420  m_admittedValues.unite(m_admittedQueue);
421  if (updateUuid) { m_admittedQueue.clear(); }
422  else if (!m_admittedQueue.isEmpty())
423  {
424  m_admittedQueue.intersect(QSet<QString>(m_timestamps.keyBegin(), m_timestamps.keyEnd()));
425  }
426 
427  for (const auto &key : m_timestamps.keys()) // clazy:exclude=container-anti-pattern,range-loop
428  {
429  if (deferrals.contains(key) && !m_admittedValues.contains(key)) { m_timestamps.remove(key); }
430  }
431 
432  m_session->updateSession();
433  auto sessionIds = sessionFromJson(json.value("session").toObject());
434  for (auto it = sessionIds.cbegin(); it != sessionIds.cend(); ++it)
435  {
436  m_sessionValues[it.key()] = it.value();
437  if (it.value() != m_session->uuid())
438  {
439  m_timestamps.remove(it.key());
440  m_originalTimestamps.remove(it.key());
441  }
442  }
443  }
444  else if (revisionFile.size() > 0)
445  {
446  CLogMessage(this).error(u"Invalid format of %1") << revisionFile.fileName();
447 
448  if (m_pendingWrite) { return guard; }
449  return {};
450  }
451  else { m_found = false; }
452  }
453 
454  m_pendingRead = true;
455  return guard;
456  }
457 
458  void CDataCacheRevision::writeNewRevision(const QMap<QString, qint64> &i_timestamps,
459  const QSet<QString> &excludeKeys)
460  {
461  QMutexLocker lock(&m_mutex);
462 
463  Q_ASSERT(m_updateInProgress);
464  Q_ASSERT(m_lockFile.isLocked());
465 
466  CAtomicFile revisionFile(CFileUtils::appendFilePaths(m_basename, ".rev"));
467  if (!revisionFile.open(QFile::WriteOnly | QFile::Text))
468  {
469  CLogMessage(this).error(u"Failed to open %1: %2") << revisionFile.fileName() << revisionFile.errorString();
470  return;
471  }
472 
473  m_uuid = CIdentifier().toUuid();
474  auto timestamps = m_originalTimestamps;
475  for (auto it = i_timestamps.cbegin(); it != i_timestamps.cend(); ++it)
476  {
477  if (it.value()) { timestamps.insert(it.key(), it.value()); }
478  }
479  for (const auto &key : excludeKeys) { timestamps.remove(key); }
480 
481  for (auto it = timestamps.cbegin(); it != timestamps.cend(); ++it)
482  {
483  if (m_sessionValues.contains(it.key())) { m_sessionValues[it.key()] = m_session->uuid(); }
484  }
485 
486  QJsonObject json;
487  json.insert("uuid", m_uuid.toString());
488  json.insert("timestamps", toJson(timestamps));
489  json.insert("ttl", toJson(m_timesToLive));
490  json.insert("pins", toJson(m_pinnedValues));
491  json.insert("deferrals", toJson(m_deferredValues));
492  json.insert("session", toJson(m_sessionValues));
493  revisionFile.write(QJsonDocument(json).toJson());
494 
495  if (!revisionFile.checkedClose())
496  {
497  static const QString advice =
498  QStringLiteral("If this error persists, try restarting your computer or delete the file manually.");
499  CLogMessage(this).error(u"Failed to replace %1: %2 (%3)")
500  << revisionFile.fileName() << revisionFile.errorString() << advice;
501  }
502  }
503 
504  void CDataCacheRevision::regenerate(const CValueCachePacket &keys)
505  {
506  QMutexLocker lock(&m_mutex);
507 
508  Q_ASSERT(m_updateInProgress);
509  Q_ASSERT(m_lockFile.isLocked());
510 
511  writeNewRevision(m_originalTimestamps = keys.toTimestampMap());
512  }
513 
514  void CDataCacheRevision::finishUpdate(bool keepPromises)
515  {
516  QMutexLocker lock(&m_mutex);
517 
518  Q_ASSERT(m_updateInProgress);
519  Q_ASSERT(m_lockFile.isLocked());
520 
521  m_updateInProgress = false;
522  m_pendingRead = false;
523  m_pendingWrite = false;
524  if (!keepPromises) { breakPromises(); }
525  m_lockFile.unlock();
526  }
527 
528  bool CDataCacheRevision::isFound() const
529  {
530  QMutexLocker lock(&m_mutex);
531 
532  Q_ASSERT(m_updateInProgress);
533  return m_found;
534  }
535 
536  bool CDataCacheRevision::isPendingRead() const
537  {
538  QMutexLocker lock(&m_mutex);
539 
540  Q_ASSERT(m_updateInProgress);
541  return !m_timestamps.isEmpty() || !m_found;
542  }
543 
544  void CDataCacheRevision::notifyPendingWrite()
545  {
546  QMutexLocker lock(&m_mutex);
547 
548  m_pendingWrite = true;
549  }
550 
551  QSet<QString> CDataCacheRevision::keysWithNewerTimestamps() const
552  {
553  QMutexLocker lock(&m_mutex);
554 
555  Q_ASSERT(m_updateInProgress);
556  return QSet<QString>(m_timestamps.keyBegin(), m_timestamps.keyEnd());
557  }
558 
559  const QMap<QString, qint64> &CDataCacheRevision::newerTimestamps() const
560  {
561  QMutexLocker lock(&m_mutex);
562 
563  Q_ASSERT(m_updateInProgress);
564  return m_timestamps;
565  }
566 
567  bool CDataCacheRevision::isNewerValueAvailable(const QString &key, qint64 timestamp)
568  {
569  QMutexLocker lock(&m_mutex);
570 
571  // Temporary guard object returned by beginUpdate is deleted at the end of the full expression,
572  // don't try to split the conditional into multiple statements.
573  // If a future is still waiting for the next update to begin, we don't want to break its associated promise.
574  return (m_updateInProgress || m_pendingWrite || beginUpdate({ { key, timestamp } }, false).keepPromises()) &&
575  (m_timestamps.contains(key) || m_admittedQueue.contains(key));
576  }
577 
578  std::future<void> CDataCacheRevision::promiseLoadedValue(const QString &key, qint64 currentTimestamp)
579  {
580  QMutexLocker lock(&m_mutex);
581 
582  if (isNewerValueAvailable(key, currentTimestamp))
583  {
584  std::promise<void> promise;
585  auto future = promise.get_future();
586  m_promises.push_back(std::move(promise));
587  return future;
588  }
589  return {};
590  }
591 
592  std::vector<std::promise<void>> CDataCacheRevision::loadedValuePromises()
593  {
594  QMutexLocker lock(&m_mutex);
595 
596  Q_ASSERT(m_updateInProgress);
597  return std::move(m_promises); // move into the return value, so m_promises becomes empty
598  }
599 
600  void CDataCacheRevision::breakPromises()
601  {
602  QMutexLocker lock(&m_mutex);
603 
604  if (!m_promises.empty())
605  {
606  CLogMessage(this).debug() << "Breaking" << m_promises.size() << "promises";
607  m_promises.clear();
608  }
609  }
610 
611  QString CDataCacheRevision::timestampsAsString() const
612  {
613  QMutexLocker lock(&m_mutex);
614 
615  QStringList result;
616  for (auto it = m_timestamps.cbegin(); it != m_timestamps.cend(); ++it)
617  {
618  result.push_back(it.key() + "(" +
619  QDateTime::fromMSecsSinceEpoch(it.value(), Qt::UTC).toString(Qt::ISODate) + ")");
620  }
621  return result.join(",");
622  }
623 
624  void CDataCacheRevision::setTimeToLive(const QString &key, int ttl)
625  {
626  QMutexLocker lock(&m_mutex);
627 
628  Q_ASSERT(!m_updateInProgress);
629  m_timesToLive.insert(key, ttl);
630  }
631 
632  void CDataCacheRevision::overrideTimestamp(const QString &key, qint64 timestamp)
633  {
634  QMutexLocker lock(&m_mutex);
635 
636  Q_ASSERT(!m_updateInProgress);
637  Q_ASSERT(!m_lockFile.isLocked());
638 
639  if (!m_lockFile.lock())
640  {
641  CLogMessage(this).error(u"Failed to lock %1: %2") << m_basename << CFileUtils::lockFileError(m_lockFile);
642  m_lockFile.unlock();
643  return;
644  }
645 
646  CAtomicFile revisionFile(CFileUtils::appendFilePaths(m_basename, ".rev"));
647  if (revisionFile.exists())
648  {
649  if (!revisionFile.open(QFile::ReadWrite | QFile::Text))
650  {
651  CLogMessage(this).error(u"Failed to open %1: %2")
652  << revisionFile.fileName() << revisionFile.errorString();
653  m_lockFile.unlock();
654  return;
655  }
656 
657  auto json = QJsonDocument::fromJson(revisionFile.readAll()).object();
658  auto timestamps = json.value("timestamps").toObject();
659  timestamps.insert(key, timestamp);
660  json.insert("timestamps", timestamps);
661 
662  if (revisionFile.seek(0) && revisionFile.resize(0) && revisionFile.write(QJsonDocument(json).toJson()))
663  {
664  if (!revisionFile.checkedClose())
665  {
666  static const QString advice = QStringLiteral(
667  "If this error persists, try restarting your computer or delete the file manually.");
668  CLogMessage(this).error(u"Failed to replace %1: %2 (%3)")
669  << revisionFile.fileName() << revisionFile.errorString() << advice;
670  }
671  }
672  else
673  {
674  CLogMessage(this).error(u"Failed to write to %1: %2")
675  << revisionFile.fileName() << revisionFile.errorString();
676  }
677  }
678  m_lockFile.unlock();
679  }
680 
681  qint64 CDataCacheRevision::getTimestampOnDisk(const QString &key)
682  {
683  QMutexLocker lock(&m_mutex);
684 
685  if (m_lockFile.isLocked()) { return m_originalTimestamps.value(key); }
686 
687  if (!m_lockFile.lock())
688  {
689  CLogMessage(this).error(u"Failed to lock %1: %2") << m_basename << CFileUtils::lockFileError(m_lockFile);
690  m_lockFile.unlock();
691  return 0;
692  }
693 
694  qint64 result = 0;
695  QFile revisionFile(CFileUtils::appendFilePaths(m_basename, ".rev"));
696  if (revisionFile.exists())
697  {
698  if (revisionFile.open(QFile::ReadOnly | QFile::Text))
699  {
700  auto json = QJsonDocument::fromJson(revisionFile.readAll()).object();
701  result = static_cast<qint64>(json.value("timestamps").toObject().value(key).toDouble());
702  }
703  else
704  {
705  CLogMessage(this).error(u"Failed to open %1: %2")
706  << revisionFile.fileName() << revisionFile.errorString();
707  }
708  }
709  m_lockFile.unlock();
710  return result;
711  }
712 
713  void CDataCacheRevision::pinValue(const QString &key)
714  {
715  QMutexLocker lock(&m_mutex);
716 
717  Q_ASSERT(!m_updateInProgress);
718  m_pinnedValues.insert(key);
719  }
720 
721  void CDataCacheRevision::deferValue(const QString &key)
722  {
723  QMutexLocker lock(&m_mutex);
724 
725  Q_ASSERT(!m_updateInProgress);
726  m_deferredValues.insert(key);
727  }
728 
729  void CDataCacheRevision::admitValue(const QString &key)
730  {
731  QMutexLocker lock(&m_mutex);
732 
733  m_admittedQueue.insert(key);
734  }
735 
736  void CDataCacheRevision::sessionValue(const QString &key)
737  {
738  QMutexLocker lock(&m_mutex);
739 
740  Q_ASSERT(!m_updateInProgress);
741  m_sessionValues[key]; // clazy:exclude=detaching-member
742  }
743 
744  QJsonObject CDataCacheRevision::toJson(const QMap<QString, qint64> &timestamps)
745  {
746  QJsonObject result;
747  for (auto it = timestamps.begin(); it != timestamps.end(); ++it) { result.insert(it.key(), it.value()); }
748  return result;
749  }
750 
751  QMap<QString, qint64> CDataCacheRevision::fromJson(const QJsonObject &timestamps)
752  {
753  QMap<QString, qint64> result;
754  for (auto it = timestamps.begin(); it != timestamps.end(); ++it)
755  {
756  result.insert(it.key(), static_cast<qint64>(it.value().toDouble()));
757  }
758  return result;
759  }
760 
761  QJsonArray CDataCacheRevision::toJson(const QSet<QString> &pins)
762  {
763  QJsonArray result;
764  for (auto it = pins.begin(); it != pins.end(); ++it) { result.push_back(*it); }
765  return result;
766  }
767 
768  QSet<QString> CDataCacheRevision::fromJson(const QJsonArray &pins)
769  {
770  QSet<QString> result;
771  for (auto it = pins.begin(); it != pins.end(); ++it) { result.insert(it->toString()); }
772  return result;
773  }
774 
775  QJsonObject CDataCacheRevision::toJson(const QMap<QString, QUuid> &timestamps)
776  {
777  QJsonObject result;
778  for (auto it = timestamps.begin(); it != timestamps.end(); ++it)
779  {
780  result.insert(it.key(), it.value().toString());
781  }
782  return result;
783  }
784 
785  QMap<QString, QUuid> CDataCacheRevision::sessionFromJson(const QJsonObject &session)
786  {
787  QMap<QString, QUuid> result;
788  for (auto it = session.begin(); it != session.end(); ++it)
789  {
790  result.insert(it.key(), QUuid(it.value().toString()));
791  }
792  return result;
793  }
794 
795  void CDataCacheRevision::Session::updateSession()
796  {
797  CAtomicFile file(m_filename);
798  bool ok = file.open(QIODevice::ReadWrite | QFile::Text);
799  if (!ok)
800  {
801  CLogMessage(this).error(u"Failed to open session file %1: %2") << m_filename << file.errorString();
802  return;
803  }
804  auto json = QJsonDocument::fromJson(file.readAll()).object();
805  QUuid id(json.value("uuid").toString());
806  CSequence<CProcessInfo> apps;
807  auto status = apps.convertFromJsonNoThrow(json.value("apps").toObject(), this,
808  QStringLiteral("Error in %1 apps object").arg(m_filename));
809  apps.removeIf([](const CProcessInfo &pi) { return !pi.exists(); });
810 
811  if (apps.isEmpty()) { id = CIdentifier().toUuid(); }
812  m_uuid = id;
813 
814  CProcessInfo currentProcess = CProcessInfo::currentProcess();
815  Q_ASSERT(currentProcess.exists());
816  apps.replaceOrAdd(currentProcess);
817  json.insert("apps", apps.toJson());
818  json.insert("uuid", m_uuid.toString());
819  if (file.seek(0) && file.resize(0) && file.write(QJsonDocument(json).toJson()))
820  {
821  if (!file.checkedClose())
822  {
823  static const QString advice =
824  QStringLiteral("If this error persists, try restarting your computer or delete the file manually.");
825  CLogMessage(this).error(u"Failed to replace %1: %2 (%3)")
826  << file.fileName() << file.errorString() << advice;
827  }
828  }
829  else { CLogMessage(this).error(u"Failed to write to %1: %2") << file.fileName() << file.errorString(); }
830  }
831 
832 } // namespace swift::misc
833 
CDataCacheRevision & operator=(const CDataCacheRevision &)=delete
Non-copyable.
CDataCacheRevision(const QString &basename)
Construct the single instance of the revision metastate.
auto keys() const
Return a range of all keys (does not allocate a temporary container)
Definition: dictionary.h:392
static const QString & cache()
Cache.
Value class used for signalling changed values in the cache.
Definition: valuecache.h:67
QMap< QString, qint64 > toTimestampMap() const
Discard values and return as map of timestamps.
QString toTimestampMapString(const QStringList &keys) const
Return map of timestamps converted to string.
CVariantMap toVariantMap() const
Discard timestamps and return as variant map.
Map of { QString, CVariant } pairs.
Definition: variantmap.h:35
Free functions in swift::misc.
void swap(Optional< T > &a, Optional< T > &b) noexcept(std::is_nothrow_swappable_v< T >)
Efficient swap for two Optional objects.
Definition: optional.h:132
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...
Definition: threadutils.h:30
#define SWIFT_MISC_EXPORT
Export a class or function from the library.