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;
}
XMLFileWriter out;
wxString tempName;
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 >;
using WxChars = ArrayOf < wxChar >;
Chars buf{ len };
return false;
}
if (file.Read(buf.get(), len) != len)
{
return false;
}
wxMemoryInputStream in(buf.get(), len);
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.
tempName = fn.CreateTempFileName(fnPath);
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;
}
IdMap mIds;
IdMapArray mIdStack;
mIds.clear();
while (!in.Eof() && !out.Error())
while ( !in.Eof() )
{
short id;
switch (in.GetC())
{
case FT_Push:
{
mIdStack.Add(mIds);
mIds.clear();
}
break;
case FT_Push:
{
mIdStack.Add(mIds);
mIds.clear();
}
break;
case FT_Pop:
{
mIds = mIdStack[mIdStack.GetCount() - 1];
mIdStack.RemoveAt(mIdStack.GetCount() - 1);
}
break;
case FT_Pop:
{
mIds = mIdStack[mIdStack.GetCount() - 1];
mIdStack.RemoveAt(mIdStack.GetCount() - 1);
}
break;
case FT_Name:
{
short len;
case FT_Name:
{
short len;
in.Read(&id, sizeof(id));
in.Read(&len, sizeof(len));
WxChars name{ len / sizeof(wxChar) };
in.Read(name.get(), len);
in.Read(&id, sizeof(id));
in.Read(&len, sizeof(len));
WxChars name{ len / sizeof(wxChar) };
in.Read(name.get(), len);
mIds[id] = wxString(name.get(), len / sizeof(wxChar));
}
break;
mIds[id] = wxString(name.get(), len / sizeof(wxChar));
}
break;
case FT_StartTag:
{
in.Read(&id, sizeof(id));
case FT_StartTag:
{
in.Read(&id, sizeof(id));
out.StartTag(mIds[id]);
}
break;
out.StartTag(mIds[id]);
}
break;
case FT_EndTag:
{
in.Read(&id, sizeof(id));
case FT_EndTag:
{
in.Read(&id, sizeof(id));
out.EndTag(mIds[id]);
}
break;
out.EndTag(mIds[id]);
}
break;
case FT_String:
{
int len;
case FT_String:
{
int len;
in.Read(&id, sizeof(id));
in.Read(&len, sizeof(len));
WxChars val{ len / sizeof(wxChar) };
in.Read(val.get(), len);
in.Read(&id, sizeof(id));
in.Read(&len, sizeof(len));
WxChars val{ len / sizeof(wxChar) };
in.Read(val.get(), len);
out.WriteAttr(mIds[id], wxString(val.get(), len / sizeof(wxChar)));
}
break;
out.WriteAttr(mIds[id], wxString(val.get(), len / sizeof(wxChar)));
}
break;
case FT_Float:
{
float val;
int dig;
case FT_Float:
{
float val;
int dig;
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
in.Read(&dig, sizeof(dig));
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
in.Read(&dig, sizeof(dig));
out.WriteAttr(mIds[id], val, dig);
}
break;
out.WriteAttr(mIds[id], val, dig);
}
break;
case FT_Double:
{
double val;
int dig;
case FT_Double:
{
double val;
int dig;
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
in.Read(&dig, sizeof(dig));
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
in.Read(&dig, sizeof(dig));
out.WriteAttr(mIds[id], val, dig);
}
break;
out.WriteAttr(mIds[id], val, dig);
}
break;
case FT_Int:
{
int val;
case FT_Int:
{
int val;
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
out.WriteAttr(mIds[id], val);
}
break;
out.WriteAttr(mIds[id], val);
}
break;
case FT_Bool:
{
bool val;
case FT_Bool:
{
bool val;
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
out.WriteAttr(mIds[id], val);
}
break;
out.WriteAttr(mIds[id], val);
}
break;
case FT_Long:
{
long val;
case FT_Long:
{
long val;
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
out.WriteAttr(mIds[id], val);
}
break;
out.WriteAttr(mIds[id], val);
}
break;
case FT_LongLong:
{
long long val;
case FT_LongLong:
{
long long val;
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
out.WriteAttr(mIds[id], val);
}
break;
out.WriteAttr(mIds[id], val);
}
break;
case FT_SizeT:
{
size_t val;
case FT_SizeT:
{
size_t val;
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
in.Read(&id, sizeof(id));
in.Read(&val, sizeof(val));
out.WriteAttr(mIds[id], val);
}
break;
out.WriteAttr(mIds[id], val);
}
break;
case FT_Data:
{
int len;
case FT_Data:
{
int len;
in.Read(&len, sizeof(len));
WxChars val{ len / sizeof(wxChar) };
in.Read(val.get(), len);
in.Read(&len, sizeof(len));
WxChars val{ len / sizeof(wxChar) };
in.Read(val.get(), len);
out.WriteData(wxString(val.get(), len / sizeof(wxChar)));
}
break;
out.WriteData(wxString(val.get(), len / sizeof(wxChar)));
}
break;
case FT_Raw:
{
int len;
case FT_Raw:
{
int len;
in.Read(&len, sizeof(len));
WxChars val{ len / sizeof(wxChar) };
in.Read(val.get(), len);
in.Read(&len, sizeof(len));
WxChars val{ len / sizeof(wxChar) };
in.Read(val.get(), len);
out.Write(wxString(val.get(), len / sizeof(wxChar)));
}
break;
out.Write(wxString(val.get(), len / sizeof(wxChar)));
}
break;
default:
wxASSERT(true);
default:
wxASSERT(true);
break;
}
}
}
bool error = out.Error();
out.Close();
out.Commit();
// Bail if decoding failed.
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;
return true;
} );
}

View File

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

View File

@ -44,44 +44,6 @@ On failure the old version is put back in place.
#include "Legacy.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)
// may throw
{
@ -291,42 +253,15 @@ static bool ConvertLegacyTrack(wxTextFile *f, XMLFileWriter &xmlFile)
bool ConvertLegacyProjectFile(const wxFileName &filename)
{
wxTextFile f;
XMLFileWriter xmlFile;
int index = 0;
wxString backupName;
do {
index++;
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);
const wxString name = filename.GetFullPath();
f.Open( name );
if (!f.IsOpened())
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"));
wxString label;
@ -360,19 +295,15 @@ bool ConvertLegacyProjectFile(const wxFileName &filename)
label = f.GetNextLine();
}
// Close original before Commit() tries to overwrite it.
f.Close();
xmlFile.EndTag(wxT("audacityproject"));
xmlFile.Close();
}
catch (const XMLFileWriterException&)
{
// Error writing XML file (e.g. disk full)
return false;
}
xmlFile.Commit();
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()),
_("Opening Audacity Project"));
return true;
return true;
} );
}

View File

@ -3803,37 +3803,21 @@ bool AudacityProject::Save(bool overwrite /* = true */ ,
}
}
// Write the AUP file.
XMLFileWriter saveFile;
try
{
saveFile.Open(mFileName, wxT("wb"));
auto success = GuardedCall< bool >( [&] {
// Write the AUP file.
XMLFileWriter saveFile{ mFileName, _("Error Saving Project") };
WriteXMLHeader(saveFile);
WriteXML(saveFile);
mStrOtherNamesArray.Clear();
saveFile.Close();
}
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);
saveFile.Commit();
// When XMLWriter throws an exception, it tries to close it before,
// 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);
}
return true;
} );
if (!success)
return false;
}
if (bWantSaveCompressed)
mWantSaveCompressed = false; // Don't want this mode for AudacityProject::WriteXML() any more.
@ -5103,7 +5087,9 @@ void AudacityProject::AutoSave()
wxString fn = wxFileName(FileNames::AutoSaveDir(),
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);
@ -5114,18 +5100,11 @@ void AudacityProject::AutoSave()
wxFFile saveFile;
saveFile.Open(fn + wxT(".tmp"), wxT("wb"));
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);
return buffer.Write(saveFile);
} );
if (!success)
return;
}
// Now that we have a NEW auto-save file, DELETE the old one
DeleteCurrentAutoSaveFile();

View File

@ -1219,12 +1219,9 @@ void TagsEditor::OnSave(wxCommandEvent & WXUNUSED(event))
return;
}
// Create/Open the file
XMLFileWriter writer;
try
{
writer.Open(fn, wxT("wb"));
GuardedCall< void >( [&] {
// Create/Open the file
XMLFileWriter writer{ fn, _("Error Saving Tags File") };
// Remember title and track in case they're read only
wxString title = mLocal.GetTag(TAG_TITLE);
@ -1240,29 +1237,23 @@ void TagsEditor::OnSave(wxCommandEvent & WXUNUSED(event))
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
mLocal.WriteXML(writer);
// Restore title
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);
}
writer.Commit();
} );
}
void TagsEditor::OnSaveDefaults(wxCommandEvent & WXUNUSED(event))

View File

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

View File

@ -1601,27 +1601,16 @@ void EffectEqualization::SaveCurves(const wxString &fileName)
else
fn = fileName;
// Create/Open the file
XMLFileWriter eqFile;
const wxString fullPath{ fn.GetFullPath() };
try
{
eqFile.Open( fullPath, wxT("wb") );
GuardedCall< void >( [&] {
// Create/Open the file
const wxString fullPath{ fn.GetFullPath() };
XMLFileWriter eqFile{ fullPath, _("Error Saving Equalization Curves") };
// Write the curves
WriteXML( eqFile );
// Close the file
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);
}
eqFile.Commit();
} );
}
//

View File

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

View File

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

View File

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

View File

@ -265,58 +265,107 @@ wxString XMLWriter::XMLEsc(const wxString & s)
///
/// 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 (IsOpened()) {
Close();
if (mKeepBackup) {
int index = 0;
wxString backupName;
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))
throw XMLFileWriterException(_("Error Opening File"));
// Don't let a destructor throw!
GuardedCall< void >( [&] {
if (IsOpened()) {
// Was not committed
auto fileName = GetName();
CloseWithoutEndingTags();
::wxRemoveFile( fileName );
}
} );
}
void XMLFileWriter::Close()
void XMLFileWriter::Commit()
// may throw
{
while (mTagstack.GetCount()) {
EndTag(mTagstack[0]);
}
auto tempPath = GetName();
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()
// may throw
{
// 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.
if (!wxFFile::Flush())
{
wxFFile::Close();
/* i18n-hint: 'flushing' means writing any remaining queued up changes
* to disk that have not yet been written.*/
throw XMLFileWriterException(_("Error Flushing File"));
ThrowException( GetName(), mCaption );
}
// Note that this should never fail if flushing worked.
if (!wxFFile::Close())
throw XMLFileWriterException(_("Error Closing File"));
ThrowException( GetName(), mCaption );
}
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
// exception, so it can at least be deleted.
wxFFile::Close();
throw XMLFileWriterException(_("Error Writing to File"));
ThrowException( GetName(), mCaption );
}
}

View File

@ -14,6 +14,8 @@
#include <wx/dynarray.h>
#include <wx/ffile.h>
#include "../FileException.h"
///
/// XMLWriter
///
@ -60,41 +62,55 @@ class AUDACITY_DLL_API XMLWriter /* not final */ {
///
/// 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:
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();
/// Open the file. Might throw XMLFileWriterException.
void Open(const wxString &name, const wxString &mode);
/// Close all tags and then close the file.
/// Might throw. If not, then create
/// or modify the file at the output path.
void Commit();
/// Close file. Might throw XMLFileWriterException.
void Close();
/// Close file without automatically ending tags.
/// Might throw XMLFileWriterException.
void CloseWithoutEndingTags(); // for auto-save files
/// Write to file. Might throw XMLFileWriterException.
/// Write to file. Might throw.
void Write(const wxString &data) override;
wxString GetBackupName() const { return mBackupName; }
private:
};
void ThrowException(
const wxFileName &fileName, const wxString &caption)
{
throw FileException{ FileException::Cause::Write, fileName, caption };
}
///
/// Exception thrown by various XMLFileWriter methods
///
class XMLFileWriterException
{
public:
XMLFileWriterException(const wxString& message) { mMessage = message; }
wxString GetMessage() const { return mMessage; }
/// Close file without automatically ending tags.
/// Might throw.
void CloseWithoutEndingTags(); // for auto-save files
protected:
wxString mMessage;
const wxString mOutputPath;
const wxString mCaption;
wxString mBackupName;
const bool mKeepBackup;
wxFFile mBackupFile;
};
///