Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "Cache.h"
#include <algorithm>
#include "common.h"
#include "EventLog.h"
#include "mozilla/Unused.h"
namespace mozilla::default_agent {
// Cache entry version documentation:
// Version 1:
// The version number is written explicitly when version 1 cache entries are
// migrated, but in their original location there is no version key.
// Required Keys:
// CacheEntryVersion: <DWORD>
// NotificationType: <string>
// NotificationShown: <string>
// NotificationAction: <string>
// Version 2:
// Required Keys:
// CacheEntryVersion: <DWORD>
// NotificationType: <string>
// NotificationShown: <string>
// NotificationAction: <string>
// PrevNotificationAction: <string>
static std::wstring MakeVersionedRegSubKey(const wchar_t* baseKey) {
std::wstring key;
if (baseKey) {
key = baseKey;
} else {
key = Cache::kDefaultPingCacheRegKey;
}
key += L"\\version";
key += std::to_wstring(Cache::kVersion);
return key;
}
Cache::Cache(const wchar_t* cacheRegKey /* = nullptr */)
: mCacheRegKey(MakeVersionedRegSubKey(cacheRegKey)),
mInitializeResult(mozilla::Nothing()),
mCapacity(Cache::kDefaultCapacity),
mFront(0),
mSize(0) {}
Cache::~Cache() {}
VoidResult Cache::Init() {
if (mInitializeResult.isSome()) {
HRESULT hr = mInitializeResult.value();
if (FAILED(hr)) {
return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
} else {
return mozilla::Ok();
}
}
VoidResult result = SetupCache();
if (result.isErr()) {
HRESULT hr = result.inspectErr().AsHResult();
mInitializeResult = mozilla::Some(hr);
return result;
}
// At this point, the cache is ready to use, so mark the initialization as
// complete. This is important so that when we attempt migration, below,
// the migration's attempts to write to the cache don't try to initialize
// the cache again.
mInitializeResult = mozilla::Some(S_OK);
// Ignore the result of the migration. If we failed to migrate, there may be
// some data loss. But that's better than failing to ever use the new cache
// just because there's something wrong with the old one.
mozilla::Unused << MaybeMigrateVersion1();
return mozilla::Ok();
}
// If the setting does not exist, the default value is written and returned.
DwordResult Cache::EnsureDwordSetting(const wchar_t* regName,
uint32_t defaultValue) {
MaybeDwordResult readResult = RegistryGetValueDword(
IsPrefixed::Unprefixed, regName, mCacheRegKey.c_str());
if (readResult.isErr()) {
HRESULT hr = readResult.unwrapErr().AsHResult();
LOG_ERROR_MESSAGE(L"Failed to read setting \"%s\": %#X", regName, hr);
return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
}
mozilla::Maybe<uint32_t> maybeValue = readResult.unwrap();
if (maybeValue.isSome()) {
return maybeValue.value();
}
VoidResult writeResult = RegistrySetValueDword(
IsPrefixed::Unprefixed, regName, defaultValue, mCacheRegKey.c_str());
if (writeResult.isErr()) {
HRESULT hr = writeResult.unwrapErr().AsHResult();
LOG_ERROR_MESSAGE(L"Failed to write setting \"%s\": %#X", regName, hr);
return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
}
return defaultValue;
}
// This function does two things:
// 1. It creates and sets the registry values used by the cache, if they don't
// already exist.
// 2. If the the values already existed, it reads the settings of the cache
// into their member variables.
VoidResult Cache::SetupCache() {
DwordResult result =
EnsureDwordSetting(Cache::kCapacityRegName, Cache::kDefaultCapacity);
if (result.isErr()) {
return mozilla::Err(result.unwrapErr());
}
mCapacity = std::min(result.unwrap(), Cache::kMaxCapacity);
result = EnsureDwordSetting(Cache::kFrontRegName, 0);
if (result.isErr()) {
return mozilla::Err(result.unwrapErr());
}
mFront = std::min(result.unwrap(), Cache::kMaxCapacity - 1);
result = EnsureDwordSetting(Cache::kSizeRegName, 0);
if (result.isErr()) {
return mozilla::Err(result.unwrapErr());
}
mSize = std::min(result.unwrap(), mCapacity);
return mozilla::Ok();
}
static MaybeStringResult ReadVersion1CacheKey(const wchar_t* baseRegKeyName,
uint32_t index) {
std::wstring regName = Cache::kVersion1KeyPrefix;
regName += baseRegKeyName;
regName += std::to_wstring(index);
MaybeStringResult result =
RegistryGetValueString(IsPrefixed::Unprefixed, regName.c_str());
if (result.isErr()) {
HRESULT hr = result.inspectErr().AsHResult();
LOG_ERROR_MESSAGE(L"Failed to read \"%s\": %#X", regName.c_str(), hr);
}
return result;
}
static VoidResult DeleteVersion1CacheKey(const wchar_t* baseRegKeyName,
uint32_t index) {
std::wstring regName = Cache::kVersion1KeyPrefix;
regName += baseRegKeyName;
regName += std::to_wstring(index);
VoidResult result =
RegistryDeleteValue(IsPrefixed::Unprefixed, regName.c_str());
if (result.isErr()) {
HRESULT hr = result.inspectErr().AsHResult();
LOG_ERROR_MESSAGE(L"Failed to delete \"%s\": %#X", regName.c_str(), hr);
}
return result;
}
static VoidResult DeleteVersion1CacheEntry(uint32_t index) {
VoidResult typeResult =
DeleteVersion1CacheKey(Cache::kNotificationTypeKey, index);
VoidResult shownResult =
DeleteVersion1CacheKey(Cache::kNotificationShownKey, index);
VoidResult actionResult =
DeleteVersion1CacheKey(Cache::kNotificationActionKey, index);
if (typeResult.isErr()) {
return typeResult;
}
if (shownResult.isErr()) {
return shownResult;
}
return actionResult;
}
VoidResult Cache::MaybeMigrateVersion1() {
for (uint32_t index = 0; index < Cache::kVersion1MaxSize; ++index) {
MaybeStringResult typeResult =
ReadVersion1CacheKey(Cache::kNotificationTypeKey, index);
if (typeResult.isErr()) {
return mozilla::Err(typeResult.unwrapErr());
}
MaybeString maybeType = typeResult.unwrap();
MaybeStringResult shownResult =
ReadVersion1CacheKey(Cache::kNotificationShownKey, index);
if (shownResult.isErr()) {
return mozilla::Err(shownResult.unwrapErr());
}
MaybeString maybeShown = shownResult.unwrap();
MaybeStringResult actionResult =
ReadVersion1CacheKey(Cache::kNotificationActionKey, index);
if (actionResult.isErr()) {
return mozilla::Err(actionResult.unwrapErr());
}
MaybeString maybeAction = actionResult.unwrap();
if (maybeType.isSome() && maybeShown.isSome() && maybeAction.isSome()) {
// If something goes wrong, we'd rather lose a little data than migrate
// over and over again. So delete the old entry before we add the new one.
VoidResult result = DeleteVersion1CacheEntry(index);
if (result.isErr()) {
return result;
}
VersionedEntry entry = VersionedEntry{
.entryVersion = 1,
.notificationType = maybeType.value(),
.notificationShown = maybeShown.value(),
.notificationAction = maybeAction.value(),
.prevNotificationAction = mozilla::Nothing(),
};
result = VersionedEnqueue(entry);
if (result.isErr()) {
// We already deleted the version 1 cache entry. No real reason to abort
// now. May as well keep attempting to migrate.
LOG_ERROR_MESSAGE(L"Warning: Version 1 cache entry %u dropped: %#X",
index, result.unwrapErr().AsHResult());
}
} else if (maybeType.isNothing() && maybeShown.isNothing() &&
maybeAction.isNothing()) {
// Looks like we've reached the end of the version 1 cache.
break;
} else {
// This cache entry seems to be missing a key. Just drop it.
LOG_ERROR_MESSAGE(
L"Warning: Version 1 cache entry %u dropped due to missing keys",
index);
mozilla::Unused << DeleteVersion1CacheEntry(index);
}
}
return mozilla::Ok();
}
std::wstring Cache::MakeEntryRegKeyName(uint32_t index) {
std::wstring regName = mCacheRegKey;
regName += L'\\';
regName += std::to_wstring(index);
return regName;
}
VoidResult Cache::WriteEntryKeys(uint32_t index, const VersionedEntry& entry) {
std::wstring subKey = MakeEntryRegKeyName(index);
VoidResult result =
RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kEntryVersionKey,
entry.entryVersion, subKey.c_str());
if (result.isErr()) {
LOG_ERROR_MESSAGE(L"Unable to write entry version to index %u: %#X", index,
result.inspectErr().AsHResult());
return result;
}
result = RegistrySetValueString(
IsPrefixed::Unprefixed, Cache::kNotificationTypeKey,
entry.notificationType.c_str(), subKey.c_str());
if (result.isErr()) {
LOG_ERROR_MESSAGE(L"Unable to write notification type to index %u: %#X",
index, result.inspectErr().AsHResult());
return result;
}
result = RegistrySetValueString(
IsPrefixed::Unprefixed, Cache::kNotificationShownKey,
entry.notificationShown.c_str(), subKey.c_str());
if (result.isErr()) {
LOG_ERROR_MESSAGE(L"Unable to write notification shown to index %u: %#X",
index, result.inspectErr().AsHResult());
return result;
}
result = RegistrySetValueString(
IsPrefixed::Unprefixed, Cache::kNotificationActionKey,
entry.notificationAction.c_str(), subKey.c_str());
if (result.isErr()) {
LOG_ERROR_MESSAGE(L"Unable to write notification type to index %u: %#X",
index, result.inspectErr().AsHResult());
return result;
}
if (entry.prevNotificationAction.isSome()) {
result = RegistrySetValueString(
IsPrefixed::Unprefixed, Cache::kPrevNotificationActionKey,
entry.prevNotificationAction.value().c_str(), subKey.c_str());
if (result.isErr()) {
LOG_ERROR_MESSAGE(
L"Unable to write prev notification type to index %u: %#X", index,
result.inspectErr().AsHResult());
return result;
}
}
return mozilla::Ok();
}
// Returns success on an attempt to delete a non-existent entry.
VoidResult Cache::DeleteEntry(uint32_t index) {
std::wstring key = AGENT_REGKEY_NAME;
key += L'\\';
key += MakeEntryRegKeyName(index);
// We could probably just delete they key here, rather than use this function,
// which deletes keys recursively. But this mechanism allows future entry
// versions to contain sub-keys without causing problems for older versions.
LSTATUS ls = RegDeleteTreeW(HKEY_CURRENT_USER, key.c_str());
if (ls != ERROR_SUCCESS && ls != ERROR_FILE_NOT_FOUND) {
return mozilla::Err(mozilla::WindowsError::FromWin32Error(ls));
}
return mozilla::Ok();
}
VoidResult Cache::SetFront(uint32_t newFront) {
VoidResult result =
RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kFrontRegName,
newFront, mCacheRegKey.c_str());
if (result.isOk()) {
mFront = newFront;
}
return result;
}
VoidResult Cache::SetSize(uint32_t newSize) {
VoidResult result =
RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kSizeRegName,
newSize, mCacheRegKey.c_str());
if (result.isOk()) {
mSize = newSize;
}
return result;
}
// The entry passed to this function MUST already be valid. This function does
// not do any validation internally. We must not, for example, pass an entry
// to it with a version of 2 and a prevNotificationAction of mozilla::Nothing()
// because a version 2 entry requires that key.
VoidResult Cache::VersionedEnqueue(const VersionedEntry& entry) {
VoidResult result = Init();
if (result.isErr()) {
return result;
}
if (mSize >= mCapacity) {
LOG_ERROR_MESSAGE(L"Attempted to add an entry to the cache, but it's full");
return mozilla::Err(mozilla::WindowsError::FromHResult(E_BOUNDS));
}
uint32_t index = (mFront + mSize) % mCapacity;
// We really don't want to write to a location that has stale cache entry data
// already lying around.
result = DeleteEntry(index);
if (result.isErr()) {
LOG_ERROR_MESSAGE(L"Unable to remove stale entry: %#X",
result.inspectErr().AsHResult());
return result;
}
result = WriteEntryKeys(index, entry);
if (result.isErr()) {
// We might have written a partial key. Attempt to clean up after ourself.
mozilla::Unused << DeleteEntry(index);
return result;
}
result = SetSize(mSize + 1);
if (result.isErr()) {
// If we failed to write the size, the new entry was not added successfully.
// Attempt to clean up after ourself.
mozilla::Unused << DeleteEntry(index);
return result;
}
return mozilla::Ok();
}
VoidResult Cache::Enqueue(const Cache::Entry& entry) {
Cache::VersionedEntry vEntry = Cache::VersionedEntry{
.entryVersion = Cache::kEntryVersion,
.notificationType = entry.notificationType,
.notificationShown = entry.notificationShown,
.notificationAction = entry.notificationAction,
.prevNotificationAction = mozilla::Some(entry.prevNotificationAction),
};
return VersionedEnqueue(vEntry);
}
VoidResult Cache::DiscardFront() {
if (mSize < 1) {
LOG_ERROR_MESSAGE(L"Attempted to discard entry from an empty cache");
return mozilla::Err(mozilla::WindowsError::FromHResult(E_BOUNDS));
}
// It's not a huge deal if we can't delete this. Moving mFront will result in
// it being excluded from the cache anyways. We'll try to delete it again
// anyways if we try to write to this index again.
mozilla::Unused << DeleteEntry(mFront);
VoidResult result = SetSize(mSize - 1);
// We don't really need to bother moving mFront to the next index if the cache
// is empty.
if (result.isErr() || mSize == 0) {
return result;
}
result = SetFront((mFront + 1) % mCapacity);
if (result.isErr()) {
// If we failed to set the front after we set the size, the cache is
// in an inconsistent state.
// But, even if the cache is inconsistent, we'll likely lose some data, but
// we should eventually be able to recover. Any expected entries with no
// data will be discarded and any unexpected entries with data will be
// cleared out before we write data there.
LOG_ERROR_MESSAGE(L"Cache inconsistent: Updated Size but not Front: %#X",
result.inspectErr().AsHResult());
}
return result;
}
/**
* This function reads a DWORD cache key's value and returns it. If the expected
* argument is true and the key is missing, this will delete the entire entry
* and return mozilla::Nothing().
*/
MaybeDwordResult Cache::ReadEntryKeyDword(const std::wstring& regKey,
const wchar_t* regName,
bool expected) {
MaybeDwordResult result =
RegistryGetValueDword(IsPrefixed::Unprefixed, regName, regKey.c_str());
if (result.isErr()) {
LOG_ERROR_MESSAGE(L"Failed to read \"%s\" from \"%s\": %#X", regName,
regKey.c_str(), result.inspectErr().AsHResult());
return mozilla::Err(result.unwrapErr());
}
MaybeDword maybeValue = result.unwrap();
if (expected && maybeValue.isNothing()) {
LOG_ERROR_MESSAGE(L"Missing expected value \"%s\" from \"%s\"", regName,
regKey.c_str());
VoidResult result = DiscardFront();
if (result.isErr()) {
return mozilla::Err(result.unwrapErr());
}
}
return maybeValue;
}
/**
* This function reads a string cache key's value and returns it. If the
* expected argument is true and the key is missing, this will delete the entire
* entry and return mozilla::Nothing().
*/
MaybeStringResult Cache::ReadEntryKeyString(const std::wstring& regKey,
const wchar_t* regName,
bool expected) {
MaybeStringResult result =
RegistryGetValueString(IsPrefixed::Unprefixed, regName, regKey.c_str());
if (result.isErr()) {
LOG_ERROR_MESSAGE(L"Failed to read \"%s\" from \"%s\": %#X", regName,
regKey.c_str(), result.inspectErr().AsHResult());
return mozilla::Err(result.unwrapErr());
}
MaybeString maybeValue = result.unwrap();
if (expected && maybeValue.isNothing()) {
LOG_ERROR_MESSAGE(L"Missing expected value \"%s\" from \"%s\"", regName,
regKey.c_str());
VoidResult result = DiscardFront();
if (result.isErr()) {
return mozilla::Err(result.unwrapErr());
}
}
return maybeValue;
}
Cache::MaybeEntryResult Cache::Dequeue() {
VoidResult result = Init();
if (result.isErr()) {
return mozilla::Err(result.unwrapErr());
}
std::wstring subKey = MakeEntryRegKeyName(mFront);
// We are going to read within a loop so that if we find incomplete entries,
// we can just discard them and try to read the next entry. We'll put a limit
// on the maximum number of times this loop can possibly run so that if
// something goes horribly wrong, we don't loop forever. If we exit this loop
// without returning, it means that not only were we not able to read
// anything, but something very unexpected happened.
// We are going to potentially loop over this mCapacity + 1 times so that if
// we end up discarding every item in the cache, we return mozilla::Nothing()
// rather than an error.
for (uint32_t i = 0; i <= mCapacity; ++i) {
if (mSize == 0) {
return MaybeEntry(mozilla::Nothing());
}
Cache::VersionedEntry entry;
// CacheEntryVersion
MaybeDwordResult dResult =
ReadEntryKeyDword(subKey, Cache::kEntryVersionKey, true);
if (dResult.isErr()) {
return mozilla::Err(dResult.unwrapErr());
}
MaybeDword maybeDValue = dResult.unwrap();
if (maybeDValue.isNothing()) {
// Note that we only call continue in this function after DiscardFront()
// has been called (either directly, or by one of the ReadEntryKey.*
// functions). So the continue call results in attempting to read the
// next entry in the cache.
continue;
}
entry.entryVersion = maybeDValue.value();
if (entry.entryVersion < 1) {
LOG_ERROR_MESSAGE(L"Invalid entry version of %u in \"%s\"",
entry.entryVersion, subKey.c_str());
VoidResult result = DiscardFront();
if (result.isErr()) {
return mozilla::Err(result.unwrapErr());
}
continue;
}
// NotificationType
MaybeStringResult sResult =
ReadEntryKeyString(subKey, Cache::kNotificationTypeKey, true);
if (sResult.isErr()) {
return mozilla::Err(sResult.unwrapErr());
}
MaybeString maybeSValue = sResult.unwrap();
if (maybeSValue.isNothing()) {
continue;
}
entry.notificationType = maybeSValue.value();
// NotificationShown
sResult = ReadEntryKeyString(subKey, Cache::kNotificationShownKey, true);
if (sResult.isErr()) {
return mozilla::Err(sResult.unwrapErr());
}
maybeSValue = sResult.unwrap();
if (maybeSValue.isNothing()) {
continue;
}
entry.notificationShown = maybeSValue.value();
// NotificationAction
sResult = ReadEntryKeyString(subKey, Cache::kNotificationActionKey, true);
if (sResult.isErr()) {
return mozilla::Err(sResult.unwrapErr());
}
maybeSValue = sResult.unwrap();
if (maybeSValue.isNothing()) {
continue;
}
entry.notificationAction = maybeSValue.value();
// PrevNotificationAction
bool expected =
entry.entryVersion >= Cache::kInitialVersionPrevNotificationActionKey;
sResult =
ReadEntryKeyString(subKey, Cache::kPrevNotificationActionKey, expected);
if (sResult.isErr()) {
return mozilla::Err(sResult.unwrapErr());
}
maybeSValue = sResult.unwrap();
if (expected && maybeSValue.isNothing()) {
continue;
}
entry.prevNotificationAction = maybeSValue;
// We successfully read the entry. Now we need to remove it from the cache.
VoidResult result = DiscardFront();
if (result.isErr()) {
// If we aren't able to remove the entry from the cache, don't return it.
// We don't want to return the same item over and over again if we get
// into a bad state.
return mozilla::Err(result.unwrapErr());
}
return mozilla::Some(entry);
}
LOG_ERROR_MESSAGE(L"Unexpected: This line shouldn't be reached");
return mozilla::Err(mozilla::WindowsError::FromHResult(E_FAIL));
}
} // namespace mozilla::default_agent