1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-08-02 17:09:26 +02:00

Strong exception safety in all uses of XMLFileWriter...

... Strong, meaning that the file at the specified path is created or modified
only if all write operations complete without exceptions, barring one very
unlikely possibility that a final file rename fails, but even in that case the
output is successfully written to some path.

This commit does not add throws, but changes the type thrown to a subclass of
AudacityException, so that GuardedCall will cause the user to see an error
dialog in all cases.

Duplicated logic for making temporary files and backups is now all in one
place, the class XMLWriter.

There may be more new GuardedCalls than necessary -- the catch-all for the
event loop, AudacityApp::OnExceptionInMainLoop, might be trusted instead in
some cases --  but they are sufficient.
This commit is contained in:
Paul Licameli 2016-12-01 20:40:05 -05:00
parent b81cdee7e3
commit 3bb04245c5
12 changed files with 323 additions and 425 deletions

View File

@ -738,243 +738,207 @@ bool AutoSaveFile::Decode(const wxString & fileName)
return true; return true;
} }
XMLFileWriter out;
wxString tempName;
len = file.Length() - len; len = file.Length() - len;
using Chars = ArrayOf < char >;
using WxChars = ArrayOf < wxChar >;
Chars buf{ len };
if (file.Read(buf.get(), len) != len)
{ {
using Chars = ArrayOf < char >; return false;
using WxChars = ArrayOf < wxChar >; }
Chars buf{ len };
if (file.Read(buf.get(), len) != len) wxMemoryInputStream in(buf.get(), len);
{
return false;
}
wxMemoryInputStream in(buf.get(), len); file.Close();
file.Close(); // JKC: ANSWER-ME: Is the try catch actually doing anything?
// If it is useful, why are we not using it everywhere?
// If it isn't useful, why are we doing it here?
// PRL: Yes, now we are doing GuardedCall everywhere that XMLFileWriter is
// used.
return GuardedCall< bool >( [&] {
XMLFileWriter out{ fileName, _("Error Decoding File") };
// Decode to a temporary file to preserve the original. IdMap mIds;
tempName = fn.CreateTempFileName(fnPath); IdMapArray mIdStack;
bool opened = false;
// JKC: ANSWER-ME: Is the try catch actually doing anything?
// If it is useful, why are we not using it everywhere?
// If it isn't useful, why are we doing it here?
try
{
out.Open(tempName, wxT("wb"));
opened = out.IsOpened();
}
catch (const XMLFileWriterException&)
{
}
if (!opened)
{
wxRemoveFile(tempName);
return false;
}
mIds.clear(); mIds.clear();
while (!in.Eof() && !out.Error()) while ( !in.Eof() )
{ {
short id; short id;
switch (in.GetC()) switch (in.GetC())
{ {
case FT_Push: case FT_Push:
{ {
mIdStack.Add(mIds); mIdStack.Add(mIds);
mIds.clear(); mIds.clear();
} }
break; break;
case FT_Pop: case FT_Pop:
{ {
mIds = mIdStack[mIdStack.GetCount() - 1]; mIds = mIdStack[mIdStack.GetCount() - 1];
mIdStack.RemoveAt(mIdStack.GetCount() - 1); mIdStack.RemoveAt(mIdStack.GetCount() - 1);
} }
break; break;
case FT_Name: case FT_Name:
{ {
short len; short len;
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
in.Read(&len, sizeof(len)); in.Read(&len, sizeof(len));
WxChars name{ len / sizeof(wxChar) }; WxChars name{ len / sizeof(wxChar) };
in.Read(name.get(), len); in.Read(name.get(), len);
mIds[id] = wxString(name.get(), len / sizeof(wxChar)); mIds[id] = wxString(name.get(), len / sizeof(wxChar));
} }
break; break;
case FT_StartTag: case FT_StartTag:
{ {
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
out.StartTag(mIds[id]); out.StartTag(mIds[id]);
} }
break; break;
case FT_EndTag: case FT_EndTag:
{ {
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
out.EndTag(mIds[id]); out.EndTag(mIds[id]);
} }
break; break;
case FT_String: case FT_String:
{ {
int len; int len;
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
in.Read(&len, sizeof(len)); in.Read(&len, sizeof(len));
WxChars val{ len / sizeof(wxChar) }; WxChars val{ len / sizeof(wxChar) };
in.Read(val.get(), len); in.Read(val.get(), len);
out.WriteAttr(mIds[id], wxString(val.get(), len / sizeof(wxChar))); out.WriteAttr(mIds[id], wxString(val.get(), len / sizeof(wxChar)));
} }
break; break;
case FT_Float: case FT_Float:
{ {
float val; float val;
int dig; int dig;
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val)); in.Read(&val, sizeof(val));
in.Read(&dig, sizeof(dig)); in.Read(&dig, sizeof(dig));
out.WriteAttr(mIds[id], val, dig); out.WriteAttr(mIds[id], val, dig);
} }
break; break;
case FT_Double: case FT_Double:
{ {
double val; double val;
int dig; int dig;
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val)); in.Read(&val, sizeof(val));
in.Read(&dig, sizeof(dig)); in.Read(&dig, sizeof(dig));
out.WriteAttr(mIds[id], val, dig); out.WriteAttr(mIds[id], val, dig);
} }
break; break;
case FT_Int: case FT_Int:
{ {
int val; int val;
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val)); in.Read(&val, sizeof(val));
out.WriteAttr(mIds[id], val); out.WriteAttr(mIds[id], val);
} }
break; break;
case FT_Bool: case FT_Bool:
{ {
bool val; bool val;
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val)); in.Read(&val, sizeof(val));
out.WriteAttr(mIds[id], val); out.WriteAttr(mIds[id], val);
} }
break; break;
case FT_Long: case FT_Long:
{ {
long val; long val;
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val)); in.Read(&val, sizeof(val));
out.WriteAttr(mIds[id], val); out.WriteAttr(mIds[id], val);
} }
break; break;
case FT_LongLong: case FT_LongLong:
{ {
long long val; long long val;
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val)); in.Read(&val, sizeof(val));
out.WriteAttr(mIds[id], val); out.WriteAttr(mIds[id], val);
} }
break; break;
case FT_SizeT: case FT_SizeT:
{ {
size_t val; size_t val;
in.Read(&id, sizeof(id)); in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val)); in.Read(&val, sizeof(val));
out.WriteAttr(mIds[id], val); out.WriteAttr(mIds[id], val);
} }
break; break;
case FT_Data: case FT_Data:
{ {
int len; int len;
in.Read(&len, sizeof(len)); in.Read(&len, sizeof(len));
WxChars val{ len / sizeof(wxChar) }; WxChars val{ len / sizeof(wxChar) };
in.Read(val.get(), len); in.Read(val.get(), len);
out.WriteData(wxString(val.get(), len / sizeof(wxChar))); out.WriteData(wxString(val.get(), len / sizeof(wxChar)));
} }
break; break;
case FT_Raw: case FT_Raw:
{ {
int len; int len;
in.Read(&len, sizeof(len)); in.Read(&len, sizeof(len));
WxChars val{ len / sizeof(wxChar) }; WxChars val{ len / sizeof(wxChar) };
in.Read(val.get(), len); in.Read(val.get(), len);
out.Write(wxString(val.get(), len / sizeof(wxChar))); out.Write(wxString(val.get(), len / sizeof(wxChar)));
} }
break; break;
default: default:
wxASSERT(true); wxASSERT(true);
break; break;
} }
} }
}
bool error = out.Error(); out.Commit();
out.Close();
// Bail if decoding failed. return true;
if (error) } );
{
// File successfully decoded
wxRemoveFile(tempName);
return false;
}
// Decoding was successful, so remove the original file and replace with decoded one.
if (wxRemoveFile(fileName))
{
if (!wxRenameFile(tempName, fileName))
{
return false;
}
}
return true;
} }

View File

@ -115,8 +115,6 @@ private:
wxMemoryOutputStream mBuffer; wxMemoryOutputStream mBuffer;
wxMemoryOutputStream mDict; wxMemoryOutputStream mDict;
NameMap mNames; NameMap mNames;
IdMap mIds;
IdMapArray mIdStack;
size_t mAllocSize; size_t mAllocSize;
}; };

View File

@ -44,44 +44,6 @@ On failure the old version is put back in place.
#include "Legacy.h" #include "Legacy.h"
#include "xml/XMLWriter.h" #include "xml/XMLWriter.h"
class AutoRollbackRenamer {
public:
AutoRollbackRenamer(wxString oldName, wxString newName) {
mOldName = oldName;
mNewName = newName;
mRenameSucceeded = ::wxRenameFile(mOldName, mNewName);
mFinished = false;
mNewFile = NULL;
}
~AutoRollbackRenamer()
{
if (mNewFile)
fclose(mNewFile);
if (mRenameSucceeded && !mFinished) {
::wxRemoveFile(mOldName);
::wxRenameFile(mNewName, mOldName);
}
}
bool RenameSucceeded()
{
return mRenameSucceeded;
}
void Finished()
{
mFinished = true;
}
void SetNewFile(FILE *f)
{
mNewFile = f;
}
wxString mOldName, mNewName;
bool mRenameSucceeded;
bool mFinished;
FILE *mNewFile;
};
static bool ConvertLegacyTrack(wxTextFile *f, XMLFileWriter &xmlFile) static bool ConvertLegacyTrack(wxTextFile *f, XMLFileWriter &xmlFile)
// may throw // may throw
{ {
@ -291,42 +253,15 @@ static bool ConvertLegacyTrack(wxTextFile *f, XMLFileWriter &xmlFile)
bool ConvertLegacyProjectFile(const wxFileName &filename) bool ConvertLegacyProjectFile(const wxFileName &filename)
{ {
wxTextFile f; wxTextFile f;
XMLFileWriter xmlFile;
int index = 0;
wxString backupName;
do { const wxString name = filename.GetFullPath();
index++; f.Open( name );
fflush(stdout);
backupName = filename.GetPath() + wxFILE_SEP_PATH + filename.GetName() +
wxT("_bak") + wxString::Format(wxT("%d"), index) + wxT(".") + filename.GetExt();
} while(::wxFileExists(backupName));
// This will move the original file out of the way, but
// move it back if we exit from this function early.
AutoRollbackRenamer renamer(filename.GetFullPath(), backupName);
if (!renamer.RenameSucceeded())
return false;
f.Open(backupName);
if (!f.IsOpened()) if (!f.IsOpened())
return false; return false;
wxString name = filename.GetFullPath(); return GuardedCall< bool >( [&] {
XMLFileWriter xmlFile{ name, _("Error Converting Legacy Project File") };
try
{
xmlFile.Open(name, wxT("wb"));
}
catch (const XMLFileWriterException&)
{
return false;
}
renamer.SetNewFile(xmlFile.fp());
try
{
xmlFile.Write(wxT("<?xml version=\"1.0\"?>\n")); xmlFile.Write(wxT("<?xml version=\"1.0\"?>\n"));
wxString label; wxString label;
@ -360,19 +295,15 @@ bool ConvertLegacyProjectFile(const wxFileName &filename)
label = f.GetNextLine(); label = f.GetNextLine();
} }
// Close original before Commit() tries to overwrite it.
f.Close();
xmlFile.EndTag(wxT("audacityproject")); xmlFile.EndTag(wxT("audacityproject"));
xmlFile.Close(); xmlFile.Commit();
}
catch (const XMLFileWriterException&)
{
// Error writing XML file (e.g. disk full)
return false;
}
renamer.Finished(); ::wxMessageBox(wxString::Format(_("Converted a 1.0 project file to the new format.\nThe old file has been saved as '%s'"), xmlFile.GetBackupName().c_str()),
_("Opening Audacity Project"));
::wxMessageBox(wxString::Format(_("Converted a 1.0 project file to the new format.\nThe old file has been saved as '%s'"), backupName.c_str()), return true;
_("Opening Audacity Project")); } );
return true;
} }

View File

@ -3803,37 +3803,21 @@ bool AudacityProject::Save(bool overwrite /* = true */ ,
} }
} }
// Write the AUP file. auto success = GuardedCall< bool >( [&] {
XMLFileWriter saveFile; // Write the AUP file.
XMLFileWriter saveFile{ mFileName, _("Error Saving Project") };
try
{
saveFile.Open(mFileName, wxT("wb"));
WriteXMLHeader(saveFile); WriteXMLHeader(saveFile);
WriteXML(saveFile); WriteXML(saveFile);
mStrOtherNamesArray.Clear(); mStrOtherNamesArray.Clear();
saveFile.Close(); saveFile.Commit();
}
catch (const XMLFileWriterException &exception)
{
wxMessageBox(wxString::Format(
_("Couldn't write to file \"%s\": %s"),
mFileName.c_str(), exception.GetMessage().c_str()),
_("Error Saving Project"), wxICON_ERROR);
// When XMLWriter throws an exception, it tries to close it before, return true;
// so we can at least try to DELETE the incomplete file and move the } );
// backup file over.
if (safetyFileName != wxT(""))
{
wxRemove(mFileName);
wxRename(safetyFileName, mFileName);
}
if (!success)
return false; return false;
}
if (bWantSaveCompressed) if (bWantSaveCompressed)
mWantSaveCompressed = false; // Don't want this mode for AudacityProject::WriteXML() any more. mWantSaveCompressed = false; // Don't want this mode for AudacityProject::WriteXML() any more.
@ -5103,7 +5087,9 @@ void AudacityProject::AutoSave()
wxString fn = wxFileName(FileNames::AutoSaveDir(), wxString fn = wxFileName(FileNames::AutoSaveDir(),
projName + wxString(wxT(" - ")) + CreateUniqueName()).GetFullPath(); projName + wxString(wxT(" - ")) + CreateUniqueName()).GetFullPath();
try // PRL: I found a try-catch and rewrote it,
// but this guard is unnecessary because AutoSaveFile does not throw
bool success = GuardedCall< bool >( [&]
{ {
VarSetter<bool> setter(&mAutoSaving, true, false); VarSetter<bool> setter(&mAutoSaving, true, false);
@ -5114,18 +5100,11 @@ void AudacityProject::AutoSave()
wxFFile saveFile; wxFFile saveFile;
saveFile.Open(fn + wxT(".tmp"), wxT("wb")); saveFile.Open(fn + wxT(".tmp"), wxT("wb"));
buffer.Write(saveFile); return buffer.Write(saveFile);
saveFile.Close(); } );
}
catch (const XMLFileWriterException &exception)
{
wxMessageBox(wxString::Format(
_("Couldn't write to file \"%s\": %s"),
(fn + wxT(".tmp")).c_str(), exception.GetMessage().c_str()),
_("Error Writing Autosave File"), wxICON_ERROR, this);
if (!success)
return; return;
}
// Now that we have a NEW auto-save file, DELETE the old one // Now that we have a NEW auto-save file, DELETE the old one
DeleteCurrentAutoSaveFile(); DeleteCurrentAutoSaveFile();

View File

@ -1219,12 +1219,9 @@ void TagsEditor::OnSave(wxCommandEvent & WXUNUSED(event))
return; return;
} }
// Create/Open the file GuardedCall< void >( [&] {
XMLFileWriter writer; // Create/Open the file
XMLFileWriter writer{ fn, _("Error Saving Tags File") };
try
{
writer.Open(fn, wxT("wb"));
// Remember title and track in case they're read only // Remember title and track in case they're read only
wxString title = mLocal.GetTag(TAG_TITLE); wxString title = mLocal.GetTag(TAG_TITLE);
@ -1240,29 +1237,23 @@ void TagsEditor::OnSave(wxCommandEvent & WXUNUSED(event))
mLocal.SetTag(TAG_TRACK, wxEmptyString); mLocal.SetTag(TAG_TRACK, wxEmptyString);
} }
auto cleanup = finally( [&] {
// Restore title
if (!mEditTitle) {
mLocal.SetTag(TAG_TITLE, title);
}
// Restore track
if (!mEditTrack) {
mLocal.SetTag(TAG_TRACK, track);
}
} );
// Write the metadata // Write the metadata
mLocal.WriteXML(writer); mLocal.WriteXML(writer);
// Restore title writer.Commit();
if (!mEditTitle) { } );
mLocal.SetTag(TAG_TITLE, title);
}
// Restore track
if (!mEditTrack) {
mLocal.SetTag(TAG_TRACK, track);
}
// Close the file
writer.Close();
}
catch (const XMLFileWriterException &exception)
{
wxMessageBox(wxString::Format(
_("Couldn't write to file \"%s\": %s"),
fn.c_str(), exception.GetMessage().c_str()),
_("Error Saving Tags File"), wxICON_ERROR, this);
}
} }
void TagsEditor::OnSaveDefaults(wxCommandEvent & WXUNUSED(event)) void TagsEditor::OnSaveDefaults(wxCommandEvent & WXUNUSED(event))

View File

@ -3580,6 +3580,7 @@ void EffectUIHost::OnImport(wxCommandEvent & WXUNUSED(evt))
void EffectUIHost::OnExport(wxCommandEvent & WXUNUSED(evt)) void EffectUIHost::OnExport(wxCommandEvent & WXUNUSED(evt))
{ {
// may throw // may throw
// exceptions are handled in AudacityApp::OnExceptionInMainLoop
mClient->ExportPresets(); mClient->ExportPresets();
return; return;

View File

@ -1601,27 +1601,16 @@ void EffectEqualization::SaveCurves(const wxString &fileName)
else else
fn = fileName; fn = fileName;
// Create/Open the file GuardedCall< void >( [&] {
XMLFileWriter eqFile; // Create/Open the file
const wxString fullPath{ fn.GetFullPath() }; const wxString fullPath{ fn.GetFullPath() };
XMLFileWriter eqFile{ fullPath, _("Error Saving Equalization Curves") };
try
{
eqFile.Open( fullPath, wxT("wb") );
// Write the curves // Write the curves
WriteXML( eqFile ); WriteXML( eqFile );
// Close the file eqFile.Commit();
eqFile.Close(); } );
}
catch (const XMLFileWriterException &exception)
{
wxMessageBox(wxString::Format(
_("Couldn't write to file \"%s\": %s"),
fullPath.c_str(), exception.GetMessage().c_str()),
_("Error Saving Equalization Curves"), wxICON_ERROR, mUIParent);
}
} }
// //

View File

@ -3562,10 +3562,7 @@ void VSTEffect::SaveFXProgram(wxMemoryBuffer & buf, int index)
void VSTEffect::SaveXML(const wxFileName & fn) void VSTEffect::SaveXML(const wxFileName & fn)
// may throw // may throw
{ {
XMLFileWriter xmlFile; XMLFileWriter xmlFile{ fn.GetFullPath(), _("Error Saving Effect Presets") };
// Create/Open the file
xmlFile.Open(fn.GetFullPath(), wxT("wb"));
xmlFile.StartTag(wxT("vstprogrampersistence")); xmlFile.StartTag(wxT("vstprogrampersistence"));
xmlFile.WriteAttr(wxT("version"), wxT("2")); xmlFile.WriteAttr(wxT("version"), wxT("2"));
@ -3616,10 +3613,7 @@ void VSTEffect::SaveXML(const wxFileName & fn)
xmlFile.EndTag(wxT("vstprogrampersistence")); xmlFile.EndTag(wxT("vstprogrampersistence"));
// Close the file xmlFile.Commit();
xmlFile.Close();
return;
} }
bool VSTEffect::HandleXMLTag(const wxChar *tag, const wxChar **attrs) bool VSTEffect::HandleXMLTag(const wxChar *tag, const wxChar **attrs)

View File

@ -491,12 +491,15 @@ FFmpegPresets::FFmpegPresets()
FFmpegPresets::~FFmpegPresets() FFmpegPresets::~FFmpegPresets()
{ {
XMLFileWriter writer; // We're in a destructor! Don't let exceptions out!
// FIXME: TRAP_ERR Catch XMLFileWriterException GuardedCall< void >( [&] {
wxFileName xmlFileName(FileNames::DataDir(), wxT("ffmpeg_presets.xml")); wxFileName xmlFileName{ FileNames::DataDir(), wxT("ffmpeg_presets.xml") };
writer.Open(xmlFileName.GetFullPath(),wxT("wb")); XMLFileWriter writer{
WriteXMLHeader(writer); xmlFileName.GetFullPath(), _("Error Saving FFmpeg Presets") };
WriteXML(writer); WriteXMLHeader(writer);
WriteXML(writer);
writer.Commit();
} );
} }
void FFmpegPresets::ImportPresets(wxString &filename) void FFmpegPresets::ImportPresets(wxString &filename)
@ -515,11 +518,12 @@ void FFmpegPresets::ImportPresets(wxString &filename)
void FFmpegPresets::ExportPresets(wxString &filename) void FFmpegPresets::ExportPresets(wxString &filename)
{ {
XMLFileWriter writer; GuardedCall< void >( [&] {
// FIXME: TRAP_ERR Catch XMLFileWriterException XMLFileWriter writer{ filename, _("Error Saving FFmpeg Presets") };
writer.Open(filename,wxT("wb")); WriteXMLHeader(writer);
WriteXMLHeader(writer); WriteXML(writer);
WriteXML(writer); writer.Commit();
} );
} }
void FFmpegPresets::GetPresetList(wxArrayString &list) void FFmpegPresets::GetPresetList(wxArrayString &list)

View File

@ -376,20 +376,11 @@ void KeyConfigPrefs::OnExport(wxCommandEvent & WXUNUSED(event))
gPrefs->Write(wxT("/DefaultExportPath"), path); gPrefs->Write(wxT("/DefaultExportPath"), path);
gPrefs->Flush(); gPrefs->Flush();
XMLFileWriter prefFile; GuardedCall< void >( [&] {
XMLFileWriter prefFile{ file, _("Error Exporting Keyboard Shortcuts") };
try
{
prefFile.Open(file, wxT("wb"));
mManager->WriteXML(prefFile); mManager->WriteXML(prefFile);
prefFile.Close(); prefFile.Commit();
} } );
catch (const XMLFileWriterException &)
{
wxMessageBox(_("Couldn't write to file: ") + file,
_("Error Exporting Keyboard Shortcuts"),
wxOK | wxCENTRE, this);
}
} }
void KeyConfigPrefs::OnDefaults(wxCommandEvent & WXUNUSED(event)) void KeyConfigPrefs::OnDefaults(wxCommandEvent & WXUNUSED(event))
@ -957,20 +948,11 @@ void KeyConfigPrefs::OnExport(wxCommandEvent & WXUNUSED(event))
gPrefs->Write(wxT("/DefaultExportPath"), path); gPrefs->Write(wxT("/DefaultExportPath"), path);
gPrefs->Flush(); gPrefs->Flush();
XMLFileWriter prefFile; GuardedCall< void >( [&] {
XMLFileWriter prefFile{ file, _("Error Exporting Keyboard Shortcuts") };
try
{
prefFile.Open(file, wxT("wb"));
mManager->WriteXML(prefFile); mManager->WriteXML(prefFile);
prefFile.Close(); prefFile.Commit();
} } );
catch (const XMLFileWriterException &)
{
wxMessageBox(_("Couldn't write to file: ") + file,
_("Error Exporting Keyboard Shortcuts"),
wxOK | wxCENTRE, this);
}
} }
void KeyConfigPrefs::OnDefaults(wxCommandEvent & WXUNUSED(event)) void KeyConfigPrefs::OnDefaults(wxCommandEvent & WXUNUSED(event))

View File

@ -265,58 +265,107 @@ wxString XMLWriter::XMLEsc(const wxString & s)
/// ///
/// XMLFileWriter class /// XMLFileWriter class
/// ///
XMLFileWriter::XMLFileWriter() XMLFileWriter::XMLFileWriter
( const wxString &outputPath, const wxString &caption, bool keepBackup )
: mOutputPath{ outputPath }
, mCaption{ caption }
, mKeepBackup{ keepBackup }
// may throw
{ {
} auto tempPath = wxFileName::CreateTempFileName( outputPath );
if (!wxFFile::Open(tempPath, wxT("wb")) || !IsOpened())
ThrowException( tempPath, mCaption );
XMLFileWriter::~XMLFileWriter() if (mKeepBackup) {
{ int index = 0;
if (IsOpened()) { wxString backupName;
Close();
do {
wxFileName outputFn{ mOutputPath };
index++;
mBackupName =
outputFn.GetPath() + wxFILE_SEP_PATH +
outputFn.GetName() + wxT("_bak") +
wxString::Format(wxT("%d"), index) + wxT(".") +
outputFn.GetExt();
} while( ::wxFileExists( mBackupName ) );
// Open the backup file to be sure we can write it and reserve it
// until committing
if (! mBackupFile.Open( mBackupName, "wb" ) || ! mBackupFile.IsOpened() )
ThrowException( mBackupName, mCaption );
} }
} }
void XMLFileWriter::Open(const wxString &name, const wxString &mode)
XMLFileWriter::~XMLFileWriter()
{ {
if (!wxFFile::Open(name, mode)) // Don't let a destructor throw!
throw XMLFileWriterException(_("Error Opening File")); GuardedCall< void >( [&] {
if (IsOpened()) {
// Was not committed
auto fileName = GetName();
CloseWithoutEndingTags();
::wxRemoveFile( fileName );
}
} );
} }
void XMLFileWriter::Close() void XMLFileWriter::Commit()
// may throw // may throw
{ {
while (mTagstack.GetCount()) { while (mTagstack.GetCount()) {
EndTag(mTagstack[0]); EndTag(mTagstack[0]);
} }
auto tempPath = GetName();
CloseWithoutEndingTags(); CloseWithoutEndingTags();
if (mKeepBackup) {
if (! mBackupFile.Close() ||
! wxRenameFile( mOutputPath, mBackupName ) )
ThrowException( mBackupName, mCaption );
}
else {
if ( ! wxRemoveFile( mOutputPath ) )
ThrowException( mOutputPath, mCaption );
}
// Now we have vacated the file at the output path and are committed.
// But not completely finished with steps of the commit operation.
// If this step fails, we haven't lost the successfully written data,
// but just failed to put it in the right place.
if (! wxRenameFile( tempPath, mOutputPath ) )
throw FileException{
FileException::Cause::Rename, tempPath, mCaption, mOutputPath
};
} }
void XMLFileWriter::CloseWithoutEndingTags() void XMLFileWriter::CloseWithoutEndingTags()
// may throw
{ {
// Before closing, we first flush it, because if Flush() fails because of a // Before closing, we first flush it, because if Flush() fails because of a
// "disk full" condition, we can still at least try to close the file. // "disk full" condition, we can still at least try to close the file.
if (!wxFFile::Flush()) if (!wxFFile::Flush())
{ {
wxFFile::Close(); wxFFile::Close();
/* i18n-hint: 'flushing' means writing any remaining queued up changes ThrowException( GetName(), mCaption );
* to disk that have not yet been written.*/
throw XMLFileWriterException(_("Error Flushing File"));
} }
// Note that this should never fail if flushing worked. // Note that this should never fail if flushing worked.
if (!wxFFile::Close()) if (!wxFFile::Close())
throw XMLFileWriterException(_("Error Closing File")); ThrowException( GetName(), mCaption );
} }
void XMLFileWriter::Write(const wxString &data) void XMLFileWriter::Write(const wxString &data)
// may throw
{ {
if (!wxFFile::Write(data, wxConvUTF8)) if (!wxFFile::Write(data, wxConvUTF8) || Error())
{ {
// When writing fails, we try to close the file before throwing the // When writing fails, we try to close the file before throwing the
// exception, so it can at least be deleted. // exception, so it can at least be deleted.
wxFFile::Close(); wxFFile::Close();
throw XMLFileWriterException(_("Error Writing to File")); ThrowException( GetName(), mCaption );
} }
} }

View File

@ -14,6 +14,8 @@
#include <wx/dynarray.h> #include <wx/dynarray.h>
#include <wx/ffile.h> #include <wx/ffile.h>
#include "../FileException.h"
/// ///
/// XMLWriter /// XMLWriter
/// ///
@ -60,41 +62,55 @@ class AUDACITY_DLL_API XMLWriter /* not final */ {
/// ///
/// XMLFileWriter /// XMLFileWriter
/// ///
class AUDACITY_DLL_API XMLFileWriter final : public wxFFile, public XMLWriter {
/// This writes to a provisional file, and replaces the previously existing
/// contents by a file rename in Commit() only after all writes succeed.
/// The original contents may also be retained at a backup path name, as
/// directed by the optional constructor argument.
/// If it is destroyed before Commit(), then the provisional file is removed.
/// If the construction and all operations are inside a GuardedCall or event
/// handler, then the default delayed handler action in case of exceptions will
/// notify the user of problems.
class AUDACITY_DLL_API XMLFileWriter final : private wxFFile, public XMLWriter {
public: public:
XMLFileWriter(); /// The caption is for message boxes to show in case of errors.
/// Might throw.
XMLFileWriter
( const wxString &outputPath, const wxString &caption,
bool keepBackup = false );
virtual ~XMLFileWriter(); virtual ~XMLFileWriter();
/// Open the file. Might throw XMLFileWriterException. /// Close all tags and then close the file.
void Open(const wxString &name, const wxString &mode); /// Might throw. If not, then create
/// or modify the file at the output path.
void Commit();
/// Close file. Might throw XMLFileWriterException. /// Write to file. Might throw.
void Close();
/// Close file without automatically ending tags.
/// Might throw XMLFileWriterException.
void CloseWithoutEndingTags(); // for auto-save files
/// Write to file. Might throw XMLFileWriterException.
void Write(const wxString &data) override; void Write(const wxString &data) override;
wxString GetBackupName() const { return mBackupName; }
private: private:
}; void ThrowException(
const wxFileName &fileName, const wxString &caption)
{
throw FileException{ FileException::Cause::Write, fileName, caption };
}
/// /// Close file without automatically ending tags.
/// Exception thrown by various XMLFileWriter methods /// Might throw.
/// void CloseWithoutEndingTags(); // for auto-save files
class XMLFileWriterException
{
public:
XMLFileWriterException(const wxString& message) { mMessage = message; }
wxString GetMessage() const { return mMessage; }
protected: const wxString mOutputPath;
wxString mMessage; const wxString mCaption;
wxString mBackupName;
const bool mKeepBackup;
wxFFile mBackupFile;
}; };
/// ///