[LYN-9953] Asset Processor: Fix file watcher event handlers calling APM methods directly instead … (#7289)

* Fix file watcher event handlers calling APM methods directly instead of queuing them to run on the APM thread

Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com>

* Add unit test

Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com>

* Fix compile errors

Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com>

* Fix compile errors

Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com>

* Fix compile errors

Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com>

* Fix compile errors

Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com>

* Change ASSERT_ checks to EXPECT_ checks

Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com>

* Refactored Test

Removed leftover direct call
Changed invokeMethod to use lambda form

Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com>

* Fix compile error

Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com>
monroegm-disable-blank-issue-2
amzn-mike 4 years ago committed by GitHub
parent 6f52fcb9f0
commit 5dd88ba05c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -32,6 +32,12 @@ set(FILES
native/tests/assetmanager/AssetProcessorManagerTest.h
native/tests/assetmanager/ModtimeScanningTests.cpp
native/tests/assetmanager/ModtimeScanningTests.h
native/tests/assetmanager/MockAssetProcessorManager.cpp
native/tests/assetmanager/MockAssetProcessorManager.h
native/tests/assetmanager/MockFileProcessor.h
native/tests/assetmanager/MockFileProcessor.cpp
native/tests/assetmanager/TestEventSignal.cpp
native/tests/assetmanager/TestEventSignal.h
native/tests/utilities/assetUtilsTest.cpp
native/tests/platformconfiguration/platformconfigurationtests.cpp
native/tests/platformconfiguration/platformconfigurationtests.h
@ -51,6 +57,8 @@ set(FILES
native/tests/SourceFileRelocatorTests.cpp
native/tests/PathDependencyManagerTests.cpp
native/tests/AssetProcessorMessagesTests.cpp
native/tests/ApplicationManagerTests.cpp
native/tests/ApplicationManagerTests.h
native/unittests/AssetProcessingStateDataUnitTests.cpp
native/unittests/AssetProcessingStateDataUnitTests.h
native/unittests/AssetProcessorManagerUnitTests.cpp

@ -272,9 +272,9 @@ namespace AssetProcessor
void AssessFilesFromScanner(QSet<AssetFileInfo> filePaths);
void AssessModifiedFile(QString filePath);
void AssessAddedFile(QString filePath);
void AssessDeletedFile(QString filePath);
virtual void AssessModifiedFile(QString filePath);
virtual void AssessAddedFile(QString filePath);
virtual void AssessDeletedFile(QString filePath);
void OnAssetScannerStatusChange(AssetProcessor::AssetScanningStatus status);
void OnJobStatusChanged(JobEntry jobEntry, JobStatus status);

@ -48,9 +48,9 @@ namespace AssetProcessor
void AssessFoldersFromScanner(QSet<AssetFileInfo> folders);
//! FileWatcher detected added file
void AssessAddedFile(QString fileName);
virtual void AssessAddedFile(QString fileName);
//! FileWatcher detected removed file
void AssessDeletedFile(QString fileName);
virtual void AssessDeletedFile(QString fileName);
//! Synchronize AssetScanner data with Files table
void Sync();

@ -148,7 +148,6 @@ void FileWatcher::StopWatching()
{
if (!m_startedWatching)
{
AZ_Warning("FileWatcher", false, "StopWatching() called when is not watching for file changes.");
return;
}

@ -0,0 +1,91 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ApplicationManagerTests.h"
#include <QCoreApplication>
#include <tests/assetmanager/MockAssetProcessorManager.h>
#include <tests/assetmanager/MockFileProcessor.h>
namespace UnitTests
{
bool DatabaseLocationListener::GetAssetDatabaseLocation(AZStd::string& location)
{
location = m_databaseLocation;
return true;
}
void ApplicationManagerTest::SetUp()
{
ScopedAllocatorSetupFixture::SetUp();
AZ::IO::Path tempDir(m_tempDir.GetDirectory());
m_databaseLocationListener.m_databaseLocation = (tempDir / "test_database.sqlite").Native();
// We need a QCoreApplication to run the event loop
int argc = 0;
m_coreApplication = AZStd::make_unique<QCoreApplication>(argc, nullptr);
m_applicationManager = AZStd::make_unique<MockBatchApplicationManager>(&argc, nullptr);
m_applicationManager->m_platformConfiguration = new AssetProcessor::PlatformConfiguration{ nullptr };
m_applicationManager->m_fileStateCache = AZStd::make_unique<AssetProcessor::FileStateCache>();
m_mockAPM = AZStd::make_unique<MockAssetProcessorManager>(m_applicationManager->m_platformConfiguration, nullptr);
m_applicationManager->m_assetProcessorManager = m_mockAPM.get();
AZStd::vector<AssetBuilderSDK::PlatformInfo> platforms;
m_applicationManager->m_platformConfiguration->EnablePlatform(AssetBuilderSDK::PlatformInfo{ "pc", { "tag" } });
m_applicationManager->m_platformConfiguration->PopulatePlatformsForScanFolder(platforms);
m_applicationManager->m_platformConfiguration->AddScanFolder(
AssetProcessor::ScanFolderInfo{ tempDir.c_str(), "test", "test", true, true, platforms });
m_apmThread = AZStd::make_unique<QThread>(nullptr);
m_apmThread->setObjectName("APM Thread");
m_applicationManager->m_assetProcessorManager->moveToThread(m_apmThread.get());
m_apmThread->start();
m_fileProcessorThread = AZStd::make_unique<QThread>(nullptr);
m_fileProcessorThread->setObjectName("File Processor Thread");
auto fileProcessor = AZStd::make_unique<MockFileProcessor>(m_applicationManager->m_platformConfiguration);
fileProcessor->moveToThread(m_fileProcessorThread.get());
m_mockFileProcessor = fileProcessor.get();
m_applicationManager->m_fileProcessor = AZStd::move(fileProcessor); // The manager is taking ownership
m_fileProcessorThread->start();
auto fileWatcher = AZStd::make_unique<FileWatcher>();
m_fileWatcher = fileWatcher.get();
// This is what we're testing, it will set up connections between the fileWatcher and the 2 QObject handlers we'll check
m_applicationManager->InitFileMonitor(AZStd::move(fileWatcher)); // The manager is going to take ownership of the file watcher
}
void ApplicationManagerTest::TearDown()
{
m_apmThread->exit();
m_fileProcessorThread->exit();
m_mockAPM = nullptr;
ScopedAllocatorSetupFixture::TearDown();
}
TEST_F(ApplicationManagerTest, FileWatcherEventsTriggered_ProperlySignalledOnCorrectThread)
{
AZ::IO::Path tempDir(m_tempDir.GetDirectory());
Q_EMIT m_fileWatcher->fileAdded((tempDir / "test").c_str());
Q_EMIT m_fileWatcher->fileModified((tempDir / "test2").c_str());
Q_EMIT m_fileWatcher->fileRemoved((tempDir / "test3").c_str());
EXPECT_TRUE(m_mockAPM->m_events[Added].WaitAndCheck()) << "APM Added event failed";
EXPECT_TRUE(m_mockAPM->m_events[Modified].WaitAndCheck()) << "APM Modified event failed";
EXPECT_TRUE(m_mockAPM->m_events[Deleted].WaitAndCheck()) << "APM Deleted event failed";
EXPECT_TRUE(m_mockFileProcessor->m_events[Added].WaitAndCheck()) << "File Processor Added event failed";
EXPECT_TRUE(m_mockFileProcessor->m_events[Deleted].WaitAndCheck()) << "File Processor Deleted event failed";
}
}

@ -0,0 +1,64 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <utilities/BatchApplicationManager.h>
#include <AzCore/UnitTest/TestTypes.h>
#include "assetmanager/MockAssetProcessorManager.h"
#include "assetmanager/MockFileProcessor.h"
namespace UnitTests
{
struct MockBatchApplicationManager : BatchApplicationManager
{
using ApplicationManagerBase::InitFileMonitor;
using ApplicationManagerBase::m_assetProcessorManager;
using ApplicationManagerBase::m_fileProcessor;
using ApplicationManagerBase::m_fileStateCache;
using ApplicationManagerBase::m_platformConfiguration;
using BatchApplicationManager::BatchApplicationManager;
};
class DatabaseLocationListener : public AzToolsFramework::AssetDatabase::AssetDatabaseRequests::Bus::Handler
{
public:
DatabaseLocationListener()
{
BusConnect();
}
~DatabaseLocationListener() override
{
BusDisconnect();
}
bool GetAssetDatabaseLocation(AZStd::string& location) override;
AZStd::string m_databaseLocation;
};
struct ApplicationManagerTest : ::UnitTest::ScopedAllocatorSetupFixture
{
protected:
void SetUp() override;
void TearDown() override;
AZ::Test::ScopedAutoTempDirectory m_tempDir;
DatabaseLocationListener m_databaseLocationListener;
AZStd::unique_ptr<QCoreApplication> m_coreApplication;
AZStd::unique_ptr<MockBatchApplicationManager> m_applicationManager;
AZStd::unique_ptr<QThread> m_apmThread;
AZStd::unique_ptr<QThread> m_fileProcessorThread;
AZStd::unique_ptr<MockAssetProcessorManager> m_mockAPM;
// These are just aliases, no need to manage/delete them
FileWatcher* m_fileWatcher{};
MockFileProcessor* m_mockFileProcessor{};
};
}

@ -130,14 +130,14 @@ namespace AssetProcessorMessagesTests
m_batchApplicationManager->m_assetCatalog = m_assetCatalog.get();
m_batchApplicationManager->InitRCController();
m_batchApplicationManager->InitFileStateCache();
m_batchApplicationManager->InitFileMonitor();
m_batchApplicationManager->InitFileMonitor(AZStd::make_unique<FileWatcher>());
m_batchApplicationManager->InitApplicationServer();
m_batchApplicationManager->InitConnectionManager();
// Note this must be constructed after InitConnectionManager is called since it will interact with the connection manager
m_assetRequestHandler = new MockAssetRequestHandler();
m_batchApplicationManager->InitAssetRequestHandler(m_assetRequestHandler);
m_batchApplicationManager->m_fileWatcher.StartWatching();
m_batchApplicationManager->m_fileWatcher->StartWatching();
QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ConnectionError, [](unsigned /*connId*/, QString error)
{

@ -12,10 +12,12 @@
namespace UnitTests
{
using AssetFileInfo = AssetProcessor::AssetFileInfo;
void FileStateCacheTests::SetUp()
{
m_temporarySourceDir = QDir(m_temporaryDir.path());
m_fileStateCache = AZStd::make_unique<FileStateCache>();
m_fileStateCache = AZStd::make_unique<AssetProcessor::FileStateCache>();
}
void FileStateCacheTests::TearDown()
@ -26,9 +28,9 @@ namespace UnitTests
void FileStateCacheTests::CheckForFile(QString path, bool shouldExist)
{
bool exists = false;
FileStateInfo fileInfo;
AssetProcessor::FileStateInfo fileInfo;
auto* fileStateInterface = AZ::Interface<IFileStateRequests>::Get();
auto* fileStateInterface = AZ::Interface<AssetProcessor::IFileStateRequests>::Get();
ASSERT_NE(fileStateInterface, nullptr);
exists = fileStateInterface->Exists(path);
@ -142,7 +144,7 @@ namespace UnitTests
TEST_F(FileStateCacheTests, PassthroughTest)
{
m_fileStateCache = nullptr; // Need to release the existing one first since only one handler can exist for the ebus
m_fileStateCache = AZStd::make_unique<FileStatePassthrough>();
m_fileStateCache = AZStd::make_unique<AssetProcessor::FileStatePassthrough>();
QString testPath = m_temporarySourceDir.absoluteFilePath("test.txt");
CheckForFile(testPath, false);

@ -16,7 +16,6 @@ namespace UnitTests
{
using namespace testing;
using ::testing::NiceMock;
using namespace AssetProcessor;
class FileStateCacheTests : public ::testing::Test
{
@ -29,6 +28,6 @@ namespace UnitTests
protected:
QTemporaryDir m_temporaryDir;
QDir m_temporarySourceDir;
AZStd::unique_ptr<FileStateBase> m_fileStateCache;
AZStd::unique_ptr<AssetProcessor::FileStateBase> m_fileStateCache;
};
}

@ -0,0 +1,27 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <tests/assetmanager/MockAssetProcessorManager.h>
namespace UnitTests
{
void MockAssetProcessorManager::AssessAddedFile(QString filePath)
{
m_events[TestEvents::Added].Signal();
}
void MockAssetProcessorManager::AssessModifiedFile(QString filePath)
{
m_events[TestEvents::Modified].Signal();
}
void MockAssetProcessorManager::AssessDeletedFile(QString filePath)
{
m_events[TestEvents::Deleted].Signal();
}
}

@ -0,0 +1,32 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <QObject>
#include <native/AssetManager/assetProcessorManager.h>
#include <tests/assetmanager/TestEventSignal.h>
namespace UnitTests
{
struct MockAssetProcessorManager : ::AssetProcessor::AssetProcessorManager
{
Q_OBJECT
using AssetProcessorManager::AssetProcessorManager;
public Q_SLOTS:
void AssessAddedFile(QString filePath) override;
void AssessModifiedFile(QString filePath) override;
void AssessDeletedFile(QString filePath) override;
public:
TestEventPair m_events[TestEvents::NumEvents];
};
}

@ -0,0 +1,22 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "MockFileProcessor.h"
namespace UnitTests
{
void MockFileProcessor::AssessAddedFile(QString fileName)
{
m_events[TestEvents::Added].Signal();
}
void MockFileProcessor::AssessDeletedFile(QString fileName)
{
m_events[TestEvents::Deleted].Signal();
}
}

@ -0,0 +1,30 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <QObject>
#include <FileProcessor/FileProcessor.h>
#include <tests/assetmanager/TestEventSignal.h>
namespace UnitTests
{
struct MockFileProcessor : ::AssetProcessor::FileProcessor
{
Q_OBJECT
using FileProcessor::FileProcessor;
public Q_SLOTS:
void AssessAddedFile(QString fileName) override;
void AssessDeletedFile(QString fileName) override;
public:
TestEventPair m_events[TestEvents::NumEvents];
};
}

@ -0,0 +1,37 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <native/tests/assetmanager/TestEventSignal.h>
#include <AzCore/std/parallel/thread.h>
#include <AzCore/UnitTest/TestTypes.h>
namespace UnitTests
{
void TestEventPair::Signal()
{
bool expected = false;
ASSERT_TRUE(m_signaled.compare_exchange_strong(expected, true));
ASSERT_EQ(m_threadId, AZStd::thread_id{});
m_threadId = AZStd::this_thread::get_id();
m_event.release();
}
bool TestEventPair::WaitAndCheck()
{
constexpr int MaxWaitTimeMilliseconds = 100;
auto thisThreadId = AZStd::this_thread::get_id();
bool acquireSuccess = m_event.try_acquire_for(AZStd::chrono::milliseconds(MaxWaitTimeMilliseconds));
EXPECT_TRUE(acquireSuccess);
EXPECT_NE(m_threadId, AZStd::thread_id{});
EXPECT_TRUE(m_threadId != thisThreadId);
return acquireSuccess && m_threadId != AZStd::thread_id{} && m_threadId != thisThreadId;
}
}

@ -0,0 +1,33 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/std/parallel/binary_semaphore.h>
namespace UnitTests
{
class TestEventPair
{
AZStd::atomic_bool m_signaled{ false };
AZStd::thread_id m_threadId;
AZStd::binary_semaphore m_event;
public:
void Signal();
bool WaitAndCheck();
};
enum TestEvents : int
{
Added = 0,
Modified,
Deleted,
NumEvents
};
}

@ -431,79 +431,114 @@ void ApplicationManagerBase::DestroyPlatformConfiguration()
}
}
void ApplicationManagerBase::InitFileMonitor()
void ApplicationManagerBase::InitFileMonitor(AZStd::unique_ptr<FileWatcher> fileWatcher)
{
m_fileWatcher = AZStd::move(fileWatcher);
for (int folderIdx = 0; folderIdx < m_platformConfiguration->GetScanFolderCount(); ++folderIdx)
{
const AssetProcessor::ScanFolderInfo& info = m_platformConfiguration->GetScanFolderAt(folderIdx);
m_fileWatcher.AddFolderWatch(info.ScanPath(), info.RecurseSubFolders());
m_fileWatcher->AddFolderWatch(info.ScanPath(), info.RecurseSubFolders());
}
QDir cacheRoot;
if (AssetUtilities::ComputeProjectCacheRoot(cacheRoot))
{
m_fileWatcher.AddFolderWatch(cacheRoot.absolutePath(), true);
m_fileWatcher->AddFolderWatch(cacheRoot.absolutePath(), true);
}
if (m_platformConfiguration->GetScanFolderCount() || !cacheRoot.path().isEmpty())
{
const auto cachePath = QDir::toNativeSeparators(cacheRoot.absolutePath());
// For the handlers below, we need to make sure to use invokeMethod on any QObjects so Qt can queue
// the callback to run on the QObject's thread if needed. The APM methods for example are not thread-safe.
const auto OnFileAdded = [this, cachePath](QString path)
{
const bool isCacheRoot = path.startsWith(cachePath);
if (isCacheRoot)
if (!isCacheRoot)
{
m_fileStateCache->AddFile(path);
[[maybe_unused]] bool result = QMetaObject::invokeMethod(m_assetProcessorManager, [this, path]()
{
m_assetProcessorManager->AssessAddedFile(path);
}, Qt::QueuedConnection);
AZ_Assert(result, "Failed to invoke m_assetProcessorManager::AssessAddedFile");
result = QMetaObject::invokeMethod(m_fileProcessor.get(), [this, path]()
{
m_fileProcessor->AssessAddedFile(path);
}, Qt::QueuedConnection);
AZ_Assert(result, "Failed to invoke m_fileProcessor::AssessAddedFile");
auto cache = AZ::Interface<AssetProcessor::ExcludedFolderCacheInterface>::Get();
if(cache)
{
cache->FileAdded(path);
}
else
{
m_assetProcessorManager->AssessAddedFile(path);
m_fileStateCache->AddFile(path);
AZ::Interface<AssetProcessor::ExcludedFolderCacheInterface>::Get()->FileAdded(path);
m_fileProcessor->AssetProcessor::FileProcessor::AssessAddedFile(path);
AZ_Error("AssetProcessor", false, "ExcludedFolderCacheInterface not found");
}
}
m_fileStateCache->AddFile(path);
};
const auto OnFileModified = [this, cachePath](QString path)
{
const bool isCacheRoot = path.startsWith(cachePath);
if (isCacheRoot)
if (!isCacheRoot)
{
m_assetProcessorManager->AssessModifiedFile(path);
m_fileStateCache->UpdateFile(path);
}
else
[[maybe_unused]] bool result = QMetaObject::invokeMethod(
m_assetProcessorManager,
[this, path]
{
m_assetProcessorManager->AssessModifiedFile(path);
m_fileStateCache->UpdateFile(path);
}
}, Qt::QueuedConnection);
AZ_Assert(result, "Failed to invoke m_assetProcessorManager::AssessModifiedFile");
};
const auto OnFileRemoved = [this, cachePath](QString path)
{
[[maybe_unused]] bool result = false;
const bool isCacheRoot = path.startsWith(cachePath);
if (isCacheRoot)
if (!isCacheRoot)
{
m_fileStateCache->RemoveFile(path);
m_assetProcessorManager->AssessDeletedFile(path);
result = QMetaObject::invokeMethod(m_fileProcessor.get(), [this, path]()
{
m_fileProcessor->AssessDeletedFile(path);
}, Qt::QueuedConnection);
AZ_Assert(result, "Failed to invoke m_fileProcessor::AssessDeletedFile");
}
else
result = QMetaObject::invokeMethod(m_assetProcessorManager, [this, path]()
{
m_assetProcessorManager->AssessDeletedFile(path);
}, Qt::QueuedConnection);
AZ_Assert(result, "Failed to invoke m_assetProcessorManager::AssessDeletedFile");
m_fileStateCache->RemoveFile(path);
m_fileProcessor->AssessDeletedFile(path);
}
};
connect(&m_fileWatcher, &FileWatcher::fileAdded, OnFileAdded);
connect(&m_fileWatcher, &FileWatcher::fileModified, OnFileModified);
connect(&m_fileWatcher, &FileWatcher::fileRemoved, OnFileRemoved);
connect(m_fileWatcher.get(), &FileWatcher::fileAdded, OnFileAdded);
connect(m_fileWatcher.get(), &FileWatcher::fileModified, OnFileModified);
connect(m_fileWatcher.get(), &FileWatcher::fileRemoved, OnFileRemoved);
}
}
void ApplicationManagerBase::DestroyFileMonitor()
{
m_fileWatcher.ClearFolderWatches();
if(m_fileWatcher)
{
m_fileWatcher->ClearFolderWatches();
m_fileWatcher = nullptr;
}
}
void ApplicationManagerBase::DestroyApplicationServer()
@ -1357,7 +1392,7 @@ bool ApplicationManagerBase::Activate()
InitFileProcessor();
InitAssetCatalog();
InitFileMonitor();
InitFileMonitor(AZStd::make_unique<FileWatcher>());
InitAssetScanner();
InitAssetServerHandler();
InitRCController();

@ -134,7 +134,7 @@ protected:
virtual void DestroyAssetScanner();
virtual bool InitPlatformConfiguration();
virtual void DestroyPlatformConfiguration();
virtual void InitFileMonitor();
virtual void InitFileMonitor(AZStd::unique_ptr<FileWatcher> fileWatcher);
virtual void DestroyFileMonitor();
virtual bool InitBuilderConfiguration();
virtual void InitControlRequestHandler();
@ -191,7 +191,7 @@ protected:
bool m_sourceControlReady = false;
bool m_fullIdle = false;
FileWatcher m_fileWatcher;
AZStd::unique_ptr<FileWatcher> m_fileWatcher;
AssetProcessor::PlatformConfiguration* m_platformConfiguration = nullptr;
AssetProcessor::AssetProcessorManager* m_assetProcessorManager = nullptr;
AssetProcessor::AssetCatalog* m_assetCatalog = nullptr;

@ -479,7 +479,7 @@ bool GUIApplicationManager::PostActivate()
return false;
}
m_fileWatcher.StartWatching();
m_fileWatcher->StartWatching();
return true;
}

Loading…
Cancel
Save