diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 28e8a8e69..80e59fe55 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -106,6 +106,8 @@ list( APPEND SOURCES CrashReport.cpp CrashReport.h DarkThemeAsCeeCode.h + DBConnection.cpp + DBConnection.h DeviceChange.cpp DeviceChange.h DeviceManager.cpp diff --git a/src/DBConnection.cpp b/src/DBConnection.cpp new file mode 100644 index 000000000..3f5a9f7a0 --- /dev/null +++ b/src/DBConnection.cpp @@ -0,0 +1,325 @@ +/********************************************************************** + +Audacity: A Digital Audio Editor + +DBConection.cpp + +Paul Licameli -- split from ProjectFileIO.cpp + +**********************************************************************/ + +#include "DBConnection.h" + +#include "sqlite3.h" + +#include +#include + +#include "Internat.h" +#include "Project.h" + +// Configuration to provide "safe" connections +static const char *SafeConfig = + "PRAGMA .locking_mode = SHARED;" + "PRAGMA .synchronous = NORMAL;" + "PRAGMA .journal_mode = WAL;" + "PRAGMA .wal_autocheckpoint = 0;"; + +// Configuration to provide "Fast" connections +static const char *FastConfig = + "PRAGMA .locking_mode = SHARED;" + "PRAGMA .synchronous = OFF;" + "PRAGMA .journal_mode = OFF;"; + +DBConnection::DBConnection(const std::weak_ptr &pProject) +: mpProject{ pProject } +{ + mDB = nullptr; + mBypass = false; +} + +DBConnection::~DBConnection() +{ + wxASSERT(mDB == nullptr); +} + +void DBConnection::SetBypass( bool bypass ) +{ + mBypass = bypass; +} + +bool DBConnection::ShouldBypass() +{ + return mBypass; +} + +bool DBConnection::Open(const char *fileName) +{ + wxASSERT(mDB == nullptr); + int rc; + + rc = sqlite3_open(fileName, &mDB); + if (rc != SQLITE_OK) + { + sqlite3_close(mDB); + mDB = nullptr; + + return false; + } + + // Set default mode + SafeMode(); + + // Kick off the checkpoint thread + mCheckpointStop = false; + mCheckpointWaitingPages = 0; + mCheckpointCurrentPages = 0; + mCheckpointThread = std::thread([this]{ CheckpointThread(); }); + + // Install our checkpoint hook + sqlite3_wal_hook(mDB, CheckpointHook, this); + + return mDB; +} + +bool DBConnection::Close() +{ + wxASSERT(mDB != nullptr); + int rc; + + // Protect... + if (mDB == nullptr) + { + return true; + } + + // Uninstall our checkpoint hook so that no additional checkpoints + // are sent our way. (Though this shouldn't really happen.) + sqlite3_wal_hook(mDB, nullptr, nullptr); + + // Display a progress dialog if there's active or pending checkpoints + if (mCheckpointWaitingPages || mCheckpointCurrentPages) + { + TranslatableString title = XO("Checkpointing project"); + + // Get access to the active project + auto project = mpProject.lock(); + if (project) + { + title = XO("Checkpointing %s").Format(project->GetProjectName()); + } + + // Provides a progress dialog with indeterminate mode + wxGenericProgressDialog pd(title.Translation(), + XO("This may take several seconds").Translation(), + 300000, // range + nullptr, // parent + wxPD_APP_MODAL | wxPD_ELAPSED_TIME | wxPD_SMOOTH); + + // Wait for the checkpoints to end + while (mCheckpointWaitingPages || mCheckpointCurrentPages) + { + wxMilliSleep(50); + pd.Pulse(); + } + } + + // Tell the checkpoint thread to shutdown + { + std::lock_guard guard(mCheckpointMutex); + mCheckpointStop = true; + mCheckpointCondition.notify_one(); + } + + // And wait for it to do so + mCheckpointThread.join(); + + // We're done with the prepared statements + for (auto stmt : mStatements) + { + sqlite3_finalize(stmt.second); + } + mStatements.clear(); + + // Close the DB + rc = sqlite3_close(mDB); + if (rc != SQLITE_OK) + { + // I guess we could try to recover by repreparing statements and reinstalling + // the hook, but who knows if that would work either. + // + // Should we throw an error??? + } + + mDB = nullptr; + + return true; +} + +bool DBConnection::SafeMode(const char *schema /* = "main" */) +{ + return ModeConfig(mDB, schema, SafeConfig); +} + +bool DBConnection::FastMode(const char *schema /* = "main" */) +{ + return ModeConfig(mDB, schema, FastConfig); +} + +bool DBConnection::ModeConfig(sqlite3 *db, const char *schema, const char *config) +{ + // Ensure attached DB connection gets configured + int rc; + + // Replace all schema "keywords" with the schema name + wxString sql = config; + sql.Replace(wxT(""), schema); + + // Set the configuration + rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); + + return rc != SQLITE_OK; +} + +sqlite3 *DBConnection::DB() +{ + wxASSERT(mDB != nullptr); + + return mDB; +} + +int DBConnection::GetLastRC() const +{ + return sqlite3_errcode(mDB); +} + +const wxString DBConnection::GetLastMessage() const +{ + return sqlite3_errmsg(mDB); +} + +sqlite3_stmt *DBConnection::Prepare(enum StatementID id, const char *sql) +{ + int rc; + + // Return an existing statement if it's already been prepared + auto iter = mStatements.find(id); + if (iter != mStatements.end()) + { + return iter->second; + } + + // Prepare the statement + sqlite3_stmt *stmt = nullptr; + rc = sqlite3_prepare_v3(mDB, sql, -1, SQLITE_PREPARE_PERSISTENT, &stmt, 0); + if (rc != SQLITE_OK) + { + wxLogDebug("prepare error %s", sqlite3_errmsg(mDB)); + THROW_INCONSISTENCY_EXCEPTION; + } + + // And remember it + mStatements.insert({id, stmt}); + + return stmt; +} + +sqlite3_stmt *DBConnection::GetStatement(enum StatementID id) +{ + // Look it up + auto iter = mStatements.find(id); + + // It should always be there + wxASSERT(iter != mStatements.end()); + + // Return it + return iter->second; +} + +void DBConnection::CheckpointThread() +{ + // Open another connection to the DB to prevent blocking the main thread. + // + // If it fails, then we won't checkpoint until the main thread closes + // the associated DB. + sqlite3 *db = nullptr; + if (sqlite3_open(sqlite3_db_filename(mDB, nullptr), &db) == SQLITE_OK) + { + // Configure it to be safe + ModeConfig(db, "main", SafeConfig); + + while (true) + { + { + // Wait for work or the stop signal + std::unique_lock lock(mCheckpointMutex); + mCheckpointCondition.wait(lock, + [&] + { + return mCheckpointWaitingPages || mCheckpointStop; + }); + + // Requested to stop, so bail + if (mCheckpointStop) + { + break; + } + + // Capture the number of pages that need checkpointing and reset + mCheckpointCurrentPages.store( mCheckpointWaitingPages ); + mCheckpointWaitingPages = 0; + } + + // And kick off the checkpoint. This may not checkpoint ALL frames + // in the WAL. They'll be gotten the next time around. + sqlite3_wal_checkpoint_v2(db, nullptr, SQLITE_CHECKPOINT_PASSIVE, nullptr, nullptr); + + // Reset + mCheckpointCurrentPages = 0; + } + } + + // All done (always close) + sqlite3_close(db); + + return; +} + +int DBConnection::CheckpointHook(void *data, sqlite3 *db, const char *schema, int pages) +{ + // Get access to our object + DBConnection *that = static_cast(data); + + // Queue the database pointer for our checkpoint thread to process + std::lock_guard guard(that->mCheckpointMutex); + that->mCheckpointWaitingPages = pages; + that->mCheckpointCondition.notify_one(); + + return SQLITE_OK; +} + +ConnectionPtr::~ConnectionPtr() = default; + +static const AudacityProject::AttachedObjects::RegisteredFactory +sConnectionPtrKey{ + []( AudacityProject & ){ + // Ignore the argument; this is just a holder of a + // unique_ptr to DBConnection, which must be filled in later + // (when we can get a weak_ptr to the project) + auto result = std::make_shared< ConnectionPtr >(); + return result; + } +}; + +ConnectionPtr &ConnectionPtr::Get( AudacityProject &project ) +{ + auto &result = + project.AttachedObjects::Get< ConnectionPtr >( sConnectionPtrKey ); + return result; +} + +const ConnectionPtr &ConnectionPtr::Get( const AudacityProject &project ) +{ + return Get( const_cast< AudacityProject & >( project ) ); +} + diff --git a/src/DBConnection.h b/src/DBConnection.h new file mode 100644 index 000000000..858175a32 --- /dev/null +++ b/src/DBConnection.h @@ -0,0 +1,106 @@ +/********************************************************************** + +Audacity: A Digital Audio Editor + +DBConection.h + +Paul Licameli -- split from ProjectFileIO.h + +**********************************************************************/ + +#ifndef __AUDACITY_DB_CONNECTION__ +#define __AUDACITY_DB_CONNECTION__ + +#include +#include +#include +#include +#include +#include + +#include "ClientData.h" + +struct sqlite3; +struct sqlite3_stmt; +class wxString; +class AudacityProject; + +class DBConnection +{ +public: + explicit + DBConnection(const std::weak_ptr &pProject); + ~DBConnection(); + + bool Open(const char *fileName); + bool Close(); + + bool SafeMode(const char *schema = "main"); + bool FastMode(const char *schema = "main"); + + bool Assign(sqlite3 *handle); + sqlite3 *Detach(); + + sqlite3 *DB(); + + int GetLastRC() const ; + const wxString GetLastMessage() const; + + enum StatementID + { + GetSamples, + GetSummary256, + GetSummary64k, + LoadSampleBlock, + InsertSampleBlock, + DeleteSampleBlock + }; + sqlite3_stmt *GetStatement(enum StatementID id); + sqlite3_stmt *Prepare(enum StatementID id, const char *sql); + + void SetBypass( bool bypass ); + bool ShouldBypass(); + +private: + bool ModeConfig(sqlite3 *db, const char *schema, const char *config); + + void CheckpointThread(); + static int CheckpointHook(void *data, sqlite3 *db, const char *schema, int pages); + +private: + std::weak_ptr mpProject; + sqlite3 *mDB; + + std::thread mCheckpointThread; + std::condition_variable mCheckpointCondition; + std::mutex mCheckpointMutex; + std::atomic_bool mCheckpointStop{ false }; + std::atomic_int mCheckpointWaitingPages{ 0 }; + std::atomic_int mCheckpointCurrentPages{ 0 }; + + std::map mStatements; + + // Bypass transactions if database will be deleted after close + bool mBypass; +}; + +using Connection = std::unique_ptr; + +// This object attached to the project simply holds the pointer to the +// project's current database connection, which is initialized on demand, +// and may be redirected, temporarily or permanently, to another connection +// when backing the project up or saving or saving-as. +class ConnectionPtr final + : public ClientData::Base + , public std::enable_shared_from_this< ConnectionPtr > +{ +public: + static ConnectionPtr &Get( AudacityProject &project ); + static const ConnectionPtr &Get( const AudacityProject &project ); + + ~ConnectionPtr() override; + + Connection mpConnection; +}; + +#endif diff --git a/src/ProjectFileIO.cpp b/src/ProjectFileIO.cpp index faf7bee08..2f18b3b9d 100644 --- a/src/ProjectFileIO.cpp +++ b/src/ProjectFileIO.cpp @@ -18,6 +18,7 @@ Paul Licameli split from AudacityProject.cpp #include #include +#include "DBConnection.h" #include "FileNames.h" #include "Project.h" #include "ProjectFileIORegistry.h" @@ -129,19 +130,6 @@ static const char *ProjectFileSchema = " samples BLOB" ");"; -// Configuration to provide "safe" connections -static const char *SafeConfig = - "PRAGMA .locking_mode = SHARED;" - "PRAGMA .synchronous = NORMAL;" - "PRAGMA .journal_mode = WAL;" - "PRAGMA .wal_autocheckpoint = 0;"; - -// Configuration to provide "Fast" connections -static const char *FastConfig = - "PRAGMA .locking_mode = SHARED;" - "PRAGMA .synchronous = OFF;" - "PRAGMA .journal_mode = OFF;"; - // This singleton handles initialization/shutdown of the SQLite library. // It is needed because our local SQLite is built with SQLITE_OMIT_AUTOINIT // defined. @@ -251,34 +239,27 @@ const ProjectFileIO &ProjectFileIO::Get( const AudacityProject &project ) return Get( const_cast< AudacityProject & >( project ) ); } -ProjectFileIO::ProjectFileIO(AudacityProject &) +ProjectFileIO::ProjectFileIO(AudacityProject &project) + : mProject{ project } { mPrevConn = nullptr; - mCurrConn = nullptr; mRecovered = false; mModified = false; mTemporary = true; - mBypass = false; UpdatePrefs(); } -void ProjectFileIO::Init( AudacityProject &project ) -{ - // This step can't happen in the ctor of ProjectFileIO because ctor of - // AudacityProject wasn't complete - mpProject = project.shared_from_this(); -} - ProjectFileIO::~ProjectFileIO() { - wxASSERT_MSG(mCurrConn == nullptr, wxT("Project file was not closed at shutdown")); + wxASSERT_MSG(!CurrConn(), wxT("Project file was not closed at shutdown")); } -Connection &ProjectFileIO::Conn() +sqlite3 *ProjectFileIO::DB() { - if (!mCurrConn) + auto &curConn = CurrConn(); + if (!curConn) { if (!OpenConnection()) { @@ -289,17 +270,13 @@ Connection &ProjectFileIO::Conn() } } - return mCurrConn; -} - -sqlite3 *ProjectFileIO::DB() -{ - return Conn()->DB(); + return curConn->DB(); } bool ProjectFileIO::OpenConnection(FilePath fileName /* = {} */) { - wxASSERT(mCurrConn == nullptr); + auto &curConn = CurrConn(); + wxASSERT(!curConn); bool temp = false; if (fileName.empty()) @@ -312,10 +289,11 @@ bool ProjectFileIO::OpenConnection(FilePath fileName /* = {} */) } } - mCurrConn = std::make_unique(this); - if (!mCurrConn->Open(fileName)) + // Pass weak_ptr to project into DBConnection constructor + curConn = std::make_unique(mProject.shared_from_this()); + if (!curConn->Open(fileName)) { - mCurrConn = nullptr; + curConn.reset(); return false; } @@ -334,13 +312,14 @@ bool ProjectFileIO::OpenConnection(FilePath fileName /* = {} */) bool ProjectFileIO::CloseConnection() { - wxASSERT(mCurrConn != nullptr); + auto &curConn = CurrConn(); + wxASSERT(curConn); - if (!mCurrConn->Close()) + if (!curConn->Close()) { return false; } - mCurrConn = nullptr; + curConn.reset(); SetFileName({}); @@ -354,7 +333,7 @@ void ProjectFileIO::SaveConnection() // Should do nothing in proper usage, but be sure not to leak a connection: DiscardConnection(); - mPrevConn = std::move(mCurrConn); + mPrevConn = std::move(CurrConn()); mPrevFileName = mFileName; mPrevTemporary = mTemporary; @@ -381,9 +360,10 @@ void ProjectFileIO::DiscardConnection() // Close any current connection and switch back to using the saved void ProjectFileIO::RestoreConnection() { - if (mCurrConn) + auto &curConn = CurrConn(); + if (curConn) { - if (!mCurrConn->Close()) + if (!curConn->Close()) { // Store an error message SetDBError( @@ -392,7 +372,7 @@ void ProjectFileIO::RestoreConnection() } } - mCurrConn = std::move(mPrevConn); + curConn = std::move(mPrevConn); SetFileName(mPrevFileName); mTemporary = mPrevTemporary; @@ -401,9 +381,10 @@ void ProjectFileIO::RestoreConnection() void ProjectFileIO::UseConnection(Connection &&conn, const FilePath &filePath) { - wxASSERT(mCurrConn == nullptr); + auto &curConn = CurrConn(); + wxASSERT(!curConn); - mCurrConn = std::move(conn); + curConn = std::move(conn); SetFileName(filePath); } @@ -724,11 +705,7 @@ Connection ProjectFileIO::CopyTo(const FilePath &destpath, const std::shared_ptr &tracks /* = nullptr */) { // Get access to the active tracklist - auto pProject = mpProject.lock(); - if (!pProject) - { - return nullptr; - } + auto pProject = &mProject; auto &tracklist = tracks ? *tracks : TrackList::Get(*pProject); BlockIDs blockids; @@ -808,7 +785,7 @@ Connection ProjectFileIO::CopyTo(const FilePath &destpath, } // Ensure attached DB connection gets configured - mCurrConn->FastMode("outbound"); + CurrConn()->FastMode("outbound"); // Install our schema into the new database if (!InstallSchema(db, "outbound")) @@ -932,7 +909,7 @@ Connection ProjectFileIO::CopyTo(const FilePath &destpath, } // Open the newly created database - destConn = std::make_unique(this); + destConn = std::make_unique(mProject.shared_from_this()); if (!destConn->Open(destpath)) { SetDBError( @@ -1025,6 +1002,12 @@ bool ProjectFileIO::ShouldVacuum(const std::shared_ptr &tracks) return true; } +Connection &ProjectFileIO::CurrConn() +{ + auto &connectionPtr = ConnectionPtr::Get( mProject ); + return connectionPtr.mpConnection; +} + void ProjectFileIO::Vacuum(const std::shared_ptr &tracks) { // Haven't vacuumed yet @@ -1076,7 +1059,8 @@ void ProjectFileIO::Vacuum(const std::shared_ptr &tracks) } // Reopen the original database using the temporary name - Connection tempConn = std::make_unique(this); + Connection tempConn = + std::make_unique(mProject.shared_from_this()); if (!tempConn->Open(tempName)) { SetDBError(XO("Failed to open project file")); @@ -1142,11 +1126,7 @@ void ProjectFileIO::UpdatePrefs() // Pass a number in to show project number, or -1 not to. void ProjectFileIO::SetProjectTitle(int number) { - auto pProject = mpProject.lock(); - if (! pProject ) - return; - - auto &project = *pProject; + auto &project = mProject; auto pWindow = project.GetFrame(); if (!pWindow) { @@ -1196,10 +1176,7 @@ const FilePath &ProjectFileIO::GetFileName() const void ProjectFileIO::SetFileName(const FilePath &fileName) { - auto pProject = mpProject.lock(); - if (! pProject ) - return; - auto &project = *pProject; + auto &project = mProject; mFileName = fileName; @@ -1217,10 +1194,7 @@ void ProjectFileIO::SetFileName(const FilePath &fileName) bool ProjectFileIO::HandleXMLTag(const wxChar *tag, const wxChar **attrs) { - auto pProject = mpProject.lock(); - if (! pProject ) - return false; - auto &project = *pProject; + auto &project = mProject; auto &window = GetProjectFrame(project); auto &viewInfo = ViewInfo::Get(project); auto &settings = ProjectSettings::Get(project); @@ -1350,10 +1324,7 @@ bool ProjectFileIO::HandleXMLTag(const wxChar *tag, const wxChar **attrs) XMLTagHandler *ProjectFileIO::HandleXMLChild(const wxChar *tag) { - auto pProject = mpProject.lock(); - if (! pProject ) - return nullptr; - auto &project = *pProject; + auto &project = mProject; auto fn = ProjectFileIORegistry::Lookup(tag); if (fn) { @@ -1383,10 +1354,7 @@ void ProjectFileIO::WriteXML(XMLWriter &xmlFile, const std::shared_ptr &tracks /* = nullptr */) // may throw { - auto pProject = mpProject.lock(); - if (! pProject ) - THROW_INCONSISTENCY_EXCEPTION; - auto &proj = *pProject; + auto &proj = mProject; auto &tracklist = tracks ? *tracks : TrackList::Get(proj); auto &viewInfo = ViewInfo::Get(proj); auto &tags = Tags::Get(proj); @@ -1655,11 +1623,7 @@ bool ProjectFileIO::ImportProject(const FilePath &fileName) }; // Get access to the active tracklist - auto pProject = mpProject.lock(); - if (!pProject) - { - return false; - } + auto pProject = &mProject; auto &tracklist = TrackList::Get(*pProject); // Search for a timetrack and remove it if the project already has one @@ -2058,10 +2022,11 @@ bool ProjectFileIO::SaveCopy(const FilePath& fileName) bool ProjectFileIO::CloseProject() { - wxASSERT(mCurrConn != nullptr); + auto &currConn = CurrConn(); + wxASSERT(currConn); // Protect... - if (mCurrConn == nullptr) + if (!currConn) { return true; } @@ -2106,7 +2071,7 @@ bool ProjectFileIO::IsRecovered() const void ProjectFileIO::Reset() { - wxASSERT_MSG(mCurrConn == nullptr, wxT("Resetting project with open project file")); + wxASSERT_MSG(!CurrConn(), wxT("Resetting project with open project file")); mModified = false; mRecovered = false; @@ -2146,13 +2111,14 @@ void ProjectFileIO::SetError(const TranslatableString &msg) void ProjectFileIO::SetDBError(const TranslatableString &msg) { + auto &currConn = CurrConn(); mLastError = msg; wxLogDebug(wxT("SQLite error: %s"), mLastError.Debug()); printf(" Lib error: %s", mLastError.Debug().mb_str().data()); - if (mCurrConn) + if (currConn) { - mLibraryError = Verbatim(sqlite3_errmsg(mCurrConn->DB())); + mLibraryError = Verbatim(sqlite3_errmsg(currConn->DB())); wxLogDebug(wxT(" Lib error: %s"), mLibraryError.Debug()); printf(" Lib error: %s", mLibraryError.Debug().mb_str().data()); } @@ -2162,13 +2128,18 @@ void ProjectFileIO::SetDBError(const TranslatableString &msg) void ProjectFileIO::SetBypass() { + auto &currConn = CurrConn(); + if (!currConn) + return; + // Determine if we can bypass sample block deletes during shutdown. // // IMPORTANT: // If the project was vacuumed, then we MUST bypass further // deletions since the new file doesn't have the blocks that the // Sequences expect to be there. - mBypass = true; + + currConn->SetBypass( true ); // Only permanent project files need cleaning at shutdown if (!IsTemporary() && !WasVacuumed()) @@ -2182,18 +2153,13 @@ void ProjectFileIO::SetBypass() // changes. if (HadUnused()) { - mBypass = false; + currConn->SetBypass( false ); } } return; } -bool ProjectFileIO::ShouldBypass() -{ - return mBypass; -} - AutoCommitTransaction::AutoCommitTransaction(ProjectFileIO &projectFileIO, const char *name) : mIO(projectFileIO), @@ -2233,259 +2199,3 @@ bool AutoCommitTransaction::Rollback() return mInTrans; } -DBConnection::DBConnection(ProjectFileIO *io) -: mIO(*io) -{ - mDB = nullptr; -} - -DBConnection::~DBConnection() -{ - wxASSERT(mDB == nullptr); -} - -bool DBConnection::Open(const char *fileName) -{ - wxASSERT(mDB == nullptr); - int rc; - - rc = sqlite3_open(fileName, &mDB); - if (rc != SQLITE_OK) - { - sqlite3_close(mDB); - mDB = nullptr; - - return false; - } - - // Set default mode - SafeMode(); - - // Kick off the checkpoint thread - mCheckpointStop = false; - mCheckpointWaitingPages = 0; - mCheckpointCurrentPages = 0; - mCheckpointThread = std::thread([this]{ CheckpointThread(); }); - - // Install our checkpoint hook - sqlite3_wal_hook(mDB, CheckpointHook, this); - - return mDB; -} - -bool DBConnection::Close() -{ - wxASSERT(mDB != nullptr); - int rc; - - // Protect... - if (mDB == nullptr) - { - return true; - } - - // Uninstall our checkpoint hook so that no additional checkpoints - // are sent our way. (Though this shouldn't really happen.) - sqlite3_wal_hook(mDB, nullptr, nullptr); - - // Display a progress dialog if there's active or pending checkpoints - if (mCheckpointWaitingPages || mCheckpointCurrentPages) - { - TranslatableString title = XO("Checkpointing project"); - - // Get access to the active project - auto project = mIO.mpProject.lock(); - if (project) - { - title = XO("Checkpointing %s").Format(project->GetProjectName()); - } - - // Provides a progress dialog with indeterminate mode - wxGenericProgressDialog pd(title.Translation(), - XO("This may take several seconds").Translation(), - 300000, // range - nullptr, // parent - wxPD_APP_MODAL | wxPD_ELAPSED_TIME | wxPD_SMOOTH); - - // Wait for the checkpoints to end - while (mCheckpointWaitingPages || mCheckpointCurrentPages) - { - wxMilliSleep(50); - pd.Pulse(); - } - } - - // Tell the checkpoint thread to shutdown - { - std::lock_guard guard(mCheckpointMutex); - mCheckpointStop = true; - mCheckpointCondition.notify_one(); - } - - // And wait for it to do so - mCheckpointThread.join(); - - // We're done with the prepared statements - for (auto stmt : mStatements) - { - sqlite3_finalize(stmt.second); - } - mStatements.clear(); - - // Close the DB - rc = sqlite3_close(mDB); - if (rc != SQLITE_OK) - { - // I guess we could try to recover by repreparing statements and reinstalling - // the hook, but who knows if that would work either. - // - // Should we throw an error??? - } - - mDB = nullptr; - - return true; -} - -bool DBConnection::SafeMode(const char *schema /* = "main" */) -{ - return ModeConfig(mDB, schema, SafeConfig); -} - -bool DBConnection::FastMode(const char *schema /* = "main" */) -{ - return ModeConfig(mDB, schema, FastConfig); -} - -bool DBConnection::ModeConfig(sqlite3 *db, const char *schema, const char *config) -{ - // Ensure attached DB connection gets configured - int rc; - - // Replace all schema "keywords" with the schema name - wxString sql = config; - sql.Replace(wxT(""), schema); - - // Set the configuration - rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); - - return rc != SQLITE_OK; -} - -sqlite3 *DBConnection::DB() -{ - wxASSERT(mDB != nullptr); - - return mDB; -} - -int DBConnection::GetLastRC() const -{ - return sqlite3_errcode(mDB); -} - -const wxString DBConnection::GetLastMessage() const -{ - return sqlite3_errmsg(mDB); -} - -sqlite3_stmt *DBConnection::Prepare(enum StatementID id, const char *sql) -{ - int rc; - - // Return an existing statement if it's already been prepared - auto iter = mStatements.find(id); - if (iter != mStatements.end()) - { - return iter->second; - } - - // Prepare the statement - sqlite3_stmt *stmt = nullptr; - rc = sqlite3_prepare_v3(mDB, sql, -1, SQLITE_PREPARE_PERSISTENT, &stmt, 0); - if (rc != SQLITE_OK) - { - wxLogDebug("prepare error %s", sqlite3_errmsg(mDB)); - THROW_INCONSISTENCY_EXCEPTION; - } - - // And remember it - mStatements.insert({id, stmt}); - - return stmt; -} - -sqlite3_stmt *DBConnection::GetStatement(enum StatementID id) -{ - // Look it up - auto iter = mStatements.find(id); - - // It should always be there - wxASSERT(iter != mStatements.end()); - - // Return it - return iter->second; -} - -void DBConnection::CheckpointThread() -{ - // Open another connection to the DB to prevent blocking the main thread. - // - // If it fails, then we won't checkpoint until the main thread closes - // the associated DB. - sqlite3 *db = nullptr; - if (sqlite3_open(sqlite3_db_filename(mDB, nullptr), &db) == SQLITE_OK) - { - // Configure it to be safe - ModeConfig(db, "main", SafeConfig); - - while (true) - { - { - // Wait for work or the stop signal - std::unique_lock lock(mCheckpointMutex); - mCheckpointCondition.wait(lock, - [&] - { - return mCheckpointWaitingPages || mCheckpointStop; - }); - - // Requested to stop, so bail - if (mCheckpointStop) - { - break; - } - - // Capture the number of pages that need checkpointing and reset - mCheckpointCurrentPages.store( mCheckpointWaitingPages ); - mCheckpointWaitingPages = 0; - } - - // And kick off the checkpoint. This may not checkpoint ALL frames - // in the WAL. They'll be gotten the next time around. - sqlite3_wal_checkpoint_v2(db, nullptr, SQLITE_CHECKPOINT_PASSIVE, nullptr, nullptr); - - // Reset - mCheckpointCurrentPages = 0; - } - } - - // All done (always close) - sqlite3_close(db); - - return; -} - -int DBConnection::CheckpointHook(void *data, sqlite3 *db, const char *schema, int pages) -{ - // Get access to our object - DBConnection *that = static_cast(data); - - // Queue the database pointer for our checkpoint thread to process - std::lock_guard guard(that->mCheckpointMutex); - that->mCheckpointWaitingPages = pages; - that->mCheckpointCondition.notify_one(); - - return SQLITE_OK; -} - diff --git a/src/ProjectFileIO.h b/src/ProjectFileIO.h index 23dd130b6..f3a800192 100644 --- a/src/ProjectFileIO.h +++ b/src/ProjectFileIO.h @@ -11,12 +11,7 @@ Paul Licameli split from AudacityProject.h #ifndef __AUDACITY_PROJECT_FILE_IO__ #define __AUDACITY_PROJECT_FILE_IO__ -#include -#include #include -#include -#include -#include #include #include "ClientData.h" // to inherit @@ -60,9 +55,6 @@ public: static const ProjectFileIO &Get( const AudacityProject &project ); explicit ProjectFileIO( AudacityProject &project ); - // unfortunate two-step construction needed because of - // enable_shared_from_this - void Init( AudacityProject &project ); ProjectFileIO( const ProjectFileIO & ) PROHIBITED; ProjectFileIO &operator=( const ProjectFileIO & ) PROHIBITED; @@ -110,7 +102,6 @@ public: // SqliteSampleBlock::~SqliteSampleBlock() // ProjectManager::OnCloseWindow() void SetBypass(); - bool ShouldBypass(); // Remove all unused space within a project file void Vacuum(const std::shared_ptr &tracks); @@ -146,8 +137,6 @@ private: // if opening fails. sqlite3 *DB(); - Connection &Conn(); - bool OpenConnection(FilePath fileName = {}); bool CloseConnection(); @@ -195,8 +184,10 @@ private: bool ShouldVacuum(const std::shared_ptr &tracks); private: + Connection &CurrConn(); + // non-static data members - std::weak_ptr mpProject; + AudacityProject &mProject; // The project's file path FilePath mFileName; @@ -210,9 +201,6 @@ private: // Is this project still a temporary/unsaved project bool mTemporary; - // Bypass transactions if database will be deleted after close - bool mBypass; - // Project was vacuumed last time Vacuum() ran bool mWasVacuumed; @@ -223,13 +211,10 @@ private: FilePath mPrevFileName; bool mPrevTemporary; - Connection mCurrConn; TranslatableString mLastError; TranslatableString mLibraryError; - friend SqliteSampleBlock; friend AutoCommitTransaction; - friend DBConnection; }; class AutoCommitTransaction @@ -247,58 +232,6 @@ private: wxString mName; }; -class DBConnection -{ -public: - DBConnection(ProjectFileIO *io); - ~DBConnection(); - - bool Open(const char *fileName); - bool Close(); - - bool SafeMode(const char *schema = "main"); - bool FastMode(const char *schema = "main"); - - bool Assign(sqlite3 *handle); - sqlite3 *Detach(); - - sqlite3 *DB(); - - int GetLastRC() const ; - const wxString GetLastMessage() const; - - enum StatementID - { - GetSamples, - GetSummary256, - GetSummary64k, - LoadSampleBlock, - InsertSampleBlock, - DeleteSampleBlock - }; - sqlite3_stmt *GetStatement(enum StatementID id); - sqlite3_stmt *Prepare(enum StatementID id, const char *sql); - -private: - bool ModeConfig(sqlite3 *db, const char *schema, const char *config); - - void CheckpointThread(); - static int CheckpointHook(void *data, sqlite3 *db, const char *schema, int pages); - -private: - ProjectFileIO &mIO; - sqlite3 *mDB; - - std::thread mCheckpointThread; - std::condition_variable mCheckpointCondition; - std::mutex mCheckpointMutex; - std::atomic_bool mCheckpointStop{ false }; - std::atomic_int mCheckpointWaitingPages{ 0 }; - std::atomic_int mCheckpointCurrentPages{ 0 }; - - std::map mStatements; -}; - class wxTopLevelWindow; // TitleRestorer restores project window titles to what they were, in its destructor. diff --git a/src/ProjectManager.cpp b/src/ProjectManager.cpp index 9f8fbaf4d..583a0f008 100644 --- a/src/ProjectManager.cpp +++ b/src/ProjectManager.cpp @@ -536,7 +536,6 @@ AudacityProject *ProjectManager::New() InitProjectWindow( window ); auto &projectFileIO = ProjectFileIO::Get( *p ); - projectFileIO.Init( *p ); projectFileIO.SetProjectTitle(); MenuManager::Get( project ).CreateMenusAndCommands( project ); diff --git a/src/SampleBlock.cpp b/src/SampleBlock.cpp index 1899631bd..233bd228b 100644 --- a/src/SampleBlock.cpp +++ b/src/SampleBlock.cpp @@ -7,6 +7,7 @@ SampleBlock.cpp **********************************************************************/ #include "Audacity.h" +#include "InconsistencyException.h" #include "SampleBlock.h" #include "SampleFormat.h" diff --git a/src/SampleBlock.h b/src/SampleBlock.h index 466633b4a..5d22e50be 100644 --- a/src/SampleBlock.h +++ b/src/SampleBlock.h @@ -9,7 +9,7 @@ SampleBlock.h #ifndef __AUDACITY_SAMPLE_BLOCK__ #define __AUDACITY_SAMPLE_BLOCK__ -#include "ClientData.h" // to inherit +#include "audacity/Types.h" #include #include diff --git a/src/SqliteSampleBlock.cpp b/src/SqliteSampleBlock.cpp index 101c3c8be..081f43b2e 100644 --- a/src/SqliteSampleBlock.cpp +++ b/src/SqliteSampleBlock.cpp @@ -11,8 +11,8 @@ Paul Licameli -- split from SampleBlock.cpp and SampleBlock.h #include #include +#include "DBConnection.h" #include "SampleFormat.h" -#include "ProjectFileIO.h" #include "xml/XMLTagHandler.h" #include "SampleBlock.h" // to inherit @@ -22,7 +22,8 @@ class SqliteSampleBlock final : public SampleBlock { public: - explicit SqliteSampleBlock(ProjectFileIO &io); + explicit SqliteSampleBlock( + const std::shared_ptr &ppConnection); ~SqliteSampleBlock() override; void CloseLock() override; @@ -75,9 +76,25 @@ private: void CalcSummary(); private: + DBConnection *Conn() const + { + auto &pConnection = mppConnection->mpConnection; + if (!pConnection) { + throw SimpleMessageBoxException + { + XO("Failed to open the project's database") + }; + } + return pConnection.get(); + } + sqlite3 *DB() const + { + return Conn()->DB(); + } + friend SqliteSampleBlockFactory; - ProjectFileIO & mIO; + const std::shared_ptr mppConnection; bool mValid; bool mDirty; bool mSilent; @@ -126,11 +143,11 @@ public: const wxChar **attrs) override; private: - std::shared_ptr mpIO; + const std::shared_ptr mppConnection; }; SqliteSampleBlockFactory::SqliteSampleBlockFactory( AudacityProject &project ) - : mpIO{ ProjectFileIO::Get(project).shared_from_this() } + : mppConnection{ ConnectionPtr::Get(project).shared_from_this() } { } @@ -140,7 +157,7 @@ SqliteSampleBlockFactory::~SqliteSampleBlockFactory() = default; SampleBlockPtr SqliteSampleBlockFactory::DoCreate( samplePtr src, size_t numsamples, sampleFormat srcformat ) { - auto sb = std::make_shared(*mpIO); + auto sb = std::make_shared(mppConnection); sb->SetSamples(src, numsamples, srcformat); return sb; } @@ -148,7 +165,7 @@ SampleBlockPtr SqliteSampleBlockFactory::DoCreate( SampleBlockPtr SqliteSampleBlockFactory::DoCreateSilent( size_t numsamples, sampleFormat srcformat ) { - auto sb = std::make_shared(*mpIO); + auto sb = std::make_shared(mppConnection); sb->SetSilent(numsamples, srcformat); return sb; } @@ -157,7 +174,7 @@ SampleBlockPtr SqliteSampleBlockFactory::DoCreateSilent( SampleBlockPtr SqliteSampleBlockFactory::DoCreateFromXML( sampleFormat srcformat, const wxChar **attrs ) { - auto sb = std::make_shared(*mpIO); + auto sb = std::make_shared(mppConnection); sb->mSampleFormat = srcformat; int found = 0; @@ -223,13 +240,14 @@ SampleBlockPtr SqliteSampleBlockFactory::DoCreateFromXML( SampleBlockPtr SqliteSampleBlockFactory::DoGet( SampleBlockID sbid ) { - auto sb = std::make_shared(*mpIO); + auto sb = std::make_shared(mppConnection); sb->Load(sbid); return sb; } -SqliteSampleBlock::SqliteSampleBlock(ProjectFileIO &io) -: mIO(io) +SqliteSampleBlock::SqliteSampleBlock( + const std::shared_ptr &ppConnection) +: mppConnection(ppConnection) { mValid = false; mSilent = false; @@ -250,7 +268,7 @@ SqliteSampleBlock::SqliteSampleBlock(ProjectFileIO &io) SqliteSampleBlock::~SqliteSampleBlock() { // See ProjectFileIO::Bypass() for a description of mIO.mBypass - if (!mLocked && !mIO.ShouldBypass()) + if (!mLocked && !Conn()->ShouldBypass()) { // In case Delete throws, don't let an exception escape a destructor, // but we can still enqueue the delayed handler so that an error message @@ -287,7 +305,7 @@ size_t SqliteSampleBlock::DoGetSamples(samplePtr dest, size_t numsamples) { // Prepare and cache statement...automatically finalized at DB close - sqlite3_stmt *stmt = mIO.Conn()->Prepare(DBConnection::GetSamples, + sqlite3_stmt *stmt = Conn()->Prepare(DBConnection::GetSamples, "SELECT samples FROM sampleblocks WHERE blockid = ?1;"); return GetBlob(dest, @@ -334,7 +352,7 @@ bool SqliteSampleBlock::GetSummary256(float *dest, size_t numframes) { // Prepare and cache statement...automatically finalized at DB close - sqlite3_stmt *stmt = mIO.Conn()->Prepare(DBConnection::GetSummary256, + sqlite3_stmt *stmt = Conn()->Prepare(DBConnection::GetSummary256, "SELECT summary256 FROM sampleblocks WHERE blockid = ?1;"); return GetSummary(dest, frameoffset, numframes, stmt, mSummary256Bytes); @@ -345,7 +363,7 @@ bool SqliteSampleBlock::GetSummary64k(float *dest, size_t numframes) { // Prepare and cache statement...automatically finalized at DB close - sqlite3_stmt *stmt = mIO.Conn()->Prepare(DBConnection::GetSummary64k, + sqlite3_stmt *stmt = Conn()->Prepare(DBConnection::GetSummary64k, "SELECT summary64k FROM sampleblocks WHERE blockid = ?1;"); return GetSummary(dest, frameoffset, numframes, stmt, mSummary256Bytes); @@ -447,7 +465,7 @@ size_t SqliteSampleBlock::GetBlob(void *dest, size_t srcoffset, size_t srcbytes) { - auto db = mIO.DB(); + auto db = DB(); wxASSERT(mBlockID > 0); @@ -516,7 +534,7 @@ size_t SqliteSampleBlock::GetBlob(void *dest, void SqliteSampleBlock::Load(SampleBlockID sbid) { - auto db = mIO.DB(); + auto db = DB(); int rc; wxASSERT(sbid > 0); @@ -531,7 +549,7 @@ void SqliteSampleBlock::Load(SampleBlockID sbid) mSumMin = 0.0; // Prepare and cache statement...automatically finalized at DB close - sqlite3_stmt *stmt = mIO.Conn()->Prepare(DBConnection::LoadSampleBlock, + sqlite3_stmt *stmt = Conn()->Prepare(DBConnection::LoadSampleBlock, "SELECT sampleformat, summin, summax, sumrms," " length('summary256'), length('summary64k'), length('samples')" " FROM sampleblocks WHERE blockid = ?1;"); @@ -579,11 +597,11 @@ void SqliteSampleBlock::Load(SampleBlockID sbid) void SqliteSampleBlock::Commit() { - auto db = mIO.DB(); + auto db = DB(); int rc; // Prepare and cache statement...automatically finalized at DB close - sqlite3_stmt *stmt = mIO.Conn()->Prepare(DBConnection::InsertSampleBlock, + sqlite3_stmt *stmt = Conn()->Prepare(DBConnection::InsertSampleBlock, "INSERT INTO sampleblocks (sampleformat, summin, summax, sumrms," " summary256, summary64k, samples)" " VALUES(?1,?2,?3,?4,?5,?6,?7);"); @@ -634,13 +652,13 @@ void SqliteSampleBlock::Commit() void SqliteSampleBlock::Delete() { - auto db = mIO.DB(); + auto db = DB(); int rc; wxASSERT(mBlockID > 0); // Prepare and cache statement...automatically finalized at DB close - sqlite3_stmt *stmt = mIO.Conn()->Prepare(DBConnection::DeleteSampleBlock, + sqlite3_stmt *stmt = Conn()->Prepare(DBConnection::DeleteSampleBlock, "DELETE FROM sampleblocks WHERE blockid = ?1;"); // Bind statement paraemters