Merge branch 'DRTVWR-591-maint-X' of https://github.com/secondlife/viewer

# Conflicts:
#	indra/llrender/llrender.cpp
#	indra/newview/llreflectionmap.cpp
#	indra/newview/llviewerassetupload.cpp
#	indra/newview/llviewerwindow.cpp
master
Ansariel 2024-03-05 03:31:18 +01:00
commit 5c93f37775
30 changed files with 145 additions and 70 deletions

View File

@ -584,8 +584,7 @@ static void bilinear_scale(const U8 *src, U32 srcW, U32 srcH, U32 srcCh, U32 src
//---------------------------------------------------------------------------
//static
std::string LLImage::sLastErrorMessage;
LLMutex* LLImage::sMutex = NULL;
thread_local std::string LLImage::sLastThreadErrorMessage;
bool LLImage::sUseNewByteRange = false;
S32 LLImage::sMinimalReverseByteRangePercent = 75;
@ -594,28 +593,24 @@ void LLImage::initClass(bool use_new_byte_range, S32 minimal_reverse_byte_range_
{
sUseNewByteRange = use_new_byte_range;
sMinimalReverseByteRangePercent = minimal_reverse_byte_range_percent;
sMutex = new LLMutex();
}
//static
void LLImage::cleanupClass()
{
delete sMutex;
sMutex = NULL;
}
//static
const std::string& LLImage::getLastError()
const std::string& LLImage::getLastThreadError()
{
static const std::string noerr("No Error");
return sLastErrorMessage.empty() ? noerr : sLastErrorMessage;
return sLastThreadErrorMessage.empty() ? noerr : sLastThreadErrorMessage;
}
//static
void LLImage::setLastError(const std::string& message)
{
LLMutexLock m(sMutex);
sLastErrorMessage = message;
sLastThreadErrorMessage = message;
}
//---------------------------------------------------------------------------

View File

@ -95,15 +95,14 @@ public:
static void initClass(bool use_new_byte_range = false, S32 minimal_reverse_byte_range_percent = 75);
static void cleanupClass();
static const std::string& getLastError();
static const std::string& getLastThreadError();
static void setLastError(const std::string& message);
static bool useNewByteRange() { return sUseNewByteRange; }
static S32 getReverseByteRangePercent() { return sMinimalReverseByteRangePercent; }
protected:
static LLMutex* sMutex;
static std::string sLastErrorMessage;
static thread_local std::string sLastThreadErrorMessage;
static bool sUseNewByteRange;
static S32 sMinimalReverseByteRangePercent;
};

View File

@ -55,6 +55,7 @@ private:
BOOL mDecodedRaw;
BOOL mDecodedAux;
LLPointer<LLImageDecodeThread::Responder> mResponder;
std::string mErrorString;
};
@ -149,8 +150,9 @@ ImageRequest::~ImageRequest()
bool ImageRequest::processRequest()
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
const F32 decode_time_slice = 0.f; //disable time slicing
bool done = true;
const F32 decode_time_slice = 0.f; //disable time slicing
bool done = true;
mErrorString.clear();
if (!mDecodedRaw && mFormattedImage.notNull())
{
// Decode primary channels
@ -159,10 +161,13 @@ bool ImageRequest::processRequest()
// parse formatted header
if (!mFormattedImage->updateData())
{
// Pick up errors from updateData
mErrorString = LLImage::getLastThreadError();
return true; // done (failed)
}
if (0 == (mFormattedImage->getWidth() * mFormattedImage->getHeight() * mFormattedImage->getComponents()))
{
mErrorString = "Invalid image size";
return true; // done (failed)
}
if (mDiscardLevel >= 0)
@ -188,6 +193,9 @@ bool ImageRequest::processRequest()
// some decoders are removing data when task is complete and there were errors
mDecodedRaw = done && mDecodedImageRaw->getData();
// Pick up errors from decoding
mErrorString = LLImage::getLastThreadError();
}
if (done && mNeedsAux && !mDecodedAux && mFormattedImage.notNull())
{
@ -200,7 +208,10 @@ bool ImageRequest::processRequest()
}
done = mFormattedImage->decodeChannels(mDecodedImageAux, decode_time_slice, 4, 4);
mDecodedAux = done && mDecodedImageAux->getData();
}
// Pick up errors from decoding
mErrorString = LLImage::getLastThreadError();
}
return done;
}
@ -211,7 +222,7 @@ void ImageRequest::finishRequest(bool completed)
if (mResponder.notNull())
{
bool success = completed && mDecodedRaw && (!mNeedsAux || mDecodedAux);
mResponder->completed(success, mDecodedImageRaw, mDecodedImageAux);
mResponder->completed(success, mErrorString, mDecodedImageRaw, mDecodedImageAux);
}
// Will automatically be deleted
}

View File

@ -39,7 +39,7 @@ public:
protected:
virtual ~Responder();
public:
virtual void completed(bool success, LLImageRaw* raw, LLImageRaw* aux) = 0;
virtual void completed(bool success, const std::string& error_message, LLImageRaw* raw, LLImageRaw* aux) = 0;
};
public:

View File

@ -862,7 +862,7 @@ LLRender::~LLRender()
shutdown();
}
void LLRender::init(bool needs_vertex_buffer)
bool LLRender::init(bool needs_vertex_buffer)
{
#if LL_WINDOWS
if (gGLManager.mHasDebugOutput && gDebugGL)
@ -884,6 +884,13 @@ void LLRender::init(bool needs_vertex_buffer)
// necessary for reflection maps
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
#if LL_WINDOWS
if (glGenVertexArrays == nullptr)
{
return false;
}
#endif
{ //bind a dummy vertex array object so we're core profile compliant
U32 ret;
glGenVertexArrays(1, &ret);
@ -904,6 +911,7 @@ void LLRender::init(bool needs_vertex_buffer)
stop_glerror();
mMaxLineWidthSmooth = range[1];
// </FS:Ansariel>
return true;
}
void LLRender::initVertexBuffer()

View File

@ -386,7 +386,7 @@ public:
LLRender();
~LLRender();
void init(bool needs_vertex_buffer);
bool init(bool needs_vertex_buffer);
void initVertexBuffer();
void resetVertexBuffer();
void shutdown();

View File

@ -191,6 +191,8 @@ LLComboBox::~LLComboBox()
// explicitly disconect this signal, since base class destructor might fire top lost
mTopLostSignalConnection.disconnect();
mImageLoadedConnection.disconnect();
LLUI::getInstance()->removePopup(this);
}

View File

@ -260,7 +260,13 @@ LLFolderView::LLFolderView(const Params& p)
// Destroys the object
LLFolderView::~LLFolderView( void )
{
closeRenamer();
mRenamerTopLostSignalConnection.disconnect();
if (mRenamer)
{
// instead of using closeRenamer remove it directly,
// since it might already be hidden
LLUI::getInstance()->removePopup(mRenamer);
}
// The release focus call can potentially call the
// scrollcontainer, which can potentially be called with a partly
@ -1109,7 +1115,10 @@ void LLFolderView::startRenamingSelectedItem( void )
mRenamer->setVisible( TRUE );
// set focus will fail unless item is visible
mRenamer->setFocus( TRUE );
mRenamer->setTopLostCallback(boost::bind(&LLFolderView::onRenamerLost, this));
if (!mRenamerTopLostSignalConnection.connected())
{
mRenamerTopLostSignalConnection = mRenamer->setTopLostCallback(boost::bind(&LLFolderView::onRenamerLost, this));
}
LLUI::getInstance()->addPopup(mRenamer);
}
}
@ -1692,7 +1701,11 @@ BOOL LLFolderView::handleDragAndDrop(S32 x, S32 y, MASK mask, BOOL drop,
void LLFolderView::deleteAllChildren()
{
closeRenamer();
mRenamerTopLostSignalConnection.disconnect();
if (mRenamer)
{
LLUI::getInstance()->removePopup(mRenamer);
}
if (mPopupMenuHandle.get()) mPopupMenuHandle.get()->die();
mPopupMenuHandle.markDead();
mScrollContainer = NULL;

View File

@ -354,6 +354,8 @@ protected:
LLUICtrl::CommitCallbackRegistry::ScopedRegistrar* mCallbackRegistrar;
LLUICtrl::EnableCallbackRegistry::ScopedRegistrar* mEnableRegistrar;
boost::signals2::connection mRenamerTopLostSignalConnection;
bool mForceArrange;
public:

View File

@ -67,6 +67,8 @@ LLModalDialog::~LLModalDialog()
{
LL_ERRS() << "Attempt to delete dialog while still in sModalStack!" << LL_ENDL;
}
LLUI::getInstance()->removePopup(this);
}
// virtual

View File

@ -1683,7 +1683,9 @@ const S32 max_format = (S32)num_formats - 1;
}
else
{
LL_WARNS("Window") << "No wgl_ARB_pixel_format extension, using default ChoosePixelFormat!" << LL_ENDL;
LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBVideoDrvErr"));
// mWindowHandle is 0, going to crash either way
LL_ERRS("Window") << "No wgl_ARB_pixel_format extension!" << LL_ENDL;
}
// Verify what pixel format we actually received.
@ -1937,12 +1939,16 @@ void LLWindowWin32::destroySharedContext(void* contextPtr)
void LLWindowWin32::toggleVSync(bool enable_vsync)
{
if (!enable_vsync && wglSwapIntervalEXT)
if (wglSwapIntervalEXT == nullptr)
{
LL_INFOS("Window") << "VSync: wglSwapIntervalEXT not initialized" << LL_ENDL;
}
else if (!enable_vsync)
{
LL_INFOS("Window") << "Disabling vertical sync" << LL_ENDL;
wglSwapIntervalEXT(0);
}
else if (wglSwapIntervalEXT)
else
{
LL_INFOS("Window") << "Enabling vertical sync" << LL_ENDL;
wglSwapIntervalEXT(1);

View File

@ -58,16 +58,19 @@ void LLAccountingCostManager::accountingCostCoro(std::string url,
try
{
LLAccountingCostManager* self = LLAccountingCostManager::getInstance();
uuid_set_t diffSet;
std::set_difference(mObjectList.begin(), mObjectList.end(),
mPendingObjectQuota.begin(), mPendingObjectQuota.end(),
std::inserter(diffSet, diffSet.begin()));
std::set_difference(self->mObjectList.begin(),
self->mObjectList.end(),
self->mPendingObjectQuota.begin(),
self->mPendingObjectQuota.end(),
std::inserter(diffSet, diffSet.begin()));
if (diffSet.empty())
return;
mObjectList.clear();
self->mObjectList.clear();
std::string keystr;
if (selectionType == Roots)
@ -91,18 +94,25 @@ void LLAccountingCostManager::accountingCostCoro(std::string url,
objectList.append(*it);
}
mPendingObjectQuota.insert(diffSet.begin(), diffSet.end());
self->mPendingObjectQuota.insert(diffSet.begin(), diffSet.end());
LLSD dataToPost = LLSD::emptyMap();
dataToPost[keystr.c_str()] = objectList;
LLAccountingCostObserver* observer = NULL;
LLSD results = httpAdapter->postAndSuspend(httpRequest, url, dataToPost);
LLSD httpResults = results["http_result"];
LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
if (LLApp::isQuitting()
|| observerHandle.isDead()
|| !LLAccountingCostManager::instanceExists())
{
return;
}
LLAccountingCostObserver* observer = NULL;
// do/while(false) allows error conditions to break out of following
// block while normal flow goes forward once.
do
@ -159,7 +169,8 @@ void LLAccountingCostManager::accountingCostCoro(std::string url,
throw;
}
mPendingObjectQuota.clear();
// self can be obsolete by this point
LLAccountingCostManager::getInstance()->mPendingObjectQuota.clear();
}
//===============================================================================
@ -172,7 +183,7 @@ void LLAccountingCostManager::fetchCosts( eSelectionType selectionType,
{
std::string coroname =
LLCoros::instance().launch("LLAccountingCostManager::accountingCostCoro",
boost::bind(&LLAccountingCostManager::accountingCostCoro, this, url, selectionType, observer_handle));
boost::bind(accountingCostCoro, url, selectionType, observer_handle));
LL_DEBUGS() << coroname << " with url '" << url << LL_ENDL;
}

View File

@ -70,7 +70,7 @@ private:
//a fetch has been instigated.
uuid_set_t mPendingObjectQuota;
void accountingCostCoro(std::string url, eSelectionType selectionType, const LLHandle<LLAccountingCostObserver> observerHandle);
static void accountingCostCoro(std::string url, eSelectionType selectionType, const LLHandle<LLAccountingCostObserver> observerHandle);
};
//===============================================================================

View File

@ -108,6 +108,8 @@ LLConversationViewSession::~LLConversationViewSession()
}
mFlashTimer->unset();
delete mFlashTimer;
mFlashStateOn = false;
}
void LLConversationViewSession::destroyView()

View File

@ -243,6 +243,12 @@ LLExpandableTextBox::LLExpandableTextBox(const Params& p)
mTextBox->setCommitCallback(boost::bind(&LLExpandableTextBox::onExpandClicked, this));
}
LLExpandableTextBox::~LLExpandableTextBox()
{
gViewerWindow->removePopup(this);
}
void LLExpandableTextBox::draw()
{
if(mBGVisible && !mExpanded)

View File

@ -154,6 +154,8 @@ public:
*/
/*virtual*/ void draw();
virtual ~LLExpandableTextBox();
protected:
LLExpandableTextBox(const Params& p);

View File

@ -370,7 +370,7 @@ void LLFloaterSimpleSnapshot::onSend()
else
{
LLSD notif_args;
notif_args["REASON"] = LLImage::getLastError().c_str();
notif_args["REASON"] = LLImage::getLastThreadError().c_str();
LLNotificationsUtil::add("CannotUploadTexture", notif_args);
}
}
@ -389,7 +389,7 @@ void LLFloaterSimpleSnapshot::uploadThumbnail(const std::string &file_path, cons
if (!LLViewerTextureList::createUploadFile(file_path, temp_file, codec, THUMBNAIL_SNAPSHOT_DIM_MAX, THUMBNAIL_SNAPSHOT_DIM_MIN, true))
{
LLSD notif_args;
notif_args["REASON"] = LLImage::getLastError().c_str();
notif_args["REASON"] = LLImage::getLastThreadError().c_str();
LLNotificationsUtil::add("CannotUploadTexture", notif_args);
LL_WARNS("Thumbnail") << "Failed to upload thumbnail for " << inventory_id << " " << task_id << ", reason: " << notif_args["REASON"].asString() << LL_ENDL;
return;
@ -404,7 +404,7 @@ void LLFloaterSimpleSnapshot::uploadThumbnail(LLPointer<LLImageRaw> raw_image, c
if (!LLViewerTextureList::createUploadFile(raw_image, temp_file, THUMBNAIL_SNAPSHOT_DIM_MAX, THUMBNAIL_SNAPSHOT_DIM_MIN))
{
LLSD notif_args;
notif_args["REASON"] = LLImage::getLastError().c_str();
notif_args["REASON"] = LLImage::getLastThreadError().c_str();
LLNotificationsUtil::add("CannotUploadTexture", notif_args);
LL_WARNS("Thumbnail") << "Failed to upload thumbnail for " << inventory_id << " " << task_id << ", reason: " << notif_args["REASON"].asString() << LL_ENDL;
return;

View File

@ -644,13 +644,13 @@ void LLModelPreview::rebuildUploadData()
// That's ok, but might not what they wanted. Use default_physics_shape if found.
std::ostringstream out;
out << "No physics model specified for " << instance.mLabel;
if (mDefaultPhysicsShapeP)
if (mDefaultPhysicsShapeP.notNull())
{
out << " - using: " << DEFAULT_PHYSICS_MESH_NAME;
lod_model = mDefaultPhysicsShapeP;
}
LL_WARNS() << out.str() << LL_ENDL;
LLFloaterModelPreview::addStringToLog(out, !mDefaultPhysicsShapeP); // Flash log tab if no default.
LLFloaterModelPreview::addStringToLog(out, mDefaultPhysicsShapeP.isNull()); // Flash log tab if no default.
}
if (lod_model)
@ -1381,8 +1381,9 @@ void LLModelPreview::loadModelCallback(S32 loaded_lod)
if (loaded_lod == LLModel::LOD_PHYSICS)
{ // Explicitly loading physics. See if there is a default mesh.
LLMatrix4 ignored_transform; // Each mesh that uses this will supply their own.
mDefaultPhysicsShapeP = nullptr;
FindModel(mScene[loaded_lod], DEFAULT_PHYSICS_MESH_NAME + getLodSuffix(loaded_lod), mDefaultPhysicsShapeP, ignored_transform);
LLModel* out_model = nullptr;
FindModel(mScene[loaded_lod], DEFAULT_PHYSICS_MESH_NAME + getLodSuffix(loaded_lod), out_model, ignored_transform);
mDefaultPhysicsShapeP = out_model;
mWarnOfUnmatchedPhyicsMeshes = true;
}
BOOL legacyMatching = gSavedSettings.getBOOL("ImporterLegacyMatching");

View File

@ -246,7 +246,7 @@ private:
/// It is set only when the user chooses a physics shape file that contains a mesh with a name that matches DEFAULT_PHYSICS_MESH_NAME.
/// It is reset when such a name is not found, and when resetting the modelpreview.
/// Not read unless mWarnOfUnmatchedPhyicsMeshes is true.
LLModel* mDefaultPhysicsShapeP{};
LLPointer<LLModel> mDefaultPhysicsShapeP;
typedef enum
{

View File

@ -1858,7 +1858,7 @@ void LLProfileImagePicker::notify(const std::vector<std::string>& filenames)
if (!LLViewerTextureList::createUploadFile(file_path, temp_file, codec, MAX_DIM))
{
LLSD notif_args;
notif_args["REASON"] = LLImage::getLastError().c_str();
notif_args["REASON"] = LLImage::getLastThreadError().c_str();
LLNotificationsUtil::add("CannotUploadTexture", notif_args);
LL_WARNS("AvatarProperties") << "Failed to upload profile image of type " << (S32)mType << ", " << notif_args["REASON"].asString() << LL_ENDL;
return;

View File

@ -256,16 +256,16 @@ void LLPopupView::removePopup(LLView* popup)
void LLPopupView::clearPopups()
{
for (popup_list_t::iterator popup_it = mPopups.begin();
popup_it != mPopups.end();)
while (!mPopups.empty())
{
LLView* popup = popup_it->get();
popup_list_t::iterator cur_popup_it = popup_it;
++popup_it;
mPopups.erase(cur_popup_it);
popup->onTopLost();
popup_list_t::iterator popup_it = mPopups.begin();
LLView* popup = popup_it->get();
// Remove before notifying in case it will cause removePopup
mPopups.erase(popup_it);
if (popup)
{
popup->onTopLost();
}
}
}

View File

@ -74,11 +74,9 @@ void LLReflectionMap::autoAdjustOrigin()
{
const LLVector4a* bounds = mGroup->getBounds();
auto* node = mGroup->getOctreeNode();
// <FS:Beq> Bugsplat-Fix
// if (mGroup->getSpatialPartition()->mPartitionType == LLViewerRegion::PARTITION_VOLUME)
const auto* partition = mGroup->getSpatialPartition();
if (partition && partition->mPartitionType == LLViewerRegion::PARTITION_VOLUME)
// </FS:Beq>
LLSpatialPartition* part = mGroup->getSpatialPartition();
if (part && part->mPartitionType == LLViewerRegion::PARTITION_VOLUME)
{
mPriority = 0;
// cast a ray towards 8 corners of bounding box

View File

@ -243,7 +243,14 @@ LLSplitButton::LLSplitButton(const LLSplitButton::Params& p)
item_top -= (rc.getHeight() + BUTTON_PAD);
}
setTopLostCallback(boost::bind(&LLSplitButton::hideButtons, this));
mTopLostSignalConnection = setTopLostCallback(boost::bind(&LLSplitButton::hideButtons, this));
}
LLSplitButton::~LLSplitButton()
{
// explicitly disconect to avoid hideButtons with
// dead pointers being called on destruction
mTopLostSignalConnection.disconnect();
}

View File

@ -67,7 +67,7 @@ public:
};
virtual ~LLSplitButton() {};
virtual ~LLSplitButton();
//Overridden
virtual void onFocusLost();
@ -99,6 +99,8 @@ protected:
LLButton* mShownItem;
EArrowPosition mArrowPosition;
boost::signals2::connection mTopLostSignalConnection;
commit_callback_t mSelectionCallback;
};

View File

@ -352,13 +352,13 @@ private:
}
// Threads: Tid
virtual void completed(bool success, LLImageRaw* raw, LLImageRaw* aux)
virtual void completed(bool success, const std::string& error_message, LLImageRaw* raw, LLImageRaw* aux)
{
LL_PROFILE_ZONE_SCOPED;
LLTextureFetchWorker* worker = mFetcher->getWorker(mID);
if (worker)
{
worker->callbackDecoded(success, raw, aux);
worker->callbackDecoded(success, error_message, raw, aux);
}
}
private:
@ -402,7 +402,7 @@ public:
void callbackCacheWrite(bool success);
// Threads: Tid
void callbackDecoded(bool success, LLImageRaw* raw, LLImageRaw* aux);
void callbackDecoded(bool success, const std::string& error_message, LLImageRaw* raw, LLImageRaw* aux);
// Threads: T*
void setGetStatus(LLCore::HttpStatus status, const std::string& reason)
@ -2530,7 +2530,7 @@ void LLTextureFetchWorker::callbackCacheWrite(bool success)
//////////////////////////////////////////////////////////////////////////////
// Threads: Tid
void LLTextureFetchWorker::callbackDecoded(bool success, LLImageRaw* raw, LLImageRaw* aux)
void LLTextureFetchWorker::callbackDecoded(bool success, const std::string &error_message, LLImageRaw* raw, LLImageRaw* aux)
{
LLMutexLock lock(&mWorkMutex); // +Mw
if (mDecodeHandle == 0)
@ -2557,7 +2557,7 @@ void LLTextureFetchWorker::callbackDecoded(bool success, LLImageRaw* raw, LLImag
}
else
{
LL_WARNS(LOG_TXT) << "DECODE FAILED: " << mID << " Discard: " << (S32)mFormattedImage->getDiscardLevel() << LL_ENDL;
LL_WARNS(LOG_TXT) << "DECODE FAILED: " << mID << " Discard: " << (S32)mFormattedImage->getDiscardLevel() << ", reason: " << error_message << LL_ENDL;
removeFromCache();
mDecodedDiscard = -1; // Redundant, here for clarity and paranoia
}

View File

@ -150,6 +150,7 @@ void LLUploadDialog::setMessage( const std::string& msg)
LLUploadDialog::~LLUploadDialog()
{
gViewerWindow->removePopup(this);
gFocusMgr.releaseFocusIfNeeded( this );
// LLFilePicker::instance().reset();

View File

@ -435,8 +435,8 @@ LLSD LLNewFileResourceUploadInfo::exportTempFile()
{
// <FS:Ansariel> Duplicate error message output
//errorMessage = llformat("Problem with file %s:\n\n%s\n",
// getFileName().c_str(), LLImage::getLastError().c_str());
errorMessage = LLImage::getLastError();
// getFileName().c_str(), LLImage::getLastThreadError().c_str());
errorMessage = LLImage::getLastThreadError();
// </FS:Ansariel>
errorLabel = "ProblemWithFile";
error = true;

View File

@ -1027,7 +1027,7 @@ void handle_compress_image(void*)
}
else
{
LL_INFOS() << "Compression failed: " << LLImage::getLastError() << LL_ENDL;
LL_INFOS() << "Compression failed: " << LLImage::getLastThreadError() << LL_ENDL;
}
infile = picker.getNextFile();

View File

@ -2067,7 +2067,11 @@ LLViewerWindow::LLViewerWindow(const Params& p)
// Initialize OpenGL Renderer
LLVertexBuffer::initClass(mWindow);
LL_INFOS("RenderInit") << "LLVertexBuffer initialization done." << LL_ENDL ;
gGL.init(true);
if (!gGL.init(true))
{
LLError::LLUserWarningMsg::show(LLTrans::getString("MBVideoDrvErr"));
LL_ERRS() << "gGL not initialized" << LL_ENDL;
}
// <FS:Ansariel> Exodus vignette
if (LLFeatureManager::getInstance()->isSafe()

View File

@ -79,7 +79,10 @@ def save_xml(tree, file_path, xml_decl, indent_text=False, indent_tab=False, rm_
with io.open(file_path, 'wb') as file:
file.write(xml_decl.encode('utf-8'))
file.write('\n'.encode('utf-8'))
file.write(xml_string.encode('utf-8'))
if xml_string:
file.write(xml_string.encode('utf-8'))
if not xml_string.endswith('\n'):
file.write('\n'.encode('utf-8'))
except IOError as e:
print(f"Error saving file {file_path}: {e}")