/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "ActorsParent.h"
// Local includes
#include "LSInitializationTypes.h"
#include "LSObject.h"
#include "ReportInternalError.h"
// Global includes
#include <cinttypes>
#include <cstdlib>
#include <cstring>
#include <
new>
#include <tuple>
#include <type_traits>
#include <utility>
#include "ErrorList.h"
#include "MainThreadUtils.h"
#include "mozIStorageAsyncConnection.h"
#include "mozIStorageConnection.h"
#include "mozIStorageFunction.h"
#include "mozIStorageService.h"
#include "mozIStorageStatement.h"
#include "mozIStorageValueArray.h"
#include "mozStorageCID.h"
#include "mozStorageHelper.h"
#include "mozilla/Assertions.h"
#include "mozilla/Atomics.h"
#include "mozilla/Attributes.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Logging.h"
#include "mozilla/MacroForEach.h"
#include "mozilla/Maybe.h"
#include "mozilla/Monitor.h"
#include "mozilla/Mutex.h"
#include "mozilla/NotNull.h"
#include "mozilla/OriginAttributes.h"
#include "mozilla/Preferences.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Result.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/StoragePrincipalHelper.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/Utf8.h"
#include "mozilla/Variant.h"
#include "mozilla/dom/ClientManagerService.h"
#include "mozilla/dom/FlippedOnce.h"
#include "mozilla/dom/LSSnapshot.h"
#include "mozilla/dom/LSValue.h"
#include "mozilla/dom/LSWriteOptimizer.h"
#include "mozilla/dom/LSWriteOptimizerImpl.h"
#include "mozilla/dom/LocalStorageCommon.h"
#include "mozilla/dom/Nullable.h"
#include "mozilla/dom/PBackgroundLSDatabase.h"
#include "mozilla/dom/PBackgroundLSDatabaseParent.h"
#include "mozilla/dom/PBackgroundLSObserverParent.h"
#include "mozilla/dom/PBackgroundLSRequestParent.h"
#include "mozilla/dom/PBackgroundLSSharedTypes.h"
#include "mozilla/dom/PBackgroundLSSimpleRequestParent.h"
#include "mozilla/dom/PBackgroundLSSnapshotParent.h"
#include "mozilla/dom/SnappyUtils.h"
#include "mozilla/dom/StorageDBUpdater.h"
#include "mozilla/dom/StorageUtils.h"
#include "mozilla/dom/ipc/IdType.h"
#include "mozilla/dom/quota/CachingDatabaseConnection.h"
#include "mozilla/dom/quota/CheckedUnsafePtr.h"
#include "mozilla/dom/quota/Client.h"
#include "mozilla/dom/quota/ClientDirectoryLock.h"
#include "mozilla/dom/quota/ClientImpl.h"
#include "mozilla/dom/quota/DirectoryLock.h"
#include "mozilla/dom/quota/DirectoryLockInlines.h"
#include "mozilla/dom/quota/FirstInitializationAttemptsImpl.h"
#include "mozilla/dom/quota/HashKeys.h"
#include "mozilla/dom/quota/OriginScope.h"
#include "mozilla/dom/quota/PersistenceScope.h"
#include "mozilla/dom/quota/PersistenceType.h"
#include "mozilla/dom/quota/PrincipalUtils.h"
#include "mozilla/dom/quota/QuotaCommon.h"
#include "mozilla/dom/quota/StorageHelpers.h"
#include "mozilla/dom/quota/QuotaManager.h"
#include "mozilla/dom/quota/QuotaObject.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/dom/quota/ThreadUtils.h"
#include "mozilla/dom/quota/UsageInfo.h"
#include "mozilla/glean/DomLocalstorageMetrics.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/PBackgroundParent.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "mozilla/ipc/ProtocolUtils.h"
#include "mozilla/storage/Variant.h"
#include "NotifyUtils.h"
#include "nsBaseHashtable.h"
#include "nsCOMPtr.h"
#include "nsClassHashtable.h"
#include "nsTHashMap.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsHashKeys.h"
#include "nsIBinaryInputStream.h"
#include "nsIBinaryOutputStream.h"
#include "nsIDirectoryEnumerator.h"
#include "nsIEventTarget.h"
#include "nsIFile.h"
#include "nsIInputStream.h"
#include "nsIObjectInputStream.h"
#include "nsIObjectOutputStream.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsIOutputStream.h"
#include "nsIRunnable.h"
#include "nsISerialEventTarget.h"
#include "nsISupports.h"
#include "nsIThread.h"
#include "nsITimer.h"
#include "nsIVariant.h"
#include "nsInterfaceHashtable.h"
#include "nsLiteralString.h"
#include "nsNetUtil.h"
#include "nsPointerHashKeys.h"
#include "nsPrintfCString.h"
#include "nsRefPtrHashtable.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsStringFlags.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTHashSet.h"
#include "nsTLiteralString.h"
#include "nsTStringRepr.h"
#include "nsThreadUtils.h"
#include "nsVariant.h"
#include "nsXPCOM.h"
#include "nsXULAppAPI.h"
#include "nscore.h"
#include "prenv.h"
#include "prtime.h"
#define LS_LOG_TEST() MOZ_LOG_TEST(GetLocalStorageLogger(), LogLevel::Info)
#define LS_LOG(_args) MOZ_LOG(GetLocalStorageLogger(), LogLevel::Info, _args)
#if defined(MOZ_WIDGET_ANDROID)
# define LS_MOBILE
#endif
namespace mozilla::dom {
using namespace mozilla::dom::quota;
using namespace mozilla::dom::StorageUtils;
using namespace mozilla::ipc;
namespace {
struct ArchivedOriginInfo;
class ArchivedOriginScope;
class Connection;
class ConnectionThread;
class Database;
class Observer;
class PrepareDatastoreOp;
class PreparedDatastore;
class QuotaClient;
class Snapshot;
using ArchivedOriginHashtable =
nsClassHashtable<nsCStringHashKey, ArchivedOriginInfo>;
/*******************************************************************************
* Constants
******************************************************************************/
// Major schema version. Bump for almost everything.
const uint32_t kMajorSchemaVersion = 5;
// Minor schema version. Should almost always be 0 (maybe bump on release
// branches if we have to).
const uint32_t kMinorSchemaVersion = 0;
// The schema version we store in the SQLite database is a (signed) 32-bit
// integer. The major version is left-shifted 4 bits so the max value is
// 0xFFFFFFF. The minor version occupies the lower 4 bits and its max is 0xF.
static_assert(kMajorSchemaVersion <= 0xFFFFFFF,
"Major version needs to fit in 28 bits.");
static_assert(kMinorSchemaVersion <= 0xF,
"Minor version needs to fit in 4 bits.");
const int32_t kSQLiteSchemaVersion =
int32_t((kMajorSchemaVersion << 4) + kMinorSchemaVersion);
// Changing the value here will override the page size of new databases only.
// A journal mode change and VACUUM are needed to change existing databases, so
// the best way to do that is to use the schema version upgrade mechanism.
const uint32_t kSQLitePageSizeOverride =
#ifdef LS_MOBILE
512;
#else
1024;
#endif
static_assert(kSQLitePageSizeOverride ==
/* mozStorage default */ 0 ||
(kSQLitePageSizeOverride % 2 == 0 &&
kSQLitePageSizeOverride >= 512 &&
kSQLitePageSizeOverride <= 65536),
"Must be 0 (disabled) or a power of 2 between 512 and 65536!");
// Set to some multiple of the page size to grow the database in larger chunks.
const uint32_t kSQLiteGrowthIncrement = kSQLitePageSizeOverride * 2;
static_assert(kSQLiteGrowthIncrement >= 0 &&
kSQLiteGrowthIncrement % kSQLitePageSizeOverride == 0 &&
kSQLiteGrowthIncrement < uint32_t(INT32_MAX),
"Must be 0 (disabled) or a positive multiple of the page size!");
/**
* The database name for LocalStorage data in a per-origin directory.
*/
constexpr
auto kDataFileName = u
"data.sqlite"_ns;
/**
* The journal corresponding to kDataFileName. (We don't use WAL mode.)
* Currently only needed in QuotaClient::InitOrigin and only in DEBUG builds.
* See the corresponding comment in QuotaClient::InitOrigin.
*/
#ifdef DEBUG
constexpr
auto kJournalFileName = u
"data.sqlite-journal"_ns;
#endif
/**
* This file contains the current usage of the LocalStorage database as defined
* by the mozLength totals of all keys and values for the database, which
* differs from the actual size on disk. We store this value in a separate
* file as a cache so that we can initialize the QuotaClient faster.
* In the future, this file will be eliminated and the information will be
* stored in PROFILE/storage.sqlite or similar QuotaManager-wide storage.
*
* The file contains a binary verification cookie (32-bits) followed by the
* actual usage (64-bits).
*/
constexpr
auto kUsageFileName = u
"usage"_ns;
/**
* Following a QuotaManager idiom, this journal file's existence is a marker
* that the usage file was in the process of being updated and is currently
* invalid. This file is created prior to updating the usage file and only
* deleted after the usage file has been written and closed and any pending
* database transactions have been committed. Note that this idiom is expected
* to work if Gecko crashes in the middle of a write, but is not expected to be
* foolproof in the face of a system crash, as we do not explicitly attempt to
* fsync the directory containing the journal file.
*
* If the journal file is found to exist at origin initialization time, the
* usage will be re-computed from the current state of DATA_FILE_NAME.
*/
constexpr
auto kUsageJournalFileName = u
"usage-journal"_ns;
static const uint32_t kUsageFileSize = 12;
static const uint32_t kUsageFileCookie = 0x420a420a;
/**
* How long between the first moment we know we have data to be written on a
* `Connection` and when we should actually perform the write. This helps
* limit disk churn under silly usage patterns and is historically consistent
* with the previous, legacy implementation.
*
* Note that flushing happens downstream of Snapshot checkpointing and its
* batch mechanism which helps avoid wasteful IPC in the case of silly content
* code.
*/
const uint32_t kFlushTimeoutMs = 5000;
const bool kDefaultShadowWrites =
false;
const uint32_t kDefaultSnapshotPrefill = 16384;
const uint32_t kDefaultSnapshotGradualPrefill = 4096;
const bool kDefaultClientValidation =
true;
/**
* Should all mutations also be reflected in the "shadow" database, which is
* the legacy webappsstore.sqlite database. When this is enabled, users can
* downgrade their version of Firefox and/or otherwise fall back to the legacy
* implementation without loss of data. (Older versions of Firefox will
* recognize the presence of ls-archive.sqlite and purge it and the other
* LocalStorage directories so privacy is maintained.)
*/
const char kShadowWritesPref[] =
"dom.storage.shadow_writes";
/**
* Byte budget for sending data down to the LSSnapshot instance when it is first
* created. If there is less data than this (measured by tallying the string
* length of the keys and values), all data is sent, otherwise partial data is
* sent. See `Snapshot`.
*/
const char kSnapshotPrefillPref[] =
"dom.storage.snapshot_prefill";
/**
* When a specific value is requested by an LSSnapshot that is not already fully
* populated, gradual prefill is used. This preference specifies the number of
* bytes to be used to send values beyond the specific value that is requested.
* (The size of the explicitly requested value does not impact this preference.)
* Setting the value to 0 disables gradual prefill. Tests may set this value to
* -1 which is converted to INT_MAX in order to cause gradual prefill to send
* all values not previously sent.
*/
const char kSnapshotGradualPrefillPref[] =
"dom.storage.snapshot_gradual_prefill";
const char kClientValidationPref[] =
"dom.storage.client_validation";
/**
* The amount of time a PreparedDatastore instance should stick around after a
* preload is triggered in order to give time for the page to use LocalStorage
* without triggering worst-case synchronous jank.
*/
const uint32_t kPreparedDatastoreTimeoutMs = 20000;
/**
* Cold storage for LocalStorage data extracted from webappsstore.sqlite at
* LSNG first-run that has not yet been migrated to its own per-origin directory
* by use.
*
* In other words, at first run, LSNG copies the contents of webappsstore.sqlite
* into this database. As requests are made for that LocalStorage data, the
* contents are removed from this database and placed into per-origin QM
* storage. So the contents of this database are always old, unused
* LocalStorage data that we can potentially get rid of at some point in the
* future.
*/
#define LS_ARCHIVE_FILE_NAME u
"ls-archive.sqlite"
/**
* The legacy LocalStorage database. Its contents are maintained as our
* "shadow" database so that LSNG can be disabled without loss of user data.
*/
#define WEB_APPS_STORE_FILE_NAME u
"webappsstore.sqlite"
// Shadow database Write Ahead Log's maximum size is 512KB
const uint32_t kShadowMaxWALSize = 512 * 1024;
bool IsOnGlobalConnectionThread();
void AssertIsOnGlobalConnectionThread();
/*******************************************************************************
* SQLite functions
******************************************************************************/
int32_t MakeSchemaVersion(uint32_t aMajorSchemaVersion,
uint32_t aMinorSchemaVersion) {
return int32_t((aMajorSchemaVersion << 4) + aMinorSchemaVersion);
}
nsCString GetArchivedOriginHashKey(
const nsACString& aOriginSuffix,
const nsACString& aOriginNoSuffix) {
return aOriginSuffix +
":"_ns + aOriginNoSuffix;
}
nsresult CreateDataTable(mozIStorageConnection* aConnection) {
return aConnection->ExecuteSimpleSQL(
"CREATE TABLE data"
"( key TEXT PRIMARY KEY"
", utf16_length INTEGER NOT NULL"
", conversion_type INTEGER NOT NULL"
", compression_type INTEGER NOT NULL"
", last_access_time INTEGER NOT NULL DEFAULT 0"
", value BLOB NOT NULL"
");"_ns);
}
nsresult CreateTables(mozIStorageConnection* aConnection) {
MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
MOZ_ASSERT(aConnection);
// Table `database`
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"CREATE TABLE database"
"( origin TEXT NOT NULL"
", usage INTEGER NOT NULL DEFAULT 0"
", last_vacuum_time INTEGER NOT NULL DEFAULT 0"
", last_analyze_time INTEGER NOT NULL DEFAULT 0"
", last_vacuum_size INTEGER NOT NULL DEFAULT 0"
");"_ns)));
// Table `data`
QM_TRY(MOZ_TO_RESULT(CreateDataTable(aConnection)));
QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(kSQLiteSchemaVersion)));
return NS_OK;
}
nsresult UpgradeSchemaFrom1_0To2_0(mozIStorageConnection* aConnection) {
AssertIsOnIOThread();
MOZ_ASSERT(aConnection);
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"ALTER TABLE database ADD COLUMN usage INTEGER NOT NULL DEFAULT 0;"_ns)));
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"UPDATE database "
"SET usage = (SELECT total(utf16Length(key) + utf16Length(value)) "
"FROM data);"_ns)));
QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(2, 0))));
return NS_OK;
}
nsresult UpgradeSchemaFrom2_0To3_0(mozIStorageConnection* aConnection) {
AssertIsOnIOThread();
MOZ_ASSERT(aConnection);
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"ALTER TABLE data ADD COLUMN utf16Length INTEGER NOT NULL DEFAULT 0;"_ns)));
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"UPDATE data SET utf16Length = utf16Length(value);"_ns)));
QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(3, 0))));
return NS_OK;
}
nsresult UpgradeSchemaFrom3_0To4_0(mozIStorageConnection* aConnection) {
AssertIsOnIOThread();
MOZ_ASSERT(aConnection);
QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(4, 0))));
return NS_OK;
}
nsresult UpgradeSchemaFrom4_0To5_0(mozIStorageConnection* aConnection) {
AssertIsOnIOThread();
MOZ_ASSERT(aConnection);
// Recreate data table in new format following steps at
// https://www.sqlite.org/lang_altertable.html
// section "Making Other Kinds Of Table Schema Changes"
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"CREATE TABLE migrated_data"
"( key TEXT PRIMARY KEY"
", utf16_length INTEGER NOT NULL"
", conversion_type INTEGER NOT NULL"
", compression_type INTEGER NOT NULL"
", last_access_time INTEGER NOT NULL DEFAULT 0"
", value BLOB NOT NULL"
");"_ns)));
// Reinsert old data, all legacy data is UTF8
static_assert(1u ==
static_cast<uint8_t>(LSValue::ConversionType::UTF16_UTF8));
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"INSERT INTO migrated_data (key, utf16_length, conversion_type, "
"compression_type, last_access_time, value) "
"SELECT key, utf16Length, 1, compressed, lastAccessTime, value "
"FROM data;"_ns)));
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"DROP TABLE data;"_ns)));
// Rename to data
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"ALTER TABLE migrated_data RENAME TO data;"_ns)));
QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(5, 0))));
return NS_OK;
}
nsresult SetDefaultPragmas(mozIStorageConnection* aConnection) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(aConnection);
QM_TRY(MOZ_TO_RESULT(
aConnection->ExecuteSimpleSQL(
"PRAGMA synchronous = FULL;"_ns)));
#ifndef LS_MOBILE
if (kSQLiteGrowthIncrement) {
// This is just an optimization so ignore the failure if the disk is
// currently too full.
QM_TRY(QM_OR_ELSE_WARN_IF(
// Expression.
MOZ_TO_RESULT(
aConnection->SetGrowthIncrement(kSQLiteGrowthIncrement,
""_ns)),
// Predicate.
IsSpecificError<NS_ERROR_FILE_TOO_BIG>,
// Fallback.
ErrToDefaultOk<>));
}
#endif // LS_MOBILE
return NS_OK;
}
Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateStorageConnection(
nsIFile& aDBFile, nsIFile& aUsageFile,
const nsACString& aOrigin) {
MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
// XXX Common logic should be refactored out of this method and
// cache::DBAction::OpenDBConnection, and maybe other similar functions.
QM_TRY_INSPECT(
const auto& storageService,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
MOZ_SELECT_OVERLOAD(do_GetService),
MOZ_STORAGE_SERVICE_CONTRACTID));
QM_TRY_UNWRAP(
auto connection, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageConnection>,
storageService, OpenDatabase, &aDBFile,
mozIStorageService::CONNECTION_DEFAULT));
QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(connection)));
// Check to make sure that the database schema is correct.
// XXX Try to make schemaVersion const.
QM_TRY_UNWRAP(int32_t schemaVersion,
MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion));
QM_TRY(OkIf(schemaVersion <= kSQLiteSchemaVersion), Err(NS_ERROR_FAILURE));
if (schemaVersion != kSQLiteSchemaVersion) {
const bool newDatabase = !schemaVersion;
if (newDatabase) {
// Set the page size first.
if (kSQLitePageSizeOverride) {
QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(nsPrintfCString(
"PRAGMA page_size = %" PRIu32
";", kSQLitePageSizeOverride))));
}
// We have to set the auto_vacuum mode before opening a transaction.
QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(
#ifdef LS_MOBILE
// Turn on full auto_vacuum mode to reclaim disk space on mobile
// devices (at the cost of some COMMIT speed).
"PRAGMA auto_vacuum = FULL;"_ns
#else
// Turn on incremental auto_vacuum mode on desktop builds.
"PRAGMA auto_vacuum = INCREMENTAL;"_ns
#endif
)));
}
bool vacuumNeeded =
false;
if (newDatabase) {
mozStorageTransaction transaction(
connection,
/* aCommitOnComplete */ false,
mozIStorageConnection::TRANSACTION_IMMEDIATE);
QM_TRY(MOZ_TO_RESULT(transaction.Start()));
QM_TRY(MOZ_TO_RESULT(CreateTables(connection)));
#ifdef DEBUG
{
QM_TRY_INSPECT(
const int32_t& schemaVersion,
MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion),
QM_ASSERT_UNREACHABLE);
MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
}
#endif
QM_TRY_INSPECT(
const auto& stmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
"INSERT INTO database (origin) VALUES (:origin)"_ns));
QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName(
"origin"_ns, aOrigin)));
QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
}
else {
// This logic needs to change next time we change the schema!
static_assert(kSQLiteSchemaVersion == int32_t((5 << 4) + 0),
"Upgrade function needed due to schema version increase.");
while (schemaVersion != kSQLiteSchemaVersion) {
mozStorageTransaction transaction(
connection,
/* aCommitOnComplete */ false,
mozIStorageConnection::TRANSACTION_IMMEDIATE);
QM_TRY(MOZ_TO_RESULT(transaction.Start()));
if (schemaVersion == MakeSchemaVersion(1, 0)) {
QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom1_0To2_0(connection)));
}
else if (schemaVersion == MakeSchemaVersion(2, 0)) {
QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom2_0To3_0(connection)));
}
else if (schemaVersion == MakeSchemaVersion(3, 0)) {
QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom3_0To4_0(connection)));
}
else if (schemaVersion == MakeSchemaVersion(4, 0)) {
QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom4_0To5_0(connection)));
vacuumNeeded =
true;
}
else {
LS_WARNING(
"Unable to open LocalStorage database, no upgrade path is "
"available!");
return Err(NS_ERROR_FAILURE);
}
QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
QM_TRY_UNWRAP(schemaVersion, MOZ_TO_RESULT_INVOKE_MEMBER(
connection, GetSchemaVersion));
}
MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
}
if (vacuumNeeded) {
QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(
"VACUUM;"_ns)));
}
if (newDatabase) {
// Windows caches the file size, let's force it to stat the file again.
QM_TRY_INSPECT(
const bool& exists,
MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, Exists));
Unused << exists;
QM_TRY_INSPECT(
const int64_t& fileSize,
MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, GetFileSize));
MOZ_ASSERT(fileSize > 0);
const PRTime vacuumTime = PR_Now();
MOZ_ASSERT(vacuumTime);
QM_TRY_INSPECT(
const auto& vacuumTimeStmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<mozIStorageStatement>,
connection, CreateStatement,
"UPDATE database "
"SET last_vacuum_time = :time"
", last_vacuum_size = :size;"_ns));
QM_TRY(MOZ_TO_RESULT(
vacuumTimeStmt->BindInt64ByName(
"time"_ns, vacuumTime)));
QM_TRY(
MOZ_TO_RESULT(vacuumTimeStmt->BindInt64ByName(
"size"_ns, fileSize)));
QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->Execute()));
}
}
return connection;
}
template <
typename CorruptedFileHandler>
Result<nsCOMPtr<mozIStorageConnection>, nsresult>
CreateStorageConnectionWithRecovery(
nsIFile& aDBFile, nsIFile& aUsageFile,
const nsACString& aOrigin,
CorruptedFileHandler&& aCorruptedFileHandler) {
QM_TRY_RETURN(QM_OR_ELSE_WARN_IF(
// Expression.
CreateStorageConnection(aDBFile, aUsageFile, aOrigin),
// Predicate.
IsDatabaseCorruptionError,
// Fallback.
([&aDBFile, &aUsageFile, &aOrigin,
&aCorruptedFileHandler](
const nsresult rv)
-> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
// Remove the usage file first (it might not exist at all due
// to corrupted state, which is ignored here).
// Usually we only use QM_OR_ELSE_LOG_VERBOSE(_IF) with Remove and
// NS_ERROR_FILE_NOT_FOUND check, but we're already in the rare case
// of corruption here, so the use of QM_OR_ELSE_WARN_IF is ok here.
QM_TRY(QM_OR_ELSE_WARN_IF(
// Expression.
MOZ_TO_RESULT(aUsageFile.Remove(
false)),
// Predicate.
([](
const nsresult rv) {
return rv == NS_ERROR_FILE_NOT_FOUND; }),
// Fallback.
ErrToDefaultOk<>));
// Call the corrupted file handler before trying to remove the
// database file, which might fail.
aCorruptedFileHandler();
// Nuke the database file.
QM_TRY(MOZ_TO_RESULT(aDBFile.Remove(
false)));
QM_TRY_RETURN(CreateStorageConnection(aDBFile, aUsageFile, aOrigin));
})));
}
Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetStorageConnection(
const nsAString& aDatabaseFilePath) {
AssertIsOnGlobalConnectionThread();
MOZ_ASSERT(!aDatabaseFilePath.IsEmpty());
MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, u
".sqlite"_ns));
QM_TRY_INSPECT(
const auto& databaseFile, QM_NewLocalFile(aDatabaseFilePath));
QM_TRY_INSPECT(
const bool& exists,
MOZ_TO_RESULT_INVOKE_MEMBER(databaseFile, Exists));
QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE));
QM_TRY_INSPECT(
const auto& ss,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
MOZ_SELECT_OVERLOAD(do_GetService),
MOZ_STORAGE_SERVICE_CONTRACTID));
QM_TRY_UNWRAP(
auto connection,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageConnection>, ss, OpenDatabase,
databaseFile, mozIStorageService::CONNECTION_DEFAULT));
QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(connection)));
return connection;
}
Result<nsCOMPtr<nsIFile>, nsresult> GetArchiveFile(
const nsAString& aStoragePath) {
AssertIsOnIOThread();
MOZ_ASSERT(!aStoragePath.IsEmpty());
QM_TRY_UNWRAP(
auto archiveFile, QM_NewLocalFile(aStoragePath));
QM_TRY(MOZ_TO_RESULT(
archiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME))));
return archiveFile;
}
Result<nsCOMPtr<mozIStorageConnection>, nsresult>
CreateArchiveStorageConnection(
const nsAString& aStoragePath) {
AssertIsOnIOThread();
MOZ_ASSERT(!aStoragePath.IsEmpty());
QM_TRY_INSPECT(
const auto& archiveFile, GetArchiveFile(aStoragePath));
// QuotaManager ensures this file always exists.
DebugOnly<
bool> exists;
MOZ_ASSERT(NS_SUCCEEDED(archiveFile->Exists(&exists)));
MOZ_ASSERT(exists);
QM_TRY_INSPECT(
const bool& isDirectory,
MOZ_TO_RESULT_INVOKE_MEMBER(archiveFile, IsDirectory));
if (isDirectory) {
LS_WARNING(
"ls-archive is not a file!");
return nsCOMPtr<mozIStorageConnection>{};
}
QM_TRY_INSPECT(
const auto& ss,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
MOZ_SELECT_OVERLOAD(do_GetService),
MOZ_STORAGE_SERVICE_CONTRACTID));
QM_TRY_UNWRAP(
auto connection,
QM_OR_ELSE_WARN_IF(
// Expression.
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
archiveFile, mozIStorageService::CONNECTION_DEFAULT),
// Predicate.
IsDatabaseCorruptionError,
// Fallback. Don't throw an error, leave a corrupted ls-archive
// database as it is.
ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>));
if (connection) {
const nsresult rv = StorageDBUpdater::Update(connection);
if (NS_FAILED(rv)) {
// Don't throw an error, leave a non-updateable ls-archive database as
// it is.
return nsCOMPtr<mozIStorageConnection>{};
}
}
return connection;
}
Result<nsCOMPtr<nsIFile>, nsresult> GetShadowFile(
const nsAString& aBasePath) {
MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
MOZ_ASSERT(!aBasePath.IsEmpty());
QM_TRY_UNWRAP(
auto archiveFile, QM_NewLocalFile(aBasePath));
QM_TRY(MOZ_TO_RESULT(
archiveFile->Append(nsLiteralString(WEB_APPS_STORE_FILE_NAME))));
return archiveFile;
}
nsresult SetShadowJournalMode(mozIStorageConnection* aConnection) {
MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
MOZ_ASSERT(aConnection);
// Try enabling WAL mode. This can fail in various circumstances so we have to
// check the results here.
constexpr
auto journalModeQueryStart =
"PRAGMA journal_mode = "_ns;
constexpr
auto journalModeWAL =
"wal"_ns;
QM_TRY_INSPECT(
const auto& stmt,
CreateAndExecuteSingleStepStatement(
*aConnection, journalModeQueryStart + journalModeWAL));
QM_TRY_INSPECT(
const auto& journalMode,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, *stmt,
GetUTF8String, 0));
if (journalMode.Equals(journalModeWAL)) {
// WAL mode successfully enabled. Set limits on its size here.
// Set the threshold for auto-checkpointing the WAL. We don't want giant
// logs slowing down us.
QM_TRY_INSPECT(
const auto& stmt, CreateAndExecuteSingleStepStatement(
*aConnection,
"PRAGMA page_size;"_ns));
QM_TRY_INSPECT(
const int32_t& pageSize,
MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0));
MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536);
// Note there is a default journal_size_limit set by mozStorage.
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"PRAGMA wal_autocheckpoint = "_ns +
IntToCString(
static_cast<int32_t>(kShadowMaxWALSize / pageSize)))));
}
else {
QM_TRY(MOZ_TO_RESULT(
aConnection->ExecuteSimpleSQL(journalModeQueryStart +
"truncate"_ns)));
}
return NS_OK;
}
Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateShadowStorageConnection(
const nsAString& aBasePath) {
MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
MOZ_ASSERT(!aBasePath.IsEmpty());
QM_TRY_INSPECT(
const auto& shadowFile, GetShadowFile(aBasePath));
QM_TRY_INSPECT(
const auto& ss,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
MOZ_SELECT_OVERLOAD(do_GetService),
MOZ_STORAGE_SERVICE_CONTRACTID));
QM_TRY_UNWRAP(
auto connection,
QM_OR_ELSE_WARN_IF(
// Expression.
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
shadowFile, mozIStorageService::CONNECTION_DEFAULT),
// Predicate.
IsDatabaseCorruptionError,
// Fallback.
([&shadowFile, &ss](
const nsresult rv)
-> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(
false)));
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
shadowFile, mozIStorageService::CONNECTION_DEFAULT));
})));
QM_TRY(MOZ_TO_RESULT(SetShadowJournalMode(connection)));
// XXX Depending on whether the *first* call to OpenUnsharedDatabase above
// failed, we (a) might or (b) might not be dealing with a fresh database
// here. This is confusing, since in a failure of case (a) we would do the
// same thing again. Probably, the control flow should be changed here so that
// it's clear we only delete & create a fresh database once. If we still have
// a failure then, we better give up. Or, if we really want to handle that,
// the number of 2 retries seems arbitrary, and we should better do this in
// some loop until a maximum number of retries is reached.
//
// Compare this with QuotaManager::CreateLocalStorageArchiveConnection, which
// actually tracks if the file was removed before, but it's also more
// complicated than it should be. Maybe these two methods can be merged (which
// would mean that a parameter must be added that indicates whether it's
// handling the shadow file or not).
QM_TRY(QM_OR_ELSE_WARN(
// Expression.
MOZ_TO_RESULT(StorageDBUpdater::Update(connection)),
// Fallback.
([&connection, &shadowFile, &ss](
const nsresult) -> Result<Ok, nsresult> {
QM_TRY(MOZ_TO_RESULT(connection->Close()));
QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(
false)));
QM_TRY_UNWRAP(connection, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageConnection>, ss,
OpenUnsharedDatabase, shadowFile,
mozIStorageService::CONNECTION_DEFAULT));
QM_TRY(MOZ_TO_RESULT(SetShadowJournalMode(connection)));
QM_TRY(
MOZ_TO_RESULT(StorageDBUpdater::CreateCurrentSchema(connection)));
return Ok{};
})));
return connection;
}
Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetShadowStorageConnection(
const nsAString& aBasePath) {
AssertIsOnIOThread();
MOZ_ASSERT(!aBasePath.IsEmpty());
QM_TRY_INSPECT(
const auto& shadowFile, GetShadowFile(aBasePath));
QM_TRY_INSPECT(
const bool& exists,
MOZ_TO_RESULT_INVOKE_MEMBER(shadowFile, Exists));
QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE));
QM_TRY_INSPECT(
const auto& ss,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
MOZ_SELECT_OVERLOAD(do_GetService),
MOZ_STORAGE_SERVICE_CONTRACTID));
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, shadowFile,
mozIStorageService::CONNECTION_DEFAULT));
}
nsresult AttachShadowDatabase(
const nsAString& aBasePath,
mozIStorageConnection* aConnection) {
AssertIsOnGlobalConnectionThread();
MOZ_ASSERT(!aBasePath.IsEmpty());
MOZ_ASSERT(aConnection);
QM_TRY_INSPECT(
const auto& shadowFile, GetShadowFile(aBasePath));
#ifdef DEBUG
{
QM_TRY_INSPECT(
const bool& exists,
MOZ_TO_RESULT_INVOKE_MEMBER(shadowFile, Exists));
MOZ_ASSERT(exists);
}
#endif
QM_TRY_INSPECT(
const auto& path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsString, shadowFile, GetPath));
QM_TRY_INSPECT(
const auto& stmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConnection,
CreateStatement,
"ATTACH DATABASE :path AS shadow;"_ns));
QM_TRY(MOZ_TO_RESULT(stmt->BindStringByName(
"path"_ns, path)));
QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
return NS_OK;
}
nsresult DetachShadowDatabase(mozIStorageConnection* aConnection) {
AssertIsOnGlobalConnectionThread();
MOZ_ASSERT(aConnection);
QM_TRY(MOZ_TO_RESULT(
aConnection->ExecuteSimpleSQL(
"DETACH DATABASE shadow"_ns)));
return NS_OK;
}
Result<nsCOMPtr<nsIFile>, nsresult> GetUsageFile(
const nsAString& aDirectoryPath) {
MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
MOZ_ASSERT(!aDirectoryPath.IsEmpty());
QM_TRY_UNWRAP(
auto usageFile, QM_NewLocalFile(aDirectoryPath));
QM_TRY(MOZ_TO_RESULT(usageFile->Append(kUsageFileName)));
return usageFile;
}
Result<nsCOMPtr<nsIFile>, nsresult> GetUsageJournalFile(
const nsAString& aDirectoryPath) {
MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
MOZ_ASSERT(!aDirectoryPath.IsEmpty());
QM_TRY_UNWRAP(
auto usageJournalFile, QM_NewLocalFile(aDirectoryPath));
QM_TRY(MOZ_TO_RESULT(usageJournalFile->Append(kUsageJournalFileName)));
return usageJournalFile;
}
// Checks if aFile exists and is a file. Returns true if it exists and is a
// file, false if it doesn't exist, and an error if it exists but isn't a file.
Result<
bool, nsresult> ExistsAsFile(nsIFile& aFile) {
enum class ExistsAsFileResult { DoesNotExist, IsDirectory, IsFile };
// This is an optimization to check both properties in one OS case, rather
// than calling Exists first, and then IsDirectory. IsDirectory also checks
// if the path exists. QM_OR_ELSE_WARN_IF is not used here since we just want
// to log NS_ERROR_FILE_NOT_FOUND result and not spam the reports.
QM_TRY_INSPECT(
const auto& res,
QM_OR_ELSE_LOG_VERBOSE_IF(
// Expression.
MOZ_TO_RESULT_INVOKE_MEMBER(aFile, IsDirectory)
.map([](
const bool isDirectory) {
return isDirectory ? ExistsAsFileResult::IsDirectory
: ExistsAsFileResult::IsFile;
}),
// Predicate.
([](
const nsresult rv) {
return rv == NS_ERROR_FILE_NOT_FOUND; }),
// Fallback.
ErrToOk<ExistsAsFileResult::DoesNotExist>));
QM_TRY(OkIf(res != ExistsAsFileResult::IsDirectory), Err(NS_ERROR_FAILURE));
return res == ExistsAsFileResult::IsFile;
}
nsresult UpdateUsageFile(nsIFile* aUsageFile, nsIFile* aUsageJournalFile,
int64_t aUsage) {
MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
MOZ_ASSERT(aUsageFile);
MOZ_ASSERT(aUsageJournalFile);
MOZ_ASSERT(aUsage >= 0);
QM_TRY_INSPECT(
const bool& usageJournalFileExists,
ExistsAsFile(*aUsageJournalFile));
if (!usageJournalFileExists) {
QM_TRY(MOZ_TO_RESULT(
aUsageJournalFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644)));
}
QM_TRY_INSPECT(
const auto& stream, NS_NewLocalFileOutputStream(aUsageFile));
nsCOMPtr<nsIBinaryOutputStream> binaryStream =
NS_NewObjectOutputStream(stream);
QM_TRY(MOZ_TO_RESULT(binaryStream->Write32(kUsageFileCookie)));
QM_TRY(MOZ_TO_RESULT(binaryStream->Write64(aUsage)));
#if defined(EARLY_BETA_OR_EARLIER) ||
defined(DEBUG)
QM_TRY(MOZ_TO_RESULT(stream->Flush()));
#endif
QM_TRY(MOZ_TO_RESULT(stream->Close()));
return NS_OK;
}
Result<UsageInfo, nsresult> LoadUsageFile(nsIFile& aUsageFile) {
AssertIsOnIOThread();
QM_TRY_INSPECT(
const int64_t& fileSize,
MOZ_TO_RESULT_INVOKE_MEMBER(aUsageFile, GetFileSize));
QM_TRY(OkIf(fileSize == kUsageFileSize), Err(NS_ERROR_FILE_CORRUPTED));
QM_TRY_UNWRAP(
auto stream, NS_NewLocalFileInputStream(&aUsageFile));
QM_TRY_INSPECT(
const auto& bufferedStream,
NS_NewBufferedInputStream(stream.forget(), 16));
const nsCOMPtr<nsIBinaryInputStream> binaryStream =
NS_NewObjectInputStream(bufferedStream);
QM_TRY_INSPECT(
const uint32_t& cookie,
MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
QM_TRY(OkIf(cookie == kUsageFileCookie), Err(NS_ERROR_FILE_CORRUPTED));
QM_TRY_INSPECT(
const uint64_t& usage,
MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64));
return UsageInfo{DatabaseUsageType(Some(usage))};
}
/*******************************************************************************
* Non-actor class declarations
******************************************************************************/
/**
* Coalescing manipulation queue used by `Datastore`. Used by `Datastore` to
* update `Datastore::mOrderedItems` efficiently/for code simplification.
* (Datastore does not actually depend on the coalescing, as mutations are
* applied atomically when a Snapshot Checkpoints, and with `Datastore::mValues`
* being updated at the same time the mutations are applied to Datastore's
* mWriteOptimizer.)
*/
class DatastoreWriteOptimizer final :
public LSWriteOptimizer<LSValue> {
public:
void ApplyAndReset(nsTArray<LSItemInfo>& aOrderedItems);
};
/**
* Coalescing manipulation queue used by `Connection`. Used by `Connection` to
* buffer and coalesce manipulations applied to the Datastore in batches by
* Snapshot Checkpointing until flushed to disk.
*/
class ConnectionWriteOptimizer final :
public LSWriteOptimizer<LSValue> {
public:
// Returns the usage as the success value.
Result<int64_t, nsresult> Perform(Connection* aConnection,
bool aShadowWrites);
private:
/**
* Handlers for specific mutations. Each method knows how to `Perform` the
* manipulation against a `Connection` and the "shadow" database (legacy
* webappsstore.sqlite database that exists so LSNG can be disabled/safely
* downgraded from.)
*/
nsresult PerformInsertOrUpdate(Connection* aConnection,
bool aShadowWrites,
const nsAString& aKey,
const LSValue& aValue);
nsresult PerformDelete(Connection* aConnection,
bool aShadowWrites,
const nsAString& aKey);
nsresult PerformTruncate(Connection* aConnection,
bool aShadowWrites);
};
class DatastoreOperationBase :
public Runnable {
nsCOMPtr<nsIEventTarget> mOwningEventTarget;
nsresult mResultCode;
Atomic<
bool> mMayProceedOnNonOwningThread;
bool mMayProceed;
public:
nsIEventTarget* OwningEventTarget()
const {
MOZ_ASSERT(mOwningEventTarget);
return mOwningEventTarget;
}
bool IsOnOwningThread()
const {
MOZ_ASSERT(mOwningEventTarget);
bool current;
return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) &&
current;
}
void AssertIsOnOwningThread()
const {
MOZ_ASSERT(IsOnBackgroundThread());
MOZ_ASSERT(IsOnOwningThread());
}
nsresult ResultCode()
const {
return mResultCode; }
void SetFailureCode(nsresult aErrorCode) {
MOZ_ASSERT(NS_SUCCEEDED(mResultCode));
MOZ_ASSERT(NS_FAILED(aErrorCode));
mResultCode = aErrorCode;
}
void MaybeSetFailureCode(nsresult aErrorCode) {
MOZ_ASSERT(NS_FAILED(aErrorCode));
if (NS_SUCCEEDED(mResultCode)) {
mResultCode = aErrorCode;
}
}
void NoteComplete() {
AssertIsOnOwningThread();
mMayProceed =
false;
mMayProceedOnNonOwningThread =
false;
}
bool MayProceed()
const {
AssertIsOnOwningThread();
return mMayProceed;
}
// May be called on any thread, but you should call MayProceed() if you know
// you're on the background thread because it is slightly faster.
bool MayProceedOnNonOwningThread()
const {
return mMayProceedOnNonOwningThread;
}
protected:
DatastoreOperationBase()
: Runnable(
"dom::DatastoreOperationBase"),
mOwningEventTarget(GetCurrentSerialEventTarget()),
mResultCode(NS_OK),
mMayProceedOnNonOwningThread(
true),
mMayProceed(
true) {}
~DatastoreOperationBase() override { MOZ_ASSERT(!mMayProceed); }
};
class ConnectionDatastoreOperationBase :
public DatastoreOperationBase {
protected:
RefPtr<Connection> mConnection;
/**
* This boolean flag is used by the CloseOp to avoid creating empty databases.
*/
const bool mEnsureStorageConnection;
public:
// This callback will be called on the background thread before releasing the
// final reference to this request object. Subclasses may perform any
// additional cleanup here but must always call the base class implementation.
virtual void Cleanup();
protected:
ConnectionDatastoreOperationBase(Connection* aConnection,
bool aEnsureStorageConnection =
true);
~ConnectionDatastoreOperationBase();
// Must be overridden in subclasses. Called on the target thread to allow the
// subclass to perform necessary datastore operations. A successful return
// value will trigger an OnSuccess callback on the background thread while
// while a failure value will trigger an OnFailure callback.
virtual nsresult DoDatastoreWork() = 0;
// Methods that subclasses may implement.
virtual void OnSuccess();
virtual void OnFailure(nsresult aResultCode);
private:
void RunOnConnectionThread();
void RunOnOwningThread();
// Not to be overridden by subclasses.
NS_DECL_NSIRUNNABLE
};
class Connection final :
public CachingDatabaseConnection {
friend class ConnectionThread;
class GetOrCreateTemporaryOriginDirectoryHelper;
class FlushOp;
class CloseOp;
RefPtr<ConnectionThread> mConnectionThread;
RefPtr<QuotaClient> mQuotaClient;
nsCOMPtr<nsITimer> mFlushTimer;
UniquePtr<ArchivedOriginScope> mArchivedOriginScope;
ConnectionWriteOptimizer mWriteOptimizer;
// XXX Consider changing this to ClientMetadata.
const OriginMetadata mOriginMetadata;
nsString mDirectoryPath;
/**
* Propagated from PrepareDatastoreOp. PrepareDatastoreOp may defer the
* creation of the localstorage client directory and database on the
* QuotaManager IO thread in its DatabaseWork method to
* Connection::EnsureStorageConnection, in which case the method needs to know
* it is responsible for taking those actions (without redundantly performing
* the existence checks).
*/
const bool mDatabaseWasNotAvailable;
bool mHasCreatedDatabase;
bool mFlushScheduled;
#ifdef DEBUG
bool mInUpdateBatch;
bool mFinished;
#endif
public:
NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Connection)
void AssertIsOnOwningThread()
const { NS_ASSERT_OWNINGTHREAD(Connection); }
QuotaClient* GetQuotaClient()
const {
MOZ_ASSERT(mQuotaClient);
return mQuotaClient;
}
ArchivedOriginScope* GetArchivedOriginScope()
const {
return mArchivedOriginScope.get();
}
const nsCString& Origin()
const {
return mOriginMetadata.mOrigin; }
const nsString& DirectoryPath()
const {
return mDirectoryPath; }
void GetFinishInfo(
bool& aDatabaseWasNotAvailable,
bool& aHasCreatedDatabase)
const {
AssertIsOnOwningThread();
MOZ_ASSERT(mFinished);
aDatabaseWasNotAvailable = mDatabaseWasNotAvailable;
aHasCreatedDatabase = mHasCreatedDatabase;
}
//////////////////////////////////////////////////////////////////////////////
// Methods which can only be called on the owning thread.
// This method is used to asynchronously execute a connection datastore
// operation on the connection thread.
void Dispatch(ConnectionDatastoreOperationBase* aOp);
// This method is used to asynchronously close the storage connection on the
// connection thread.
void Close(nsIRunnable* aCallback);
void SetItem(
const nsString& aKey,
const LSValue& aValue, int64_t aDelta,
bool aIsNewItem);
void RemoveItem(
const nsString& aKey, int64_t aDelta);
void Clear(int64_t aDelta);
void BeginUpdateBatch();
void EndUpdateBatch();
//////////////////////////////////////////////////////////////////////////////
// Methods which can only be called on the connection thread.
nsresult EnsureStorageConnection();
mozIStorageConnection* StorageConnection()
const {
AssertIsOnGlobalConnectionThread();
return &MutableStorageConnection();
}
void CloseStorageConnection();
nsresult BeginWriteTransaction();
nsresult CommitWriteTransaction();
nsresult RollbackWriteTransaction();
private:
// Only created by ConnectionThread.
Connection(ConnectionThread* aConnectionThread,
const OriginMetadata& aOriginMetadata,
UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
bool aDatabaseWasNotAvailable);
~Connection();
void ScheduleFlush();
void Flush();
static void FlushTimerCallback(nsITimer* aTimer,
void* aClosure);
};
/**
* Helper to invoke GetOrCreateTemporaryOriginDirectory on the QuotaManager IO
* thread from the LocalStorage connection thread when creating a database
* connection on demand. This is necessary because we attempt to defer the
* creation of the origin directory and the database until absolutely needed,
* but the directory creation must happen on the QM IO thread for invariant
* reasons. (We can't just use a mutex because there could be logic on the IO
* thread that also wants to deal with the same origin, so we need to queue a
* runnable and wait our turn.)
*/
class Connection::GetOrCreateTemporaryOriginDirectoryHelper final
:
public Runnable {
mozilla::Monitor mMonitor MOZ_UNANNOTATED;
const OriginMetadata mOriginMetadata;
nsString mOriginDirectoryPath;
nsresult mIOThreadResultCode;
bool mWaiting;
public:
explicit GetOrCreateTemporaryOriginDirectoryHelper(
const OriginMetadata& aOriginMetadata)
: Runnable(
"dom::localstorage::Connection::"
"GetOrCreateTemporaryOriginDirectoryHelper"),
mMonitor(
"GetOrCreateTemporaryOriginDirectoryHelper::mMonitor"),
mOriginMetadata(aOriginMetadata),
mIOThreadResultCode(NS_OK),
mWaiting(
true) {
AssertIsOnGlobalConnectionThread();
}
Result<nsString, nsresult> BlockAndReturnOriginDirectoryPath();
private:
~GetOrCreateTemporaryOriginDirectoryHelper() =
default;
nsresult RunOnIOThread();
NS_DECL_NSIRUNNABLE
};
class Connection::FlushOp final :
public ConnectionDatastoreOperationBase {
ConnectionWriteOptimizer mWriteOptimizer;
bool mShadowWrites;
public:
FlushOp(Connection* aConnection, ConnectionWriteOptimizer&& aWriteOptimizer);
private:
nsresult DoDatastoreWork() override;
void Cleanup() override;
};
class Connection::CloseOp final :
public ConnectionDatastoreOperationBase {
nsCOMPtr<nsIRunnable> mCallback;
public:
CloseOp(Connection* aConnection, nsIRunnable* aCallback)
: ConnectionDatastoreOperationBase(aConnection,
/* aEnsureStorageConnection */ false),
mCallback(aCallback) {}
private:
nsresult DoDatastoreWork() override;
void Cleanup() override;
};
class ConnectionThread final {
friend class Connection;
nsCOMPtr<nsIThread> mThread;
nsRefPtrHashtable<nsCStringHashKey, Connection> mConnections;
public:
ConnectionThread();
void AssertIsOnOwningThread()
const {
NS_ASSERT_OWNINGTHREAD(ConnectionThread);
}
bool IsOnConnectionThread();
void AssertIsOnConnectionThread();
already_AddRefed<Connection> CreateConnection(
const OriginMetadata& aOriginMetadata,
UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
bool aDatabaseWasNotAvailable);
void Shutdown();
NS_INLINE_DECL_REFCOUNTING(ConnectionThread)
private:
~ConnectionThread();
};
/**
* Canonical state of Storage for an origin, containing all keys and their
* values in the parent process. Specifically, this is the state that will
* be handed out to freshly created Snapshots and that will be persisted to disk
* when the Connection's flush completes. State is mutated in batches as
* Snapshot instances Checkpoint their mutations locally accumulated in the
* child LSSnapshots.
*/
class Datastore final
:
public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
RefPtr<ClientDirectoryLock> mDirectoryLock;
RefPtr<Connection> mConnection;
RefPtr<QuotaObject> mQuotaObject;
nsCOMPtr<nsIRunnable> mCompleteCallback;
/**
* PrepareDatastoreOps register themselves with the Datastore at
* and unregister in PrepareDatastoreOp::Cleanup.
*/
nsTHashSet<PrepareDatastoreOp*> mPrepareDatastoreOps;
/**
* PreparedDatastore instances register themselves with their associated
* Datastore at construction time and unregister at destruction time. They
* hang around for kPreparedDatastoreTimeoutMs in order to keep the Datastore
* from closing itself via MaybeClose(), thereby giving the document enough
* time to load and access LocalStorage.
*/
nsTHashSet<PreparedDatastore*> mPreparedDatastores;
/**
* A database is live (and in this hashtable) if it has a live LSDatabase
* actor. There is at most one Database per origin per content process. Each
* Database corresponds to an LSDatabase in its associated content process.
*/
nsTHashSet<Database*> mDatabases;
/**
* A database is active if it has a non-null `mSnapshot`. As long as there
* are any active databases final deltas can't be calculated and
* `UpdateUsage()` can't be invoked.
*/
nsTHashSet<Database*> mActiveDatabases;
/**
* Non-authoritative hashtable representation of mOrderedItems for efficient
* lookup.
*/
nsTHashMap<nsStringHashKey, LSValue> mValues;
/**
* The authoritative ordered state of the Datastore; mValue also exists as an
* unordered hashtable for efficient lookup.
*/
nsTArray<LSItemInfo> mOrderedItems;
nsTArray<int64_t> mPendingUsageDeltas;
DatastoreWriteOptimizer mWriteOptimizer;
const OriginMetadata mOriginMetadata;
const uint32_t mPrivateBrowsingId;
int64_t mUsage;
int64_t mUpdateBatchUsage;
int64_t mSizeOfKeys;
int64_t mSizeOfItems;
bool mClosed;
bool mInUpdateBatch;
bool mHasLivePrivateDatastore;
public:
// Created by PrepareDatastoreOp.
Datastore(
const OriginMetadata& aOriginMetadata, uint32_t aPrivateBrowsingId,
int64_t aUsage, int64_t aSizeOfKeys, int64_t aSizeOfItems,
RefPtr<ClientDirectoryLock>&& aDirectoryLock,
RefPtr<Connection>&& aConnection,
RefPtr<QuotaObject>&& aQuotaObject,
nsTHashMap<nsStringHashKey, LSValue>& aValues,
nsTArray<LSItemInfo>&& aOrderedItems);
Maybe<ClientDirectoryLock&> MaybeDirectoryLockRef()
const {
AssertIsOnBackgroundThread();
return ToMaybeRef(mDirectoryLock.get());
}
const nsCString& Origin()
const {
return mOriginMetadata.mOrigin; }
uint32_t PrivateBrowsingId()
const {
return mPrivateBrowsingId; }
bool IsPersistent()
const {
// Private-browsing is forbidden from touching disk.
return mPrivateBrowsingId == 0;
}
void Close();
bool IsClosed()
const {
AssertIsOnBackgroundThread();
return mClosed;
}
void WaitForConnectionToComplete(nsIRunnable* aCallback);
void NoteLivePrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp);
void NoteFinishedPrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp);
void NoteLivePrivateDatastore();
void NoteFinishedPrivateDatastore();
void NoteLivePreparedDatastore(PreparedDatastore* aPreparedDatastore);
void NoteFinishedPreparedDatastore(PreparedDatastore* aPreparedDatastore);
bool HasOtherProcessDatabases(Database* aDatabase);
void NoteLiveDatabase(Database* aDatabase);
void NoteFinishedDatabase(Database* aDatabase);
void NoteActiveDatabase(Database* aDatabase);
void NoteInactiveDatabase(Database* aDatabase);
void GetSnapshotLoadInfo(
const nsAString& aKey,
bool& aAddKeyToUnknownItems,
nsTHashtable<nsStringHashKey>& aLoadedItems,
nsTArray<LSItemInfo>& aItemInfos,
uint32_t& aNextLoadIndex,
LSSnapshot::LoadState& aLoadState);
uint32_t GetLength()
const {
return mValues.Count(); }
const nsTArray<LSItemInfo>& GetOrderedItems()
const {
return mOrderedItems; }
void GetItem(
const nsAString& aKey, LSValue& aValue)
const;
void GetKeys(nsTArray<nsString>& aKeys)
const;
//////////////////////////////////////////////////////////////////////////////
// Mutation Methods
//
// These are only called during Snapshot::Checkpoint
/**
* Used by Snapshot::Checkpoint to set a key/value pair as part of an
* explicit batch.
*/
void SetItem(Database* aDatabase,
const nsString& aKey,
const LSValue& aValue);
void RemoveItem(Database* aDatabase,
const nsString& aKey);
void Clear(Database* aDatabase);
void BeginUpdateBatch(int64_t aSnapshotUsage);
int64_t EndUpdateBatch(int64_t aSnapshotPeakUsage);
int64_t GetUsage()
const {
return mUsage; }
int64_t AttemptToUpdateUsage(int64_t aMinSize,
bool aInitial);
bool HasOtherProcessObservers(Database* aDatabase);
void NotifyOtherProcessObservers(Database* aDatabase,
const nsString& aDocumentURI,
const nsString& aKey,
const LSValue& aOldValue,
const LSValue& aNewValue);
void NoteChangedObserverArray(
const nsTArray<NotNull<Observer*>>& aObservers);
void Stringify(nsACString& aResult)
const;
NS_INLINE_DECL_REFCOUNTING(Datastore)
private:
// Reference counted.
~Datastore();
bool UpdateUsage(int64_t aDelta);
void MaybeClose();
void ConnectionClosedCallback();
void CleanupMetadata();
void NotifySnapshots(Database* aDatabase,
const nsAString& aKey,
const LSValue& aOldValue,
bool aAffectsOrder);
void NoteChangedDatabaseMap();
};
class PrivateDatastore {
const NotNull<RefPtr<Datastore>> mDatastore;
public:
explicit PrivateDatastore(MovingNotNull<RefPtr<Datastore>> aDatastore)
: mDatastore(std::move(aDatastore)) {
AssertIsOnBackgroundThread();
mDatastore->NoteLivePrivateDatastore();
}
~PrivateDatastore() { mDatastore->NoteFinishedPrivateDatastore(); }
const Datastore& DatastoreRef()
const {
AssertIsOnBackgroundThread();
return *mDatastore;
}
Datastore& MutableDatastoreRef()
const {
AssertIsOnBackgroundThread();
return *mDatastore;
}
};
class PreparedDatastore {
RefPtr<Datastore> mDatastore;
nsCOMPtr<nsITimer> mTimer;
const Maybe<ContentParentId> mContentParentId;
// Strings share buffers if possible, so it's not a problem to duplicate the
// origin here.
const nsCString mOrigin;
uint64_t mDatastoreId;
bool mForPreload;
bool mInvalidated;
public:
PreparedDatastore(Datastore* aDatastore,
const Maybe<ContentParentId>& aContentParentId,
const nsACString& aOrigin, uint64_t aDatastoreId,
bool aForPreload)
: mDatastore(aDatastore),
mTimer(NS_NewTimer()),
mContentParentId(aContentParentId),
mOrigin(aOrigin),
mDatastoreId(aDatastoreId),
mForPreload(aForPreload),
mInvalidated(
false) {
AssertIsOnBackgroundThread();
MOZ_ASSERT(aDatastore);
MOZ_ASSERT(mTimer);
aDatastore->NoteLivePreparedDatastore(
this);
MOZ_ALWAYS_SUCCEEDS(mTimer->InitWithNamedFuncCallback(
TimerCallback,
this, kPreparedDatastoreTimeoutMs,
nsITimer::TYPE_ONE_SHOT,
"PreparedDatastore::TimerCallback"));
}
~PreparedDatastore() {
MOZ_ASSERT(mDatastore);
MOZ_ASSERT(mTimer);
mTimer->Cancel();
mDatastore->NoteFinishedPreparedDatastore(
this);
}
const Datastore& DatastoreRef()
const {
AssertIsOnBackgroundThread();
MOZ_ASSERT(mDatastore);
return *mDatastore;
}
Datastore& MutableDatastoreRef()
const {
AssertIsOnBackgroundThread();
MOZ_ASSERT(mDatastore);
return *mDatastore;
}
const Maybe<ContentParentId>& GetContentParentId()
const {
return mContentParentId;
}
const nsCString& Origin()
const {
return mOrigin; }
void Invalidate() {
AssertIsOnBackgroundThread();
mInvalidated =
true;
if (mForPreload) {
mTimer->Cancel();
MOZ_ALWAYS_SUCCEEDS(mTimer->InitWithNamedFuncCallback(
TimerCallback,
this, 0, nsITimer::TYPE_ONE_SHOT,
"PreparedDatastore::TimerCallback"));
}
}
bool IsInvalidated()
const {
AssertIsOnBackgroundThread();
return mInvalidated;
}
private:
void Destroy();
static void TimerCallback(nsITimer* aTimer,
void* aClosure);
};
/*******************************************************************************
* Actor class declarations
******************************************************************************/
class Database final
:
public PBackgroundLSDatabaseParent,
public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
RefPtr<Datastore> mDatastore;
Snapshot* mSnapshot;
const PrincipalInfo mPrincipalInfo;
const Maybe<ContentParentId> mContentParentId;
// Strings share buffers if possible, so it's not a problem to duplicate the
// origin here.
nsCString mOrigin;
mozilla::glean::TimerId mRequestAllowToCloseTimerId;
uint32_t mPrivateBrowsingId;
bool mAllowedToClose;
bool mActorDestroyed;
bool mRequestedAllowToClose;
#ifdef DEBUG
bool mActorWasAlive;
#endif
public:
// Created in AllocPBackgroundLSDatabaseParent.
Database(
const PrincipalInfo& aPrincipalInfo,
const Maybe<ContentParentId>& aContentParentId,
const nsACString& aOrigin, uint32_t aPrivateBrowsingId);
void AssertIsOnOwningThread()
const {
AssertIsOnBackgroundThread();
NS_ASSERT_OWNINGTHREAD(mozilla::dom::Database);
}
Datastore* GetDatastore()
const {
AssertIsOnOwningThread();
return mDatastore;
}
Maybe<Datastore&> MaybeDatastoreRef()
const {
AssertIsOnOwningThread();
return ToMaybeRef(mDatastore.get());
}
const PrincipalInfo& GetPrincipalInfo()
const {
return mPrincipalInfo; }
const Maybe<ContentParentId>& ContentParentIdRef()
const {
return mContentParentId;
}
uint32_t PrivateBrowsingId()
const {
return mPrivateBrowsingId; }
const nsCString& Origin()
const {
return mOrigin; }
void SetActorAlive(Datastore* aDatastore);
void RegisterSnapshot(Snapshot* aSnapshot);
void UnregisterSnapshot(Snapshot* aSnapshot);
Snapshot* GetSnapshot()
const {
AssertIsOnOwningThread();
return mSnapshot;
}
void RequestAllowToClose();
void ForceKill();
void Stringify(nsACString& aResult)
const;
NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Database, override)
private:
// Reference counted.
~Database();
void AllowToClose();
// IPDL methods are only called by IPDL.
void ActorDestroy(ActorDestroyReason aWhy) override;
mozilla::ipc::IPCResult RecvAllowToClose() override;
PBackgroundLSSnapshotParent* AllocPBackgroundLSSnapshotParent(
const nsAString& aDocumentURI,
const nsAString& aKey,
const bool& aIncreasePeakUsage,
const int64_t& aMinSize,
LSSnapshotInitInfo* aInitInfo) override;
mozilla::ipc::IPCResult RecvPBackgroundLSSnapshotConstructor(
PBackgroundLSSnapshotParent* aActor,
const nsAString& aDocumentURI,
const nsAString& aKey,
const bool& aIncreasePeakUsage,
const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) override;
bool DeallocPBackgroundLSSnapshotParent(
PBackgroundLSSnapshotParent* aActor) override;
};
/**
* Attempts to capture the state of the underlying Datastore at the time of its
* creation so run-to-completion semantics can be honored.
*
* Rather than simply duplicate the contents of `DataStore::mValues` and
* `Datastore::mOrderedItems` at the time of their creation, the Snapshot tracks
* mutations to the Datastore as they happen, saving off the state of values as
* they existed when the Snapshot was created. In other words, given an initial
* Datastore state of { foo: 'bar', bar: 'baz' }, the Snapshot won't store those
* values until it hears via `SaveItem` that "foo" is being over-written. At
* that time, it will save off foo='bar' in mValues.
*
* ## Quota Allocation ##
*
* ## States ##
*
*/
class Snapshot final :
public PBackgroundLSSnapshotParent {
/**
* The Database that owns this snapshot. There is a 1:1 relationship between
* snapshots and databases.
*/
RefPtr<Database> mDatabase;
RefPtr<Datastore> mDatastore;
/**
* The set of keys for which values have been sent to the child LSSnapshot.
* Cleared once all values have been sent as indicated by
* mLoadedItems.Count()==mTotalLength and therefore mLoadedAllItems should be
* true. No requests should be received for keys already in this set, and
* this is enforced by fatal IPC error (unless fuzzing).
*/
nsTHashtable<nsStringHashKey> mLoadedItems;
/**
* The set of keys for which a RecvLoadValueAndMoreItems request was received
* but there was no such key, and so null was returned. The child LSSnapshot
* will also cache these values, so redundant requests are also handled with
* fatal process termination just like for mLoadedItems. Also cleared when
* mLoadedAllItems becomes true because then the child can infer that all
* other values must be null. (Note: this could also be done when
* mLoadKeysReceived is true as a further optimization, but is not.)
*/
nsTHashSet<nsString> mUnknownItems;
/**
* Values that have changed in mDatastore as reported by SaveItem
* notifications that are not yet known to the child LSSnapshot.
*
* The naive way to snapshot the state of mDatastore would be to duplicate its
* internal mValues at the time of our creation, but that is wasteful if few
* changes are made to the Datastore's state. So we only track values that
* are changed/evicted from the Datastore as they happen, as reported to us by
* SaveItem notifications.
*/
nsTHashMap<nsStringHashKey, LSValue> mValues;
/**
* Latched state of mDatastore's keys during a SaveItem notification with
--> --------------------
--> maximum size reached
--> --------------------