1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-06-16 16:10:06 +02:00

Fail safe save as (#606)

* ProjectFileIO::SaveProject() won't close original db until done...

... So on the failure path, don't risk closing the original too early, failing
with the new database, and then failing to reopen the original.

Just keep the original open and a new connection open too, until it is
certain that the initial population of the new database is successful.

* Check return value from AutoSaveDelete when saving-as

* Fix the remaining unchecked sqlite3_bind* calls, which are in Autosave...

...similarly to checks in 8b3f9fa

* When saving-as or -copy, open the new database only once

* Check return value from sqlite_initialize
This commit is contained in:
Paul Licameli 2020-07-07 16:41:33 -04:00 committed by GitHub
parent ba7ebbb032
commit 590d8c6d09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 149 additions and 52 deletions

View File

@ -86,6 +86,7 @@ It handles initialization and termination by subclassing wxApp.
#include "Project.h"
#include "ProjectAudioIO.h"
#include "ProjectAudioManager.h"
#include "ProjectFileIO.h"
#include "ProjectFileManager.h"
#include "ProjectHistory.h"
#include "ProjectManager.h"
@ -1029,6 +1030,15 @@ bool AudacityApp::OnInit()
// Ensure we have an event loop during initialization
wxEventLoopGuarantor eventLoop;
// Fire up SQLite
if ( !ProjectFileIO::InitializeSQL() )
this->CallAfter([]{
::AudacityMessageBox(
XO("SQLite library failed to initialize. Audacity cannot continue.") );
QuitAudacity( true );
});
// cause initialization of wxWidgets' global logger target
(void) AudacityLogger::Get();

View File

@ -118,14 +118,22 @@ class SQLiteIniter
public:
SQLiteIniter()
{
sqlite3_initialize();
mRc = sqlite3_initialize();
}
~SQLiteIniter()
{
sqlite3_shutdown();
// This function must be called single-threaded only
// It returns a value, but there's nothing we can do with it
(void) sqlite3_shutdown();
}
int mRc;
};
static SQLiteIniter sqliteIniter;
bool ProjectFileIO::InitializeSQL()
{
static SQLiteIniter sqliteIniter;
return sqliteIniter.mRc == SQLITE_OK;
}
static void RefreshAllTitles(bool bShowProjectNumbers )
{
@ -189,6 +197,7 @@ const ProjectFileIO &ProjectFileIO::Get( const AudacityProject &project )
ProjectFileIO::ProjectFileIO(AudacityProject &)
{
mPrevDB = nullptr;
mDB = nullptr;
mRecovered = false;
@ -256,6 +265,57 @@ sqlite3 *ProjectFileIO::DB()
return mDB;
}
// Put the current database connection aside, keeping it open, so that
// another may be opened with OpenDB()
void ProjectFileIO::SaveConnection()
{
// Should do nothing in proper usage, but be sure not to leak a connection:
DiscardConnection();
mPrevDB = mDB;
mDB = nullptr;
}
// Close any set-aside connection
void ProjectFileIO::DiscardConnection()
{
if ( mPrevDB )
{
auto rc = sqlite3_close( mPrevDB );
if ( rc != SQLITE_OK )
{
// Store an error message
SetDBError(
XO("Failed to successfully close the source project file")
);
}
mPrevDB = nullptr;
}
}
// Close any current connection and switch back to using the saved
void ProjectFileIO::RestoreConnection()
{
if ( mDB )
{
auto rc = sqlite3_close( mDB );
if ( rc != SQLITE_OK )
{
// Store an error message
SetDBError(
XO("Failed to successfully close the destination project file")
);
}
}
mDB = mPrevDB;
}
void ProjectFileIO::UseConnection( sqlite3 *db )
{
wxASSERT(mDB == nullptr);
mDB = db;
}
sqlite3 *ProjectFileIO::OpenDB(FilePath fileName)
{
wxASSERT(mDB == nullptr);
@ -592,11 +652,10 @@ bool ProjectFileIO::UpgradeSchema()
return true;
}
bool ProjectFileIO::CopyTo(const FilePath &destpath)
sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath)
{
auto db = DB();
int rc;
bool success = true;
ProgressResult res = ProgressResult::Success;
sqlite3 *destdb = nullptr;
@ -605,6 +664,7 @@ bool ProjectFileIO::CopyTo(const FilePath &destpath)
rc = sqlite3_open(destpath, &destdb);
if (rc == SQLITE_OK)
{
bool success = true;
sqlite3_backup *backup = sqlite3_backup_init(destdb, "main", db, "main");
if (backup)
{
@ -614,6 +674,8 @@ bool ProjectFileIO::CopyTo(const FilePath &destpath)
do
{
// These two calls always return zero before any sqlite3_backup_step
// but that doesn't matter
int remaining = sqlite3_backup_remaining(backup);
int total = sqlite3_backup_pagecount(backup);
@ -644,21 +706,21 @@ bool ProjectFileIO::CopyTo(const FilePath &destpath)
SetDBError(
XO("Unable to initiate the backup process.")
);
}
// Close the DB
rc = sqlite3_close(destdb);
if (rc != SQLITE_OK)
{
SetDBError(
XO("Failed to successfully close the destination project file:\n\n%s")
);
success = false;
}
if (!success)
{
// Don't give this DB connection back to the caller
rc = sqlite3_close(destdb);
if (rc != SQLITE_OK)
{
SetDBError(
XO("Failed to successfully close the destination project file:\n\n%s")
);
}
wxRemoveFile(destpath);
return nullptr;
}
}
else
@ -668,10 +730,11 @@ bool ProjectFileIO::CopyTo(const FilePath &destpath)
SetDBError(
XO("Unable to open the destination project file:\n\n%s").Format(destpath)
);
success = false;
return nullptr;
}
return success;
// Let the caller use this connection and close it later
return destdb;
}
void ProjectFileIO::UpdatePrefs()
@ -1015,8 +1078,13 @@ bool ProjectFileIO::AutoSave(const AutoSaveFile &autosave)
const wxMemoryBuffer &data = autosave.GetData();
// BIND SQL autosave
sqlite3_bind_blob(stmt, 1, dict.GetData(), dict.GetDataLen(), SQLITE_STATIC);
sqlite3_bind_blob(stmt, 2, data.GetData(), data.GetDataLen(), SQLITE_STATIC);
// Might return SQL_MISUSE which means it's our mistake that we violated
// preconditions; should return SQL_OK which is 0
if (
sqlite3_bind_blob(stmt, 1, dict.GetData(), dict.GetDataLen(), SQLITE_STATIC) ||
sqlite3_bind_blob(stmt, 2, data.GetData(), data.GetDataLen(), SQLITE_STATIC)
)
THROW_INCONSISTENCY_EXCEPTION;
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
@ -1139,24 +1207,25 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
{
if (success)
{
// The Save was successful, so remove the original file if
// it was a temporary file
// The Save was successful, so now it is safe to abandon the
// original connection
DiscardConnection();
// And also remove the original file if it was a temporary file
if (wasTemp)
{
wxRemoveFile(origName);
}
}
else
{
// Close the new database
CloseDB();
// Close the new database and go back to using the original
// connection
RestoreConnection();
// Reopen the original database
if (OpenDB(origName))
{
// And delete the new database
wxRemoveFile(fileName);
}
// And delete the new database
wxRemoveFile(fileName);
}
}
});
@ -1165,7 +1234,8 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
// current to the new file and make it the active file.
if (mFileName != fileName)
{
if (!CopyTo(fileName))
auto newDB = CopyTo(fileName);
if (!newDB)
{
return false;
}
@ -1176,11 +1246,11 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
origName = mFileName;
wasTemp = mTemporary;
// Close the original project file and open the new one
if (!CloseDB() || !OpenDB(fileName))
{
return false;
}
// Save the original database connection and try to switch to a new one
// (also ensuring closing of one of the connections, with the cooperation
// of the finally above)
SaveConnection();
UseConnection( newDB );
}
auto db = DB();
@ -1243,7 +1313,14 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
// clean and unmodified. Otherwise, it would be considered "recovered"
// when next opened.
AutoSaveDelete();
if ( !AutoSaveDelete() )
return false;
// Reaching this point defines success and all the rest are no-fail
// operations:
// Tell the finally block to behave
success = true;
// No longer modified
mModified = false;
@ -1257,20 +1334,17 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
// Adjust the title
SetProjectTitle();
// Tell the finally block to behave
success = true;
return true;
}
bool ProjectFileIO::SaveCopy(const FilePath& fileName)
{
if (!CopyTo(fileName))
auto db = CopyTo(fileName);
if (!db)
{
return false;
}
sqlite3 *db = nullptr;
int rc;
bool success = false;
@ -1278,7 +1352,7 @@ bool ProjectFileIO::SaveCopy(const FilePath& fileName)
{
if (db)
{
sqlite3_close(db);
(void) sqlite3_close(db);
}
if (!success)
@ -1287,15 +1361,6 @@ bool ProjectFileIO::SaveCopy(const FilePath& fileName)
}
});
rc = sqlite3_open(fileName, &db);
if (rc != SQLITE_OK)
{
// sqlite3 docs say you should close anyway to avoid leaks
sqlite3_close( db );
SetDBError(XO("Failed to open backup file"));
return false;
}
XMLStringWriter doc;
WriteXMLHeader(doc);
WriteXML(doc);

View File

@ -35,6 +35,10 @@ class ProjectFileIO final
, public std::enable_shared_from_this<ProjectFileIO>
{
public:
// Call this static function once before constructing any instances of this
// class. Reinvocations have no effect. Return value is true for success.
static bool InitializeSQL();
static ProjectFileIO &Get( AudacityProject &project );
static const ProjectFileIO &Get( const AudacityProject &project );
@ -114,6 +118,19 @@ private:
// if opening fails.
sqlite3 *DB();
// Put the current database connection aside, keeping it open, so that
// another may be opened with OpenDB()
void SaveConnection();
// Close any set-aside connection
void DiscardConnection();
// Close any current connection and switch back to using the saved
void RestoreConnection();
// Use a connection that is already open rather than invoke OpenDB
void UseConnection( sqlite3 *db );
sqlite3 *OpenDB(FilePath fileName = {});
bool CloseDB();
bool DeleteDB();
@ -129,7 +146,8 @@ private:
bool InstallSchema();
bool UpgradeSchema();
bool CopyTo(const FilePath &destpath);
// Return a database connection if successful, which caller must close
sqlite3 *CopyTo(const FilePath &destpath);
void SetError(const TranslatableString & msg);
void SetDBError(const TranslatableString & msg);
@ -153,6 +171,7 @@ private:
// Bypass transactions if database will be deleted after close
bool mBypass;
sqlite3 *mPrevDB;
sqlite3 *mDB;
FilePath mDBPath;
TranslatableString mLastError;

View File

@ -694,7 +694,10 @@ void ProjectManager::OnCloseWindow(wxCloseEvent & event)
// The project is now either saved or the user doesn't want to save it,
// so there's no need to keep auto save info around anymore
projectFileIO.AutoSaveDelete();
// PRL: not clear what to do if the following fails, but the worst should
// be, the project may reopen in its present state as a recovery file, not
// at the last saved state.
(void) projectFileIO.AutoSaveDelete();
// DMM: Save the size of the last window the user closes
//