1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
13 #include "mozilla/Unused.h"
15 namespace mozilla::default_agent
{
17 // Cache entry version documentation:
19 // The version number is written explicitly when version 1 cache entries are
20 // migrated, but in their original location there is no version key.
22 // CacheEntryVersion: <DWORD>
23 // NotificationType: <string>
24 // NotificationShown: <string>
25 // NotificationAction: <string>
28 // CacheEntryVersion: <DWORD>
29 // NotificationType: <string>
30 // NotificationShown: <string>
31 // NotificationAction: <string>
32 // PrevNotificationAction: <string>
34 static std::wstring
MakeVersionedRegSubKey(const wchar_t* baseKey
) {
39 key
= Cache::kDefaultPingCacheRegKey
;
42 key
+= std::to_wstring(Cache::kVersion
);
46 Cache::Cache(const wchar_t* cacheRegKey
/* = nullptr */)
47 : mCacheRegKey(MakeVersionedRegSubKey(cacheRegKey
)),
48 mInitializeResult(mozilla::Nothing()),
49 mCapacity(Cache::kDefaultCapacity
),
55 VoidResult
Cache::Init() {
56 if (mInitializeResult
.isSome()) {
57 HRESULT hr
= mInitializeResult
.value();
59 return mozilla::Err(mozilla::WindowsError::FromHResult(hr
));
65 VoidResult result
= SetupCache();
67 HRESULT hr
= result
.inspectErr().AsHResult();
68 mInitializeResult
= mozilla::Some(hr
);
72 // At this point, the cache is ready to use, so mark the initialization as
73 // complete. This is important so that when we attempt migration, below,
74 // the migration's attempts to write to the cache don't try to initialize
76 mInitializeResult
= mozilla::Some(S_OK
);
78 // Ignore the result of the migration. If we failed to migrate, there may be
79 // some data loss. But that's better than failing to ever use the new cache
80 // just because there's something wrong with the old one.
81 mozilla::Unused
<< MaybeMigrateVersion1();
86 // If the setting does not exist, the default value is written and returned.
87 DwordResult
Cache::EnsureDwordSetting(const wchar_t* regName
,
88 uint32_t defaultValue
) {
89 MaybeDwordResult readResult
= RegistryGetValueDword(
90 IsPrefixed::Unprefixed
, regName
, mCacheRegKey
.c_str());
91 if (readResult
.isErr()) {
92 HRESULT hr
= readResult
.unwrapErr().AsHResult();
93 LOG_ERROR_MESSAGE(L
"Failed to read setting \"%s\": %#X", regName
, hr
);
94 return mozilla::Err(mozilla::WindowsError::FromHResult(hr
));
96 mozilla::Maybe
<uint32_t> maybeValue
= readResult
.unwrap();
97 if (maybeValue
.isSome()) {
98 return maybeValue
.value();
101 VoidResult writeResult
= RegistrySetValueDword(
102 IsPrefixed::Unprefixed
, regName
, defaultValue
, mCacheRegKey
.c_str());
103 if (writeResult
.isErr()) {
104 HRESULT hr
= writeResult
.unwrapErr().AsHResult();
105 LOG_ERROR_MESSAGE(L
"Failed to write setting \"%s\": %#X", regName
, hr
);
106 return mozilla::Err(mozilla::WindowsError::FromHResult(hr
));
111 // This function does two things:
112 // 1. It creates and sets the registry values used by the cache, if they don't
114 // 2. If the the values already existed, it reads the settings of the cache
115 // into their member variables.
116 VoidResult
Cache::SetupCache() {
118 EnsureDwordSetting(Cache::kCapacityRegName
, Cache::kDefaultCapacity
);
119 if (result
.isErr()) {
120 return mozilla::Err(result
.unwrapErr());
122 mCapacity
= std::min(result
.unwrap(), Cache::kMaxCapacity
);
124 result
= EnsureDwordSetting(Cache::kFrontRegName
, 0);
125 if (result
.isErr()) {
126 return mozilla::Err(result
.unwrapErr());
128 mFront
= std::min(result
.unwrap(), Cache::kMaxCapacity
- 1);
130 result
= EnsureDwordSetting(Cache::kSizeRegName
, 0);
131 if (result
.isErr()) {
132 return mozilla::Err(result
.unwrapErr());
134 mSize
= std::min(result
.unwrap(), mCapacity
);
136 return mozilla::Ok();
139 static MaybeStringResult
ReadVersion1CacheKey(const wchar_t* baseRegKeyName
,
141 std::wstring regName
= Cache::kVersion1KeyPrefix
;
142 regName
+= baseRegKeyName
;
143 regName
+= std::to_wstring(index
);
145 MaybeStringResult result
=
146 RegistryGetValueString(IsPrefixed::Unprefixed
, regName
.c_str());
147 if (result
.isErr()) {
148 HRESULT hr
= result
.inspectErr().AsHResult();
149 LOG_ERROR_MESSAGE(L
"Failed to read \"%s\": %#X", regName
.c_str(), hr
);
154 static VoidResult
DeleteVersion1CacheKey(const wchar_t* baseRegKeyName
,
156 std::wstring regName
= Cache::kVersion1KeyPrefix
;
157 regName
+= baseRegKeyName
;
158 regName
+= std::to_wstring(index
);
161 RegistryDeleteValue(IsPrefixed::Unprefixed
, regName
.c_str());
162 if (result
.isErr()) {
163 HRESULT hr
= result
.inspectErr().AsHResult();
164 LOG_ERROR_MESSAGE(L
"Failed to delete \"%s\": %#X", regName
.c_str(), hr
);
169 static VoidResult
DeleteVersion1CacheEntry(uint32_t index
) {
170 VoidResult typeResult
=
171 DeleteVersion1CacheKey(Cache::kNotificationTypeKey
, index
);
172 VoidResult shownResult
=
173 DeleteVersion1CacheKey(Cache::kNotificationShownKey
, index
);
174 VoidResult actionResult
=
175 DeleteVersion1CacheKey(Cache::kNotificationActionKey
, index
);
177 if (typeResult
.isErr()) {
180 if (shownResult
.isErr()) {
186 VoidResult
Cache::MaybeMigrateVersion1() {
187 for (uint32_t index
= 0; index
< Cache::kVersion1MaxSize
; ++index
) {
188 MaybeStringResult typeResult
=
189 ReadVersion1CacheKey(Cache::kNotificationTypeKey
, index
);
190 if (typeResult
.isErr()) {
191 return mozilla::Err(typeResult
.unwrapErr());
193 MaybeString maybeType
= typeResult
.unwrap();
195 MaybeStringResult shownResult
=
196 ReadVersion1CacheKey(Cache::kNotificationShownKey
, index
);
197 if (shownResult
.isErr()) {
198 return mozilla::Err(shownResult
.unwrapErr());
200 MaybeString maybeShown
= shownResult
.unwrap();
202 MaybeStringResult actionResult
=
203 ReadVersion1CacheKey(Cache::kNotificationActionKey
, index
);
204 if (actionResult
.isErr()) {
205 return mozilla::Err(actionResult
.unwrapErr());
207 MaybeString maybeAction
= actionResult
.unwrap();
209 if (maybeType
.isSome() && maybeShown
.isSome() && maybeAction
.isSome()) {
210 // If something goes wrong, we'd rather lose a little data than migrate
211 // over and over again. So delete the old entry before we add the new one.
212 VoidResult result
= DeleteVersion1CacheEntry(index
);
213 if (result
.isErr()) {
217 VersionedEntry entry
= VersionedEntry
{
219 .notificationType
= maybeType
.value(),
220 .notificationShown
= maybeShown
.value(),
221 .notificationAction
= maybeAction
.value(),
222 .prevNotificationAction
= mozilla::Nothing(),
224 result
= VersionedEnqueue(entry
);
225 if (result
.isErr()) {
226 // We already deleted the version 1 cache entry. No real reason to abort
227 // now. May as well keep attempting to migrate.
228 LOG_ERROR_MESSAGE(L
"Warning: Version 1 cache entry %u dropped: %#X",
229 index
, result
.unwrapErr().AsHResult());
231 } else if (maybeType
.isNothing() && maybeShown
.isNothing() &&
232 maybeAction
.isNothing()) {
233 // Looks like we've reached the end of the version 1 cache.
236 // This cache entry seems to be missing a key. Just drop it.
238 L
"Warning: Version 1 cache entry %u dropped due to missing keys",
240 mozilla::Unused
<< DeleteVersion1CacheEntry(index
);
243 return mozilla::Ok();
246 std::wstring
Cache::MakeEntryRegKeyName(uint32_t index
) {
247 std::wstring regName
= mCacheRegKey
;
249 regName
+= std::to_wstring(index
);
253 VoidResult
Cache::WriteEntryKeys(uint32_t index
, const VersionedEntry
& entry
) {
254 std::wstring subKey
= MakeEntryRegKeyName(index
);
257 RegistrySetValueDword(IsPrefixed::Unprefixed
, Cache::kEntryVersionKey
,
258 entry
.entryVersion
, subKey
.c_str());
259 if (result
.isErr()) {
260 LOG_ERROR_MESSAGE(L
"Unable to write entry version to index %u: %#X", index
,
261 result
.inspectErr().AsHResult());
265 result
= RegistrySetValueString(
266 IsPrefixed::Unprefixed
, Cache::kNotificationTypeKey
,
267 entry
.notificationType
.c_str(), subKey
.c_str());
268 if (result
.isErr()) {
269 LOG_ERROR_MESSAGE(L
"Unable to write notification type to index %u: %#X",
270 index
, result
.inspectErr().AsHResult());
274 result
= RegistrySetValueString(
275 IsPrefixed::Unprefixed
, Cache::kNotificationShownKey
,
276 entry
.notificationShown
.c_str(), subKey
.c_str());
277 if (result
.isErr()) {
278 LOG_ERROR_MESSAGE(L
"Unable to write notification shown to index %u: %#X",
279 index
, result
.inspectErr().AsHResult());
283 result
= RegistrySetValueString(
284 IsPrefixed::Unprefixed
, Cache::kNotificationActionKey
,
285 entry
.notificationAction
.c_str(), subKey
.c_str());
286 if (result
.isErr()) {
287 LOG_ERROR_MESSAGE(L
"Unable to write notification type to index %u: %#X",
288 index
, result
.inspectErr().AsHResult());
292 if (entry
.prevNotificationAction
.isSome()) {
293 result
= RegistrySetValueString(
294 IsPrefixed::Unprefixed
, Cache::kPrevNotificationActionKey
,
295 entry
.prevNotificationAction
.value().c_str(), subKey
.c_str());
296 if (result
.isErr()) {
298 L
"Unable to write prev notification type to index %u: %#X", index
,
299 result
.inspectErr().AsHResult());
304 return mozilla::Ok();
307 // Returns success on an attempt to delete a non-existent entry.
308 VoidResult
Cache::DeleteEntry(uint32_t index
) {
309 std::wstring key
= AGENT_REGKEY_NAME
;
311 key
+= MakeEntryRegKeyName(index
);
312 // We could probably just delete they key here, rather than use this function,
313 // which deletes keys recursively. But this mechanism allows future entry
314 // versions to contain sub-keys without causing problems for older versions.
315 LSTATUS ls
= RegDeleteTreeW(HKEY_CURRENT_USER
, key
.c_str());
316 if (ls
!= ERROR_SUCCESS
&& ls
!= ERROR_FILE_NOT_FOUND
) {
317 return mozilla::Err(mozilla::WindowsError::FromWin32Error(ls
));
319 return mozilla::Ok();
322 VoidResult
Cache::SetFront(uint32_t newFront
) {
324 RegistrySetValueDword(IsPrefixed::Unprefixed
, Cache::kFrontRegName
,
325 newFront
, mCacheRegKey
.c_str());
332 VoidResult
Cache::SetSize(uint32_t newSize
) {
334 RegistrySetValueDword(IsPrefixed::Unprefixed
, Cache::kSizeRegName
,
335 newSize
, mCacheRegKey
.c_str());
342 // The entry passed to this function MUST already be valid. This function does
343 // not do any validation internally. We must not, for example, pass an entry
344 // to it with a version of 2 and a prevNotificationAction of mozilla::Nothing()
345 // because a version 2 entry requires that key.
346 VoidResult
Cache::VersionedEnqueue(const VersionedEntry
& entry
) {
347 VoidResult result
= Init();
348 if (result
.isErr()) {
352 if (mSize
>= mCapacity
) {
353 LOG_ERROR_MESSAGE(L
"Attempted to add an entry to the cache, but it's full");
354 return mozilla::Err(mozilla::WindowsError::FromHResult(E_BOUNDS
));
357 uint32_t index
= (mFront
+ mSize
) % mCapacity
;
359 // We really don't want to write to a location that has stale cache entry data
360 // already lying around.
361 result
= DeleteEntry(index
);
362 if (result
.isErr()) {
363 LOG_ERROR_MESSAGE(L
"Unable to remove stale entry: %#X",
364 result
.inspectErr().AsHResult());
368 result
= WriteEntryKeys(index
, entry
);
369 if (result
.isErr()) {
370 // We might have written a partial key. Attempt to clean up after ourself.
371 mozilla::Unused
<< DeleteEntry(index
);
375 result
= SetSize(mSize
+ 1);
376 if (result
.isErr()) {
377 // If we failed to write the size, the new entry was not added successfully.
378 // Attempt to clean up after ourself.
379 mozilla::Unused
<< DeleteEntry(index
);
383 return mozilla::Ok();
386 VoidResult
Cache::Enqueue(const Cache::Entry
& entry
) {
387 Cache::VersionedEntry vEntry
= Cache::VersionedEntry
{
388 .entryVersion
= Cache::kEntryVersion
,
389 .notificationType
= entry
.notificationType
,
390 .notificationShown
= entry
.notificationShown
,
391 .notificationAction
= entry
.notificationAction
,
392 .prevNotificationAction
= mozilla::Some(entry
.prevNotificationAction
),
394 return VersionedEnqueue(vEntry
);
397 VoidResult
Cache::DiscardFront() {
399 LOG_ERROR_MESSAGE(L
"Attempted to discard entry from an empty cache");
400 return mozilla::Err(mozilla::WindowsError::FromHResult(E_BOUNDS
));
402 // It's not a huge deal if we can't delete this. Moving mFront will result in
403 // it being excluded from the cache anyways. We'll try to delete it again
404 // anyways if we try to write to this index again.
405 mozilla::Unused
<< DeleteEntry(mFront
);
407 VoidResult result
= SetSize(mSize
- 1);
408 // We don't really need to bother moving mFront to the next index if the cache
410 if (result
.isErr() || mSize
== 0) {
413 result
= SetFront((mFront
+ 1) % mCapacity
);
414 if (result
.isErr()) {
415 // If we failed to set the front after we set the size, the cache is
416 // in an inconsistent state.
417 // But, even if the cache is inconsistent, we'll likely lose some data, but
418 // we should eventually be able to recover. Any expected entries with no
419 // data will be discarded and any unexpected entries with data will be
420 // cleared out before we write data there.
421 LOG_ERROR_MESSAGE(L
"Cache inconsistent: Updated Size but not Front: %#X",
422 result
.inspectErr().AsHResult());
428 * This function reads a DWORD cache key's value and returns it. If the expected
429 * argument is true and the key is missing, this will delete the entire entry
430 * and return mozilla::Nothing().
432 MaybeDwordResult
Cache::ReadEntryKeyDword(const std::wstring
& regKey
,
433 const wchar_t* regName
,
435 MaybeDwordResult result
=
436 RegistryGetValueDword(IsPrefixed::Unprefixed
, regName
, regKey
.c_str());
437 if (result
.isErr()) {
438 LOG_ERROR_MESSAGE(L
"Failed to read \"%s\" from \"%s\": %#X", regName
,
439 regKey
.c_str(), result
.inspectErr().AsHResult());
440 return mozilla::Err(result
.unwrapErr());
442 MaybeDword maybeValue
= result
.unwrap();
443 if (expected
&& maybeValue
.isNothing()) {
444 LOG_ERROR_MESSAGE(L
"Missing expected value \"%s\" from \"%s\"", regName
,
446 VoidResult result
= DiscardFront();
447 if (result
.isErr()) {
448 return mozilla::Err(result
.unwrapErr());
455 * This function reads a string cache key's value and returns it. If the
456 * expected argument is true and the key is missing, this will delete the entire
457 * entry and return mozilla::Nothing().
459 MaybeStringResult
Cache::ReadEntryKeyString(const std::wstring
& regKey
,
460 const wchar_t* regName
,
462 MaybeStringResult result
=
463 RegistryGetValueString(IsPrefixed::Unprefixed
, regName
, regKey
.c_str());
464 if (result
.isErr()) {
465 LOG_ERROR_MESSAGE(L
"Failed to read \"%s\" from \"%s\": %#X", regName
,
466 regKey
.c_str(), result
.inspectErr().AsHResult());
467 return mozilla::Err(result
.unwrapErr());
469 MaybeString maybeValue
= result
.unwrap();
470 if (expected
&& maybeValue
.isNothing()) {
471 LOG_ERROR_MESSAGE(L
"Missing expected value \"%s\" from \"%s\"", regName
,
473 VoidResult result
= DiscardFront();
474 if (result
.isErr()) {
475 return mozilla::Err(result
.unwrapErr());
481 Cache::MaybeEntryResult
Cache::Dequeue() {
482 VoidResult result
= Init();
483 if (result
.isErr()) {
484 return mozilla::Err(result
.unwrapErr());
487 std::wstring subKey
= MakeEntryRegKeyName(mFront
);
489 // We are going to read within a loop so that if we find incomplete entries,
490 // we can just discard them and try to read the next entry. We'll put a limit
491 // on the maximum number of times this loop can possibly run so that if
492 // something goes horribly wrong, we don't loop forever. If we exit this loop
493 // without returning, it means that not only were we not able to read
494 // anything, but something very unexpected happened.
495 // We are going to potentially loop over this mCapacity + 1 times so that if
496 // we end up discarding every item in the cache, we return mozilla::Nothing()
497 // rather than an error.
498 for (uint32_t i
= 0; i
<= mCapacity
; ++i
) {
500 return MaybeEntry(mozilla::Nothing());
503 Cache::VersionedEntry entry
;
506 MaybeDwordResult dResult
=
507 ReadEntryKeyDword(subKey
, Cache::kEntryVersionKey
, true);
508 if (dResult
.isErr()) {
509 return mozilla::Err(dResult
.unwrapErr());
511 MaybeDword maybeDValue
= dResult
.unwrap();
512 if (maybeDValue
.isNothing()) {
513 // Note that we only call continue in this function after DiscardFront()
514 // has been called (either directly, or by one of the ReadEntryKey.*
515 // functions). So the continue call results in attempting to read the
516 // next entry in the cache.
519 entry
.entryVersion
= maybeDValue
.value();
520 if (entry
.entryVersion
< 1) {
521 LOG_ERROR_MESSAGE(L
"Invalid entry version of %u in \"%s\"",
522 entry
.entryVersion
, subKey
.c_str());
523 VoidResult result
= DiscardFront();
524 if (result
.isErr()) {
525 return mozilla::Err(result
.unwrapErr());
531 MaybeStringResult sResult
=
532 ReadEntryKeyString(subKey
, Cache::kNotificationTypeKey
, true);
533 if (sResult
.isErr()) {
534 return mozilla::Err(sResult
.unwrapErr());
536 MaybeString maybeSValue
= sResult
.unwrap();
537 if (maybeSValue
.isNothing()) {
540 entry
.notificationType
= maybeSValue
.value();
543 sResult
= ReadEntryKeyString(subKey
, Cache::kNotificationShownKey
, true);
544 if (sResult
.isErr()) {
545 return mozilla::Err(sResult
.unwrapErr());
547 maybeSValue
= sResult
.unwrap();
548 if (maybeSValue
.isNothing()) {
551 entry
.notificationShown
= maybeSValue
.value();
553 // NotificationAction
554 sResult
= ReadEntryKeyString(subKey
, Cache::kNotificationActionKey
, true);
555 if (sResult
.isErr()) {
556 return mozilla::Err(sResult
.unwrapErr());
558 maybeSValue
= sResult
.unwrap();
559 if (maybeSValue
.isNothing()) {
562 entry
.notificationAction
= maybeSValue
.value();
564 // PrevNotificationAction
566 entry
.entryVersion
>= Cache::kInitialVersionPrevNotificationActionKey
;
568 ReadEntryKeyString(subKey
, Cache::kPrevNotificationActionKey
, expected
);
569 if (sResult
.isErr()) {
570 return mozilla::Err(sResult
.unwrapErr());
572 maybeSValue
= sResult
.unwrap();
573 if (expected
&& maybeSValue
.isNothing()) {
576 entry
.prevNotificationAction
= maybeSValue
;
578 // We successfully read the entry. Now we need to remove it from the cache.
579 VoidResult result
= DiscardFront();
580 if (result
.isErr()) {
581 // If we aren't able to remove the entry from the cache, don't return it.
582 // We don't want to return the same item over and over again if we get
584 return mozilla::Err(result
.unwrapErr());
587 return mozilla::Some(entry
);
590 LOG_ERROR_MESSAGE(L
"Unexpected: This line shouldn't be reached");
591 return mozilla::Err(mozilla::WindowsError::FromHResult(E_FAIL
));
594 } // namespace mozilla::default_agent