|
@@ -1751,6 +1751,9 @@ namespace AssetProcessor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ // Indicates if this CheckSource event was originally started due to a file change from a metadata file
|
|
|
|
+ bool triggeredByMetadata = false;
|
|
|
|
+
|
|
// if metadata file change, pretend the actual file changed
|
|
// if metadata file change, pretend the actual file changed
|
|
// the fingerprint will be different anyway since metadata file is folded in
|
|
// the fingerprint will be different anyway since metadata file is folded in
|
|
|
|
|
|
@@ -1762,6 +1765,7 @@ namespace AssetProcessor
|
|
if (normalizedFilePath.endsWith("." + metaInfo.first, Qt::CaseInsensitive))
|
|
if (normalizedFilePath.endsWith("." + metaInfo.first, Qt::CaseInsensitive))
|
|
{
|
|
{
|
|
//its a meta file. What was the original?
|
|
//its a meta file. What was the original?
|
|
|
|
+ triggeredByMetadata = true;
|
|
|
|
|
|
normalizedFilePath = normalizedFilePath.left(normalizedFilePath.length() - (metaInfo.first.length() + 1));
|
|
normalizedFilePath = normalizedFilePath.left(normalizedFilePath.length() - (metaInfo.first.length() + 1));
|
|
if (!metaInfo.second.isEmpty())
|
|
if (!metaInfo.second.isEmpty())
|
|
@@ -1811,6 +1815,24 @@ namespace AssetProcessor
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ // Check if this event is part of an in-process file move.
|
|
|
|
+ // If so, ignore the event.
|
|
|
|
+ if (ShouldIgnorePendingMove(normalizedFilePath.toUtf8().constData(), triggeredByMetadata, source.m_isDelete))
|
|
|
|
+ {
|
|
|
|
+ AZ_Trace(
|
|
|
|
+ AssetProcessor::DebugChannel,
|
|
|
|
+ "Ignoring processing of " AZ_STRING_FORMAT " - file is marked as pending move\n",
|
|
|
|
+ AZ_STRING_ARG(normalizedFilePath));
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Check if this event should be delayed for a while to allow time for moving/renaming to be completed
|
|
|
|
+ if (ShouldDelayProcessingFile(source, normalizedFilePath, triggeredByMetadata))
|
|
|
|
+ {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
// even if the entry already exists,
|
|
// even if the entry already exists,
|
|
// overwrite the entry here, so if you modify, then delete it, its the latest action thats always on the list.
|
|
// overwrite the entry here, so if you modify, then delete it, its the latest action thats always on the list.
|
|
|
|
|
|
@@ -3084,9 +3106,8 @@ namespace AssetProcessor
|
|
{
|
|
{
|
|
AssetProcessor::FileStateInfo fileStateInfo;
|
|
AssetProcessor::FileStateInfo fileStateInfo;
|
|
|
|
|
|
- if (fileStateInterface->GetFileInfo(
|
|
|
|
- sourceAssetReference.AbsolutePath().c_str(), &fileStateInfo) &&
|
|
|
|
- fileStateInfo.m_absolutePath.compare(sourceAssetReference.AbsolutePath().c_str()) != 0)
|
|
|
|
|
|
+ if (fileStateInterface->GetFileInfo(sourceAssetReference.AbsolutePath().c_str(), &fileStateInfo) &&
|
|
|
|
+ sourceAssetReference.AbsolutePath().Compare(fileStateInfo.m_absolutePath.toUtf8().constData()) != 0)
|
|
{
|
|
{
|
|
// File on disk has different case compared to the file being processed
|
|
// File on disk has different case compared to the file being processed
|
|
// This usually means a file was renamed and a "change" event was fired for both the old and new name
|
|
// This usually means a file was renamed and a "change" event was fired for both the old and new name
|
|
@@ -5832,4 +5853,208 @@ namespace AssetProcessor
|
|
}
|
|
}
|
|
return filesFound;
|
|
return filesFound;
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ void AssetProcessorManager::SetMetaCreationDelay(AZ::u32 milliseconds)
|
|
|
|
+ {
|
|
|
|
+ m_metaCreationDelayMs = milliseconds;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void AssetProcessorManager::PrepareForFileMove(AZ::IO::PathView oldPath, AZ::IO::PathView newPath)
|
|
|
|
+ {
|
|
|
|
+ // Note - this code is likely not running on the APM thread, be careful with variable access
|
|
|
|
+ auto* fileStateInterface = AZ::Interface<IFileStateRequests>::Get();
|
|
|
|
+ AZ_Assert(fileStateInterface, "Programmer Error - IFileStateRequests is not available.");
|
|
|
|
+
|
|
|
|
+ AssetProcessor::FileStateInfo fileInfo;
|
|
|
|
+ QString oldPathQString = QString::fromUtf8(oldPath.Native().data(), azlossy_caster(oldPath.Native().size()));
|
|
|
|
+ if (fileStateInterface->GetFileInfo(oldPathQString, &fileInfo) && fileInfo.m_isDirectory)
|
|
|
|
+ {
|
|
|
|
+ // If we know the old path is a directory just exit out, a directory rename doesn't have any create/delete issues
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ AZStd::scoped_lock lock(m_pendingMovesMutex);
|
|
|
|
+ m_pendingMoves.emplace(oldPath, false);
|
|
|
|
+ m_pendingMoves.emplace(newPath, true);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bool AssetProcessorManager::CheckMetadataIsAvailable(AZ::IO::PathView absolutePath)
|
|
|
|
+ {
|
|
|
|
+ auto* fileStateInterface = AZ::Interface<IFileStateRequests>::Get();
|
|
|
|
+ auto* uuidInterface = AZ::Interface<IUuidRequests>::Get();
|
|
|
|
+
|
|
|
|
+ AZ_Assert(fileStateInterface, "Programmer Error - IFileStateRequests is not available.");
|
|
|
|
+ AZ_Assert(uuidInterface, "Programmer Error - IUuidRequests is not available.");
|
|
|
|
+
|
|
|
|
+ return !uuidInterface->IsGenerationEnabledForFile(absolutePath) ||
|
|
|
|
+ fileStateInterface->Exists(AzToolsFramework::MetadataManager::ToMetadataPath(absolutePath).c_str());
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bool AssetProcessorManager::ShouldIgnorePendingMove(AZ::IO::PathView absolutePath, bool triggeredByMetadata, bool isDelete)
|
|
|
|
+ {
|
|
|
|
+ AZStd::scoped_lock lock(m_pendingMovesMutex);
|
|
|
|
+ auto itr = m_pendingMoves.find(absolutePath);
|
|
|
|
+
|
|
|
|
+ if (itr != m_pendingMoves.end())
|
|
|
|
+ {
|
|
|
|
+ const bool isNewFile = itr->second;
|
|
|
|
+
|
|
|
|
+ if (!isNewFile)
|
|
|
|
+ {
|
|
|
|
+ if (triggeredByMetadata && isDelete)
|
|
|
|
+ {
|
|
|
|
+ // Deletion of the old metadata file typically would cause the metadata file to be recreated.
|
|
|
|
+ // Since this file is moving, ignore the deletion event.
|
|
|
|
+ m_pendingMoves.erase(itr);
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else if (!triggeredByMetadata && !isDelete)
|
|
|
|
+ {
|
|
|
|
+ // The new file has been created.
|
|
|
|
+ m_pendingMoves.erase(itr);
|
|
|
|
+
|
|
|
|
+ // If the metadata is not available yet, ignore this event.
|
|
|
|
+ if (!CheckMetadataIsAvailable(absolutePath))
|
|
|
|
+ {
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bool AssetProcessorManager::HasDelayProcessTimerElapsed(qint64 elapsedTime)
|
|
|
|
+ {
|
|
|
|
+ // QTimer is not a precise timer, it could fire several milliseconds before or after the required wait time.
|
|
|
|
+ // Just check if the elapsed time is relatively close; 30ms is arbitrary but should be sufficient.
|
|
|
|
+ // Precise timing isn't necessary here anyway.
|
|
|
|
+ constexpr double ToleranceMs = 30;
|
|
|
|
+ return elapsedTime + ToleranceMs >= m_metaCreationDelayMs;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bool AssetProcessorManager::ShouldDelayProcessingFile(
|
|
|
|
+ const AssetProcessorManager::FileEntry& source, QString normalizedFilePath, bool triggeredByMetadata)
|
|
|
|
+ {
|
|
|
|
+ if (m_metaCreationDelayMs > 0)
|
|
|
|
+ {
|
|
|
|
+ AZ::IO::Path absolutePath = normalizedFilePath.toUtf8().constData();
|
|
|
|
+
|
|
|
|
+ // There are 7 possible relevant events here:
|
|
|
|
+ // 1) An existing source file is deleted
|
|
|
|
+ // 2) An existing metadata file is deleted
|
|
|
|
+ // 3) A new source file is added
|
|
|
|
+ // 4) A new metadata file is added
|
|
|
|
+ // 5) Delay has expired and the file must be proccessed now
|
|
|
|
+ // 6) A delayed file was deleted
|
|
|
|
+ // 7) A delayed file is updated
|
|
|
|
+
|
|
|
|
+ // Normally Events 2, 3 and 7 would cause a new metadata to be generated.
|
|
|
|
+
|
|
|
|
+ // Event 1 requires no action (since an orphan metadata file is harmless).
|
|
|
|
+ // Event 2 will need to be delayed if Event 1 has not occurred yet.
|
|
|
|
+ // Event 3 will need to be delayed if Event 4 has not occurred yet.
|
|
|
|
+ // Event 4 will end a delay early if Event 3 has already occurred.
|
|
|
|
+ // Event 5 & 6 will remove the file from the queue and start processing.
|
|
|
|
+ // Event 7 will simply continue waiting.
|
|
|
|
+
|
|
|
|
+ // Event 4: Metadata file added, check if Event 3 has already occurred.
|
|
|
|
+ if (triggeredByMetadata && !source.m_isDelete)
|
|
|
|
+ {
|
|
|
|
+ auto itr = m_delayProcessMetadataFiles.find(absolutePath);
|
|
|
|
+
|
|
|
|
+ if (itr != m_delayProcessMetadataFiles.end())
|
|
|
|
+ {
|
|
|
|
+ // Events 3 and 4 have occurred, clear to proceed.
|
|
|
|
+ m_delayProcessMetadataFiles.erase(itr);
|
|
|
|
+ Q_EMIT ProcessingResumed(normalizedFilePath);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ // Events 1-3, 5-7
|
|
|
|
+ if (m_delayProcessMetadataFiles.contains(absolutePath))
|
|
|
|
+ {
|
|
|
|
+ auto duration = m_delayProcessMetadataFiles[absolutePath].msecsTo(QDateTime::currentDateTime());
|
|
|
|
+ if (!HasDelayProcessTimerElapsed(duration))
|
|
|
|
+ {
|
|
|
|
+ // Event 7: Already waiting on file, keep waiting
|
|
|
|
+ if (!m_delayProcessMetadataQueued)
|
|
|
|
+ {
|
|
|
|
+ m_delayProcessMetadataQueued = true;
|
|
|
|
+ QTimer::singleShot(m_metaCreationDelayMs, this, SLOT(DelayedMetadataFileCheck()));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Event 5-6: Times up, process the file
|
|
|
|
+ m_delayProcessMetadataFiles.erase(absolutePath);
|
|
|
|
+ Q_EMIT ProcessingResumed(normalizedFilePath);
|
|
|
|
+ }
|
|
|
|
+ else if ((triggeredByMetadata || !source.m_isDelete) && !CheckMetadataIsAvailable(absolutePath))
|
|
|
|
+ {
|
|
|
|
+ // Events 2-3: File not in queue and invalid metadata, add to queue
|
|
|
|
+ AZ_Trace(
|
|
|
|
+ AssetProcessor::DebugChannel,
|
|
|
|
+ "Source " AZ_STRING_FORMAT " has no metadata file yet, delaying processing to wait for metadata file.\n",
|
|
|
|
+ AZ_STRING_ARG(absolutePath.Native()));
|
|
|
|
+
|
|
|
|
+ m_delayProcessMetadataFiles.emplace(absolutePath, QDateTime::currentDateTime());
|
|
|
|
+
|
|
|
|
+ if (!m_delayProcessMetadataQueued)
|
|
|
|
+ {
|
|
|
|
+ m_delayProcessMetadataQueued = true;
|
|
|
|
+ QTimer::singleShot(m_metaCreationDelayMs, this, SLOT(DelayedMetadataFileCheck()));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Q_EMIT ProcessingDelayed(normalizedFilePath);
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void AssetProcessorManager::DelayedMetadataFileCheck()
|
|
|
|
+ {
|
|
|
|
+ m_delayProcessMetadataQueued = false;
|
|
|
|
+ auto now = QDateTime::currentDateTime();
|
|
|
|
+ bool rerun = false;
|
|
|
|
+ int minWaitTime = m_metaCreationDelayMs;
|
|
|
|
+
|
|
|
|
+ for (const auto& [file, time] : m_delayProcessMetadataFiles)
|
|
|
|
+ {
|
|
|
|
+ auto duration = time.msecsTo(now);
|
|
|
|
+
|
|
|
|
+ if (HasDelayProcessTimerElapsed(duration))
|
|
|
|
+ {
|
|
|
|
+ // Times up, process it
|
|
|
|
+ auto* fileStateCache = AZ::Interface<IFileStateRequests>::Get();
|
|
|
|
+ AZ_Assert(fileStateCache, "Programmer Error - IFileStateRequests is not available.");
|
|
|
|
+
|
|
|
|
+ AssessFileInternal(file.c_str(), !fileStateCache->Exists(file.c_str()));
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ rerun = true;
|
|
|
|
+ // Figure out the shortest amount of time left to wait for the next file.
|
|
|
|
+ // This avoids waiting the full duration again in the case that a file was added to the wait list after the timer was already started.
|
|
|
|
+ // Example:
|
|
|
|
+ // t=0, A is added and the timer is started with 5000ms delay
|
|
|
|
+ // t=1000, B is added
|
|
|
|
+ // t=5000, this function runs and A is processed. Timer is started again with 1000ms wait
|
|
|
|
+ minWaitTime = AZ::GetMin(minWaitTime, AZ::GetMax(0, int(m_metaCreationDelayMs - duration)));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (rerun)
|
|
|
|
+ {
|
|
|
|
+ m_delayProcessMetadataQueued = true;
|
|
|
|
+ QTimer::singleShot(minWaitTime, this, SLOT(DelayedMetadataFileCheck()));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
} // namespace AssetProcessor
|
|
} // namespace AssetProcessor
|