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/. */
6 
7 #ifndef __DEFAULT_BROWSER_AGENT_CACHE_H__
8 #define __DEFAULT_BROWSER_AGENT_CACHE_H__
9 
10 #include <cstdint>
11 #include <string>
12 #include <windows.h>
13 
14 #include "Registry.h"
15 
16 using DwordResult = mozilla::WindowsErrorResult<uint32_t>;
17 
18 /**
19  * This cache functions as a FIFO queue which writes its data to the Windows
20  * registry.
21  *
22  * Note that the cache is not thread-safe, so it is recommended that the WDBA's
23  * RegistryMutex be acquired before accessing it.
24  *
25  * Some of the terminology used in this module is a easy to mix up, so let's
26  * just be clear about it:
27  *  - registry key/sub-key
28  *       A registry key is sort of like the registry's equivalent of a
29  *       directory. It can contain values, each of which is made up of a name
30  *       and corresponding data. We may also refer to a "sub-key", meaning a
31  *       registry key nested in a registry key.
32  *  - cache key/entry key
33  *       A cache key refers to the string that we use to look up a single
34  *       element of cache entry data. Example: "CacheEntryVersion"
35  *  - entry
36  *      This refers to an entire record stored using Cache::Enqueue or retrieved
37  *      using Cache::Dequeue. It consists of numerous cache keys and their
38  *      corresponding data.
39  *
40  * The first version of this cache was problematic because of how hard it was to
41  * extend. This version attempts to overcome this. It first migrates all data
42  * out of the version 1 cache. This means that the stored ping data will not
43  * be accessible to out-of-date clients, but presumably they will eventually
44  * be updated or the up-to-date client that performed the migration will send
45  * the pings itself. Because the WDBA telemetry has no client ID, all analysis
46  * is stateless, so even if the other clients send some pings before the stored
47  * ones get sent, that's ok. The ordering isn't really important.
48  *
49  * This version of the cache attempts to correct the problem of how hard it was
50  * to extend the old cache. The biggest problem that the old cache had was that
51  * when it dequeued data it had to shift data, but it wouldn't shift keys that
52  * it didn't know about, causing them to become associated with the wrong cache
53  * entries.
54  *
55  * Version 2 of the cache will make 4 improvements to attempt to avoid problems
56  * like this in the future:
57  *  1. Each cache entry will get its own registry key. This will help to keep
58  *     cache entries isolated from each other.
59  *  2. Each cache entry will include version data so that we know what cache
60  *     keys to expect when we read it.
61  *  3. Rather than having to shift every entry every time we dequeue, we will
62  *     implement a circular queue so that we just have to update what index
63  *     currently represents the front
64  *  4. We will store the cache capacity in the cache so that we can expand the
65  *     cache later, if we want, without breaking previous versions.
66  */
67 class Cache {
68  public:
69   // cacheRegKey is the registry sub-key that the cache will be stored in. If
70   // null is passed (the default), we will use the default cache name. This is
71   // what ought to be used in production. When testing, we will pass a different
72   // key in so that our testing caches don't conflict with each other or with
73   // a possible production cache on the test machine.
74   explicit Cache(const wchar_t* cacheRegKey = nullptr);
75   ~Cache();
76 
77   // The version of the cache (not to be confused with the version of the cache
78   // entries). This should only be incremented if we need to make breaking
79   // changes that require migration to a new cache location, like we did between
80   // versions 1 and 2. This value will be used as part of the sub-key that the
81   // cache is stored in (ex: "PingCache\version2").
82   static constexpr const uint32_t kVersion = 2;
83   // This value will be written into each entry. This allows us to know what
84   // cache keys to expect in the event that additional cache keys are added in
85   // later entry versions.
86   static constexpr const uint32_t kEntryVersion = 2;
87   static constexpr const uint32_t kDefaultCapacity = 2;
88   // We want to allow the cache to be expandable, but we don't really want it to
89   // be infinitely expandable. So we'll set an upper bound.
90   static constexpr const uint32_t kMaxCapacity = 100;
91   static constexpr const wchar_t* kDefaultPingCacheRegKey = L"PingCache";
92 
93   // Used to read the version 1 cache entries during data migration. Full cache
94   // key names are formatted like: "<keyPrefix><baseKeyName><cacheIndex>"
95   // For example: "PingCacheNotificationType0"
96   static constexpr const wchar_t* kVersion1KeyPrefix = L"PingCache";
97   static constexpr const uint32_t kVersion1MaxSize = 2;
98 
99   static constexpr const wchar_t* kCapacityRegName = L"Capacity";
100   static constexpr const wchar_t* kFrontRegName = L"Front";
101   static constexpr const wchar_t* kSizeRegName = L"Size";
102 
103   // Cache Entry keys
104   static constexpr const wchar_t* kEntryVersionKey = L"CacheEntryVersion";
105   // Note that the next 3 must also match the base key names from version 1
106   // since we use them to construct those key names.
107   static constexpr const wchar_t* kNotificationTypeKey = L"NotificationType";
108   static constexpr const wchar_t* kNotificationShownKey = L"NotificationShown";
109   static constexpr const wchar_t* kNotificationActionKey =
110       L"NotificationAction";
111   static constexpr const wchar_t* kPrevNotificationActionKey =
112       L"PrevNotificationAction";
113 
114   // The version key wasn't added until version 2, but we add it to the version
115   // 1 entries when migrating them to the cache.
116   static constexpr const uint32_t kInitialVersionEntryVersionKey = 1;
117   static constexpr const uint32_t kInitialVersionNotificationTypeKey = 1;
118   static constexpr const uint32_t kInitialVersionNotificationShownKey = 1;
119   static constexpr const uint32_t kInitialVersionNotificationActionKey = 1;
120   static constexpr const uint32_t kInitialVersionPrevNotificationActionKey = 2;
121 
122   // We have two cache entry structs: one for the current version, and one
123   // generic one that can handle any version. There are a couple of reasons
124   // for this:
125   //  - We only want to support writing the current version, but we want to
126   //    support reading any version.
127   //  - It makes things a bit nicer for the caller when Enqueue-ing, since
128   //    they don't have to set the version or wrap values that were added
129   //    later in a mozilla::Maybe.
130   //  - It keeps us from having to worry about writing an invalid cache entry,
131   //    such as one that claims to be version 2, but doesn't have
132   //    prevNotificationAction.
133   // Note that the entry struct for the current version does not contain a
134   // version member value because we already know that its version is equal to
135   // Cache::kEntryVersion.
136   struct Entry {
137     std::string notificationType;
138     std::string notificationShown;
139     std::string notificationAction;
140     std::string prevNotificationAction;
141   };
142   struct VersionedEntry {
143     uint32_t entryVersion;
144     std::string notificationType;
145     std::string notificationShown;
146     std::string notificationAction;
147     mozilla::Maybe<std::string> prevNotificationAction;
148   };
149 
150   using MaybeEntry = mozilla::Maybe<VersionedEntry>;
151   using MaybeEntryResult = mozilla::WindowsErrorResult<MaybeEntry>;
152 
153   VoidResult Init();
154   VoidResult Enqueue(const Entry& entry);
155   MaybeEntryResult Dequeue();
156 
157  private:
158   const std::wstring mCacheRegKey;
159 
160   // We can't easily copy a VoidResult, so just store the raw HRESULT here.
161   mozilla::Maybe<HRESULT> mInitializeResult;
162   // How large the cache will grow before it starts rejecting new entries.
163   uint32_t mCapacity;
164   // The index of the first present cache entry.
165   uint32_t mFront;
166   // How many entries are present in the cache.
167   uint32_t mSize;
168 
169   DwordResult EnsureDwordSetting(const wchar_t* regName, uint32_t defaultValue);
170   VoidResult SetupCache();
171   VoidResult MaybeMigrateVersion1();
172   std::wstring MakeEntryRegKeyName(uint32_t index);
173   VoidResult WriteEntryKeys(uint32_t index, const VersionedEntry& entry);
174   VoidResult DeleteEntry(uint32_t index);
175   VoidResult SetFront(uint32_t newFront);
176   VoidResult SetSize(uint32_t newSize);
177   VoidResult VersionedEnqueue(const VersionedEntry& entry);
178   VoidResult DiscardFront();
179   MaybeDwordResult ReadEntryKeyDword(const std::wstring& regKey,
180                                      const wchar_t* regName, bool expected);
181   MaybeStringResult ReadEntryKeyString(const std::wstring& regKey,
182                                        const wchar_t* regName, bool expected);
183 };
184 
185 #endif  // __DEFAULT_BROWSER_AGENT_CACHE_H__
186