mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-03 03:31:04 +03:00
Fix FileSystemModel crash on stale filesystem-model indexes
- dispatch filesystem events on the main thread in FileSystemListener, preserving model, tree, document, and callback ordering - add address + generation node identity to close the allocator-reuse hole - return null/empty results for stale indexes instead of root data - assert the main-thread contract and token-guard queued listener actions - add attached-view and stale-selection crash regression tests Fixes crash reported in discussion SpartanJ/ecode#952
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
#include "filesystemlistener.hpp"
|
||||
#include <eepp/scene/actionmanager.hpp>
|
||||
#include <eepp/scene/scenemanager.hpp>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/md5.hpp>
|
||||
#include <eepp/system/time.hpp>
|
||||
#include <eepp/window/engine.hpp>
|
||||
|
||||
namespace ecode {
|
||||
|
||||
@@ -22,7 +26,28 @@ std::string getFileSystemEventTypeName( FileSystemEventType action ) {
|
||||
FileSystemListener::FileSystemListener( UICodeEditorSplitter* splitter,
|
||||
std::shared_ptr<FileSystemModel> fileSystemModel,
|
||||
const std::vector<std::string>& ignoreFiles ) :
|
||||
mSplitter( splitter ), mFileSystemModel( fileSystemModel ), mIgnoredFiles( ignoreFiles ) {}
|
||||
mSplitter( splitter ), mFileSystemModel( fileSystemModel ), mIgnoredFiles( ignoreFiles ) {
|
||||
// Namespaced 64-bit tag: a stable class-name prefix plus an atomic instance
|
||||
// counter, which minimizes (but cannot mathematically eliminate) collision
|
||||
// with unrelated scene action tags.
|
||||
static std::atomic<Uint64> nextEventActionTag{ 0 };
|
||||
mEventActionTag =
|
||||
( static_cast<Uint64>( EE::String::hash( "ecode::FileSystemListener" ) ) << 32 ) |
|
||||
( ++nextEventActionTag & 0xFFFFFFFFULL );
|
||||
mLifetime = std::make_shared<std::atomic<bool>>( true );
|
||||
}
|
||||
|
||||
FileSystemListener::~FileSystemListener() {
|
||||
// The lifetime token is complete only because destruction runs on the main
|
||||
// thread, where queued actions also execute: a runnable cannot be
|
||||
// interleaved between its token check and its use of `this`. Assert that
|
||||
// invariant; cross-thread destruction would need a synchronized control
|
||||
// block instead of an atomic flag.
|
||||
eeASSERT( !Engine::existsSingleton() || Engine::isMainThread() );
|
||||
*mLifetime = false;
|
||||
if ( auto* scene = SceneManager::instance()->getUISceneNode() )
|
||||
scene->getActionManager()->removeActionsByTagFromTarget( scene, mEventActionTag );
|
||||
}
|
||||
|
||||
static inline bool endsWithSlash( const std::string& dir ) {
|
||||
return !dir.empty() && ( dir.back() == '\\' || dir.back() == '/' );
|
||||
@@ -31,6 +56,27 @@ static inline bool endsWithSlash( const std::string& dir ) {
|
||||
void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir,
|
||||
const std::string& filename, efsw::Action action,
|
||||
const std::string& oldFilename ) {
|
||||
// The whole logical event (model, directory tree, open documents,
|
||||
// callbacks) must run on the main thread in its original order: the model
|
||||
// update used to complete synchronously before the other consequences, and
|
||||
// all of them touch UI-owned state. The destructor cancels queued actions
|
||||
// (tagged with mEventActionTag), so a runnable can never outlive the
|
||||
// listener.
|
||||
if ( !Engine::isMainThread() ) {
|
||||
if ( auto* scene = SceneManager::instance()->getUISceneNode() ) {
|
||||
scene->runOnMainThread(
|
||||
[lifetime = mLifetime, this, dir, filename, action, oldFilename]() {
|
||||
if ( !lifetime->load( std::memory_order_acquire ) )
|
||||
return;
|
||||
handleFileAction( 0, dir, filename, action, oldFilename );
|
||||
},
|
||||
Time::Zero, mEventActionTag );
|
||||
return;
|
||||
}
|
||||
// No scene node: cannot safely touch UI state from this thread.
|
||||
return;
|
||||
}
|
||||
|
||||
FileInfo file( ( endsWithSlash( dir ) ? dir : ( dir + FileSystem::getOSSlash() ) ) + filename );
|
||||
|
||||
switch ( action ) {
|
||||
|
||||
@@ -25,7 +25,7 @@ class FileSystemListener : public efsw::FileWatchListener {
|
||||
std::shared_ptr<FileSystemModel> fileSystemModel,
|
||||
const std::vector<std::string>& ignoreFiles );
|
||||
|
||||
virtual ~FileSystemListener() {}
|
||||
virtual ~FileSystemListener();
|
||||
|
||||
void handleFileAction( efsw::WatchID, const std::string& dir, const std::string& filename,
|
||||
efsw::Action action, const std::string& oldFilename );
|
||||
@@ -46,6 +46,11 @@ class FileSystemListener : public efsw::FileWatchListener {
|
||||
std::unordered_map<Uint64, FileEventFn> mCbs;
|
||||
std::vector<std::string> mIgnoredFiles;
|
||||
Mutex mCbsMutex;
|
||||
// Tag of the queued main-thread file-event actions; the destructor cancels
|
||||
// them and marks the lifetime token dead so a queued runnable can never
|
||||
// dereference the destroyed listener.
|
||||
Uint64 mEventActionTag{ 0 };
|
||||
std::shared_ptr<std::atomic<bool>> mLifetime;
|
||||
|
||||
bool isFileOpen( const FileInfo& file );
|
||||
|
||||
|
||||
@@ -137,7 +137,9 @@ class UITreeViewCellFS : public UITreeViewCell {
|
||||
}
|
||||
|
||||
const std::string& getCurrentPath() const {
|
||||
return getModel()->node( getCurIndex() ).fullPath();
|
||||
static const std::string empty;
|
||||
const auto* node = getModel()->nodePtr( getCurIndex() );
|
||||
return node ? node->fullPath() : empty;
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -343,9 +345,9 @@ void UITreeViewFS::copyFiles( const std::vector<std::string>& paths, const std::
|
||||
}
|
||||
|
||||
std::string UITreeViewFS::getSelectionPath() const {
|
||||
return static_cast<const FileSystemModel*>( getModel() )
|
||||
->node( getSelection().first() )
|
||||
.fullPath();
|
||||
const auto* node =
|
||||
static_cast<const FileSystemModel*>( getModel() )->nodePtr( getSelection().first() );
|
||||
return node ? node->fullPath() : "";
|
||||
}
|
||||
|
||||
std::string UITreeViewFS::getSelectionPathAtIndex( int index ) const {
|
||||
@@ -353,9 +355,9 @@ std::string UITreeViewFS::getSelectionPathAtIndex( int index ) const {
|
||||
if ( index < 0 || static_cast<size_t>( index ) >= static_cast<size_t>( selection.size() ) )
|
||||
return "";
|
||||
auto indexVec = selection.indexes();
|
||||
return static_cast<const FileSystemModel*>( getModel() )
|
||||
->node( indexVec[static_cast<size_t>( index )] )
|
||||
.fullPath();
|
||||
const auto* node = static_cast<const FileSystemModel*>( getModel() )
|
||||
->nodePtr( indexVec[static_cast<size_t>( index )] );
|
||||
return node ? node->fullPath() : "";
|
||||
}
|
||||
|
||||
std::vector<FileInfo> UITreeViewFS::getSelectionsFileInfo() const {
|
||||
@@ -363,7 +365,8 @@ std::vector<FileInfo> UITreeViewFS::getSelectionsFileInfo() const {
|
||||
auto indexVec = getSelection().indexes();
|
||||
auto model = static_cast<const FileSystemModel*>( getModel() );
|
||||
for ( const auto& index : indexVec ) {
|
||||
ret.emplace_back( FileInfo( model->node( index ).fullPath() ) );
|
||||
if ( const auto* node = model->nodePtr( index ) )
|
||||
ret.emplace_back( FileInfo( node->fullPath() ) );
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user