Initial commit

This commit is contained in:
alexpete
2021-03-05 11:26:34 -08:00
commit a10351f38d
27091 changed files with 5521199 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,236 @@
#ifndef ASSETPROCESSOR_ASSETDATABASE_H
#define ASSETPROCESSOR_ASSETDATABASE_H
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Asset/AssetCommon.h>
#include <AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h>
#include <QtCore/QSet>
#include <QtCore/QString>
#include "AzToolsFramework/API/EditorAssetSystemAPI.h"
class QStringList;
namespace AssetProcessor
{
//! the Asset Processor's database manager's job is to create and modify the actual underlying
//! SQL database. All queries to make changes to the database go through here. This includes
//! connecting to existing database and altering or creating database tables, etc.
class AssetDatabaseConnection
: public AzToolsFramework::AssetDatabase::AssetDatabaseConnection
{
public:
AZ_CLASS_ALLOCATOR(AssetDatabaseConnection, AZ::SystemAllocator, 0);
AssetDatabaseConnection();
~AssetDatabaseConnection();
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::AssetDatabase::Connection
public:
bool IsReadOnly() const override
{
return false;// return false, we actually curate/write to this database.
}
void VacuumAndAnalyze();
protected:
void CreateStatements() override;
bool PostOpenDatabase() override;
//////////////////////////////////////////////////////////////////////////
public:
bool DataExists();
void LoadData();
void ClearData();
//////////////////////////////////////////////////////////////////////////
//Queries
//NOTE: When passing in a structure to the Set<> functions, a default constructed structure has -1 for
//the table generated id and is filled in by the query, that is why it is passed by non const reference.
//For instance when SetSource(scanFolderEntry); is called it evaluates the query and fills in the
//main m_sourceID from the query. If you pass in a structure with the id already filled in, i.e. not -1,
//it is interpreted as an update to the database only if the contents differ in any way from whats
//already in he database. Obviously if it is filled in and does not exist it returns false.
//NOTE: The return bool for these queries only return true if both the query succeeded AND you got a result
//////////////////////////////////////////////////////////////////////////
//scan folders
bool GetScanFolders(AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntryContainer& container);
bool GetScanFolderByScanFolderID(AZ::s64 scanFolderID, AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& entry);
bool GetScanFolderBySourceID(AZ::s64 sourceID, AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& entry);
bool GetScanFolderByJobID(AZ::s64 jobID, AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& entry);
bool GetScanFolderByProductID(AZ::s64 productID, AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& entry);
bool GetScanFolderByPortableKey(QString portableKey, AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& entry);
bool SetScanFolder(AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& entry); //on success sets scanfolderID, if already exists updates it
bool RemoveScanFolder(AZ::s64 scanFolderID);
bool RemoveScanFolders(AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntryContainer& container);
//sources
bool GetSources(AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer& container);
bool GetSourceBySourceGuid(AZ::Uuid sourceGuid, AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry);
bool GetSourceBySourceID(AZ::s64 sourceID, AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry);
bool GetSourceBySourceName(QString exactSourceName, AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry);
bool GetSourcesBySourceName(QString exactSourceName, AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer& source);
bool GetSourcesBySourceNameScanFolderId(QString exactSourceName, AZ::s64 scanFolderID, AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer& source);
bool GetSourcesLikeSourceName(QString likeSourceName, LikeType likeType, AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer& container);
bool GetSourceByJobID(AZ::s64 jobID, AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry);
bool GetSourceByProductID(AZ::s64 productID, AzToolsFramework::AssetDatabase::SourceDatabaseEntry& source);
bool GetSourcesByProductName(QString exactProductName, AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer& container);
bool GetSourcesLikeProductName(QString likeProductName, LikeType likeType, AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer& container);
bool SetSource(AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry); //on success sets sourceID, if it already exists updates it
bool RemoveSource(AZ::s64 sourceID);
bool RemoveSources(AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer& container);
bool RemoveSourcesByScanFolderID(AZ::s64 scanFolderID);
bool InvalidateSourceAnalysisFingerprints();
//jobs
// used to initialize the predictor for job Run Keys
AZ::s64 GetHighestJobRunKey();
bool GetJobs(AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool GetJobByJobID(AZ::s64 jobID, AzToolsFramework::AssetDatabase::JobDatabaseEntry& entry);
bool GetJobByProductID(AZ::s64 productID, AzToolsFramework::AssetDatabase::JobDatabaseEntry& entry);
bool GetJobsBySourceID(AZ::s64 sourceID, AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool GetJobsBySourceName(QString exactSourceName, AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool GetJobsLikeSourceName(QString likeSourceName, LikeType likeType, AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool GetJobsByProductName(QString exactProductName, AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool GetJobsLikeProductName(QString likeProductName, LikeType likeType, AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool SetJob(AzToolsFramework::AssetDatabase::JobDatabaseEntry& entry); //on success sets jobID, if it already exists updates it
bool RemoveJob(AZ::s64 jobID);
bool RemoveJobs(AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer& container);
bool RemoveJobByProductID(AZ::s64 productID);
//products
bool GetProducts(AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool GetProductsByJobID(AZ::s64 jobID, AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container);
// note that the pair of (JobID, SubID) uniquely identifies a single job, and thus the result is always only one entry:
bool GetProductByJobIDSubId(AZ::s64 jobID, AZ::u32 subID, AzToolsFramework::AssetDatabase::ProductDatabaseEntry& result);
bool GetProductBySourceGuidSubId(AZ::Uuid sourceGuid, AZ::u32 subId, AzToolsFramework::AssetDatabase::ProductDatabaseEntry& result);
bool GetProductByProductID(AZ::s64 productID, AzToolsFramework::AssetDatabase::ProductDatabaseEntry& entry);
bool GetProductsByProductName(QString exactProductName, AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool GetProductsLikeProductName(QString likeProductName, LikeType likeType, AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool GetProductsBySourceID(AZ::s64 sourceID, AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool GetProductsBySourceName(QString exactSourceName, AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool GetProductsLikeSourceName(QString likeSourceName, LikeType likeType, AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
bool SetProduct(AzToolsFramework::AssetDatabase::ProductDatabaseEntry& entry); //on success sets productID, if it already exists updates it
bool SetProducts(AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container); //on success sets productID, if it already exists updates it
bool RemoveProducts(AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container);
bool RemoveProduct(AZ::s64 productID);
bool RemoveProductsByJobID(AZ::s64 jobID);
bool RemoveProductsBySourceID(AZ::s64 sourceID, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
//jobinfo
bool GetJobInfoByJobID(AZ::s64 jobID, AzToolsFramework::AssetSystem::JobInfo& jobInfo);
bool GetJobInfoByJobKey(AZStd::string jobKey, AzToolsFramework::AssetSystem::JobInfoContainer& container);
bool GetJobInfoByJobRunKey(AZ::u64 jobRunKey, AzToolsFramework::AssetSystem::JobInfoContainer& container);
bool GetJobInfoBySourceName(QString exactSourceName, AzToolsFramework::AssetSystem::JobInfoContainer& container, AZ::Uuid builderGuid = AZ::Uuid::CreateNull(), QString jobKey = QString(), QString platform = QString(), AzToolsFramework::AssetSystem::JobStatus status = AzToolsFramework::AssetSystem::JobStatus::Any);
/* --------------------- Source Dependency Table -------------------
* For example, this table records when a source file depends on another source file either directly (DEP_SourceToSource)
* but also whether then source file depends on another source file indirectly because it depends on a job which processes
* that source file
*/
/// Set a row in the table. It is invalid to overwrite existing rows without removing them first.
bool SetSourceFileDependency(AzToolsFramework::AssetDatabase::SourceFileDependencyEntry& entry);
/// Set a batch of rows. It is invalid to overwrite existing rows, so consider using RemoveSourceFileDependencies first.
bool SetSourceFileDependencies(AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container);
/// Remove a dependency, given a row ID
bool RemoveSourceFileDependency(AZ::s64 sourceFileDependencyId);
/// Batch remove a bunch of rows by container
bool RemoveSourceFileDependencies(const AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container);
/// Batch remove a bunch of rows by IDs
bool RemoveSourceFileDependencies(const AZStd::unordered_set<AZ::s64>& container);
/// Direct retrieval by ID (does not use any filtering)
bool GetSourceFileDependencyBySourceDependencyId(AZ::s64 sourceDependencyId, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry& sourceDependencyEntry);
// The following functions are all search functions (as opposed to the above functions which fetch or operate on specific rows)
// They tend to take a "Type of Dependency" filter - you can use DEP_Any to query all kinds of dependencies.
/// Given a source file, what does it DEPEND ON?
bool GetDependsOnSourceBySource(const char* source, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container);
/// Given a source file and a builder UUID, does it DEPEND ON?
bool GetSourceFileDependenciesByBuilderGUIDAndSource(const AZ::Uuid& builderGuid, const char* source, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container);
/// Given a source file, what depends ON IT? ('reverse dependency')
bool GetSourceFileDependenciesByDependsOnSource(const QString& dependsOnSource, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container);
// --------------------- Legacy SUBID table -------------------
bool CreateOrUpdateLegacySubID(AzToolsFramework::AssetDatabase::LegacySubIDsEntry& entry); // create or overwrite operation.
bool RemoveLegacySubID(AZ::s64 legacySubIDsEntryID);
bool RemoveLegacySubIDsByProductID(AZ::s64 productID);
//ProductDependencies
bool GetProductDependencies(AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& container);
bool GetProductDependencyByProductDependencyID(AZ::s64 productDependencyID, AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry& productDependencyEntry);
bool GetProductDependenciesByProductID(AZ::s64 productID, AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& container);
bool GetDirectProductDependencies(AZ::s64 productID, AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container);
bool GetDirectReverseProductDependenciesBySourceGuidSubId(AZ::Uuid dependencySourceGuid, AZ::u32 dependencySubId, AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container);
bool GetAllProductDependencies(AZ::s64 productID, AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& container);
bool GetUnresolvedProductDependencies(AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& container);
bool SetProductDependency(AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry& entry);
// Missing product dependencies
bool SetMissingProductDependency(AzToolsFramework::AssetDatabase::MissingProductDependencyDatabaseEntry& entry);
bool GetMissingProductDependenciesByProductId(AZ::s64 productId, AzToolsFramework::AssetDatabase::MissingProductDependencyDatabaseEntryContainer& container);
bool GetMissingProductDependencyByMissingProductDependencyId(AZ::s64 missingProductDependencyId, AzToolsFramework::AssetDatabase::MissingProductDependencyDatabaseEntry& missingProductDependencyEntry);
// updates or inserts multiple dependencies in a single transaction. Unlike SetProductDependencies, this does *not* delete existing dependencies
bool UpdateProductDependencies(AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& container);
// bulk inserts are lighter weight and don't change the input data. Note that this also deletes old dependencies for the products mentioned in the container.
bool SetProductDependencies(const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& container);
bool RemoveProductDependencyByProductId(AZ::s64 productID);
// bulk replace builder info table with new builder info table. Replaces the existing table of data.
// Note: newEntries will have their m_builderInfoID member set to their inserted rowId if this call succeeds.
bool SetBuilderInfoTable(AzToolsFramework::AssetDatabase::BuilderInfoEntryContainer& newEntries);
//Files
bool GetFileByFileID(AZ::s64 fileID, AzToolsFramework::AssetDatabase::FileDatabaseEntry& entry);
bool GetFileByFileNameAndScanFolderId(QString fileName, AZ::s64 scanFolderId, AzToolsFramework::AssetDatabase::FileDatabaseEntry& entry);
bool GetFilesLikeFileName(QString likeFileName, LikeType likeType, AzToolsFramework::AssetDatabase::FileDatabaseEntryContainer& container);
bool InsertFiles(AzToolsFramework::AssetDatabase::FileDatabaseEntryContainer& entry);
bool InsertFile(AzToolsFramework::AssetDatabase::FileDatabaseEntry& entry, bool& entryAlreadyExists);
bool UpdateFile(AzToolsFramework::AssetDatabase::FileDatabaseEntry& entry, bool& entryAlreadyExists);
// updates the modtime and hash for a file if it exists. Only returns true if the row existed and was successfully updated
bool UpdateFileModTimeAndHashByFileNameAndScanFolderId(QString fileName, AZ::s64 scanFolderId, AZ::u64 modTime, AZ::u64 hash);
bool RemoveFile(AZ::s64 sourceID);
protected:
void SetDatabaseVersion(AzToolsFramework::AssetDatabase::DatabaseVersion ver);
void ExecuteCreateStatements();
private:
AZStd::vector<AZStd::string> m_createStatements; // contains all statements required to create the tables
};
}//namespace EditorFramework
#endif // ASSETPROCESSOR_ASSETDATABASE_H
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,220 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <QObject>
#include <QString>
#include <QTimer>
#include <QStringList>
#include <QHash>
#include <QDir>
#include "native/AssetDatabase/AssetDatabase.h"
#include "native/assetprocessor.h"
#include "native/utilities/AssetUtilEBusHelper.h"
#include "native/utilities/PlatformConfiguration.h"
#include <AzFramework/Asset/AssetRegistry.h>
#include <QMutex>
#include <QMultiMap>
#include <AzCore/IO/SystemFile.h>
#include <AzToolsFramework/ToolsComponents/ToolsAssetCatalogBus.h>
#endif
#include "AssetRequestHandler.h"
namespace AzFramework
{
class AssetRegistry;
namespace AssetSystem
{
class AssetNotificationMessage;
}
}
namespace AssetProcessor
{
class AssetDatabaseConnection;
class AssetCatalog
: public QObject
, private AssetRegistryRequestBus::Handler
, private AzToolsFramework::AssetSystemRequestBus::Handler
, private AzToolsFramework::ToolsAssetSystemBus::Handler
, private AZ::Data::AssetCatalogRequestBus::Handler
{
using NetworkRequestID = AssetProcessor::NetworkRequestID;
using BaseAssetProcessorMessage = AzFramework::AssetSystem::BaseAssetProcessorMessage;
Q_OBJECT;
public:
AssetCatalog(QObject* parent, AssetProcessor::PlatformConfiguration* platformConfiguration);
virtual ~AssetCatalog();
Q_SIGNALS:
// outgoing message to the network
void SendAssetMessage(AzFramework::AssetSystem::AssetNotificationMessage message);
void AsyncAssetCatalogStatusResponse(AssetCatalogStatus status);
public Q_SLOTS:
// incoming message from the AP
void OnAssetMessage(AzFramework::AssetSystem::AssetNotificationMessage message);
void OnDependencyResolved(const AZ::Data::AssetId& assetId, const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry& entry);
void SaveRegistry_Impl();
virtual AzFramework::AssetSystem::GetUnresolvedDependencyCountsResponse HandleGetUnresolvedDependencyCountsRequest(MessageData<AzFramework::AssetSystem::GetUnresolvedDependencyCountsRequest> messageData);
virtual void HandleSaveAssetCatalogRequest(MessageData<AzFramework::AssetSystem::SaveAssetCatalogRequest> messageData);
void BuildRegistry();
void OnSourceQueued(AZ::Uuid sourceUuid, AZ::Uuid legacyUuid, QString rootPath, QString relativeFilePath);
void OnSourceFinished(AZ::Uuid sourceUuid, AZ::Uuid legacyUuid);
void AsyncAssetCatalogStatusRequest();
protected:
//////////////////////////////////////////////////////////////////////////
// AssetRegistryRequestBus::Handler overrides
int SaveRegistry() override;
void ValidatePreLoadDependency() override;
//////////////////////////////////////////////////////////////////////////
void RegistrySaveComplete(int assetCatalogVersion, bool allCatalogsSaved);
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::AssetSystem::AssetSystemRequestBus::Handler overrides
const char* GetAbsoluteDevGameFolderPath() override;
const char* GetAbsoluteDevRootFolderPath() override;
bool GetRelativeProductPathFromFullSourceOrProductPath(const AZStd::string& fullPath, AZStd::string& relativeProductPath) override;
bool GetFullSourcePathFromRelativeProductPath(const AZStd::string& relPath, AZStd::string& fullSourcePath) override;
bool GetAssetInfoById(const AZ::Data::AssetId& assetId, const AZ::Data::AssetType& assetType, const AZStd::string& platformName, AZ::Data::AssetInfo& assetInfo, AZStd::string& rootFilePath) override;
bool GetSourceInfoBySourcePath(const char* sourcePath, AZ::Data::AssetInfo& assetInfo, AZStd::string& watchFolder) override;
bool GetSourceInfoBySourceUUID(const AZ::Uuid& sourceUuid, AZ::Data::AssetInfo& assetInfo, AZStd::string& watchFolder) override;
bool GetScanFolders(AZStd::vector<AZStd::string>& scanFolders) override;
bool GetAssetSafeFolders(AZStd::vector<AZStd::string>& assetSafeFolders) override;
bool IsAssetPlatformEnabled(const char* platform) override;
int GetPendingAssetsForPlatform(const char* platform) override;
bool GetAssetsProducedBySourceUUID(const AZ::Uuid& sourceUuid, AZStd::vector<AZ::Data::AssetInfo>& productsAssetInfo) override;
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
// AssetCatalogRequestBus overrides
AZStd::string GetAssetPathById(const AZ::Data::AssetId& id) override;
AZ::Data::AssetId GetAssetIdByPath(const char* path, const AZ::Data::AssetType& typeToRegister, bool autoRegisterIfNotFound) override;
AZ::Data::AssetInfo GetAssetInfoById(const AZ::Data::AssetId& id) override;
AZ::Outcome<AZStd::vector<AZ::Data::ProductDependency>, AZStd::string> GetDirectProductDependencies(const AZ::Data::AssetId& id) override;
AZ::Outcome<AZStd::vector<AZ::Data::ProductDependency>, AZStd::string> GetAllProductDependencies(const AZ::Data::AssetId& id) override;
AZ::Outcome<AZStd::vector<AZ::Data::ProductDependency>, AZStd::string> GetLoadBehaviorProductDependencies(
const AZ::Data::AssetId& id, AZStd::unordered_set<AZ::Data::AssetId>& noloadSet,
AZ::Data::PreloadAssetListType& preloadAssetList) override;
////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::ToolsAssetSystemBus::Handler
void RegisterSourceAssetType(const AZ::Data::AssetType& assetType, const char* assetFileFilter);
void UnregisterSourceAssetType(const AZ::Data::AssetType& assetType);
//////////////////////////////////////////////////////////////////////////
//! given some absolute path, please respond with its relative product path. For now, this will be a
//! string like 'textures/blah.tif' (we don't care about extensions), but eventually, this will
//! be an actual asset UUID.
void ProcessGetRelativeProductPathFromFullSourceOrProductPathRequest(const AZStd::string& fullPath, AZStd::string& relativeProductPath);
//! This function helps in determining the full product path of an relative product path.
//! In the future we will be sending an asset UUID to this function to request for full path.
void ProcessGetFullSourcePathFromRelativeProductPathRequest(const AZStd::string& relPath, AZStd::string& fullSourcePath);
//! Gets the source file info for an Asset by checking the DB first and the APM queue second
bool GetSourceFileInfoFromAssetId(const AZ::Data::AssetId &assetId, AZStd::string& watchFolder, AZStd::string& relativePath);
//! Gets the product AssetInfo based on a platform and assetId. If you specify a null or empty platform the current or first available will be used.
AZ::Data::AssetInfo GetProductAssetInfo(const char* platformName, const AZ::Data::AssetId& id);
//! GetAssetInfo that tries to figure out if the asset is a product or source so it can return info about the product or source respectively
bool GetAssetInfoByIdOnly(const AZ::Data::AssetId& id, const AZStd::string& platformName, AZ::Data::AssetInfo& assetInfo, AZStd::string& rootFilePath);
//! Checks in the currently-in-queue assets list for info on an asset (by source Id)
bool GetQueuedAssetInfoById(const AZ::Uuid& guid, AZStd::string& watchFolder, AZStd::string& relativePath);
//! Checks in the currently-in-queue assets list for info on an asset (by source name)
bool GetQueuedAssetInfoByRelativeSourceName(const char* sourceName, AZ::Data::AssetInfo& assetInfo, AZStd::string& watchFolder);
//! Gets the source info for a source that is not in the DB or APM queue
bool GetUncachedSourceInfoFromDatabaseNameAndWatchFolder(const char* sourceDatabasePath, const char* watchFolder, AZ::Data::AssetInfo& assetInfo);
bool ConnectToDatabase();
bool CheckValidatedAssets(AZ::Data::AssetId assetId, const QString& platform);
//! For lookups that don't provide a specific platform, provide a default platform to use.
QString GetDefaultAssetPlatform();
AZ::Outcome<AZStd::vector<AZ::Data::ProductDependency>, AZStd::string> GetAllProductDependenciesFilter(
const AZ::Data::AssetId& id,
const AZStd::unordered_set<AZ::Data::AssetId>& exclusionList,
const AZStd::vector<AZStd::string>& wildcardPatternExclusionList);
bool DoesAssetIdMatchWildcardPattern(const AZ::Data::AssetId& assetId, const AZStd::string& wildcardPattern);
void AddAssetDependencies(
const AZ::Data::AssetId& searchAssetId,
AZStd::unordered_set<AZ::Data::AssetId>& assetSet,
AZStd::vector<AZ::Data::ProductDependency>& dependencyList,
const AZStd::unordered_set<AZ::Data::AssetId>& exclusionList,
const AZStd::vector<AZStd::string>& wildcardPatternExclusionList,
AZ::Data::PreloadAssetListType& preloadAssetList);
//! List of AssetTypes that should return info for the source instead of the product
AZStd::unordered_set<AZ::Data::AssetType> m_sourceAssetTypes;
AZStd::unordered_map<AZStd::string, AZ::Data::AssetType> m_sourceAssetTypeFilters;
AZStd::mutex m_sourceAssetTypesMutex;
//! Used to protect access to the database connection, only one thread can use it at a time
AZStd::mutex m_databaseMutex;
struct SourceInfo
{
QString m_watchFolder;
QString m_sourceName;
};
AZStd::mutex m_sourceUUIDToSourceNameMapMutex;
using SourceUUIDToSourceNameMap = AZStd::unordered_map<AZ::Uuid, SourceInfo>;
using SourceNameToSourceUuidMap = AZStd::unordered_map<AZStd::string, AZ::Uuid>;
SourceUUIDToSourceNameMap m_sourceUUIDToSourceNameMap; // map of uuids to source file names for assets that are currently in the processing queue
SourceNameToSourceUuidMap m_sourceNameToSourceUUIDMap;
QMutex m_registriesMutex;
QHash<QString, AzFramework::AssetRegistry> m_registries; // per platform.
AssetProcessor::PlatformConfiguration* m_platformConfig;
QStringList m_platforms;
AZStd::unique_ptr<AssetDatabaseConnection> m_db;
QDir m_cacheRoot;
bool m_registryBuiltOnce;
bool m_catalogIsDirty = true;
bool m_currentlySavingCatalog = false;
bool m_currentlyValidatingPreloadDependency = false;
int m_currentRegistrySaveVersion = 0;
QMutex m_savingRegistryMutex;
QMultiMap<int, AssetProcessor::NetworkRequestID> m_queuedSaveCatalogRequest;
AZStd::vector<AZStd::pair<AZ::Data::AssetId, QString>> m_preloadAssetList;
AZStd::unordered_multimap<AZ::Data::AssetId, QString> m_cachedNoPreloadDependenyAssetList;
AZStd::vector<char> m_saveBuffer; // so that we don't realloc all the time
char m_absoluteDevFolderPath[AZ_MAX_PATH_LEN];
char m_absoluteDevGameFolderPath[AZ_MAX_PATH_LEN];
QDir m_cacheRootDir;
};
}
@@ -0,0 +1,138 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef ASSETPROCESSOR_ASSETDATA_H
#define ASSETPROCESSOR_ASSETDATA_H
#include <QMetaType>
#include <QString>
#include <QSet>
#include <AzCore/Math/Uuid.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/Asset/AssetCommon.h>
#include <AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h>
namespace AssetProcessor
{
using namespace AzToolsFramework::AssetDatabase;
//Check the extension of all the products
//return true if any one of the product extension matches the input extension, else return false
bool CheckProductsExtension( const ProductDatabseEntryContainer& products, const char* ext );
//! this is the interface which we use to speak to the legacy database tables.
// its known as the legacy database interface because the forthcoming tables will completely replace these
// but this layer exits for compatibility with the previous version and allows us to upgrade in place.
class AssetDatabaseInterface
{
public:
AssetDatabaseInterface()
{
qRegisterMetaType<SourceDatabaseEntry>( "SourceEntry" );
qRegisterMetaType<ProductDatabaseEntry>( "ProductEntry" );
qRegisterMetaType<SourceDatabaseEntryContainer>( "SourceEntryContainer" );
qRegisterMetaType<ProductDatabseEntryContainer>( "ProductEntryContainer" );
}
virtual ~AssetDatabaseInterface()
{
}
//! Returns true if the database or file exists already
virtual bool DataExists() = 0;
//! Actually connects to the database, loads it, or creates empty database depending on above.
virtual void LoadData() = 0;
//! Use with care. Resets all data! This causes an immediate commit and save!
virtual void ClearData() = 0;
//! Retrieve the scan folders
virtual void GetScanFolders(QStringList& scanFolderList) = 0;
//! Retrieves a specific scan folder by id, return false if not found
virtual bool GetScanFolder(AZ::s64 scanFolderID, QString& scanFolder) = 0;
//! Adds a scan folder
virtual AZ::s64 AddScanFolder(QString scanFolder) = 0;
// ! remove a scanfolder
virtual void RemoveScanFolder(AZ::s64 scanFolderID) = 0;
virtual void RemoveScanFolder(QString scanFolder) = 0;
//! query the scanFolder ID for a given folder, return false if not found
virtual bool GetScanFolderID(QString scanfolder, AZ::s64& scanFolderID) = 0;
//! query the sourceID of a source
virtual bool GetSourceID(QString sourceName, QString jobDescription, AZ::s64& sourceID) = 0;
//! Retrieve the fingerprint for a given source name on a given platform for a particular jobDescription
//! This could return zero if its never seen this file before.
virtual bool GetFingerprintForSource(QString sourceName, QString jobDescription, AZ::u32& fingerprint) = 0;
//! Set the fingerprint for the given source name, platform and jobDescription to the value provided
//! If updating a existing fingerprint you do not have to supply guid or scanfolderid
virtual void SetSource(QString sourceName, QString jobDescription, AZ::u32 fingerprint, AZ::Uuid guid = AZ::Uuid::CreateNull(), AZ::s64 scanFolderID = 0) = 0;
//! Removing a fingerprint will destroy its entry in the database
//! and any entries that refer to it (products, etc). if you want to merely set it dirty
//! Then instead call SetSource to zero
virtual void RemoveSource(QString sourceName, QString jobDescription) = 0;
virtual void RemoveSource(AZ::s64 sourceID) = 0;
//! Given a source name, jobDescription, and platform return the list of products by the last compile of that file
//! returns false if it doesn't know about source or if the source did not emitted any products.
virtual bool GetProductsForSource(QString sourceName, QString jobDescription, ProductDatabseEntryContainer& products, QString platform = QString()) = 0;
//! Given a source name and platform return the list of all jobDescriptions associated with them from the last compile of that file
//! returns false if it doesn't know about any job description for that source and platform.
virtual bool GetJobDescriptionsForSource(QString sourceName, QStringList& jobDescription) = 0;
//! Given an product file name, compute source file name
//! False is returned if its never heard of that product.
virtual bool GetSourceFromProductName(QString productName, SourceDatabaseEntry& source) = 0;
//! For a given source, set the list of products for that source.
//! Removes any data that's present and overwrites it with the new list
//! Note that an empty list is acceptable data, it means the source emitted no products
virtual void SetProductsForSource(QString sourceName, QString jobDescription, const ProductDatabseEntryContainer& productList = ProductDatabseEntryContainer(), QString platform = QString()) = 0;
//! Clear the products for a given source. This removes the entry entirely, not just sets it to empty.
virtual void RemoveProducts(QString sourceName, QString jobDescription, QString platform = QString()) = 0;
virtual void RemoveProduct(AZ::s64 productID) = 0;
//! GetMatchingProductFiles - checks the database for all products that begin with the given match check
//! Note that the input string is expected to not include the cache folder
//! so it probably starts with platform name.
virtual void GetMatchingProducts(QString matchCheck, ProductDatabseEntryContainer& products, QString platform = QString()) = 0;
//! GetMatchingSourceFiles - checks the database for all source files that begin with the given match check
//! note that the input string is expected to be the relative path name
//! and the output is the relative name (so to convert it to a full path, you will need to call the appropriate function)
virtual void GetMatchingSources(QString matchCheck, SourceDatabaseEntryContainer& sources) = 0;
//! Get a giant list of ALL known source files in the database.
virtual void GetSources(SourceDatabaseEntryContainer& sources) = 0;
//! Get a giant list of ALL known products in the database.
virtual void GetProducts(ProductDatabseEntryContainer& products, QString platform = QString()) = 0;
//! finds all elements in the database that ends with the given input. (Used to look things up by extensions, in general)
virtual void GetSourcesByExtension(QString extension, SourceDatabaseEntryContainer& sources) = 0;
//! SetJobLogForSource updates the Job Log table to record the status of a particular job.
//! It also sets all prior jobs that match that job exactly to not be the "latest one" but keeps them in the database.
virtual void SetJobLogForSource(AZ::s64 jobId, const AZStd::string& sourceName, const AZStd::string& platform, const AZ::Uuid& builderUuid, const AZStd::string& jobKey, AzToolsFramework::AssetProcessor::JobStatus status) = 0;
};
} // namespace AssetProcessor
#endif // ASSETPROCESSOR_ASSETDATA_H
@@ -0,0 +1,559 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "AssetRequestHandler.h"
#include <QDir>
#include <QTimer>
#include <AzToolsFramework/ToolsComponents/ToolsAssetCatalogBus.h>
using namespace AssetProcessor;
namespace
{
static const uint32_t s_assetPath = AssetUtilities::ComputeCRC32Lowercase("assetPath");
}
AssetRequestHandler::AssetRequestLine::AssetRequestLine(QString platform, QString searchTerm, const AZ::Data::AssetId& assetId, bool isStatusRequest, int searchType)
: m_platform(platform)
, m_searchTerm(searchTerm)
, m_isStatusRequest(isStatusRequest)
, m_assetId(assetId)
, m_searchType(searchType)
{
}
bool AssetRequestHandler::AssetRequestLine::IsStatusRequest() const
{
return m_isStatusRequest;
}
QString AssetRequestHandler::AssetRequestLine::GetPlatform() const
{
return m_platform;
}
QString AssetRequestHandler::AssetRequestLine::GetSearchTerm() const
{
return m_searchTerm;
}
int AssetRequestHandler::AssetRequestLine::GetSearchType() const
{
return m_searchType;
}
const AZ::Data::AssetId& AssetRequestHandler::AssetRequestLine::GetAssetId() const
{
return m_assetId;
}
QString AssetRequestHandler::AssetRequestLine::GetDisplayString() const
{
if (m_assetId.IsValid())
{
return QString::fromUtf8(m_assetId.ToString<AZStd::string>().c_str());
}
return m_searchTerm;
}
int AssetRequestHandler::GetNumOutstandingAssetRequests() const
{
return m_pendingAssetRequests.size();
}
namespace
{
using namespace AzToolsFramework::AssetSystem;
using namespace AzFramework::AssetSystem;
GetFullSourcePathFromRelativeProductPathResponse HandleGetFullSourcePathFromRelativeProductPathRequest(MessageData<GetFullSourcePathFromRelativeProductPathRequest> messageData)
{
bool fullPathFound = false;
AZStd::string fullSourcePath;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(fullPathFound, &AzToolsFramework::AssetSystemRequestBus::Events::GetFullSourcePathFromRelativeProductPath, messageData.m_message->m_relativeProductPath, fullSourcePath);
if (!fullPathFound)
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Could not find full source path from the relative product path (%s).\n", messageData.m_message->m_relativeProductPath.c_str());
}
return GetFullSourcePathFromRelativeProductPathResponse(fullPathFound, fullSourcePath);
}
GetRelativeProductPathFromFullSourceOrProductPathResponse HandleGetRelativeProductPathFromFullSourceOrProductPathRequest(MessageData<GetRelativeProductPathFromFullSourceOrProductPathRequest> messageData)
{
bool relPathFound = false;
AZStd::string relProductPath;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(relPathFound, &AzToolsFramework::AssetSystemRequestBus::Events::GetRelativeProductPathFromFullSourceOrProductPath, messageData.m_message->m_sourceOrProductPath, relProductPath);
if (!relPathFound)
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Could not find relative product path for the source file (%s).", messageData.m_message->m_sourceOrProductPath.c_str());
}
return GetRelativeProductPathFromFullSourceOrProductPathResponse(relPathFound, relProductPath);
}
SourceAssetInfoResponse HandleSourceAssetInfoRequest(MessageData<SourceAssetInfoRequest> messageData)
{
SourceAssetInfoResponse response;
if (messageData.m_message->m_assetId.IsValid())
{
AZStd::string rootFolder;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(response.m_found, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetSourceInfoBySourceUUID, messageData.m_message->m_assetId.m_guid, response.m_assetInfo, rootFolder);
if (response.m_found)
{
response.m_assetInfo.m_assetId.m_subId = messageData.m_message->m_assetId.m_subId;
response.m_assetInfo.m_assetType = messageData.m_message->m_assetType;
response.m_rootFolder = rootFolder.c_str();
}
else
{
response.m_assetInfo.m_assetId.SetInvalid();
}
}
else if (!messageData.m_message->m_assetPath.empty())
{
AZStd::string rootFolder;
// its being asked for via path instead of ID. slightly different call.
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(response.m_found, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetSourceInfoBySourcePath, messageData.m_message->m_assetPath.c_str(), response.m_assetInfo, rootFolder);
response.m_rootFolder = rootFolder.c_str();
}
// note that in the case of an invalid request, response is defaulted to false for m_found, so there is no need to
// populate the response in that case.
return response;
}
SourceAssetProductsInfoResponse HandleSourceAssetProductsInfoRequest(MessageData<SourceAssetProductsInfoRequest> messageData)
{
SourceAssetProductsInfoResponse response;
if (messageData.m_message->m_assetId.IsValid())
{
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(response.m_found, &AssetSystemRequest::GetAssetsProducedBySourceUUID,
messageData.m_message->m_assetId.m_guid, response.m_productsAssetInfo);
}
// note that in the case of an invalid request, response is defaulted to false for m_found, so there is no need to
// populate the response in that case.
return response;
}
GetScanFoldersResponse HandleGetScanFoldersRequest(MessageData<GetScanFoldersRequest> messageData)
{
bool success = true;
AZStd::vector<AZStd::string> scanFolders;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(success, &AzToolsFramework::AssetSystemRequestBus::Events::GetScanFolders, scanFolders);
if (!success)
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Could not acquire a list of scan folders from the database.");
}
return GetScanFoldersResponse(move(scanFolders));
}
GetAssetSafeFoldersResponse HandleGetAssetSafeFoldersRequest(MessageData<GetAssetSafeFoldersRequest> messageData)
{
bool success = true;
AZStd::vector<AZStd::string> assetSafeFolders;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(success, &AzToolsFramework::AssetSystemRequestBus::Events::GetAssetSafeFolders, assetSafeFolders);
if (!success)
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Could not acquire a list of asset safe folders from the database.");
}
return GetAssetSafeFoldersResponse(move(assetSafeFolders));
}
void HandleRegisterSourceAssetRequest(MessageData<RegisterSourceAssetRequest> messageData)
{
AzToolsFramework::ToolsAssetSystemBus::Broadcast(&AzToolsFramework::ToolsAssetSystemRequests::RegisterSourceAssetType, messageData.m_message->m_assetType, messageData.m_message->m_assetFileFilter.c_str());
}
void HandleUnregisterSourceAssetRequest(MessageData<UnregisterSourceAssetRequest> messageData)
{
AzToolsFramework::ToolsAssetSystemBus::Broadcast(&AzToolsFramework::ToolsAssetSystemRequests::UnregisterSourceAssetType, messageData.m_message->m_assetType);
}
AssetInfoResponse HandleAssetInfoRequest(MessageData<AssetInfoRequest> messageData)
{
AssetInfoResponse response;
if (messageData.m_message->m_assetId.IsValid())
{
AZStd::string rootFilePath;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(response.m_found, &AzToolsFramework::AssetSystemRequestBus::Events::GetAssetInfoById,
messageData.m_message->m_assetId, messageData.m_message->m_assetType, messageData.m_message->m_platformName, response.m_assetInfo, rootFilePath);
response.m_rootFolder = rootFilePath;
}
else if (!messageData.m_message->m_assetPath.empty())
{
bool autoRegisterIfNotFound = false;
AZ::Data::AssetCatalogRequestBus::BroadcastResult(response.m_assetInfo.m_assetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, messageData.m_message->m_assetPath.c_str(), AZ::Data::s_invalidAssetType, autoRegisterIfNotFound);
response.m_found = response.m_assetInfo.m_assetId.IsValid();
}
return response;
}
AssetDependencyInfoResponse HandleAssetDependencyInfoRequest(MessageData<AssetDependencyInfoRequest> messageData)
{
using namespace AzFramework::AssetSystem;
AssetDependencyInfoResponse response;
if (messageData.m_message->m_assetId.IsValid())
{
AZ::Outcome<AZStd::vector<AZ::Data::ProductDependency>, AZStd::string> result = AZ::Failure(AZStd::string());
// Call the appropriate AssetCatalog API based on the type of dependencies requested.
switch (messageData.m_message->m_dependencyType)
{
case AssetDependencyInfoRequest::DependencyType::DirectDependencies:
AZ::Data::AssetCatalogRequestBus::BroadcastResult(result,
&AZ::Data::AssetCatalogRequestBus::Events::GetDirectProductDependencies, messageData.m_message->m_assetId);
break;
case AssetDependencyInfoRequest::DependencyType::AllDependencies:
AZ::Data::AssetCatalogRequestBus::BroadcastResult(result,
&AZ::Data::AssetCatalogRequestBus::Events::GetAllProductDependencies, messageData.m_message->m_assetId);
break;
case AssetDependencyInfoRequest::DependencyType::LoadBehaviorDependencies:
AZ::Data::AssetCatalogRequestBus::BroadcastResult(result,
&AZ::Data::AssetCatalogRequestBus::Events::GetLoadBehaviorProductDependencies,
messageData.m_message->m_assetId, response.m_noloadSet, response.m_preloadAssetList);
break;
}
// Decompose the AZ::Outcome into separate variables, since AZ::Outcome is not a serializable type.
response.m_found = result.IsSuccess();
if (response.m_found)
{
response.m_dependencies = result.GetValue();
}
else
{
response.m_errorString = result.GetError();
}
}
else
{
response.m_found = false;
response.m_errorString.assign("Invalid Asset Id");
}
return response;
}
}
void AssetRequestHandler::HandleRequestEscalateAsset(MessageData<RequestEscalateAsset> messageData)
{
if (!messageData.m_message->m_assetUuid.IsNull())
{
// search by UUID is preferred.
Q_EMIT RequestEscalateAssetByUuid(messageData.m_platform, messageData.m_message->m_assetUuid);
}
else if (!messageData.m_message->m_searchTerm.empty())
{
// fall back to search term.
Q_EMIT RequestEscalateAssetBySearchTerm(messageData.m_platform, QString::fromUtf8(messageData.m_message->m_searchTerm.c_str()));
}
else
{
AZ_Warning(AssetProcessor::DebugChannel, false, "Invalid RequestEscalateAsset. Both the search term and uuid are empty/null\n");
}
}
bool AssetRequestHandler::InvokeHandler(MessageData<AzFramework::AssetSystem::BaseAssetProcessorMessage> messageData)
{
// This function checks to see whether the incoming message is either one of those request, which require decoding the type of message and then invoking the appropriate EBUS handler.
// If the message is not one of those type than it checks to see whether some one has registered a request handler for that message type and then invokes it.
using namespace AzFramework::AssetSystem;
{
auto located = m_requestRouter.m_messageHandlers.find(messageData.m_message->GetMessageType());
if(located != m_requestRouter.m_messageHandlers.end())
{
located->second(messageData);
return false;
}
AZ_Warning(AssetProcessor::DebugChannel, false, "OnNewIncomingRequest: Message Handler not found for message type %d, ignoring."
" Make sure to register new messages with IRequestRouter::RegisterMessageHandler", messageData.m_message->GetMessageType());
return true;
}
}
void AssetRequestHandler::ProcessAssetRequest(MessageData<RequestAssetStatus> messageData)
{
if ((messageData.m_message->m_searchTerm.empty())&&(!messageData.m_message->m_assetId.IsValid()))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Failed to decode incoming RequestAssetStatus - both path and uuid is empty\n");
SendAssetStatus(messageData.m_key, RequestAssetStatus::MessageType, AssetStatus_Unknown);
return;
}
AssetRequestLine newLine(messageData.m_platform, QString::fromUtf8(messageData.m_message->m_searchTerm.c_str()), messageData.m_message->m_assetId, messageData.m_message->m_isStatusRequest, messageData.m_message->m_searchType);
AZ_TracePrintf(AssetProcessor::DebugChannel, "GetAssetStatus/CompileAssetSync: %s.\n", newLine.GetDisplayString().toUtf8().constData());
QString assetPath = QString::fromUtf8(messageData.m_message->m_searchTerm.c_str()); // utf8-decode just once here, reuse below
m_pendingAssetRequests.insert(messageData.m_key, newLine);
Q_EMIT RequestCompileGroup(messageData.m_key, messageData.m_platform, assetPath, messageData.m_message->m_assetId, messageData.m_message->m_isStatusRequest, messageData.m_message->m_searchType);
}
void AssetRequestHandler::OnCompileGroupCreated(NetworkRequestID groupID, AssetStatus status)
{
using namespace AzFramework::AssetSystem;
auto located = m_pendingAssetRequests.find(groupID);
if (located == m_pendingAssetRequests.end())
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "OnCompileGroupCreated: No such asset group found, ignoring.\n");
return;
}
if (status == AssetStatus_Unknown)
{
// if this happens it means we made an async request and got a response from the build queue that no such thing
// exists in the queue. It might still be a valid asset - for example, it may have already finished compiling and thus
// won't be in the queue. To cover this we also make a request to the asset manager here (its also async)
Q_EMIT RequestAssetExists(groupID, located.value().GetPlatform(), located.value().GetSearchTerm(), located.value().GetAssetId(), located.value().GetSearchType());
}
else
{
// if its a status request, return it immediately and then remove it.
if (located.value().IsStatusRequest())
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "GetAssetStatus: Responding with status of: %s\n", located.value().GetDisplayString().toUtf8().constData());
SendAssetStatus(groupID, RequestAssetStatus::MessageType, status);
m_pendingAssetRequests.erase(located);
}
// if its not a status request then we'll wait for OnCompileGroupFinished before responding.
}
}
void AssetRequestHandler::OnCompileGroupFinished(NetworkRequestID groupID, AssetStatus status)
{
auto located = m_pendingAssetRequests.find(groupID);
if (located == m_pendingAssetRequests.end())
{
// this is okay to happen if its a status request.
return;
}
// if the compile group finished, but the request was for a SPECIFIC asset, we have to take an extra step since
// the compile group being finished just means the source file has compiled, doesn't necessarly mean that specific asset is emitted.
if (located.value().GetAssetId().IsValid())
{
Q_EMIT RequestAssetExists(groupID, located.value().GetPlatform(), located.value().GetSearchTerm(), located.value().GetAssetId(), located.value().GetSearchType());
}
else
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Compile Group finished: %s.\n", located.value().GetDisplayString().toUtf8().constData());
SendAssetStatus(groupID, RequestAssetStatus::MessageType, status);
m_pendingAssetRequests.erase(located);
}
}
//! Called from the outside in response to a RequestAssetExists.
void AssetRequestHandler::OnRequestAssetExistsResponse(NetworkRequestID groupID, bool exists)
{
using namespace AzFramework::AssetSystem;
auto located = m_pendingAssetRequests.find(groupID);
if (located == m_pendingAssetRequests.end())
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "OnRequestAssetExistsResponse: No such compile group found, ignoring.\n");
return;
}
AZ_TracePrintf(AssetProcessor::DebugChannel, "GetAssetStatus / CompileAssetSync: Asset %s is %s.\n",
located.value().GetDisplayString().toUtf8().constData(),
exists ? "compiled already" : "missing" );
SendAssetStatus(groupID, RequestAssetStatus::MessageType, exists ? AssetStatus_Compiled : AssetStatus_Missing);
m_pendingAssetRequests.erase(located);
}
void AssetRequestHandler::SendAssetStatus(NetworkRequestID groupID, unsigned int /*type*/, AssetStatus status)
{
ResponseAssetStatus resp;
resp.m_assetStatus = status;
EBUS_EVENT_ID(groupID.first, AssetProcessor::ConnectionBus, SendResponse, groupID.second, resp);
}
AssetRequestHandler::AssetRequestHandler()
{
m_requestRouter.RegisterQueuedCallbackHandler(this, &AssetRequestHandler::ProcessAssetRequest);
m_requestRouter.RegisterMessageHandler(&HandleGetFullSourcePathFromRelativeProductPathRequest);
m_requestRouter.RegisterMessageHandler(&HandleGetRelativeProductPathFromFullSourceOrProductPathRequest);
m_requestRouter.RegisterMessageHandler(&HandleSourceAssetInfoRequest);
m_requestRouter.RegisterMessageHandler(&HandleSourceAssetProductsInfoRequest);
m_requestRouter.RegisterMessageHandler(&HandleGetScanFoldersRequest);
m_requestRouter.RegisterMessageHandler(&HandleGetAssetSafeFoldersRequest);
m_requestRouter.RegisterMessageHandler(&HandleRegisterSourceAssetRequest);
m_requestRouter.RegisterMessageHandler(&HandleUnregisterSourceAssetRequest);
m_requestRouter.RegisterMessageHandler(&HandleAssetInfoRequest);
m_requestRouter.RegisterMessageHandler(&HandleAssetDependencyInfoRequest);
m_requestRouter.RegisterMessageHandler(ToFunction(&AssetRequestHandler::HandleRequestEscalateAsset));
}
QString AssetRequestHandler::CreateFenceFile(unsigned int fenceId)
{
QDir fenceDir;
if (!AssetUtilities::ComputeFenceDirectory(fenceDir))
{
return QString();
}
QString fileName = QString("fenceFile~%1.%2").arg(fenceId).arg(FENCE_FILE_EXTENSION);
QString fenceFileName = fenceDir.filePath(fileName);
QFileInfo fileInfo(fenceFileName);
if (!fileInfo.absoluteDir().exists())
{
// if fence dir does not exists ,than try to create it
if (!fileInfo.absoluteDir().mkpath("."))
{
return QString();
}
}
QFile fenceFile(fenceFileName);
if (fenceFile.exists())
{
return QString();
}
bool result = fenceFile.open(QFile::WriteOnly);
if (!result)
{
return QString();
}
fenceFile.close();
return fileInfo.absoluteFilePath();
}
bool AssetRequestHandler::DeleteFenceFile(QString fenceFileName)
{
return QFile::remove(fenceFileName);
}
void AssetRequestHandler::DeleteFenceFile_Retry(unsigned int fenceId, QString fenceFileName, NetworkRequestID key, AZStd::shared_ptr<BaseAssetProcessorMessage> message, QString platform, int retriesRemaining)
{
if (DeleteFenceFile(fenceFileName))
{
// add an entry in map
// We have successfully created and deleted the fence file, insert an entry for it in the pendingFenceRequest map
// and return, we will only process this request once the APM indicates that it has detected the fence file
m_pendingFenceRequestMap[fenceId] = AZStd::move(RequestInfo(key, AZStd::move(message), platform));
return;
}
retriesRemaining--;
if (retriesRemaining == 0)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "AssetProcessor was unable to delete the fence file");
// send request to the appropriate handler with fencingfailed set to true and return
InvokeHandler(MessageData(AZStd::move(message), key, platform, true));
}
else
{
auto deleteFenceFilefunctor = [this, fenceId, fenceFileName, key, message = AZStd::move(message), platform, retriesRemaining]() mutable
{
DeleteFenceFile_Retry(fenceId, fenceFileName, key, AZStd::move(message), platform, retriesRemaining);
};
QTimer::singleShot(100, this, AZStd::move(deleteFenceFilefunctor));
}
}
void AssetRequestHandler::OnNewIncomingRequest(unsigned int connId, unsigned int serial, QByteArray payload, QString platform)
{
AZ::SerializeContext* serializeContext = nullptr;
EBUS_EVENT_RESULT(serializeContext, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(serializeContext, "Unable to retrieve serialize context.");
AZStd::shared_ptr<BaseAssetProcessorMessage> message{ AZ::Utils::LoadObjectFromBuffer<BaseAssetProcessorMessage>(payload.constData(), payload.size(), serializeContext) };
if (!message)
{
AZ_Warning("Asset Request Handler", false, "OnNewIncomingRequest: Invalid object sent as network message to AssetRequestHandler.");
return;
}
NetworkRequestID key(connId, serial);
QString fenceFileName;
if (message->RequireFencing())
{
bool successfullyCreatedFenceFile = false;
int fenceID = 0;
for (int idx = 0; idx < g_RetriesForFenceFile; ++idx)
{
fenceID = ++m_fenceId;
fenceFileName = CreateFenceFile(fenceID);
if (!fenceFileName.isEmpty())
{
successfullyCreatedFenceFile = true;
break;
}
}
if (!successfullyCreatedFenceFile)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "AssetProcessor was unable to create the fence file");
// send request to the appropriate handler with fencingFailed set to true and return
InvokeHandler(MessageData(AZStd::move(message), key, platform, true));
}
else
{
// if we are here it means that we were able to create the fence file, we will try to delete it now with a fixed number of retries
DeleteFenceFile_Retry(fenceID, fenceFileName, key, AZStd::move(message), platform, g_RetriesForFenceFile);
}
}
else
{
// If we are here it indicates that the request does not require fencing, we either call the required bus or invoke the handler directly
InvokeHandler(MessageData(AZStd::move(message), key, platform));
}
}
void AssetRequestHandler::OnFenceFileDetected(unsigned int fenceId)
{
auto fenceRequestFound = m_pendingFenceRequestMap.find(fenceId);
if (fenceRequestFound == m_pendingFenceRequestMap.end())
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "OnFenceFileDetected: Fence File Request not found, ignoring.\n");
return;
}
InvokeHandler(MessageData(fenceRequestFound->second.m_message, fenceRequestFound->second.m_requestId, fenceRequestFound->second.m_platform));
m_pendingFenceRequestMap.erase(fenceRequestFound);
}
@@ -0,0 +1,307 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include "native/assetprocessor.h"
#include "native/utilities/assetUtils.h"
#include <QString>
#include <QByteArray>
#include <QHash>
#include <QObject>
#include <AzCore/Interface/Interface.h>
#include <AzFramework/Asset/AssetSystemTypes.h>
#include <connection/connectionManager.h>
#endif
namespace AzFramework
{
namespace AssetSystem
{
class BaseAssetProcessorMessage;
} // namespace AssetSystem
} // namespace AzFramework
namespace AssetProcessor
{
class AssetRequestHandler;
template<typename TRequest>
struct MessageData
{
static_assert(AZStd::is_base_of<AzFramework::AssetSystem::BaseAssetProcessorMessage, TRequest>::value, "TRequest must derive from BaseAssetProcessorMessage");
AZStd::shared_ptr<TRequest> m_message;
NetworkRequestID m_key;
QString m_platform;
bool m_fencingFailed{ false };
MessageData() = default;
MessageData(AZStd::shared_ptr<TRequest> message, NetworkRequestID key, QString platform, bool fencingFailed = false)
: m_message(message), m_key(key), m_platform(platform), m_fencingFailed(fencingFailed)
{}
template<typename TOther>
MessageData(const MessageData<TOther>& rhs)
{
m_message = AZStd::rtti_pointer_cast<TRequest>(rhs.m_message);
m_key = rhs.m_key;
m_platform = rhs.m_platform;
m_fencingFailed = rhs.m_fencingFailed;
}
};
struct IRequestRouter
{
friend class AssetRequestHandler;
AZ_RTTI(IRequestRouter, "{FC7F875C-2CD1-4CD2-AC63-71097DF612AC}");
IRequestRouter(AZStd::function<void(unsigned int, unsigned int, QByteArray, QString)> requestHandler)
: m_requestHandler(AZStd::move(requestHandler))
{
AZ::Interface<IRequestRouter>::Register(this);
}
virtual ~IRequestRouter()
{
AZ::Interface<IRequestRouter>::Unregister(this);
}
//! Registers a QT object callback as a handler for a TRequest type of message.
//! The callback function will be run on obj's thread
//! If the return value of the handler is void, no response will be sent.
//! Not thread-safe, do not call after AP initialization stage
template<typename TRequest, typename TResponse, typename TClass>
void RegisterQueuedCallbackHandler(TClass* obj, TResponse(TClass::* handler)(AssetProcessor::MessageData<TRequest>))
{
// Return type is set to void here since the response needs to be delayed along with the handler call
// HandleResponse gets called twice in this whole chain but the first time won't attempt to send a response because of this void
RegisterMessageHandler<TRequest, void>([=](MessageData<TRequest> messageData)
{
QMetaObject::invokeMethod(obj, [=]()
{
// This will run on the obj's thread and handle sending the response now that we're ready to process
HandleResponse<TRequest, TResponse>([obj, handler](MessageData<TRequest> messageData) -> TResponse
{
return (obj->*handler)(messageData);
}, messageData);
}, Qt::ConnectionType::QueuedConnection);
});
}
//! Registers a callback as a handler for a TRequest type of message.
//! If the return value of the handler is void, no response will be sent.
//! Not thread-safe, do not call after AP initialization stage
template <class TRequest, class TResponse>
void RegisterMessageHandler(TResponse(*handler)(MessageData<TRequest> messageData))
{
RegisterMessageHandler<TRequest, TResponse>(AZStd::function<TResponse(MessageData<TRequest>)>(AZStd::move(handler)));
}
//! Registers a callback as a handler for a TRequest type of message.
//! If the return value of the handler is void, no response will be sent.
//! Not thread-safe, do not call after AP initialization stage
template <class TRequest, class TResponse>
void RegisterMessageHandler(AZStd::function<TResponse(MessageData<TRequest>)> handler)
{
static constexpr unsigned int MessageType = TRequest::MessageType;
m_messageHandlers[MessageType] = [this, handler = AZStd::move(handler)](MessageData<AzFramework::AssetSystem::BaseAssetProcessorMessage> messageData)
{
MessageData<TRequest> downcastData = messageData;
if (downcastData.m_message)
{
IRequestRouter::HandleResponse<TRequest, TResponse>(AZStd::move(handler), downcastData);
}
else
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Expected message type (%d) but incoming message type is %d.\n", MessageType, messageData.m_message->GetMessageType());
}
};
using namespace AZStd::placeholders;
ConnectionManagerRequestBus::Broadcast(&ConnectionManagerRequestBus::Events::RegisterService, MessageType, AZStd::bind(m_requestHandler, _1, _3, _4, _5));
}
template<class TRequest>
void UnregisterMessageHandler()
{
static constexpr unsigned int MessageType = TRequest::MessageType;
auto messageItr = m_messageHandlers.find(MessageType);
if(messageItr != m_messageHandlers.end())
{
m_messageHandlers.erase(messageItr);
}
}
AZ_DISABLE_COPY_MOVE(IRequestRouter);
protected:
//! Helper to handle sending a response for a message if one is needed.
template<class TRequest, class TResponse, typename AZStd::enable_if_t<!AZStd::is_void_v<TResponse>>* = nullptr>
static void HandleResponse(AZStd::function<TResponse(MessageData<TRequest>)> handler, MessageData<TRequest> messageData)
{
auto&& response = handler(messageData);
ConnectionBus::Event(messageData.m_key.first, &ConnectionBus::Events::SendResponse, messageData.m_key.second, response);
}
template<class TRequest, class TResponse, typename AZStd::enable_if_t<AZStd::is_void_v<TResponse>>* = nullptr>
static void HandleResponse(AZStd::function<TResponse(MessageData<TRequest>)> handler, MessageData<TRequest> messageData)
{
// This template handles void returns which mean no response should be sent
handler(messageData);
}
using MessageHandler = AZStd::function<void(MessageData<AzFramework::AssetSystem::BaseAssetProcessorMessage>)>;
//! Map of messageType to message handler callback
AZStd::unordered_map<unsigned int /*messageType*/, MessageHandler> m_messageHandlers;
//! Parent object callback which will be registered with the ConnectionManager for each message
AZStd::function<void(unsigned int, unsigned int, QByteArray, QString)> m_requestHandler;
};
//! AssetRequestHandler
//! this exists to handle requests from outside sources to compile assets.
//! or to get the status of groups of assets.
class AssetRequestHandler
: public QObject
{
using AssetStatus = AzFramework::AssetSystem::AssetStatus;
using BaseAssetProcessorMessage = AzFramework::AssetSystem::BaseAssetProcessorMessage;
Q_OBJECT
public:
AssetRequestHandler();
protected:
//! This function creates a fence file.
//! It will return the fencefile path if it succeeds, otherwise it returns an empty string
virtual QString CreateFenceFile(unsigned int fenceId);
//! This function delete a fence file.
//! it will return true if it succeeds, otherwise it returns false.
virtual bool DeleteFenceFile(QString fenceFileName);
Q_SIGNALS:
//! Request that a compile group is created for all assets that match that platform and search term.
//! emitting this signal will ultimately result in OnCompileGroupCreated and OnCompileGroupFinished being executed
//! at some later time with the same groupID.
void RequestCompileGroup(NetworkRequestID groupID, QString platform, QString searchTerm, AZ::Data::AssetId assetId, bool isStatusRequest, int searchType);
//! This request goes out to ask the system in general whether an asset can be found (as a product).
void RequestAssetExists(NetworkRequestID groupID, QString platform, QString searchTerm, AZ::Data::AssetId assetId, int searchType);
void RequestEscalateAssetByUuid(QString platform, AZ::Uuid escalatedAssetUUID);
void RequestEscalateAssetBySearchTerm(QString platform, QString escalatedSearchTerm);
public Q_SLOTS:
//! ProcessGetAssetStatus - someone on the network wants to know about the status of an asset.
//! isStatusRequest will be TRUE if its a status request. If its false it means its a compile request
void ProcessAssetRequest(MessageData<AzFramework::AssetSystem::RequestAssetStatus> messageData);
//! OnCompileGroupCreated is invoked in response to asking for a compile group to be created.
//! Its status will either be Unknown if no assets are queued or in flight that match that pattern
//! or it will be Queued or Compiling if some were matched.
//! If you get a Queued or Compiling, you will eventually get a OnCompileGroupFinished with the same group ID.
void OnCompileGroupCreated(NetworkRequestID groupID, AssetStatus status);
//! OnCompileGroupFinished is expected to be called when a compile group completes or fails.
//! the status is expected to be either Compiled or Failed.
void OnCompileGroupFinished(NetworkRequestID groupID, AssetStatus status);
//! Called from the outside in response to a RequestAssetExists.
void OnRequestAssetExistsResponse(NetworkRequestID groupID, bool exists);
void OnFenceFileDetected(unsigned int fenceId);
//! This will get called for every asset related messages or messages that require fencing
void OnNewIncomingRequest(unsigned int connId, unsigned int serial, QByteArray payload, QString platform);
public:
//! Just return how many in flight requests there are.
int GetNumOutstandingAssetRequests() const;
protected:
template<typename TRequest, typename TResponse>
AZStd::function<TResponse(MessageData<TRequest>)> ToFunction(TResponse(AssetRequestHandler::* func)(MessageData<TRequest>))
{
using namespace AZStd::placeholders;
return AZStd::function<TResponse(MessageData<TRequest>)>(AZStd::bind(func, this, _1));
}
// Invokes the appropriate handler and returns true if the message should be deleted by the caller and false if the request handler is responsible for deleting the message
virtual bool InvokeHandler(MessageData<AzFramework::AssetSystem::BaseAssetProcessorMessage> message);
private:
void DeleteFenceFile_Retry(unsigned fenceId, QString fenceFileName, NetworkRequestID key, AZStd::shared_ptr<BaseAssetProcessorMessage> message, QString platform, int retriesRemaining);
void SendAssetStatus(NetworkRequestID groupID, unsigned int type, AssetStatus status);
void HandleRequestEscalateAsset(MessageData<AzFramework::AssetSystem::RequestEscalateAsset> messageData);
// we keep state about a request in this class:
class AssetRequestLine
{
public:
AssetRequestLine(QString platform, QString searchTerm, const AZ::Data::AssetId& assetId, bool isStatusRequest, int searchType);
bool IsStatusRequest() const;
QString GetPlatform() const;
QString GetSearchTerm() const;
const AZ::Data::AssetId& GetAssetId() const;
QString GetDisplayString() const;
int GetSearchType() const;
private:
bool m_isStatusRequest;
QString m_platform;
QString m_searchTerm;
AZ::Data::AssetId m_assetId;
int m_searchType{ 0 };
};
// this map keeps track of whether a request was for a compile (FALSE), or a status (TRUE)
QHash<NetworkRequestID, AssetRequestLine> m_pendingAssetRequests;
//! This is an internal struct that is used for storing all the necessary information for requests that require fencing
struct RequestInfo
{
RequestInfo() = default;
RequestInfo(NetworkRequestID requestId, AZStd::shared_ptr<BaseAssetProcessorMessage> message, QString platform)
:m_requestId(requestId)
, m_message(AZStd::move(message))
, m_platform(platform)
{
}
NetworkRequestID m_requestId{};
AZStd::shared_ptr<BaseAssetProcessorMessage> m_message{};
QString m_platform{};
};
AZStd::unordered_map<unsigned int, RequestInfo> m_pendingFenceRequestMap;
unsigned int m_fenceId = 0;
IRequestRouter m_requestRouter{ [this](unsigned int connId, unsigned int serial, QByteArray payload, QString platform) {OnNewIncomingRequest(connId, serial, payload, platform); } };
};
} // namespace AssetProcessor
@@ -0,0 +1,163 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <native/AssetManager/ControlRequestHandler.h>
#if !defined(Q_MOC_RUN)
#include <QHostAddress>
#include <QTcpSocket>
#include <QTcpServer>
#endif
#include <native/assetprocessor.h>
#include <native/utilities/ApplicationManagerBase.h>
#include <AzCore/Casting/numeric_cast.h>
#include <AzCore/Debug/Trace.h>
ControlRequestHandler::ControlRequestHandler(ApplicationManagerBase* parent) : QObject(parent),
m_applicationManager(parent)
{
connect(m_applicationManager, &ApplicationManagerBase::FullIdle, this, &ControlRequestHandler::AssetManagerIdleStateChange);
StartListening(0);
}
ControlRequestHandler::~ControlRequestHandler()
{
}
bool ControlRequestHandler::StartListening(unsigned short port)
{
if (!m_tcpServer)
{
m_tcpServer = new QTcpServer(this);
}
if (!m_tcpServer->isListening())
{
if (!m_tcpServer->listen(QHostAddress::LocalHost, port))
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Control Request Handler couldn't listen on requested port %d", port);
return false;
}
port = m_tcpServer->serverPort();
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Control Port: %d\n", port);
connect(m_tcpServer, &QTcpServer::newConnection, this, &ControlRequestHandler::GotConnection);
AZ_TracePrintf(AssetProcessor::DebugChannel, "Asset Processor Control Request Handler listening on port %d\n", port);
}
return true;
}
void ControlRequestHandler::GotConnection()
{
if (m_tcpServer->hasPendingConnections())
{
QTcpSocket* newSocket = m_tcpServer->nextPendingConnection();
connect(newSocket, &QTcpSocket::stateChanged, this, &ControlRequestHandler::SocketStateUpdate);
connect(newSocket, &QTcpSocket::readyRead, this, &ControlRequestHandler::DataReceived);
connect(newSocket, &QTcpSocket::disconnected, this, &ControlRequestHandler::Disconnected);
m_listenSockets.push_back(newSocket);
AZ_TracePrintf(AssetProcessor::DebugChannel, "Asset Processor Control Request Handler got new connection\n");
if (newSocket->bytesAvailable())
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Asset Processor Control Request Handler socket had data available\n");
ReadData(newSocket);
}
}
}
void ControlRequestHandler::SocketStateUpdate(QAbstractSocket::SocketState newState)
{
if (newState == QAbstractSocket::UnconnectedState)
{
m_listenSockets.removeOne(static_cast<QTcpSocket*>(QObject::sender()));
}
}
void ControlRequestHandler::DataReceived()
{
QTcpSocket* incoming = static_cast<QTcpSocket*>(QObject::sender());
ReadData(incoming);
}
void ControlRequestHandler::ReadData(QTcpSocket* incoming)
{
if (!incoming)
{
AZ_Error(AssetProcessor::DebugChannel, false, "Attempting to read from null QTcpSocket in ControlRequestHandler");
return;
}
auto sentMessage = incoming->readAll().toStdString();
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Got Control request %s\n", sentMessage.c_str());
if (sentMessage == "quit")
{
QMetaObject::invokeMethod(parent(), "QuitRequested", Qt::QueuedConnection);
}
else if (sentMessage == "ping")
{
incoming->write("pong");
}
else if (sentMessage == "isidle")
{
bool isIdle = m_applicationManager->IsAssetProcessorManagerIdle();
incoming->write(isIdle ? "true" : "false");
}
else if (sentMessage == "waitforidle")
{
bool isIdle = m_applicationManager->CheckFullIdle();
if (isIdle)
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Control request responding idle\n");
incoming->write("idle");
}
else
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Control request adding wait idle waiter\n");
m_idleWaitSockets.push_back(incoming);
}
}
else if (sentMessage == "signalidle")
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Control request adding signal idle waiter\n");
m_idleWaitSockets.push_back(incoming);
}
}
void ControlRequestHandler::Disconnected()
{
QTcpSocket* incoming = static_cast<QTcpSocket*>(QObject::sender());
m_listenSockets.removeOne(incoming);
incoming->deleteLater();
}
void ControlRequestHandler::AssetManagerIdleStateChange(bool isIdle)
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Control Request Got idle state %d with %d waiters\n", isIdle, m_idleWaitSockets.size());
if (!isIdle)
{
// We only currently care when transitioning to idle
return;
}
for (auto& thisConnection : m_idleWaitSockets)
{
if (m_listenSockets.indexOf(thisConnection) != -1)
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Control request sending idle state to socket\n");
thisConnection->write("idle");
}
}
m_idleWaitSockets.clear();
}
@@ -0,0 +1,52 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QList>
#include <QAbstractSocket>
#endif
class QTcpSocket;
class QTcpServer;
class ApplicationManagerBase;
/** This Class is responsible for listening and getting new connections and
* responding to text queries and commands from the socket. The original purpose
* is to enable writing more reliable and better performing tests which launch
* AP as a subprocess such as our python test modules.
*/
class ControlRequestHandler : public QObject
{
Q_OBJECT
public:
explicit ControlRequestHandler(ApplicationManagerBase* parent = 0);
~ControlRequestHandler();
public slots:
void GotConnection();
void SocketStateUpdate(QAbstractSocket::SocketState newSocketState);
void DataReceived();
void Disconnected();
void AssetManagerIdleStateChange(bool isIdle);
protected:
bool StartListening(unsigned short port = 0);
void ReadData(QTcpSocket* incoming);
private:
QList<QTcpSocket*> m_listenSockets;
QList<QTcpSocket*> m_idleWaitSockets;
QTcpServer* m_tcpServer{ nullptr };
ApplicationManagerBase* m_applicationManager{ nullptr };
};
@@ -0,0 +1,217 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "FileStateCache.h"
#include "native/utilities/assetUtils.h"
#include <AssetProcessor_Traits_Platform.h>
#include <QDir>
namespace AssetProcessor
{
bool FileStateCache::GetFileInfo(const QString& absolutePath, FileStateInfo* foundFileInfo) const
{
LockGuardType scopeLock(m_mapMutex);
auto itr = m_fileInfoMap.find(PathToKey(absolutePath));
if (itr != m_fileInfoMap.end())
{
*foundFileInfo = itr.value();
return true;
}
return false;
}
bool FileStateCache::Exists(const QString& absolutePath) const
{
LockGuardType scopeLock(m_mapMutex);
auto itr = m_fileInfoMap.find(PathToKey(absolutePath));
return itr != m_fileInfoMap.end();
}
bool FileStateCache::GetHash(const QString& absolutePath, FileHash* foundHash)
{
LockGuardType scopeLock(m_mapMutex);
auto fileInfoItr = m_fileInfoMap.find(PathToKey(absolutePath));
if(fileInfoItr == m_fileInfoMap.end())
{
// No info on this file, return false
return false;
}
auto itr = m_fileHashMap.find(PathToKey(absolutePath));
if (itr != m_fileHashMap.end())
{
*foundHash = itr.value();
return true;
}
// There's no hash stored yet or its been invalidated, calculate it
*foundHash = AssetUtilities::GetFileHash(absolutePath.toUtf8().constData(), true);
m_fileHashMap[PathToKey(absolutePath)] = *foundHash;
return true;
}
void FileStateCache::AddInfoSet(QSet<AssetFileInfo> infoSet)
{
LockGuardType scopeLock(m_mapMutex);
for (const AssetFileInfo& info : infoSet)
{
m_fileInfoMap[PathToKey(info.m_filePath)] = FileStateInfo(info);
}
}
void FileStateCache::AddFile(const QString& absolutePath)
{
QFileInfo fileInfo(absolutePath);
LockGuardType scopeLock(m_mapMutex);
AddOrUpdateFileInternal(fileInfo);
InvalidateHash(absolutePath);
if(fileInfo.isDir())
{
ScanFolder(absolutePath);
}
}
void FileStateCache::UpdateFile(const QString& absolutePath)
{
QFileInfo fileInfo(absolutePath);
LockGuardType scopeLock(m_mapMutex);
AddOrUpdateFileInternal(fileInfo);
InvalidateHash(absolutePath);
}
void FileStateCache::RemoveFile(const QString& absolutePath)
{
LockGuardType scopeLock(m_mapMutex);
auto itr = m_fileInfoMap.find(PathToKey(absolutePath));
if (itr != m_fileInfoMap.end())
{
bool isDirectory = itr.value().m_isDirectory;
QString parentPath = itr.value().m_absolutePath;
m_fileInfoMap.erase(itr);
if (isDirectory)
{
for (itr = m_fileInfoMap.begin(); itr != m_fileInfoMap.end(); )
{
if (itr.value().m_absolutePath.startsWith(parentPath))
{
itr = m_fileInfoMap.erase(itr);
continue;
}
++itr;
}
}
}
InvalidateHash(absolutePath);
}
void FileStateCache::InvalidateHash(const QString& absolutePath)
{
auto fileHashItr = m_fileHashMap.find(PathToKey(absolutePath));
if (fileHashItr != m_fileHashMap.end())
{
m_fileHashMap.erase(fileHashItr);
}
}
//////////////////////////////////////////////////////////////////////////
QString FileStateCache::PathToKey(const QString& absolutePath) const
{
QString normalized = AssetUtilities::NormalizeFilePath(absolutePath);
if constexpr (!ASSETPROCESSOR_TRAIT_CASE_SENSITIVE_FILESYSTEM)
{
return normalized.toLower();
}
return normalized;
}
void FileStateCache::AddOrUpdateFileInternal(QFileInfo fileInfo)
{
m_fileInfoMap[PathToKey(fileInfo.absoluteFilePath())] = FileStateInfo(fileInfo.absoluteFilePath(), fileInfo.lastModified(), fileInfo.size(), fileInfo.isDir());
}
void FileStateCache::ScanFolder(const QString& absolutePath)
{
QDir inputFolder(absolutePath);
QFileInfoList entries = inputFolder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Files);
for (const QFileInfo& entry : entries)
{
AddOrUpdateFileInternal(entry);
if (entry.isDir())
{
ScanFolder(entry.absoluteFilePath());
}
}
}
//////////////////////////////////////////////////////////////////////////
bool FileStatePassthrough::GetFileInfo(const QString& absolutePath, FileStateInfo* foundFileInfo) const
{
QFileInfo fileInfo(absolutePath);
if (fileInfo.exists())
{
*foundFileInfo = FileStateInfo(fileInfo.absoluteFilePath(), fileInfo.lastModified(), fileInfo.size(), fileInfo.isDir());
return true;
}
return false;
}
bool FileStatePassthrough::Exists(const QString& absolutePath) const
{
return QFile(absolutePath).exists();
}
bool FileStatePassthrough::GetHash(const QString& absolutePath, FileHash* foundHash)
{
if(!Exists(absolutePath))
{
return false;
}
*foundHash = AssetUtilities::GetFileHash(absolutePath.toUtf8().constData(), true);
return true;
}
bool FileStateInfo::operator==(const FileStateInfo& rhs) const
{
return m_absolutePath == rhs.m_absolutePath
&& m_modTime == rhs.m_modTime
&& m_fileSize == rhs.m_fileSize
&& m_isDirectory == rhs.m_isDirectory;
}
} // namespace AssetProcessor
@@ -0,0 +1,139 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/EBus/EBus.h>
#include <native/AssetManager/assetScanFolderInfo.h>
#include <QString>
#include <QSet>
#include <QFileInfo>
#include <AzCore/Interface/Interface.h>
namespace AssetProcessor
{
struct FileStateInfo
{
FileStateInfo() = default;
FileStateInfo(QString filePath, QDateTime modTime, AZ::u64 fileSize, bool isDirectory)
: m_absolutePath(filePath), m_modTime(modTime), m_fileSize(fileSize), m_isDirectory(isDirectory) {}
explicit FileStateInfo(const AssetFileInfo& assetFileInfo)
: m_absolutePath(assetFileInfo.m_filePath), m_fileSize(assetFileInfo.m_fileSize), m_isDirectory(assetFileInfo.m_isDirectory), m_modTime(assetFileInfo.m_modTime)
{
}
bool operator==(const FileStateInfo& rhs) const;
QString m_absolutePath{};
QDateTime m_modTime{};
AZ::u64 m_fileSize{};
bool m_isDirectory{};
};
struct IFileStateRequests
{
AZ_RTTI(IFileStateRequests, "{2D883B3A-DCA3-4CE0-976C-4511C3277371}");
IFileStateRequests() = default;
virtual ~IFileStateRequests() = default;
using FileHash = AZ::u64;
/// Fetches info on the file/directory if it exists. Returns true if it exists, false otherwise
virtual bool GetFileInfo(const QString& absolutePath, FileStateInfo* foundFileInfo) const = 0;
/// Convenience function to check if a file or directory exists.
virtual bool Exists(const QString& absolutePath) const = 0;
virtual bool GetHash(const QString& absolutePath, FileHash* foundHash) = 0;
AZ_DISABLE_COPY_MOVE(IFileStateRequests);
};
class FileStateBase
: public IFileStateRequests
{
public:
FileStateBase()
{
AZ::Interface<IFileStateRequests>::Register(this);
}
virtual ~FileStateBase()
{
AZ::Interface<IFileStateRequests>::Unregister(this);
}
/// Bulk adds file state to the cache
virtual void AddInfoSet(QSet<AssetFileInfo> /*infoSet*/) {}
/// Adds a single file to the cache. This will query the OS for the current state
virtual void AddFile(const QString& /*absolutePath*/) {}
/// Updates a single file in the cache. This will query the OS for the current state
virtual void UpdateFile(const QString& /*absolutePath*/) {}
/// Removes a file from the cache
virtual void RemoveFile(const QString& /*absolutePath*/) {}
};
/// Caches file state information retrieved by the file scanner and file watcher
/// Profiling has shown it is faster (at least on windows) compared to asking the OS for file information every time
class FileStateCache final :
public FileStateBase
{
public:
// FileStateRequestBus implementation
bool GetFileInfo(const QString& absolutePath, FileStateInfo* foundFileInfo) const override;
bool Exists(const QString& absolutePath) const override;
bool GetHash(const QString& absolutePath, FileHash* foundHash) override;
void AddInfoSet(QSet<AssetFileInfo> infoSet) override;
void AddFile(const QString& absolutePath) override;
void UpdateFile(const QString& absolutePath) override;
void RemoveFile(const QString& absolutePath) override;
private:
/// Invalidates the hash for a file so it will be re-computed next time it's requested
void InvalidateHash(const QString& absolutePath);
/// Handles converting a file path into a uniform format for use as a map key
QString PathToKey(const QString& absolutePath) const;
/// Add/Update a single file
void AddOrUpdateFileInternal(QFileInfo fileInfo);
/// Recursively collects all the files contained in the directory specified by absolutePath
void ScanFolder(const QString& absolutePath);
mutable AZStd::recursive_mutex m_mapMutex;
QHash<QString, FileStateInfo> m_fileInfoMap;
QHash<QString, FileHash> m_fileHashMap;
using LockGuardType = AZStd::lock_guard<decltype(m_mapMutex)>;
};
/// Pass through version of the FileStateCache which does not cache anything. Every request is redirected to the OS
class FileStatePassthrough final :
public FileStateBase
{
public:
// FileStateRequestBus implementation
bool GetFileInfo(const QString& absolutePath, FileStateInfo* foundFileInfo) const override;
bool Exists(const QString& absolutePath) const override;
bool GetHash(const QString& absolutePath, FileHash* foundHash) override;
};
} // namespace AssetProcessor
@@ -0,0 +1,619 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "PathDependencyManager.h"
#include <AzCore/std/string/wildcard.h>
#include <AzCore/Asset/AssetCommon.h>
#include <utilities/PlatformConfiguration.h>
#include <utilities/assetUtils.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace AssetProcessor
{
void SanitizeForDatabase(AZStd::string& str)
{
// Not calling normalize because wildcards should be preserved.
AZStd::to_lower(str.begin(), str.end());
AZStd::replace(str.begin(), str.end(), AZ_WRONG_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR);
AzFramework::StringFunc::Replace(str, AZ_DOUBLE_CORRECT_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR_STRING);
}
PathDependencyManager::PathDependencyManager(AZStd::shared_ptr<AssetDatabaseConnection> stateData, PlatformConfiguration* platformConfig)
: m_stateData(stateData), m_platformConfig(platformConfig)
{
}
void PathDependencyManager::SaveUnresolvedDependenciesToDatabase(AssetBuilderSDK::ProductPathDependencySet& unresolvedDependencies, const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& productEntry, const AZStd::string& platform)
{
using namespace AzToolsFramework::AssetDatabase;
ProductDependencyDatabaseEntryContainer dependencyContainer;
for (const auto& unresolvedPathDep : unresolvedDependencies)
{
auto dependencyType = unresolvedPathDep.m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::SourceFile ?
ProductDependencyDatabaseEntry::ProductDep_SourceFile :
ProductDependencyDatabaseEntry::ProductDep_ProductFile;
ProductDependencyDatabaseEntry placeholderDependency(
productEntry.m_productID,
AZ::Uuid::CreateNull(),
0,
AZStd::bitset<64>(),
platform,
0,
// Use a string that will make it easy to route errors back here correctly. An empty string can be a symptom of many
// other problems. This string says that something went wrong in this function.
AZStd::string("INVALID_PATH"),
dependencyType);
AZStd::string path = AssetUtilities::NormalizeFilePath(unresolvedPathDep.m_dependencyPath.c_str()).toUtf8().constData();
bool isExactDependency = IsExactDependency(path);
if (isExactDependency && unresolvedPathDep.m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::SourceFile)
{
QString relativePath, scanFolder;
if (!AzFramework::StringFunc::Path::IsRelative(path.c_str()))
{
if (m_platformConfig->ConvertToRelativePath(QString::fromUtf8(path.c_str()), relativePath, scanFolder, true))
{
auto* scanFolderInfo = m_platformConfig->GetScanFolderByPath(scanFolder);
path = ToScanFolderPrefixedPath(aznumeric_cast<int>(scanFolderInfo->ScanFolderID()), relativePath.toUtf8().constData());
}
}
}
SanitizeForDatabase(path);
placeholderDependency.m_unresolvedPath = path;
dependencyContainer.push_back(placeholderDependency);
}
if(!m_stateData->UpdateProductDependencies(dependencyContainer))
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to save unresolved dependencies to database for product %d (%s)",
productEntry.m_productID, productEntry.m_productName.c_str());
}
}
void PathDependencyManager::SetDependencyResolvedCallback(const DependencyResolvedCallback& callback)
{
m_dependencyResolvedCallback = callback;
}
bool PathDependencyManager::IsExactDependency(AZStd::string_view path)
{
return path.find('*') == AZStd::string_view::npos;
}
void PathDependencyManager::GetMatchedExclusions(
const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry,
const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& productEntry,
AZStd::vector<AZStd::pair<DependencyProductIdInfo, bool>>& excludedDependencies,
AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::DependencyType dependencyType,
const MapSet& exclusionMaps) const
{
bool handleProductDependencies = dependencyType == AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::DependencyType::ProductDep_ProductFile;
AZStd::string_view assetName = handleProductDependencies ? productEntry.m_productName : sourceEntry.m_sourceName;
const DependencyProductMap& excludedPathDependencyIds = handleProductDependencies ? exclusionMaps.m_productPathDependencyIds : exclusionMaps.m_sourcePathDependencyIds;
const DependencyProductMap& excludedWildcardPathDependencyIds = handleProductDependencies ? exclusionMaps.m_wildcardProductPathDependencyIds : exclusionMaps.m_wildcardSourcePathDependencyIds;
// strip path of /<platform>/<project>/
AZStd::string strippedPath = handleProductDependencies ? StripPlatformAndProject(assetName) : sourceEntry.m_sourceName;
SanitizeForDatabase(strippedPath);
auto unresolvedIter = excludedPathDependencyIds.find(ExcludedDependenciesSymbol + strippedPath);
if (unresolvedIter != excludedPathDependencyIds.end())
{
for (const auto& dependencyProductIdInfo : unresolvedIter->second)
{
excludedDependencies.emplace_back(dependencyProductIdInfo, true); // true = is exact dependency
}
}
for (const auto& pair : excludedWildcardPathDependencyIds)
{
AZStd::string filter = pair.first.substr(1);
if (wildcard_match(filter, strippedPath))
{
for (const auto& dependencyProductIdInfo : pair.second)
{
excludedDependencies.emplace_back(dependencyProductIdInfo, false); // false = wildcard dependency
}
}
}
}
AZStd::string PathDependencyManager::StripPlatformAndProject(AZStd::string_view productName)
{
auto nextSlash = productName.find('/'); // platform/
nextSlash = productName.find('/', nextSlash + 1) + 1; // project/
return productName.substr(nextSlash, productName.size() - nextSlash);
}
PathDependencyManager::DependencyProductMap& PathDependencyManager::SelectMap(MapSet& mapSet, bool wildcard, AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::DependencyType type)
{
const bool isSource = type == AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::DependencyType::ProductDep_SourceFile;
if (wildcard)
{
if (isSource)
{
return mapSet.m_wildcardSourcePathDependencyIds;
}
return mapSet.m_wildcardProductPathDependencyIds;
}
if (isSource)
{
return mapSet.m_sourcePathDependencyIds;
}
return mapSet.m_productPathDependencyIds;
}
PathDependencyManager::MapSet PathDependencyManager::PopulateExclusionMaps() const
{
using namespace AzToolsFramework::AssetDatabase;
MapSet mapSet;
m_stateData->QueryProductDependencyExclusions([&mapSet](ProductDependencyDatabaseEntry& unresolvedDep)
{
DependencyProductIdInfo idPair;
idPair.m_productDependencyId = unresolvedDep.m_productDependencyID;
idPair.m_productId = unresolvedDep.m_productPK;
idPair.m_platform = unresolvedDep.m_platform;
AZStd::string path = unresolvedDep.m_unresolvedPath;
AZStd::to_lower(path.begin(), path.end());
const bool isExactDependency = IsExactDependency(path);
auto& map = SelectMap(mapSet, !isExactDependency, unresolvedDep.m_dependencyType);
map[path].push_back(AZStd::move(idPair));
return true;
});
return mapSet;
}
void PathDependencyManager::NotifyResolvedDependencies(const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& dependencyContainer) const
{
if(!m_dependencyResolvedCallback)
{
return;
}
for (const auto& dependency : dependencyContainer)
{
AzToolsFramework::AssetDatabase::ProductDatabaseEntry productEntry;
if (!m_stateData->GetProductByProductID(dependency.m_productPK, productEntry))
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to get existing product with productId %i from the database", dependency.m_productPK);
}
AzToolsFramework::AssetDatabase::SourceDatabaseEntry dependentSource;
if (!m_stateData->GetSourceByJobID(productEntry.m_jobPK, dependentSource))
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to get existing product from job ID of product %i from the database", dependency.m_productPK);
}
m_dependencyResolvedCallback(AZ::Data::AssetId(dependentSource.m_sourceGuid, productEntry.m_subID), dependency);
}
}
void PathDependencyManager::SaveResolvedDependencies(const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry, const MapSet& exclusionMaps, const AZStd::string& sourceNameWithScanFolder,
const AZStd::vector<AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry>& dependencyEntries,
AZStd::string_view matchedPath, bool isSourceDependency, const AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& matchedProducts,
AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& dependencyContainer) const
{
for (const auto& productDependencyDatabaseEntry : dependencyEntries)
{
const bool isExactDependency = IsExactDependency(productDependencyDatabaseEntry.m_unresolvedPath);
AZ::s64 dependencyId = isExactDependency ? productDependencyDatabaseEntry.m_productDependencyID : AzToolsFramework::AssetDatabase::InvalidEntryId;
if(isSourceDependency && !isExactDependency && matchedPath == sourceNameWithScanFolder)
{
// Since we did a search for the source 2 different ways, filter one out
// Scanfolder-prefixes are only for exact dependencies
break;
}
for (const auto& matchedProduct : matchedProducts)
{
// Check if this match is excluded before continuing
AZStd::vector<AZStd::pair<DependencyProductIdInfo, bool>> exclusions; // bool = is exact dependency
GetMatchedExclusions(sourceEntry, matchedProduct, exclusions, productDependencyDatabaseEntry.m_dependencyType, exclusionMaps);
if(!exclusions.empty())
{
bool isExclusionForThisProduct = false;
bool isExclusionExact = false;
for (const auto& exclusionPair : exclusions)
{
if(exclusionPair.first.m_productId == productDependencyDatabaseEntry.m_productPK && exclusionPair.first.m_platform == productDependencyDatabaseEntry.m_platform)
{
isExclusionExact = exclusionPair.second;
isExclusionForThisProduct = true;
break;
}
}
if(isExclusionForThisProduct)
{
if (isExactDependency && isExclusionExact)
{
AZ_Error("PathDependencyManager", false, "Dependency exclusion found for an exact dependency. It is not valid to both include and exclude a file by the same rule. File: %s", isSourceDependency ? sourceEntry.m_sourceName.c_str() : matchedProduct.m_productName.c_str());
}
continue;
}
}
// We need to make sure this product is for the same platform the dependency is for
AzToolsFramework::AssetDatabase::JobDatabaseEntry jobEntry;
if (!m_stateData->GetJobByJobID(matchedProduct.m_jobPK, jobEntry))
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to get job entry for product %s", matchedProduct.ToString().c_str());
}
if (jobEntry.m_platform != productDependencyDatabaseEntry.m_platform)
{
continue;
}
// All checks passed, this is a valid dependency we need to save to the db
dependencyContainer.push_back();
auto& entry = dependencyContainer.back();
entry.m_productDependencyID = dependencyId;
entry.m_productPK = productDependencyDatabaseEntry.m_productPK;
entry.m_dependencySourceGuid = sourceEntry.m_sourceGuid;
entry.m_dependencySubID = matchedProduct.m_subID;
entry.m_platform = productDependencyDatabaseEntry.m_platform;
// If there's more than 1 product, reset the ID so further products create new db entries
dependencyId = AzToolsFramework::AssetDatabase::InvalidEntryId;
}
}
}
void PathDependencyManager::RetryDeferredDependencies(const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry)
{
MapSet exclusionMaps = PopulateExclusionMaps();
// Gather a list of all the products this source file produced
AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
if (!m_stateData->GetProductsBySourceName(sourceEntry.m_sourceName.c_str(), products))
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Source %s did not have any products. Skipping dependency processing.\n", sourceEntry.m_sourceName.c_str());
return;
}
AZStd::unordered_map<AZStd::string, AZStd::vector<AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry>> map;
// Build up a list of all the paths we need to search for: products + 2 variations of the source path
AZStd::vector<AZStd::string> searchPaths;
for (const auto& productEntry : products)
{
const AZStd::string& productName = productEntry.m_productName;
// strip path of /<platform>/<project>/
AZStd::string strippedPath = StripPlatformAndProject(productName);
SanitizeForDatabase(strippedPath);
searchPaths.push_back(strippedPath);
}
AZStd::string sourceNameWithScanFolder = ToScanFolderPrefixedPath(aznumeric_cast<int>(sourceEntry.m_scanFolderPK), sourceEntry.m_sourceName.c_str());
AZStd::string sanitizedSourceName = sourceEntry.m_sourceName;
SanitizeForDatabase(sourceNameWithScanFolder);
SanitizeForDatabase(sanitizedSourceName);
searchPaths.push_back(sourceNameWithScanFolder);
searchPaths.push_back(sanitizedSourceName);
m_stateData->QueryProductDependenciesUnresolvedAdvanced(searchPaths, [&map](AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry& entry, const AZStd::string& matchedPath)
{
map[matchedPath].push_back(AZStd::move(entry));
return true;
});
AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer dependencyContainer;
// Go through all the matched dependencies
for (const auto& pair : map)
{
AZStd::string_view matchedPath = pair.first;
const bool isSourceDependency = matchedPath == sanitizedSourceName || matchedPath == sourceNameWithScanFolder;
AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer matchedProducts;
// Figure out the list of products to work with, for a source match, use all the products, otherwise just use the matched products
if(isSourceDependency)
{
matchedProducts = products;
}
else
{
for (const auto& productEntry : products)
{
const AZStd::string& productName = productEntry.m_productName;
// strip path of /<platform>/<project>/
AZStd::string strippedPath = StripPlatformAndProject(productName);
SanitizeForDatabase(strippedPath);
if(strippedPath == matchedPath)
{
matchedProducts.push_back(productEntry);
}
}
}
// Go through each dependency we're resolving and create a db entry for each product that resolved it (wildcard/source dependencies will generally create more than 1)
SaveResolvedDependencies(sourceEntry, exclusionMaps, sourceNameWithScanFolder, pair.second, matchedPath, isSourceDependency, matchedProducts, dependencyContainer);
}
// Save everything to the db
if(!m_stateData->UpdateProductDependencies(dependencyContainer))
{
AZ_Error("PathDependencyManager", false, "Failed to update product dependencies");
}
else
{
// Send a notification for each dependency that has been resolved
NotifyResolvedDependencies(dependencyContainer);
}
}
void CleanupPathDependency(AssetBuilderSDK::ProductPathDependency& pathDependency)
{
if(pathDependency.m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::SourceFile)
{
// Nothing to cleanup if the dependency type was already pointing at source.
return;
}
// Many workflows use source and product extensions for textures interchangeably, assuming that a later system will clean up the path.
// Multiple systems use the AZ Serialization system to reference assets and collect these asset references. Not all of these systems
// check if the references are to source or product asset types.
// Instead requiring each of these systems to handle this (and failing in hard to track down ways later when they don't), check here, and clean things up.
const AZStd::vector<AZStd::string> sourceImageExtensions = { ".tif", ".tiff", ".bmp", ".gif", ".jpg", ".jpeg", ".tga", ".png" };
for (const AZStd::string& sourceImageExtension : sourceImageExtensions)
{
if (AzFramework::StringFunc::Path::IsExtension(pathDependency.m_dependencyPath.c_str(), sourceImageExtension.c_str()))
{
// This was a source format image reported initially as a product file dependency. Fix that to be a source file dependency.
pathDependency.m_dependencyType = AssetBuilderSDK::ProductPathDependencyType::SourceFile;
break;
}
}
}
void PathDependencyManager::ResolveDependencies(AssetBuilderSDK::ProductPathDependencySet& pathDeps, AZStd::vector<AssetBuilderSDK::ProductDependency>& resolvedDeps, const AZStd::string& platform, [[maybe_unused]] const AZStd::string& productName)
{
const AZ::Data::ProductDependencyInfo::ProductDependencyFlags productDependencyFlags =
AZ::Data::ProductDependencyInfo::CreateFlags(AZ::Data::AssetLoadBehavior::NoLoad);
const QString gameName = AssetUtilities::ComputeGameName();
AZStd::vector<AssetBuilderSDK::ProductDependency> excludedDeps;
// Check the path dependency set and find any conflict (include and exclude the same path dependency)
AssetBuilderSDK::ProductPathDependencySet conflicts;
for (const AssetBuilderSDK::ProductPathDependency& pathDep : pathDeps)
{
auto conflictItr = find_if(pathDeps.begin(), pathDeps.end(),
[&pathDep](const AssetBuilderSDK::ProductPathDependency& pathDepForComparison)
{
return (pathDep.m_dependencyPath == ExcludedDependenciesSymbol + pathDepForComparison.m_dependencyPath ||
pathDepForComparison.m_dependencyPath == ExcludedDependenciesSymbol + pathDep.m_dependencyPath) &&
pathDep.m_dependencyType == pathDepForComparison.m_dependencyType;
});
if (conflictItr != pathDeps.end())
{
conflicts.insert(pathDep);
}
}
auto pathIter = pathDeps.begin();
while (pathIter != pathDeps.end())
{
if (conflicts.find(*pathIter) != conflicts.end())
{
// Ignore conflicted path dependencies
AZ_Error(AssetProcessor::DebugChannel, false,
"Cannot resolve path dependency %s for product %s since there's a conflict\n",
pathIter->m_dependencyPath.c_str(), productName.c_str());
++pathIter;
continue;
}
AssetBuilderSDK::ProductPathDependency cleanedupDependency(*pathIter);
CleanupPathDependency(cleanedupDependency);
AZStd::string dependencyPathSearch = cleanedupDependency.m_dependencyPath;
bool isExcludedDependency = dependencyPathSearch.starts_with(ExcludedDependenciesSymbol);
dependencyPathSearch = isExcludedDependency ? dependencyPathSearch.substr(1) : dependencyPathSearch;
bool isExactDependency = !AzFramework::StringFunc::Replace(dependencyPathSearch, '*', '%');
SanitizeForDatabase(dependencyPathSearch);
if (cleanedupDependency.m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::ProductFile)
{
AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer productInfoContainer;
QString productNameWithPlatform = QString("%1%2%3").arg(platform.c_str(), AZ_CORRECT_DATABASE_SEPARATOR_STRING, dependencyPathSearch.c_str());
QString productNameWithPlatformAndGameName = QString("%1%2%3%2%4").arg(platform.c_str(), AZ_CORRECT_DATABASE_SEPARATOR_STRING, gameName, dependencyPathSearch.c_str());
if (AzFramework::StringFunc::Equal(productNameWithPlatformAndGameName.toUtf8().data(), productName.c_str()))
{
AZ_Warning(AssetProcessor::ConsoleChannel, false,
"Invalid dependency: Product Asset ( %s ) has listed itself as one of its own Product Dependencies.",
productName.c_str());
pathIter = pathDeps.erase(pathIter);
continue;
}
if (isExactDependency)
{
m_stateData->GetProductsByProductName(productNameWithPlatformAndGameName, productInfoContainer);
// Not all products will be in the game subfolder.
// Items in dev, like bootstrap.cfg, end up in just the root platform folder.
// These two checks search for products in both location.
// Example: If a path dependency was just "bootstrap.cfg" in SamplesProject on PC, this would search both
// "cache/SamplesProject/pc/bootstrap.cfg" and "cache/SamplesProject/pc/SamplesProject/bootstrap.cfg".
m_stateData->GetProductsByProductName(productNameWithPlatform, productInfoContainer);
}
else
{
m_stateData->GetProductsLikeProductName(productNameWithPlatformAndGameName, AzToolsFramework::AssetDatabase::AssetDatabaseConnection::LikeType::Raw, productInfoContainer);
}
// See if path matches any product files
if (!productInfoContainer.empty())
{
AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceDatabaseEntry;
for (const auto& productDatabaseEntry : productInfoContainer)
{
if (m_stateData->GetSourceByJobID(productDatabaseEntry.m_jobPK, sourceDatabaseEntry))
{
AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencyList = isExcludedDependency ? excludedDeps : resolvedDeps;
productDependencyList.emplace_back(AZ::Data::AssetId(sourceDatabaseEntry.m_sourceGuid, productDatabaseEntry.m_subID), productDependencyFlags);
}
else
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Source for JobID %i not found (from product %s)", productDatabaseEntry.m_jobPK, dependencyPathSearch.c_str());
}
// For exact dependencies we expect that there is only 1 match. Even if we processed more than 1, the results could be inconsistent since the other assets may not be finished processing yet
if (isExactDependency)
{
break;
}
}
// Wildcard and excluded dependencies never get removed since they can be fulfilled by a future product
if (isExactDependency && !isExcludedDependency)
{
pathIter = pathDeps.erase(pathIter);
continue;
}
}
}
else
{
// See if path matches any source files
AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sourceInfoContainer;
if (isExactDependency)
{
QString databaseName;
QString scanFolder;
if (ProcessInputPathToDatabasePathAndScanFolder(dependencyPathSearch.c_str(), databaseName, scanFolder))
{
m_stateData->GetSourcesBySourceNameScanFolderId(databaseName, m_platformConfig->GetScanFolderByPath(scanFolder)->ScanFolderID(), sourceInfoContainer);
}
}
else
{
m_stateData->GetSourcesLikeSourceName(dependencyPathSearch.c_str(), AzToolsFramework::AssetDatabase::AssetDatabaseConnection::LikeType::Raw, sourceInfoContainer);
}
if (!sourceInfoContainer.empty())
{
bool productsAvailable = false;
for (const auto& sourceDatabaseEntry : sourceInfoContainer)
{
AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer productInfoContainer;
if (m_stateData->GetProductsBySourceID(sourceDatabaseEntry.m_sourceID, productInfoContainer, AZ::Uuid::CreateNull(), "", platform.c_str()))
{
productsAvailable = true;
// Add a dependency on every product of this source file
for (const auto& productDatabaseEntry : productInfoContainer)
{
AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencyList = isExcludedDependency ? excludedDeps : resolvedDeps;
productDependencyList.emplace_back(AZ::Data::AssetId(sourceDatabaseEntry.m_sourceGuid, productDatabaseEntry.m_subID), productDependencyFlags);
}
}
// For exact dependencies we expect that there is only 1 match. Even if we processed more than 1, the results could be inconsistent since the other assets may not be finished processing yet
if (isExactDependency)
{
break;
}
}
if (isExactDependency && productsAvailable && !isExcludedDependency)
{
pathIter = pathDeps.erase(pathIter);
continue;
}
}
}
pathIter->m_dependencyPath = cleanedupDependency.m_dependencyPath;
pathIter->m_dependencyType = cleanedupDependency.m_dependencyType;
++pathIter;
}
// Remove the excluded dependency from the resolved dependency list and leave them unresolved
resolvedDeps.erase(AZStd::remove_if(resolvedDeps.begin(), resolvedDeps.end(),
[&excludedDeps](const AssetBuilderSDK::ProductDependency& resolvedDependency)
{
auto excludedDependencyItr = AZStd::find_if(excludedDeps.begin(), excludedDeps.end(),
[&resolvedDependency](const AssetBuilderSDK::ProductDependency& excludedDependency)
{
return resolvedDependency.m_dependencyId == excludedDependency.m_dependencyId &&
resolvedDependency.m_flags == excludedDependency.m_flags;
});
return excludedDependencyItr != excludedDeps.end();
}), resolvedDeps.end());
}
bool PathDependencyManager::ProcessInputPathToDatabasePathAndScanFolder(const char* dependencyPathSearch, QString& databaseName, QString& scanFolder) const
{
if (!AzFramework::StringFunc::Path::IsRelative(dependencyPathSearch))
{
// absolute paths just get converted directly
return m_platformConfig->ConvertToRelativePath(QString::fromUtf8(dependencyPathSearch), databaseName, scanFolder);
}
else
{
// relative paths get the first matching asset, and then they get the usual call.
QString absolutePath = m_platformConfig->FindFirstMatchingFile(QString::fromUtf8(dependencyPathSearch));
if (!absolutePath.isEmpty())
{
return m_platformConfig->ConvertToRelativePath(absolutePath, databaseName, scanFolder);
}
}
return false;
}
AZStd::string PathDependencyManager::ToScanFolderPrefixedPath(int scanFolderId, const char* relativePath) const
{
static constexpr char ScanFolderSeparator = '$';
return AZStd::string::format("%c%d%c%s", ScanFolderSeparator, scanFolderId, ScanFolderSeparator, relativePath);
}
} // namespace AssetProcessor
@@ -0,0 +1,104 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <AzCore/std/string/string.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include <AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h>
#include <native/AssetManager/assetProcessorManager.h>
class QString;
namespace AssetProcessor
{
class PlatformConfiguration;
class AssetDatabaseConnection;
const char ExcludedDependenciesSymbol = ':';
/// Handles resolving and saving product path dependencies
class PathDependencyManager
{
public:
// The two Ids needed for a ProductDependency entry, and platform. Used for saving ProductDependencies that are pending resolution
struct DependencyProductIdInfo
{
AZ::s64 m_productId{};
AZ::s64 m_productDependencyId{};
AZStd::string m_platform;
};
using DependencyProductMap = AZStd::unordered_map<AZStd::string, AZStd::vector<DependencyProductIdInfo>>;
PathDependencyManager(AZStd::shared_ptr<AssetDatabaseConnection> stateData, PlatformConfiguration* platformConfig);
/// This function is responsible for looking up existing, unresolved dependencies that the current asset satisfies.
/// These can be dependencies on either the source asset or one of the product assets
void RetryDeferredDependencies(const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry);
/// This function is responsible for taking the path dependencies output by the current asset and trying to resolve them to AssetIds
/// This does not look for dependencies that the current asset satisfies.
void ResolveDependencies(AssetBuilderSDK::ProductPathDependencySet& pathDeps, AZStd::vector<AssetBuilderSDK::ProductDependency>& resolvedDeps, const AZStd::string& platform, const AZStd::string& productName);
/// Saves a product's unresolved dependencies to the database
void SaveUnresolvedDependenciesToDatabase(AssetBuilderSDK::ProductPathDependencySet& unresolvedDependencies, const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& productEntry, const AZStd::string& platform);
using DependencyResolvedCallback = AZStd::function<void(const AZ::Data::AssetId&, const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry&)>;
void SetDependencyResolvedCallback(const DependencyResolvedCallback& callback);
private:
struct MapSet
{
DependencyProductMap m_sourcePathDependencyIds;
DependencyProductMap m_productPathDependencyIds;
DependencyProductMap m_wildcardSourcePathDependencyIds;
DependencyProductMap m_wildcardProductPathDependencyIds;
};
MapSet PopulateExclusionMaps() const;
void NotifyResolvedDependencies(const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& dependencyContainer) const;
void SaveResolvedDependencies(const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry, const MapSet& exclusionMaps, const AZStd::string& sourceNameWithScanFolder, const AZStd::vector<AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry>& dependencyEntries, AZStd::string_view matchedPath, bool isSourceDependency, const AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& matchedProducts, AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& dependencyContainer) const;
static DependencyProductMap& SelectMap(MapSet& mapSet, bool wildcard, AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::DependencyType type);
/// Returns false if a path contains wildcards, true otherwise
static bool IsExactDependency(AZStd::string_view path);
/// Removes /platform/project/ from the start of a product path
static AZStd::string StripPlatformAndProject(AZStd::string_view relativeProductPath);
/// Prefixes the scanFolderId to the relativePath
AZStd::string ToScanFolderPrefixedPath(int scanFolderId, const char* relativePath) const;
/// Takes a path and breaks it into a database-prefixed relative path and scanFolder path
/// This function can accept an absolute source path, an un-prefixed relative path, and a prefixed relative path
/// The file returned will be the first one matched based on scanfolder priority
bool ProcessInputPathToDatabasePathAndScanFolder(const char* dependencyPathSearch, QString& databaseName, QString& scanFolder) const;
/// Gets any matched dependency exclusions
/// @param sourceEntry source database entry corresponds to the newly finished product
/// @param productEntry product database entry corresponds to the newly finished product
/// @param excludedDependencies dependencies that should be ignored even if their file paths match any existing wildcard pattern
/// @param dependencyType type of the dependencies we are handling
/// @param exclusionMaps MapSet containing all the path dependency exclusions
void GetMatchedExclusions(const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry, const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& productEntry,
AZStd::vector<AZStd::pair<DependencyProductIdInfo, bool>>& excludedDependencies, AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::DependencyType dependencyType,
const MapSet& exclusionMaps) const;
AZStd::shared_ptr<AssetDatabaseConnection> m_stateData;
PlatformConfiguration* m_platformConfig{};
DependencyResolvedCallback m_dependencyResolvedCallback{};
};
} // namespace AssetProcessor
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,241 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <QHash>
#include <AzCore/base.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/string/string.h>
#include <utility>
#include "AzCore/EBus/EBus.h"
#include "AzCore/Interface/Interface.h"
#include "AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h"
#include <utilities/PlatformConfiguration.h>
#include "AssetDatabase/AssetDatabase.h"
// This needs to be up here so it is declared before the hash, which needs to be declared before the first usage
namespace AssetProcessor
{
struct FileUpdateTask
{
FileUpdateTask(AZStd::vector<AZStd::string> oldString, AZStd::vector<AZStd::string> newString, AZStd::string absPathFileToUpdate, bool isAssetIdReference, bool skipTask)
: m_oldStrings(AZStd::move(oldString)),
m_newStrings(AZStd::move(newString)),
m_absPathFileToUpdate(AZStd::move(absPathFileToUpdate)),
m_isAssetIdReference(isAssetIdReference),
m_skipTask(skipTask)
{
}
bool operator==(const FileUpdateTask& rhs) const
{
return m_isAssetIdReference == rhs.m_isAssetIdReference
&& m_absPathFileToUpdate == rhs.m_absPathFileToUpdate
&& m_oldStrings == rhs.m_oldStrings
&& m_newStrings == rhs.m_newStrings;
}
AZStd::vector<AZStd::string> m_oldStrings; // The old path or uuid strings to search for
AZStd::vector<AZStd::string> m_newStrings; // The new path or uuid strings to replace
AZStd::string m_absPathFileToUpdate;
bool m_isAssetIdReference = false;
bool m_succeeded = false;
bool m_skipTask = false;
};
}
namespace AZStd
{
template<>
struct hash<AssetProcessor::FileUpdateTask>
{
size_t operator()(const AssetProcessor::FileUpdateTask& obj) const
{
size_t h = 0;
hash_combine(h, obj.m_isAssetIdReference);
hash_combine(h, obj.m_absPathFileToUpdate);
hash_range(h, obj.m_oldStrings.begin(), obj.m_oldStrings.end());
hash_range(h, obj.m_newStrings.begin(), obj.m_newStrings.end());
return h;
}
};
}
namespace AssetProcessor
{
enum class SourceFileRelocationStatus
{
None,
Failed,
Succeeded
};
static constexpr int SourceFileRelocationInvalidIndex = -1;
struct SourceFileRelocationInfo
{
SourceFileRelocationInfo(AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceEntry, AZStd::unordered_map<int, AzToolsFramework::AssetDatabase::ProductDatabaseEntry> products, const AZStd::string& oldRelativePath, const ScanFolderInfo* scanFolder)
: m_sourceEntry(AZStd::move(sourceEntry)),
m_products(AZStd::move(products)),
m_oldRelativePath(oldRelativePath)
{
AzFramework::StringFunc::Path::ConstructFull(scanFolder->ScanPath().toUtf8().constData(), m_oldRelativePath.c_str(), m_oldAbsolutePath, false);
m_oldAbsolutePath = AssetUtilities::NormalizeFilePath(m_oldAbsolutePath.c_str()).toUtf8().constData();
}
SourceFileRelocationInfo(const AZStd::string& filePath, const ScanFolderInfo* scanFolder)
{
QString relFilePath;
PlatformConfiguration::ConvertToRelativePath(filePath.c_str(), scanFolder, relFilePath, true);
m_oldRelativePath = relFilePath.toUtf8().data();
AzFramework::StringFunc::Path::ConstructFull(scanFolder->ScanPath().toUtf8().constData(), m_oldRelativePath.c_str(), m_oldAbsolutePath, false);
m_oldAbsolutePath = AssetUtilities::NormalizeFilePath(m_oldAbsolutePath.c_str()).toUtf8().constData();
}
AzToolsFramework::AssetDatabase::SourceDatabaseEntry m_sourceEntry;
AZStd::unordered_map<int, AzToolsFramework::AssetDatabase::ProductDatabaseEntry> m_products; // Key = product SubId
AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer m_sourceDependencyEntries;
AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer m_productDependencyEntries;
AZ::Uuid m_newUuid;
AZStd::string m_oldRelativePath;
AZStd::string m_newRelativePath;
AZStd::string m_oldAbsolutePath;
AZStd::string m_newAbsolutePath;
bool m_hasPathDependencies = false;
SourceFileRelocationStatus m_operationStatus = SourceFileRelocationStatus::None;
bool m_isMetaDataFile = false;
// This is a cached index of the SourceFile in the SourceFileRelocationContainer.
// This is only used by the metadata file to determine the destination path if needed.
int m_sourceFileIndex = AssetProcessor::SourceFileRelocationInvalidIndex;
};
using SourceFileRelocationContainer = AZStd::vector<SourceFileRelocationInfo>;
using FileUpdateTasks = AZStd::unordered_set<FileUpdateTask>;
struct MoveFailure
{
MoveFailure(AZStd::string reason, bool dependencyFailure)
: m_reason(AZStd::move(reason)),
m_dependencyFailure(dependencyFailure)
{
}
AZStd::string m_reason;
bool m_dependencyFailure{};
};
struct RelocationSuccess
{
RelocationSuccess() = default;
RelocationSuccess(int moveSuccessCount, int moveFailureCount, int moveTotalCount, int updateSuccessCount, int updateFailureCount, int updateTotalCount, SourceFileRelocationContainer sourceFileRelocationInfos, FileUpdateTasks fileUpdateTasks)
: m_moveSuccessCount(moveSuccessCount),
m_moveFailureCount(moveFailureCount),
m_moveTotalCount(moveTotalCount),
m_updateSuccessCount(updateSuccessCount),
m_updateFailureCount(updateFailureCount),
m_updateTotalCount(updateTotalCount),
m_relocationContainer(AZStd::move(sourceFileRelocationInfos)),
m_updateTasks(AZStd::move(fileUpdateTasks))
{
}
int m_moveSuccessCount{};
int m_moveFailureCount{};
int m_moveTotalCount{};
int m_updateSuccessCount{};
int m_updateFailureCount{};
int m_updateTotalCount{};
SourceFileRelocationContainer m_relocationContainer;
FileUpdateTasks m_updateTasks;
};
class ISourceFileRelocation
{
public:
AZ_RTTI(ISourceFileRelocation, "{FEDD188E-D5FF-4852-B945-F82F7CC1CA5F}");
ISourceFileRelocation() = default;
virtual ~ISourceFileRelocation() = default;
//! Moves source files or renames a file. Source and destination can be absolute paths or scanfolder relative paths. Wildcards are supported for source.
//! By default no changes are made to the disk. Set previewOnly to false to actually move files.
//! If allowDependencyBreaking is false, the move will fail if moving any files will break existing dependencies. Set to true to ignore and move anyway.
virtual AZ::Outcome<RelocationSuccess, MoveFailure> Move(const AZStd::string& source, const AZStd::string& destination, bool previewOnly = true, bool allowDependencyBreaking = false, bool removeEmptyFolders = true, bool updateReferences = false, bool excludeMetaDataFiles = false) = 0;
//! Deletes source files. Source can be an absolute path or a scanfolder relative path. Wildcards are supported.
//! By default no changes are made to the disk. Set previewOnly to false to actually delete files.
//! If allowDependencyBreaking is false, the delete will fail if deleting any file breaks existing dependencies. Set to true to ignore and delete anyway.
virtual AZ::Outcome<RelocationSuccess, AZStd::string> Delete(const AZStd::string& source, bool previewOnly = true, bool allowDependencyBreaking = false, bool removeEmptyFolders = true, bool excludeMetaDataFiles = false) = 0;
//! Takes a relocation set and builds a string report to output the result of what files will change and what dependencies will break
virtual AZStd::string BuildReport(const SourceFileRelocationContainer& relocationEntries, const FileUpdateTasks& updateTasks, bool isMove, bool updateReference) const = 0;
AZ_DISABLE_COPY_MOVE(ISourceFileRelocation);
};
class SourceFileRelocator
: public ISourceFileRelocation
{
public:
SourceFileRelocator(AZStd::shared_ptr<AzToolsFramework::AssetDatabase::AssetDatabaseConnection> stateData, PlatformConfiguration* platformConfiguration);
~SourceFileRelocator();
static AZStd::string RemoveDatabasePrefix(const ScanFolderInfo* scanFolder, AZStd::string sourceName);
static void MakePathRelative(const AZStd::string& parentPath, const AZStd::string& childPath, AZStd::string& parentRelative, AZStd::string& childRelative);
static AZ::Outcome<AZStd::string, AZStd::string> HandleWildcard(AZStd::string_view absFile, AZStd::string_view absSearch, AZStd::string destination);
static void FixDestinationMissingFilename(AZStd::string& destination, const AZStd::string& source);
// Takes a relocation set, scanfolder, source, and destination and calculates the new file path of every file
AZ::Outcome<void, AZStd::string> ComputeDestination(SourceFileRelocationContainer& relocationContainer, const ScanFolderInfo* sourceScanFolder, const AZStd::string& source, AZStd::string destination, const ScanFolderInfo*& destinationScanFolderOut) const;
// Takes a QStringList of paths and populates sources with all the corresponding source database entries
QHash<QString, int> GetSources(QStringList pathMatches, const ScanFolderInfo* scanFolderInfo, SourceFileRelocationContainer& sources) const;
// Takes a QStringList of paths and populates metadata files.
void HandleMetaDataFiles(QStringList pathMatches, QHash<QString, int>& pathIndexMap, const ScanFolderInfo* scanFolderInfo, SourceFileRelocationContainer& metadataFiles, bool excludeMetaDataFiles) const;
// Returns a map of SubId -> ProductEntry for all the products of a source
AZStd::unordered_map<int, AzToolsFramework::AssetDatabase::ProductDatabaseEntry> GetProductMapForSource(AZ::s64 sourceId) const;
bool GetFilesFromSourceControl(SourceFileRelocationContainer& sources, const ScanFolderInfo* scanFolderInfo, QString absolutePath, bool excludeMetaDataFiles = false) const;
// Populates a relocation set with all direct source and product dependency database entries for every file
void PopulateDependencies(SourceFileRelocationContainer& relocationContainer) const;
// Gets the scanfolder and relative path given an input of an absolute or relative path (wildcard paths not supported). Fails if the source path is not within a scanfolder or can't be made relative
AZ::Outcome<void, AZStd::string> GetScanFolderAndRelativePath(const AZStd::string& normalizedSource, bool allowNonexistentPath, const ScanFolderInfo*& scanFolderInfo, AZStd::string& relativePath) const;
// Given a path, populates a relocation set with all source files that match. Will fail if a scanfolder itself is selected or the source string matches files from multiple scanfolders
AZ::Outcome<void, AZStd::string> GetSourcesByPath(const AZStd::string& normalizedSource, SourceFileRelocationContainer& sources, const ScanFolderInfo*& scanFolderInfoOut, bool excludeMetaDataFiles = false) const;
int DoSourceControlMoveFiles(AZStd::string normalizedSource, AZStd::string normalizedDestination, SourceFileRelocationContainer& relocationContainer, const ScanFolderInfo* sourceScanFolderInfo, const ScanFolderInfo* destinationScanFolderInfo, bool removeEmptyFolders) const;
int DoSourceControlDeleteFiles(AZStd::string normalizedSource, SourceFileRelocationContainer& relocationContainer, const ScanFolderInfo* sourceScanFolderInfo, bool removeEmptyFolders) const;
static bool UpdateFileReferences(const FileUpdateTask& updateTask);
bool ComputeProductDependencyUpdatePaths(const SourceFileRelocationInfo& relocationInfo, const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry& productDependency, AZStd::vector<AZStd::string>& oldPaths, AZStd::vector<AZStd::string>& newPaths, AZStd::string& absPathFileToUpdate) const;
FileUpdateTasks UpdateReferences(const SourceFileRelocationContainer& relocationContainer, bool useSourceControl) const;
// ISourceFileRelocation implementation
AZ::Outcome<RelocationSuccess, MoveFailure> Move(const AZStd::string& source, const AZStd::string& destination, bool previewOnly = true, bool allowDependencyBreaking = false, bool removeEmptyFolders = true, bool updateReferences = false, bool excludeMetaDataFiles = false) override;
AZ::Outcome<RelocationSuccess, AZStd::string> Delete(const AZStd::string& source, bool previewOnly = true, bool allowDependencyBreaking = false, bool removeEmptyFolders = true, bool excludeMetaDataFiles = false) override;
AZStd::string BuildReport(const SourceFileRelocationContainer& relocationEntries, const FileUpdateTasks& updateTasks, bool isMove, bool updateReference) const override;
private:
AZStd::shared_ptr<AzToolsFramework::AssetDatabase::AssetDatabaseConnection> m_stateData;
PlatformConfiguration* m_platformConfig;
AZStd::unordered_map<AZStd::string, AZStd::string> m_additionalHelpTextMap;
};
} // namespace AssetProcessor
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,552 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QString>
#include <QByteArray>
#include <QQueue>
#include <QVector>
#include <QHash>
#include <QDir>
#include <QSet>
#include <QMap>
#include <QPair>
#include <QMutex>
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include <AssetBuilderSDK/AssetBuilderBusses.h>
#include "native/assetprocessor.h"
#include "native/utilities/AssetUtilEBusHelper.h"
#include "native/utilities/MissingDependencyScanner.h"
#include "native/utilities/ThreadHelper.h"
#include "native/AssetManager/AssetCatalog.h"
#include "native/AssetDatabase/AssetDatabase.h"
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/std/containers/map.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <AzCore/IO/SystemFile.h> // for AZ_MAX_PATH_LEN
#include "AssetRequestHandler.h"
#include "native/utilities/JobDiagnosticTracker.h"
#include "SourceFileRelocator.h"
#endif
class FileWatcher;
namespace AzFramework
{
namespace AssetSystem
{
class BaseAssetProcessorMessage;
class GetRelativeProductPathFromFullSourceOrProductPathRequest;
class GetRelativeProductPathFromFullSourceOrProductPathResponse;
class GetFullSourcePathFromRelativeProductPathRequest;
class GetFullSourcePathFromRelativeProductPathResponse;
class AssetNotificationMessage;
} // namespace AssetSystem
} // namespace AzFramework
namespace AzToolsFramework
{
namespace AssetSystem
{
class AssetJobLogRequest;
class AssetJobLogResponse;
class AssetJobsInfoRequest;
class AssetJobsInfoResponse;
class GetAbsoluteAssetDatabaseLocationRequest;
class GetAbsoluteAssetDatabaseLocationResponse;
} // namespace AssetSystem
} // namespace AzToolsFramework
namespace AssetProcessor
{
class AssetProcessingStateData;
struct AssetRecognizer;
class PlatformConfiguration;
class ScanFolderInfo;
class PathDependencyManager;
//! The Asset Processor Manager is the heart of the pipeline
//! It is what makes the critical decisions about what should and should not be processed
//! It emits signals when jobs need to be performed and when assets are complete or have failed.
class AssetProcessorManager
: public QObject
, public AssetProcessor::ProcessingJobInfoBus::Handler
{
using BaseAssetProcessorMessage = AzFramework::AssetSystem::BaseAssetProcessorMessage;
using AssetJobsInfoRequest = AzToolsFramework::AssetSystem::AssetJobsInfoRequest;
using AssetJobsInfoResponse = AzToolsFramework::AssetSystem::AssetJobsInfoResponse;
using JobInfo = AzToolsFramework::AssetSystem::JobInfo;
using JobStatus = AzToolsFramework::AssetSystem::JobStatus;
using AssetJobLogRequest = AzToolsFramework::AssetSystem::AssetJobLogRequest;
using AssetJobLogResponse = AzToolsFramework::AssetSystem::AssetJobLogResponse;
using GetAbsoluteAssetDatabaseLocationRequest = AzToolsFramework::AssetSystem::GetAbsoluteAssetDatabaseLocationRequest;
using GetAbsoluteAssetDatabaseLocationResponse = AzToolsFramework::AssetSystem::GetAbsoluteAssetDatabaseLocationResponse;
using GetRelativeProductPathFromFullSourceOrProductPathRequest = AzFramework::AssetSystem::GetRelativeProductPathFromFullSourceOrProductPathRequest;
using GetRelativeProductPathFromFullSourceOrProductPathResponse = AzFramework::AssetSystem::GetRelativeProductPathFromFullSourceOrProductPathResponse;
using GetFullSourcePathFromRelativeProductPathRequest = AzFramework::AssetSystem::GetFullSourcePathFromRelativeProductPathRequest;
using GetFullSourcePathFromRelativeProductPathResponse = AzFramework::AssetSystem::GetFullSourcePathFromRelativeProductPathResponse;
Q_OBJECT
private:
struct FileEntry
{
QString m_fileName;
bool m_isDelete = false;
bool m_isFromScanner = false;
FileEntry() = default;
FileEntry(const QString& fileName, bool isDelete, bool isFromScanner=false)
: m_fileName(fileName)
, m_isDelete(isDelete)
, m_isFromScanner(isFromScanner)
{
}
};
struct AssetProcessedEntry
{
JobEntry m_entry;
AssetBuilderSDK::ProcessJobResponse m_response;
AssetProcessedEntry() = default;
AssetProcessedEntry(JobEntry& entry, AssetBuilderSDK::ProcessJobResponse& response)
: m_entry(AZStd::move(entry))
, m_response(AZStd::move(response))
{
}
AssetProcessedEntry(const AssetProcessedEntry& other) = default;
AssetProcessedEntry(AssetProcessedEntry&& other)
: m_entry(AZStd::move(other.m_entry))
, m_response(AZStd::move(other.m_response))
{
}
AssetProcessedEntry& operator=(AssetProcessedEntry&& other)
{
if (this != &other)
{
m_entry = AZStd::move(other.m_entry);
m_response = AZStd::move(other.m_response);
}
return *this;
}
};
//! Internal structure that will hold all the necessary source info
struct SourceFileInfo
{
QString m_databasePath; // clarification: this is the database path (ie, includes outputprefix)
QString m_pathRelativeToScanFolder;
AZ::Uuid m_uuid;
const ScanFolderInfo* m_scanFolder{ nullptr };
};
public:
explicit AssetProcessorManager(AssetProcessor::PlatformConfiguration* config, QObject* parent = nullptr);
virtual ~AssetProcessorManager();
bool IsIdle();
bool HasProcessedCriticalAssets() const;
//////////////////////////////////////////////////////////////////////////
// ProcessingJobInfoBus::Handler overrides
void BeginCacheFileUpdate(const char* productPath) override;
void EndCacheFileUpdate(const char* productPath, bool queueAgainForDeletion) override;
AZ::u32 GetJobFingerprint(const AssetProcessor::JobIndentifier& jobIndentifier) override;
//////////////////////////////////////////////////////////////////////////
//! Controls whether or not we are allowed to skip analysis on a file when the source files modtimes have not changed
//! and neither have any builders.
void SetEnableModtimeSkippingFeature(bool enable);
//! Query logging will log every asset database query.
void SetQueryLogging(bool enableLogging);
void SetBuilderDebugFlag(bool enabled);
//! Scans assets that match the given pattern for content that looks like a missing product dependency.
//! Note that the database pattern is used as an SQL query, so use SQL syntax for the search (wildcard is %, not *).
//! FilePattern is just a normal wildcard pattern that can be used to filter files in the provided scan folders.
void ScanForMissingProductDependencies(QString dbPattern, QString filePattern, const AZStd::vector<AZStd::string>& dependencyAdditionalScanFolders, int maxScanIteration=AssetProcessor::MissingDependencyScanner::DefaultMaxScanIteration);
AZStd::shared_ptr<AssetDatabaseConnection> GetDatabaseConnection() const;
void EmitResolvedDependency(const AZ::Data::AssetId& assetId, const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry& entry);
//! Internal structure that will hold all the necessary information to process jobs later.
//! We need to hold these jobs because they have declared either source dependency on other sources
//! or a job dependency and we can only resolve these dependencies once all the create jobs are completed.
struct JobToProcessEntry
{
SourceFileInfo m_sourceFileInfo;
AZStd::vector<JobDetails> m_jobsToAnalyze;
// a vector of pairs of <builder which emitted it, the dependency>
AZStd::vector<AZStd::pair<AZ::Uuid, AssetBuilderSDK::SourceFileDependency>> m_sourceFileDependencies;
};
//! Request to invalidate and reprocess a source asset or folder containing source assets
AZ::u64 RequestReprocess(const QString& sourcePath);
Q_SIGNALS:
void NumRemainingJobsChanged(int newNumJobs);
void AssetToProcess(JobDetails jobDetails);
//! Emit whenever a new asset is found or an existing asset is updated
void AssetMessage(AzFramework::AssetSystem::AssetNotificationMessage message);
// InputAssetProcessed - uses absolute asset path of input file - no outputprefix
void InputAssetProcessed(QString fullAssetPath, QString platform);
void RequestInputAssetStatus(QString inputAssetPath, QString platform, QString jobDescription);
void RequestPriorityAssetCompile(QString inputAssetPath, QString platform, QString jobDescription);
//! AssetProcessorManagerIdleState is emitted when APM idle state changes, we emit true when
//! APM is waiting for outside stimulus i.e its has eaten through all of its queues and is only waiting for
//! responses back from other systems (like its waiting for responses back from the compiler)
void AssetProcessorManagerIdleState(bool state);
void ReadyToQuit(QObject* source);
void CreateAssetsRequest(unsigned int nonce, QString name, QString platform, bool onlyExactMatch = true, bool syncRequest = false);
void SendAssetExistsResponse(NetworkRequestID groupID, bool exists);
void FenceFileDetected(unsigned int fenceId);
void EscalateJobs(AssetProcessor::JobIdEscalationList jobIdEscalationList);
void SourceDeleted(QString relSourceFile);
void SourceFolderDeleted(QString folderPath);
void SourceQueued(AZ::Uuid sourceUuid, AZ::Uuid legacyUuid, QString rootPath, QString relativeFilePath);
void SourceFinished(AZ::Uuid sourceUuid, AZ::Uuid legacyUuid);
void JobRemoved(AzToolsFramework::AssetSystem::JobInfo jobInfo);
void JobComplete(JobEntry jobEntry, AzToolsFramework::AssetSystem::JobStatus status);
//! Send a message when a new path dependency is resolved, so that downstream tools know the AssetId of the resolved dependency.
void PathDependencyResolved(const AZ::Data::AssetId& assetId, const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry& entry);
void AddedToCatalog(JobEntry jobEntry);
public Q_SLOTS:
void AssetProcessed(JobEntry jobEntry, AssetBuilderSDK::ProcessJobResponse response);
void AssetProcessed_Impl();
void AssetFailed(JobEntry jobEntry);
void AssetCancelled(JobEntry jobEntry);
void AssessFilesFromScanner(QSet<AssetFileInfo> filePaths);
void AssessModifiedFile(QString filePath);
void AssessAddedFile(QString filePath);
void AssessDeletedFile(QString filePath);
void OnAssetScannerStatusChange(AssetProcessor::AssetScanningStatus status);
void OnJobStatusChanged(JobEntry jobEntry, JobStatus status);
void CheckAssetProcessorIdleState();
void QuitRequested();
//! A network request came in asking, for a given input asset, what the status is of any jobs related to that request
AssetJobsInfoResponse ProcessGetAssetJobsInfoRequest(MessageData<AssetJobsInfoRequest> messageData);
//! A network request came in, Given a JOB ID (from the above Job Request), asking for the actual log for that job.
AssetJobLogResponse ProcessGetAssetJobLogRequest(MessageData<AssetJobLogRequest> messageData);
//! A network request came in asking for asset database location
GetAbsoluteAssetDatabaseLocationResponse ProcessGetAbsoluteAssetDatabaseLocationRequest(MessageData<GetAbsoluteAssetDatabaseLocationRequest> messageData);
//! This request comes in and is expected to do whatever heuristic is required in order to determine if an asset actually exists in the database.
void OnRequestAssetExists(NetworkRequestID requestId, QString platform, QString searchTerm, AZ::Data::AssetId assetId);
//! Searches the product and source asset tables to try and find a match
QString GuessProductOrSourceAssetName(QString searchTerm, QString platform, bool useLikeSearch);
void ProcessFilesToExamineQueue();
void CheckForIdle();
void CheckMissingFiles();
void ProcessGetAssetJobsInfoRequest(AssetJobsInfoRequest& request, AssetJobsInfoResponse& response);
void ProcessGetAssetJobLogRequest(const AssetJobLogRequest& request, AssetJobLogResponse& response);
void ScheduleNextUpdate();
void ProcessJobs();
void RemoveEmptyFolders();
void OnBuildersRegistered();
private:
template <class R>
bool Recv(unsigned int connId, QByteArray payload, R& request);
void AssessFileInternal(QString fullFile, bool isDelete, bool fromScanner = false);
void CheckSource(const FileEntry& source);
void CheckMissingJobs(QString relativeSourceFile, const ScanFolderInfo* scanFolder, const AZStd::vector<JobDetails>& jobsThisTime);
void CheckDeletedProductFile(QString normalizedPath);
void CheckDeletedSourceFile(QString normalizedPath, QString relativePath, QString databaseSourceFile);
void CheckModifiedSourceFile(QString normalizedPath, QString databaseSourceFile, const ScanFolderInfo* scanFolderInfo);
bool AnalyzeJob(JobDetails& details);
void CheckDeletedCacheFolder(QString normalizedPath);
void CheckDeletedSourceFolder(QString normalizedPath, QString relativePath, const ScanFolderInfo* scanFolderInfo);
void CheckCreatedSourceFolder(QString normalizedPath);
void CheckMetaDataRealFiles(QString relativePath);
bool DeleteProducts(const AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& products);
void DispatchFileChange();
bool InitializeCacheRoot();
void PopulateJobStateCache();
void AutoFailJob(const AZStd::string& consoleMsg, const AZStd::string& autoFailReason, const AZStd::vector<AssetProcessedEntry>::iterator& assetIter);
using ProductInfoList = AZStd::vector<AZStd::pair<AzToolsFramework::AssetDatabase::ProductDatabaseEntry, const AssetBuilderSDK::JobProduct*>>;
void WriteProductTableInfo(AZStd::pair<AzToolsFramework::AssetDatabase::ProductDatabaseEntry, const AssetBuilderSDK::JobProduct*>& pair, AZStd::vector<AZ::u32>& subIds, AZStd::unordered_set<AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry>& dependencyContainer, const AZStd::string& platform);
//! given a full absolute path to a file, add any metadata files you find that apply.
void AddMetadataFilesForFingerprinting(QString absolutePathToFileToCheck, SourceFilesForFingerprintingContainer& outFilesToFingerprint);
// given a file name and a root to not go beyond, add the parent folder and its parent folders recursively
// to the list of known folders.
void AddKnownFoldersRecursivelyForFile(QString file, QString root);
void CleanEmptyFolder(QString folder, QString root);
void ProcessBuilders(QString normalizedPath, QString relativePathToFile, const ScanFolderInfo* scanFolder, const AssetProcessor::BuilderInfoList& builderInfoList);
struct SourceInfo
{
QString m_watchFolder;
QString m_sourceRelativeToWatchFolder;
QString m_sourceDatabaseName;
};
struct SourceInfoWithFingerprints
{
QString m_watchFolder;
QString m_sourceRelativeToWatchFolder;
QString m_sourceDatabaseName;
QString m_analysisFingerprint;
};
//! Search the database and the the source dependency maps for the the sourceUuid. if found returns the cached info
bool SearchSourceInfoBySourceUUID(const AZ::Uuid& sourceUuid, AssetProcessorManager::SourceInfo& result);
//! Adds the source to the database and returns the corresponding sourceDatabase Entry
void AddSourceToDatabase(AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceDatabaseEntry, const ScanFolderInfo* scanFolder, QString relativeSourceFilePath);
protected:
// Checks whether or not a file can be skipped for processing (ie, file content hasn't changed, builders haven't been added/removed, builders for the file haven't changed)
bool CanSkipProcessingFile(const AssetFileInfo &fileInfo, AZ::u64& fileHash);
AZ::s64 GenerateNewJobRunKey();
// Attempt to erase a log file. Failing to erase it is not a critical problem, but should be logged.
// returns true if there is no log file there after this operation completes
bool EraseLogFile(const char* fileName);
// Load the old scan folders and match them up with new scan folders. Make sure they're
bool MigrateScanFolders();
//! Checks whether the AP is aware of any source file that has indicated the inputted
//! source file as its dependency, and if found do we need to put that file back in the asset pipeline queue again
QStringList GetSourceFilesWhichDependOnSourceFile(const QString& sourcePath);
/** Given a BuilderSDK SourceFileDependency, try to find out what actual database source name is.
* If it cannot be resolved but a UUID is available, the string result will contain the UUID (and we will return true).
* If there's a problem that makes it unusable (such as no fields being filled in), the string will be blank
* and this function will return false.
*/
bool ResolveSourceFileDependencyPath(const AssetBuilderSDK::SourceFileDependency& sourceDependency, QString& resultDatabaseSourceNames, QStringList& resolvedDependencyList);
//! Updates the database with all the changes related to source dependency / job dependency:
void UpdateSourceFileDependenciesDatabase(JobToProcessEntry& entry);
//! Analyze JobDetail for every hold jobs
void AnalyzeJobDetail(JobToProcessEntry& jobEntry);
void UpdateJobDependency(JobDetails& jobDetails);
void QueueIdleCheck();
void UpdateWildcardDependencies(JobDetails& job, size_t jobDependencySlot, QStringList& resolvedDependencyList);
//! Check whether the job can be analyzed by APM,
//! A job cannot be analyzed if any of its dependent job hasn't been fingerprinted
bool CanAnalyzeJob(const JobDetails& jobDetails);
//! Analyzes and forward the job to the RCController if the job requires processing
void ProcessJob(JobDetails& jobDetails);
AssetProcessor::PlatformConfiguration* m_platformConfig = nullptr;
bool m_queuedExamination = false;
bool m_hasProcessedCriticalAssets = false;
QQueue<FileEntry> m_activeFiles;
QSet<QString> m_alreadyActiveFiles; // a simple optimization to only do the exhaustive search if we know its there.
AZStd::vector<AssetProcessedEntry> m_assetProcessedList;
AZStd::shared_ptr<AssetDatabaseConnection> m_stateData;
ThreadController<AssetCatalog>* m_assetCatalog;
typedef QHash<QString, FileEntry> FileExamineContainer;
FileExamineContainer m_filesToExamine; // order does not actually matter in this (yet)
// this map contains a list of source files that were discovered in the database before asset scanning began.
// (so files from a previous run).
// as asset scanning encounters files, it will remove them from this map, and when its done,
// it will thus contain only the files that were in the database from last time, but were NOT found during file scan
// in other words, files that have been deleted from disk since last run.
// the key to this map is the absolute path of the file from last run, but with the current scan folder setup
QMap<QString, SourceInfoWithFingerprints> m_sourceFilesInDatabase;
// this map contains modtimes of all files AP processed last time it ran
AZStd::unordered_map<AZStd::string, AZ::u64> m_fileModTimes;
// this map contains hashes of all files AP processed last time it ran
AZStd::unordered_map<AZStd::string, AZ::u64> m_fileHashes;
QSet<QString> m_knownFolders; // a cache of all known folder names, normalized to have forward slashes.
typedef AZStd::unordered_map<AZ::u64, AzToolsFramework::AssetSystem::JobInfo> JobRunKeyToJobInfoMap; // for when network requests come in about the jobInfo
JobRunKeyToJobInfoMap m_jobRunKeyToJobInfoMap;
AZStd::multimap<AZStd::string, AZ::u64> m_jobKeyToJobRunKeyMap;
using SourceUUIDToSourceInfoMap = AZStd::unordered_map<AZ::Uuid, SourceInfo>;
SourceUUIDToSourceInfoMap m_sourceUUIDToSourceInfoMap; // contains UUID -> SourceInfo, which includes database name and relative to watch folder:
AZStd::mutex m_sourceUUIDToSourceInfoMapMutex;
QString m_normalizedCacheRootPath;
char m_absoluteDevFolderPath[AZ_MAX_PATH_LEN];
char m_absoluteDevGameFolderPath[AZ_MAX_PATH_LEN];
QDir m_cacheRootDir;
bool m_isCurrentlyScanning = false;
bool m_quitRequested = false;
bool m_processedQueued = false;
bool m_AssetProcessorIsBusy = true;
bool m_alreadyScheduledUpdate = false;
QMutex m_processingJobMutex;
AZStd::unordered_set<AZStd::string> m_processingProductInfoList;
AZ::s64 m_highestJobRunKeySoFar = 0;
AZStd::vector<JobToProcessEntry> m_jobEntries;
AZStd::unordered_set<JobDetails> m_jobsToProcess;
//! This map is required to prevent multiple sourceFile modified events been send by the APM
AZStd::unordered_map<AZ::Uuid, qint64> m_sourceFileModTimeMap;
AZStd::unordered_map<JobIndentifier, AZ::u32> m_jobFingerprintMap;
AZStd::unordered_map<JobDesc, AZStd::unordered_set<AZ::Uuid>> m_jobDescToBuilderUuidMap;
AZStd::unique_ptr<PathDependencyManager> m_pathDependencyManager;
AZStd::unique_ptr<SourceFileRelocator> m_sourceFileRelocator;
JobDiagnosticTracker m_jobDiagnosticTracker{};
QSet<QString> m_checkFoldersToRemove; //!< List of folders that needs to be checked for removal later by AP
//! List of all scanfolders that are present in the database but not currently watched by AP
AZStd::unordered_map<AZStd::string, AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry> m_scanFoldersInDatabase;
int m_numOfJobsToAnalyze = 0;
bool m_alreadyQueuedCheckForIdle = false;
//////////////////// Analysis Early-Out feature ///////////////////
// ComputeBuilderDirty builds the maps of which builders are dirty and how they have changed.
// note that until ComputeBuilderDirty is called, it is assumed that *all* are dirty, to be conservative.
// The data we actually care about for this feature:
struct BuilderData
{
AZ::u8 m_flags = 0; // the flags from the builder registration
AZ::Uuid m_fingerprint; // a hash of the fingerprint and version info
bool m_isDirty = false;
};
void ComputeBuilderDirty();
AZStd::unordered_map<AZ::Uuid, BuilderData> m_builderDataCache;
bool m_buildersAddedOrRemoved = true; //< true if any new builders exist. If this happens we actually need to re-analyze everything.
bool m_anyBuilderChange = true;
// Checks whether any of the builders specified have changed their fingerprint
bool AreBuildersUnchanged(AZStd::string_view builderEntries, int& numBuildersEmittingSourceDependencies);
/** Utility function: Given the input database row (from sources table), return an (ordered) set of all dependencies
* including dependencies-of-dependencies. These will be absolute paths to the dependency file on disk.
* Note that the output also includes the initial inputDatabasePath asset (but expanded to be absolute)
* if a file does not exist, it will still in the list at the absolute path to where it may appear, so that
* this result set can still use that for hashing.
* if a source file is missing from disk, it will not be included in the result set, since this returns
* full absolute paths.
*/
void QueryAbsolutePathDependenciesRecursive(QString inputDatabasePath, SourceFilesForFingerprintingContainer& finalDependencyList, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, bool reverseQuery);
// we can't write a job to the database as not needing analysis the next time around,
// until all jobs related to it are finished. This is becuase the jobs themselves are not written to the database
// so until all jobs are finished, we need to re-analyze the source file next time.
// (since if you terminate the asset processor while its still processing, we don't want it to skip over those
// source files next time). So we keep a map of how many remaining outstanding jobs exist for a given
// source file. Once the outstanding jobs hit zero we compute a final source fingerprint for analysis and save it.
struct AnalysisTracker
{
int m_remainingJobsSpawned = 0;
AZ::s64 m_databaseScanFolderId = -1;
AZStd::string m_databaseSourceName;
AZStd::set<AZ::Uuid> m_buildersInvolved; // this is intentionally a sorted set, since its used to generate a stable hash
bool failedStatus = false; // if it fails, we avoid writing anything to the database, so that next time around, we reprocess the file.
};
// maps "absolute source path to file (normalized)" to tracking infomation struct above.
using JobCounter = AZStd::unordered_map<AZStd::string, AnalysisTracker> ;
JobCounter m_remainingJobsForEachSourceFile;
// utility function: finds the source in the above map and updates it.
enum class AnalysisTrackerUpdateType
{
JobFailed,
JobStarted,
JobFinished,
};
// ideally you would already have the absolute path to the file, and call this function with it:
void UpdateAnalysisTrackerForFile(const char* fullPathToFile, AnalysisTrackerUpdateType updateType);
// convenience overload of the above function when you have a jobEntry but no absolute path to the file.
void UpdateAnalysisTrackerForFile(const JobEntry &entry, AnalysisTrackerUpdateType updateType);
// Used to scan through products for anything that looks like a missing product dependency;
MissingDependencyScanner m_missingDependencyScanner;
// Metrics
int m_numTotalSourcesFound = 0;
int m_numSourcesNeedingFullAnalysis = 0;
int m_numSourcesNotHandledByAnyBuilder = 0;
bool m_reportedAnalysisMetrics = false;
// cache these so we don't have to check them each time during analysis:
QSet<QString> m_metaFilesWhichActuallyExistOnDisk;
bool m_cachedMetaFilesExistMap = false;
// when true, only processes files if their modtime or builder(s) have changed
// defaults to true (in the settings) for GUI mode, false for batch mode
bool m_allowModtimeSkippingFeature = false;
// when true, a flag will be sent to builders process job indicating debug output/mode should be used
bool m_builderDebugFlag = false;
protected Q_SLOTS:
void FinishAnalysis(AZStd::string fileToCheck);
//////////////////////////////////////////////////////////
};
} // namespace AssetProcessor
@@ -0,0 +1,151 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef ASSETSCANFOLDERINFO_H
#define ASSETSCANFOLDERINFO_H
#include <QString>
#include <QDateTime>
#include <AzCore/base.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
namespace AssetProcessor
{
/** This Class contains information about the folders to be scanned
* */
class ScanFolderInfo
{
public:
ScanFolderInfo(
QString path,
QString displayName,
QString portableKey,
QString prefix,
bool isRoot,
bool recurseSubFolders,
AZStd::vector<AssetBuilderSDK::PlatformInfo> platforms = AZStd::vector<AssetBuilderSDK::PlatformInfo>{},
int order = 0,
AZ::s64 scanFolderID = 0,
bool canSaveNewAssets = false)
: m_scanPath(path)
, m_displayName(displayName)
, m_portableKey (portableKey)
, m_outputPrefix(prefix)
, m_isRoot(isRoot)
, m_recurseSubFolders(recurseSubFolders)
, m_order(order)
, m_scanFolderID(scanFolderID)
, m_platforms(platforms)
, m_canSaveNewAssets(canSaveNewAssets)
{
// note that m_scanFolderID is 0 unless its filled in from the DB.
}
ScanFolderInfo() = default;
ScanFolderInfo(const ScanFolderInfo& other) = default;
QString ScanPath() const
{
return m_scanPath;
}
QString GetDisplayName() const
{
return m_displayName;
}
QString GetOutputPrefix() const
{
return m_outputPrefix;
}
bool IsRoot() const
{
return m_isRoot;
}
bool RecurseSubFolders() const
{
return m_recurseSubFolders;
}
bool CanSaveNewAssets() const
{
return m_canSaveNewAssets;
}
int GetOrder() const
{
return m_order;
}
AZ::s64 ScanFolderID() const
{
return m_scanFolderID;
}
QString GetPortableKey() const
{
return m_portableKey;
}
const AZStd::vector<AssetBuilderSDK::PlatformInfo>& GetPlatforms() const
{
return m_platforms;
}
void SetScanFolderID(AZ::s64 scanFolderID)
{
m_scanFolderID = scanFolderID;
}
private:
QString m_scanPath; // the local path to scan ("C:\\whatever")
QString m_displayName; // the display name to show in GUIs that show it.
QString m_outputPrefix; // the output prefix to target results into (eg, put things in a certain subfolder of @assets@ rather than the relative to assets itself)
QString m_portableKey; // a key that remains the same even if the asset database is moved from computer to computer.
bool m_isRoot = false; // is it 'the' root folder?
bool m_recurseSubFolders = true;
bool m_canSaveNewAssets = false; // Tracks if it is safe to save new assets in this folder.
int m_order = 0;
AZ::s64 m_scanFolderID = 0; // this is filled in by the database - don't modify it.
AZStd::vector<AssetBuilderSDK::PlatformInfo> m_platforms; // This contains the list of platforms that are enabled for the particular scanfolder
};
struct AssetFileInfo
{
AssetFileInfo() = default;
AssetFileInfo(QString filePath, QDateTime modTime, AZ::u64 fileSize, const ScanFolderInfo* scanFolder, bool isDirectory)
: m_filePath(filePath), m_modTime(modTime), m_fileSize(fileSize), m_scanFolder(scanFolder), m_isDirectory(isDirectory) {}
bool operator==(const AssetFileInfo& rhs) const
{
return m_filePath == rhs.m_filePath
&& m_modTime == rhs.m_modTime
&& m_fileSize == rhs.m_fileSize
&& m_isDirectory == rhs.m_isDirectory;
// m_scanFolder ignored since m_filePath will already ensure this is the same file
}
QString m_filePath{}; // Absolute path of the file
QDateTime m_modTime{};
AZ::u64 m_fileSize{};
const ScanFolderInfo* m_scanFolder{};
bool m_isDirectory{};
};
inline uint qHash(const AssetFileInfo& item)
{
return qHash(item.m_filePath);
}
} // end namespace AssetProcessor
#endif //ASSETSCANFOLDERINFO_H
@@ -0,0 +1,70 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "native/AssetManager/assetScanner.h"
namespace AssetProcessor
{
AssetScanner::AssetScanner(PlatformConfiguration* config, QObject* parent)
: QObject(parent)
, m_assetScannerWorker(config)
, m_status(AssetScanningStatus::Unknown)
{
m_assetScannerWorker.moveToThread( &m_assetWorkerScannerThread );
QObject::connect(&m_assetScannerWorker, &AssetScannerWorker::FilesFound, this, &AssetScanner::FilesFound);
QObject::connect(&m_assetScannerWorker, &AssetScannerWorker::FoldersFound, this, &AssetScanner::FoldersFound);
QObject::connect(&m_assetScannerWorker, &AssetScannerWorker::ExcludedFound, this, &AssetScanner::ExcludedFound);
QObject::connect(&m_assetScannerWorker, &AssetScannerWorker::ScanningStateChanged, this,
[this](AssetProcessor::AssetScanningStatus status)
{
if (m_status == status)
{
return;
}
m_status = status;
Q_EMIT AssetScanningStatusChanged(status);
});
}
AssetScanner::~AssetScanner()
{
StopScan();
m_assetWorkerScannerThread.quit();
m_assetWorkerScannerThread.wait();
}
void AssetScanner::StartScan()
{
if (!m_workerCreated)
{
m_workerCreated = true;
m_assetWorkerScannerThread.setObjectName("AssetScannerWorker");
m_assetWorkerScannerThread.start();
}
QMetaObject::invokeMethod(&m_assetScannerWorker, "StartScan", Qt::QueuedConnection);
}
void AssetScanner::StopScan()
{
QMetaObject::invokeMethod(&m_assetScannerWorker, "StopScan", Qt::DirectConnection);
}
AssetProcessor::AssetScanningStatus AssetScanner::status() const
{
return m_status;
}
}
#include "native/AssetManager/moc_assetScanner.cpp"
@@ -0,0 +1,61 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef ASSETSCANNER_H
#define ASSETSCANNER_H
#if !defined(Q_MOC_RUN)
#include "native/assetprocessor.h"
#include "assetScannerWorker.h"
#include "assetScanFolderInfo.h"
#include <QString>
#include <QThread>
#include <QList>
#endif
namespace AssetProcessor
{
class PlatformConfiguration;
/** This Class is responsible for scanning for assets at startup
*/
class AssetScanner
: public QObject
{
Q_OBJECT
public:
explicit AssetScanner(PlatformConfiguration* config, QObject* parent = nullptr);
virtual ~AssetScanner();
void StartScan();//Should be called to start a scan
void StopScan();//Should be called to stop a scan
Q_INVOKABLE AssetScanningStatus status() const;
Q_SIGNALS:
void AssetScanningStatusChanged(AssetScanningStatus status);
void FilesFound(QSet<AssetFileInfo> files);
void FoldersFound(QSet<AssetFileInfo> folders);
void ExcludedFound(QSet<AssetFileInfo> excluded);
private:
QThread m_assetWorkerScannerThread;
AssetScannerWorker m_assetScannerWorker;
bool m_workerCreated = false;
AZStd::atomic<AssetScanningStatus> m_status;
};
}// end namespace AssetProcessor
#endif // ASSETSCANNER_H
@@ -0,0 +1,142 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "native/AssetManager/assetScannerWorker.h"
#include "native/AssetManager/assetScanner.h"
#include "native/utilities/PlatformConfiguration.h"
#include <QDir>
using namespace AssetProcessor;
AssetScannerWorker::AssetScannerWorker(PlatformConfiguration* config, QObject* parent)
: QObject(parent)
, m_platformConfiguration(config)
{
}
void AssetScannerWorker::StartScan()
{
// this must be called from the thread operating it and not the main thread.
Q_ASSERT(QThread::currentThread() == this->thread());
m_fileList.clear();
m_folderList.clear();
m_doScan = true;
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Scanning file system for changes...\n");
Q_EMIT ScanningStateChanged(AssetProcessor::AssetScanningStatus::Started);
Q_EMIT ScanningStateChanged(AssetProcessor::AssetScanningStatus::InProgress);
for (int idx = 0; idx < m_platformConfiguration->GetScanFolderCount(); idx++)
{
const ScanFolderInfo& scanFolderInfo = m_platformConfiguration->GetScanFolderAt(idx);
ScanForSourceFiles(scanFolderInfo, scanFolderInfo);
}
// we want not to emit any signals until we're finished scanning
// so that we don't interleave directory tree walking (IO access to the file table)
// with file access (IO access to file data) caused by sending signals to other classes.
if (!m_doScan)
{
m_fileList.clear();
m_folderList.clear();
Q_EMIT ScanningStateChanged(AssetProcessor::AssetScanningStatus::Stopped);
return;
}
else
{
EmitFiles();
}
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "File system scan done.\n");
Q_EMIT ScanningStateChanged(AssetProcessor::AssetScanningStatus::Completed);
}
// note: Call this directly from the main thread!
// do not queue this call.
// Join the thread if you intend to wait until its stopped
void AssetScannerWorker::StopScan()
{
m_doScan = false;
}
void AssetScannerWorker::ScanForSourceFiles(const ScanFolderInfo& scanFolderInfo, const ScanFolderInfo& rootScanFolder)
{
if (!m_doScan)
{
return;
}
QDir dir(scanFolderInfo.ScanPath());
QFileInfoList entries;
//Only scan sub folders if recurseSubFolders flag is set
if (!scanFolderInfo.RecurseSubFolders())
{
entries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files);
}
else
{
entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Files);
}
for (const QFileInfo& entry : entries)
{
if (!m_doScan) // scan was cancelled!
{
return;
}
QString absPath = entry.absoluteFilePath();
const bool isDirectory = entry.isDir();
QDateTime modTime = entry.lastModified();
AZ::u64 fileSize = isDirectory ? 0 : entry.size();
AssetFileInfo assetFileInfo(absPath, modTime, fileSize, &rootScanFolder, isDirectory);
// Filtering out excluded files
if (m_platformConfiguration->IsFileExcluded(absPath))
{
m_excludedList.insert(AZStd::move(assetFileInfo));
continue;
}
if (isDirectory)
{
//Entry is a directory
m_folderList.insert(AZStd::move(assetFileInfo));
ScanFolderInfo tempScanFolderInfo(absPath, "", "", "", false, true);
ScanForSourceFiles(tempScanFolderInfo, rootScanFolder);
}
else
{
//Entry is a file
m_fileList.insert(AZStd::move(assetFileInfo));
}
}
}
void AssetScannerWorker::EmitFiles()
{
//Loop over all source asset files and send them up the chain:
Q_EMIT FilesFound(m_fileList);
m_fileList.clear();
Q_EMIT FoldersFound(m_folderList);
m_folderList.clear();
Q_EMIT ExcludedFound(m_excludedList);
m_excludedList.clear();
}
@@ -0,0 +1,64 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef ASSETSCANNERWORKER_H
#define ASSETSCANNERWORKER_H
#if !defined(Q_MOC_RUN)
#include "native/assetprocessor.h"
#include "assetScanFolderInfo.h"
#include <QString>
#include <QSet>
#include <QObject>
#endif
namespace AssetProcessor
{
class PlatformConfiguration;
/** This Class is actually responsible for scanning the game folder
* and finding file of interest files.
* Its created on the main thread and then moved to the worker thread
* so it should contain no QObject-based classes at construction time (it can make them later)
*/
class AssetScannerWorker
: public QObject
{
Q_OBJECT
public:
explicit AssetScannerWorker(PlatformConfiguration* config, QObject* parent = 0);
Q_SIGNALS:
void ScanningStateChanged(AssetProcessor::AssetScanningStatus status);
void FilesFound(QSet<AssetFileInfo> files); // QSet<QString> is a refcounted copy-on-write object, do not pass by ref.
void FoldersFound(QSet<AssetFileInfo> folders); // QSet<QString> is a refcounted copy-on-write object, do not pass by ref.
void ExcludedFound(QSet<AssetFileInfo> excluded); // QSet<QString> is a refcounted copy-on-write object, do not pass by ref.
public Q_SLOTS:
void StartScan();
void StopScan();
protected:
// scanFolderInfo - the folder we're currently scanning (this will sometimes be a fake scanfolder created when recursing through directories)
// rootScanFolder - the actual scan folder we started with, which will either be the same as scanFolderInfo or a parent folder
void ScanForSourceFiles(const ScanFolderInfo& scanFolderInfo, const ScanFolderInfo& rootScanFolder);
void EmitFiles();
private:
volatile bool m_doScan = true;
QSet<AssetFileInfo> m_fileList; // note: neither QSet nor QString are qobject-derived
QSet<AssetFileInfo> m_folderList;
QSet<AssetFileInfo> m_excludedList;
PlatformConfiguration* m_platformConfiguration;
};
} // end namespace AssetProcessor
#endif // ASSETSCANNERWORKER_H
@@ -0,0 +1,20 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "AssetData.h"
#include <QHash>
#include <QFileInfo>
namespace AssetProcessor
{
}
@@ -0,0 +1,25 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/std/string/string_view.h>
namespace AssetProcessorBuildTarget
{
//! This file is to be added only to the AssetProcessorBatch build target
//! This function returns the build system target name
AZStd::string_view GetBuildTargetName()
{
#if !defined (LY_CMAKE_TARGET)
#error "LY_CMAKE_TARGET must be defined in order to add this source file to a CMake executable target"
#endif
return AZStd::string_view{ LY_CMAKE_TARGET };
}
}
@@ -0,0 +1,25 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/std/string/string_view.h>
namespace AssetProcessorBuildTarget
{
//! This file is to be added only to the AssetProcessor build target
//! This function returns the build system target name
AZStd::string_view GetBuildTargetName()
{
#if !defined (LY_CMAKE_TARGET)
#error "LY_CMAKE_TARGET must be defined in order to add this source file to a CMake executable target"
#endif
return AZStd::string_view{ LY_CMAKE_TARGET };
}
}
@@ -0,0 +1,301 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <native/FileProcessor/FileProcessor.h>
#include <native/utilities/PlatformConfiguration.h>
#include <QDir>
namespace FileProcessorPrivate
{
bool FinishedScanning(AssetProcessor::AssetScanningStatus status)
{
return status == AssetProcessor::AssetScanningStatus::Completed ||
status == AssetProcessor::AssetScanningStatus::Stopped;
}
QString GenerateUniqueFileKey(AZ::s64 scanFolder, const char* fileName)
{
return QString("%1:%2").arg(scanFolder).arg(fileName);
}
}
namespace AssetProcessor
{
using namespace FileProcessorPrivate;
FileProcessor::FileProcessor(PlatformConfiguration* config)
: m_platformConfig(config)
{
m_connection = AZStd::shared_ptr<AssetDatabaseConnection>(aznew AssetDatabaseConnection());
m_connection->OpenDatabase();
QDir cacheRootDir;
if (!AssetUtilities::ComputeProjectCacheRoot(cacheRootDir))
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to compute cache root folder");
}
m_normalizedCacheRootPath = AssetUtilities::NormalizeDirectoryPath(cacheRootDir.absolutePath());
}
FileProcessor::~FileProcessor() = default;
void FileProcessor::OnAssetScannerStatusChange(AssetScanningStatus status)
{
//when AssetScanner finished processing, synchronize Files table
if (FileProcessorPrivate::FinishedScanning(status))
{
QMetaObject::invokeMethod(this, "Sync", Qt::QueuedConnection);
}
}
void FileProcessor::AssessFilesFromScanner(QSet<AssetFileInfo> files)
{
for (const AssetFileInfo& file : files)
{
m_filesInAssetScanner.append(file);
}
}
void FileProcessor::AssessFoldersFromScanner(QSet<AssetFileInfo> folders)
{
for (const AssetFileInfo& folder : folders)
{
m_filesInAssetScanner.append(folder);
}
}
void FileProcessor::AssessAddedFile(QString filePath)
{
using namespace AzToolsFramework;
if (m_shutdownSignalled)
{
return;
}
QString relativeFileName;
QString scanFolderPath;
if (!GetRelativePath(filePath, relativeFileName, scanFolderPath))
{
return;
}
const ScanFolderInfo* scanFolderInfo = m_platformConfig->GetScanFolderByPath(scanFolderPath);
if (!scanFolderInfo)
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to find the scan folder for file %s", filePath.toUtf8().constData());
return;
}
AssetDatabase::FileDatabaseEntry file;
file.m_scanFolderPK = scanFolderInfo->ScanFolderID();
file.m_fileName = relativeFileName.toUtf8().constData();
file.m_isFolder = QFileInfo(filePath).isDir();
bool entryAlreadyExists;
if (m_connection->InsertFile(file, entryAlreadyExists) && !entryAlreadyExists)
{
AssetSystem::FileInfosNotificationMessage message;
message.m_type = AssetSystem::FileInfosNotificationMessage::FileAdded;
message.m_fileID = file.m_fileID;
ConnectionBus::Broadcast(&ConnectionBusTraits::Send, 0, message);
}
if (file.m_isFolder)
{
QDir folder(filePath);
for (const QFileInfo& subFile : folder.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot))
{
AssessAddedFile(subFile.absoluteFilePath());
}
}
}
void FileProcessor::AssessDeletedFile(QString filePath)
{
using namespace AzToolsFramework;
if (m_shutdownSignalled)
{
return;
}
QString relativeFileName;
QString scanFolderPath;
if (!GetRelativePath(filePath, relativeFileName, scanFolderPath))
{
return;
}
const ScanFolderInfo* scanFolderInfo = m_platformConfig->GetScanFolderByPath(scanFolderPath);
if (!scanFolderInfo)
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to find the scan folder for file %s", filePath.toUtf8().constData());
return;
}
AssetDatabase::FileDatabaseEntry file;
if (m_connection->GetFileByFileNameAndScanFolderId(relativeFileName, scanFolderInfo->ScanFolderID(), file) && DeleteFileRecursive(file))
{
AssetSystem::FileInfosNotificationMessage message;
message.m_type = AssetSystem::FileInfosNotificationMessage::FileRemoved;
message.m_fileID = file.m_fileID;
ConnectionBus::Broadcast(&ConnectionBusTraits::Send, 0, message);
}
}
void FileProcessor::Sync()
{
using namespace AzToolsFramework;
if (m_shutdownSignalled)
{
return;
}
QMap<QString, AZ::s64> filesInDatabase;
//query all current files from Files table
auto filesFunction = [&filesInDatabase](AzToolsFramework::AssetDatabase::FileDatabaseEntry& entry)
{
QString uniqueKey = GenerateUniqueFileKey(entry.m_scanFolderPK, entry.m_fileName.c_str());
filesInDatabase[uniqueKey] = entry.m_fileID;
return true;
};
m_connection->QueryFilesTable(filesFunction);
//first collect all fileIDs in Files table
QSet<AZ::s64> missingFileIDs;
for (AZ::s64 fileID : filesInDatabase.values())
{
missingFileIDs.insert(fileID);
}
AzToolsFramework::AssetDatabase::FileDatabaseEntryContainer filesToInsert;
for (const AssetFileInfo& fileInfo : m_filesInAssetScanner)
{
bool isDir = fileInfo.m_isDirectory;
QString scanFolderName;
QString relativeFileName;
if (!m_platformConfig->ConvertToRelativePath(fileInfo.m_filePath, relativeFileName, scanFolderName))
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to convert full path to relative for file %s", fileInfo.m_filePath.toUtf8().constData());
continue;
}
const ScanFolderInfo* scanFolderInfo = m_platformConfig->GetScanFolderForFile(fileInfo.m_filePath);
if (!scanFolderInfo)
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to find the scan folder for file %s", fileInfo.m_filePath.toUtf8().constData());
continue;
}
AssetDatabase::FileDatabaseEntry file;
file.m_scanFolderPK = scanFolderInfo->ScanFolderID();
file.m_fileName = relativeFileName.toUtf8().constData();
file.m_isFolder = isDir;
file.m_modTime = 0;
//when file is found by AssetScanner, remove it from the "missing" set
QString uniqueKey = GenerateUniqueFileKey(file.m_scanFolderPK, relativeFileName.toUtf8().constData());
if (filesInDatabase.contains(uniqueKey))
{
// found it, its not missing anymore. (Its also already in the db)
missingFileIDs.remove(filesInDatabase[uniqueKey]);
}
else
{
// its a new file we were previously unaware of.
filesToInsert.push_back(AZStd::move(file));
}
}
m_connection->InsertFiles(filesToInsert);
// remove remaining files from the database as they no longer exist on hard drive
for (AZ::s64 fileID : missingFileIDs)
{
m_connection->RemoveFile(fileID);
}
AssetSystem::FileInfosNotificationMessage message;
ConnectionBus::Broadcast(&ConnectionBusTraits::Send, 0, message);
// It's important to clear this out since rescanning will end up filling this up with duplicates otherwise
QList<AssetFileInfo> emptyList;
m_filesInAssetScanner.swap(emptyList);
}
// note that this function normalizes the path and also returns true only if the file is 'relevant'
// meaning something we care about tracking (ignore list/ etc taken into account).
bool FileProcessor::GetRelativePath(QString& filePath, QString& relativeFileName, QString& scanFolderPath) const
{
filePath = AssetUtilities::NormalizeFilePath(filePath);
if (filePath.startsWith(m_normalizedCacheRootPath, Qt::CaseInsensitive))
{
// modifies/adds to the cache are irrelevant. Deletions are all we care about
return false;
}
if (m_platformConfig->IsFileExcluded(filePath))
{
return false; // we don't care about this kind of file.
}
if (!m_platformConfig->ConvertToRelativePath(filePath, relativeFileName, scanFolderPath))
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to convert full path to relative for file %s", filePath.toUtf8().constData());
return false;
}
return true;
}
bool FileProcessor::DeleteFileRecursive(const AzToolsFramework::AssetDatabase::FileDatabaseEntry& file) const
{
using namespace AzToolsFramework;
if (m_shutdownSignalled)
{
return false;
}
if (file.m_isFolder)
{
AssetDatabase::FileDatabaseEntryContainer container;
AZStd::string searchStr = file.m_fileName + AZ_CORRECT_DATABASE_SEPARATOR;
m_connection->GetFilesLikeFileName(
searchStr.c_str(),
AssetDatabaseConnection::LikeType::StartsWith,
container);
for (const auto& subFile : container)
{
DeleteFileRecursive(subFile);
}
}
return m_connection->RemoveFile(file.m_fileID);
}
void FileProcessor::QuitRequested()
{
m_shutdownSignalled = true;
Q_EMIT ReadyToQuit(this);
}
} // namespace AssetProcessor
#include "native/FileProcessor/moc_FileProcessor.cpp"
@@ -0,0 +1,77 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QObject>
#include <QString>
#include <QMap>
#include <native/AssetDatabase/AssetDatabase.h>
#include <native/assetprocessor.h>
#endif
namespace AzToolsFramework
{
namespace AssetDatabase
{
class FileDatabaseEntry;
}
}
namespace AssetProcessor
{
class PlatformConfiguration;
class FileProcessor
: public QObject
{
Q_OBJECT
public:
explicit FileProcessor(PlatformConfiguration* config);
~FileProcessor();
public Q_SLOTS:
//! AssetScanner changed its status
void OnAssetScannerStatusChange(AssetScanningStatus status);
//! AssetScanner found a file
void AssessFilesFromScanner(QSet<AssetFileInfo> files);
//! AssetScanner found a folder
void AssessFoldersFromScanner(QSet<AssetFileInfo> folders);
//! FileWatcher detected added file
void AssessAddedFile(QString fileName);
//! FileWatcher detected removed file
void AssessDeletedFile(QString fileName);
//! Synchronize AssetScanner data with Files table
void Sync();
//! its time to shut down!
void QuitRequested();
Q_SIGNALS:
void ReadyToQuit(QObject* source); //After receiving QuitRequested, you must send this when its safe
private:
PlatformConfiguration* m_platformConfig = nullptr;
AZStd::shared_ptr<AssetDatabaseConnection> m_connection;
//! Files and folders located by AssetScanner during a scan
QList<AssetFileInfo> m_filesInAssetScanner;
QString m_normalizedCacheRootPath;
bool m_shutdownSignalled = false;
bool GetRelativePath(QString& filePath, QString& relativeFileName, QString& scanFolder) const;
bool DeleteFileRecursive(const AzToolsFramework::AssetDatabase::FileDatabaseEntry& file) const;
};
} // namespace AssetProcessor
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,247 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef FILESERVER_H
#define FILESERVER_H
#if !defined(Q_MOC_RUN)
#include <QByteArray>
#include <QDir>
#include <QString>
#include <QHash>
#include <memory>
// currently these headers are there to provide OS 'HANDLE' of the lock-files
#include <AzCore/PlatformIncl.h>
#endif
namespace AZ
{
namespace IO
{
class FileIOBase;
typedef uint32_t HandleType;
}
}
class Connection;
class FileServer
: public QObject
{
Q_OBJECT
Q_PROPERTY(QString rootFolder MEMBER m_displayRoot NOTIFY RootFolderChanged)
Q_PROPERTY(bool realtimeMetrics MEMBER m_realtimeMetrics NOTIFY RealtimeMetricsChanged)
//metrics
Q_PROPERTY(qint64 numOpenRequests MEMBER m_numOpenRequests NOTIFY NumOpenRequestsChanged)
Q_PROPERTY(qint64 numCloseRequests MEMBER m_numCloseRequests NOTIFY NumCloseRequestsChanged)
Q_PROPERTY(qint64 numOpened MEMBER m_numOpened NOTIFY NumOpenedChanged)
Q_PROPERTY(qint64 numClosed MEMBER m_numClosed NOTIFY NumClosedChanged)
Q_PROPERTY(qint64 numReadRequests MEMBER m_numReadRequests NOTIFY NumReadRequestsChanged)
Q_PROPERTY(qint64 numWriteRequests MEMBER m_numWriteRequests NOTIFY NumWriteRequestsChanged)
Q_PROPERTY(qint64 numSeekRequests MEMBER m_numSeekRequests NOTIFY NumSeekRequestsChanged)
Q_PROPERTY(qint64 numTellRequests MEMBER m_numTellRequests NOTIFY NumTellRequestsChanged)
Q_PROPERTY(qint64 numIsReadOnlyRequests MEMBER m_numIsReadOnlyRequests NOTIFY NumIsReadOnlyRequestsChanged)
Q_PROPERTY(qint64 numIsDirectoryRequests MEMBER m_numIsDirectoryRequests NOTIFY NumIsDirectoryRequestsChanged)
Q_PROPERTY(qint64 numSizeRequests MEMBER m_numSizeRequests NOTIFY NumSizeRequestsChanged)
Q_PROPERTY(qint64 numModificationTimeRequests MEMBER m_numModificationTimeRequests NOTIFY NumModificationTimeRequestsChanged)
Q_PROPERTY(qint64 numExistsRequests MEMBER m_numExistsRequests NOTIFY NumExistsRequestsChanged)
Q_PROPERTY(qint64 numFlushRequests MEMBER m_numFlushRequests NOTIFY NumFlushRequestsChanged)
Q_PROPERTY(qint64 numCreatePathRequests MEMBER m_numCreatePathRequests NOTIFY NumCreatePathRequestsChanged)
Q_PROPERTY(qint64 numDestroyPathRequests MEMBER m_numDestroyPathRequests NOTIFY NumDestroyPathRequestsChanged)
Q_PROPERTY(qint64 numRemoveRequests MEMBER m_numRemoveRequests NOTIFY NumRemoveRequestsChanged)
Q_PROPERTY(qint64 numCopyRequests MEMBER m_numCopyRequests NOTIFY NumCopyRequestsChanged)
Q_PROPERTY(qint64 numRenameRequests MEMBER m_numRenameRequests NOTIFY NumRenameRequestsChanged)
Q_PROPERTY(qint64 numFindFileNamesRequests MEMBER m_numFindFileNamesRequests NOTIFY NumFindFileNamesRequestsChanged)
Q_PROPERTY(qint64 bytesRead MEMBER m_bytesRead NOTIFY BytesReadChanged)
Q_PROPERTY(qint64 bytesWritten MEMBER m_bytesWritten NOTIFY BytesWrittenChanged)
Q_PROPERTY(qint64 bytesSent MEMBER m_bytesSent NOTIFY BytesSentChanged)
Q_PROPERTY(qint64 bytesReceived MEMBER m_bytesReceived NOTIFY BytesReceivedChanged)
Q_PROPERTY(qint64 numOpenFiles MEMBER m_numOpenFiles NOTIFY NumOpenFilesChanged)
Q_SIGNALS:
void RootFolderChanged();
void RealtimeMetricsChanged();
//metrics
void NumOpenRequestsChanged();
void NumCloseRequestsChanged();
void NumOpenedChanged();
void NumClosedChanged();
void NumReadRequestsChanged();
void NumWriteRequestsChanged();
void NumSeekRequestsChanged();
void NumTellRequestsChanged();
void NumIsReadOnlyRequestsChanged();
void NumIsDirectoryRequestsChanged();
void NumSizeRequestsChanged();
void NumModificationTimeRequestsChanged();
void NumExistsRequestsChanged();
void NumFlushRequestsChanged();
void NumCreatePathRequestsChanged();
void NumDestroyPathRequestsChanged();
void NumRemoveRequestsChanged();
void NumCopyRequestsChanged();
void NumRenameRequestsChanged();
void NumFindFileNamesRequestsChanged();
void BytesReadChanged();
void BytesWrittenChanged();
void BytesSentChanged();
void BytesReceivedChanged();
void NumOpenFilesChanged();
//per connection metrics
void AddBytesReceived(unsigned int connId, qint64 add, bool update);
void AddBytesSent(unsigned int connId, qint64 add, bool update);
void AddBytesRead(unsigned int connId, qint64 add, bool update);
void AddBytesWritten(unsigned int connId, qint64 add, bool update);
void AddOpenRequest(unsigned int connId, bool update);
void AddCloseRequest(unsigned int connId, bool update);
void AddOpened(unsigned int connId, bool update);
void AddClosed(unsigned int connId, bool update);
void AddReadRequest(unsigned int connId, bool update);
void AddWriteRequest(unsigned int connId, bool update);
void AddTellRequest(unsigned int connId, bool update);
void AddSeekRequest(unsigned int connId, bool update);
void AddIsReadOnlyRequest(unsigned int connId, bool update);
void AddIsDirectoryRequest(unsigned int connId, bool update);
void AddSizeRequest(unsigned int connId, bool update);
void AddModificationTimeRequest(unsigned int connId, bool update);
void AddExistsRequest(unsigned int connId, bool update);
void AddFlushRequest(unsigned int connId, bool update);
void AddCreatePathRequest(unsigned int connId, bool update);
void AddDestroyPathRequest(unsigned int connId, bool update);
void AddRemoveRequest(unsigned int connId, bool update);
void AddCopyRequest(unsigned int connId, bool update);
void AddRenameRequest(unsigned int connId, bool update);
void AddFindFileNamesRequest(unsigned int connId, bool update);
void UpdateBytesReceived(unsigned int connId);
void UpdateBytesSent(unsigned int connId);
void UpdateBytesRead(unsigned int connId);
void UpdateBytesWritten(unsigned int connId);
void UpdateOpenRequest(unsigned int connId);
void UpdateCloseRequest(unsigned int connId);
void UpdateOpened(unsigned int connId);
void UpdateClosed(unsigned int connId);
void UpdateReadRequest(unsigned int connId);
void UpdateWriteRequest(unsigned int connId);
void UpdateTellRequest(unsigned int connId);
void UpdateSeekRequest(unsigned int connId);
void UpdateIsReadOnlyRequest(unsigned int connId);
void UpdateIsDirectoryRequest(unsigned int connId);
void UpdateSizeRequest(unsigned int connId);
void UpdateModificationTimeRequest(unsigned int connId);
void UpdateExistsRequest(unsigned int connId);
void UpdateFlushRequest(unsigned int connId);
void UpdateCreatePathRequest(unsigned int connId);
void UpdateDestroyPathRequest(unsigned int connId);
void UpdateRemoveRequest(unsigned int connId);
void UpdateCopyRequest(unsigned int connId);
void UpdateRenameRequest(unsigned int connId);
void UpdateFindFileNamesRequest(unsigned int connId);
void UpdateConnectionMetrics();
public:
explicit FileServer(QObject* parent = 0);
virtual ~FileServer();
void SetSystemRoot(const QDir& systemRoot);
Q_INVOKABLE void setRealTimeMetrics(bool enable);
public Q_SLOTS:
void ProcessOpenRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessCloseRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessReadRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessWriteRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessTellRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessSeekRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessIsReadOnlyRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessIsDirectoryRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessSizeRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessModificationTimeRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessExistsRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessFlushRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessCreatePathRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessDestroyPathRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessRemoveRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessCopyRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessRenameRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessFindFileNamesRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ProcessFileTreeRequest(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void UpdateMetrics();
void ConnectionAdded(unsigned int connId, Connection* connection);
void ConnectionRemoved(unsigned int connId);
protected:
template <class R>
void Send(unsigned int connId, unsigned int serial, const R& response);
template <class R>
bool Recv(unsigned int connId, QByteArray payload, R& request);
void RecordFileOp(AZ::IO::FileIOBase* fileIO, const char* op, const AZ::IO::HandleType& fileHandle, const char* moreInfo);
void RecordFileOp(AZ::IO::FileIOBase* fileIO, const char* op, const char* filePath, const char* moreInfo);
void RecordFileOp(AZ::IO::FileIOBase* fileIO, const char* op, const char* sourceFile, const char* destFile, const char* moreInfo);
//! This makes sure that the cache folder exists but is conservative, it only should do this if the game actually makes file requests
//! So we only create a cache folder for VFS-based runs.
void EnsureCacheFolderExists(int connId);
private:
//metrics
qint64 m_numOpenRequests;
qint64 m_numCloseRequests;
qint64 m_numOpened;
qint64 m_numClosed;
qint64 m_numReadRequests;
qint64 m_numWriteRequests;
qint64 m_numTellRequests;
qint64 m_numSeekRequests;
qint64 m_numIsReadOnlyRequests;
qint64 m_numIsDirectoryRequests;
qint64 m_numSizeRequests;
qint64 m_numModificationTimeRequests;
qint64 m_numExistsRequests;
qint64 m_numFlushRequests;
qint64 m_numCreatePathRequests;
qint64 m_numDestroyPathRequests;
qint64 m_numRemoveRequests;
qint64 m_numCopyRequests;
qint64 m_numRenameRequests;
qint64 m_numFindFileNamesRequests;
qint64 m_bytesRead;
qint64 m_bytesWritten;
qint64 m_bytesSent;
qint64 m_bytesReceived;
qint64 m_numOpenFiles;
//root
QString m_displayRoot;
QDir m_systemRoot;
bool m_realtimeMetrics;
// maps connection ID -> LocalFileIO
QHash<unsigned int, std::shared_ptr<AZ::IO::FileIOBase> > m_fileIOs;
#if defined(AZ_PLATFORM_WINDOWS)
QHash<unsigned int, HANDLE> m_locks;
#endif // lockFiles. do NOT use QLockFile, it won't work if other platforms are locking it, it only works for other users of QLockFile
};
#endif // FILESERVER_H
@@ -0,0 +1,197 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "FileWatcher.h"
#include <native/assetprocessor.h>
//////////////////////////////////////////////////////////////////////////////
/// FolderWatchRoot
void FolderRootWatch::ProcessNewFileEvent(const QString& file)
{
FileChangeInfo info;
info.m_action = FileAction::FileAction_Added;
info.m_filePath = file;
const bool invoked = QMetaObject::invokeMethod(m_fileWatcher, "AnyFileChange", Qt::QueuedConnection, Q_ARG(FileChangeInfo, info));
Q_ASSERT(invoked);
}
void FolderRootWatch::ProcessDeleteFileEvent(const QString& file)
{
FileChangeInfo info;
info.m_action = FileAction::FileAction_Removed;
info.m_filePath = file;
const bool invoked = QMetaObject::invokeMethod(m_fileWatcher, "AnyFileChange", Qt::QueuedConnection, Q_ARG(FileChangeInfo, info));
Q_ASSERT(invoked);
}
void FolderRootWatch::ProcessModifyFileEvent(const QString& file)
{
FileChangeInfo info;
info.m_action = FileAction::FileAction_Modified;
info.m_filePath = file;
const bool invoked = QMetaObject::invokeMethod(m_fileWatcher, "AnyFileChange", Qt::QueuedConnection, Q_ARG(FileChangeInfo, info));
Q_ASSERT(invoked);
}
//////////////////////////////////////////////////////////////////////////
/// FileWatcher
FileWatcher::FileWatcher()
: m_nextHandle(0)
{
qRegisterMetaType<FileChangeInfo>("FileChangeInfo");
}
FileWatcher::~FileWatcher()
{
}
int FileWatcher::AddFolderWatch(FolderWatchBase* pFolderWatch)
{
if (!pFolderWatch)
{
return -1;
}
FolderRootWatch* pFolderRootWatch = nullptr;
//see if this a sub folder of an already watched root
for (auto rootsIter = m_folderWatchRoots.begin(); !pFolderRootWatch && rootsIter != m_folderWatchRoots.end(); ++rootsIter)
{
if (FolderWatchBase::IsSubfolder(pFolderWatch->m_folder, (*rootsIter)->m_root))
{
pFolderRootWatch = *rootsIter;
}
}
bool bCreatedNewRoot = false;
//if its not a sub folder
if (!pFolderRootWatch)
{
//create a new root and start listening for changes
pFolderRootWatch = new FolderRootWatch(pFolderWatch->m_folder);
//make sure the folder watcher(s) get deleted before this
pFolderRootWatch->setParent(this);
bCreatedNewRoot = true;
}
pFolderRootWatch->m_fileWatcher = this;
QObject::connect(this, &FileWatcher::AnyFileChange, pFolderWatch, &FolderWatchBase::OnAnyFileChange);
if (bCreatedNewRoot)
{
if (m_startedWatching)
{
pFolderRootWatch->Start();
}
//since we created a new root, see if the new root is a super folder
//of other roots, if it is then then fold those roots into the new super root
for (auto rootsIter = m_folderWatchRoots.begin(); rootsIter != m_folderWatchRoots.end(); )
{
if (FolderWatchBase::IsSubfolder((*rootsIter)->m_root, pFolderWatch->m_folder))
{
//union the sub folder map over to the new root
pFolderRootWatch->m_subFolderWatchesMap.insert((*rootsIter)->m_subFolderWatchesMap);
//clear the old root sub folders map so they don't get deleted when we
//delete the old root as they are now pointed to by the new root
(*rootsIter)->m_subFolderWatchesMap.clear();
//delete the empty old root, deleting a root will call Stop()
//automatically which kills the thread
delete *rootsIter;
//remove the old root pointer form the watched list
rootsIter = m_folderWatchRoots.erase(rootsIter);
}
else
{
++rootsIter;
}
}
//add the new root to the watched roots
m_folderWatchRoots.push_back(pFolderRootWatch);
}
//add to the root
pFolderRootWatch->m_subFolderWatchesMap.insert(m_nextHandle, pFolderWatch);
m_nextHandle++;
return m_nextHandle - 1;
}
void FileWatcher::RemoveFolderWatch(int handle)
{
for (auto rootsIter = m_folderWatchRoots.begin(); rootsIter != m_folderWatchRoots.end(); )
{
//find an element by the handle
auto foundIter = (*rootsIter)->m_subFolderWatchesMap.find(handle);
if (foundIter != (*rootsIter)->m_subFolderWatchesMap.end())
{
//remove the element
(*rootsIter)->m_subFolderWatchesMap.erase(foundIter);
//we removed a folder watch, if it's empty then there is no reason to keep watching it.
if ((*rootsIter)->m_subFolderWatchesMap.empty())
{
delete(*rootsIter);
rootsIter = m_folderWatchRoots.erase(rootsIter);
}
else
{
++rootsIter;
}
}
else
{
++rootsIter;
}
}
}
void FileWatcher::StartWatching()
{
if (m_startedWatching)
{
AZ_Warning("FileWatcher", false, "StartWatching() called when already watching for file changes.");
return;
}
for (FolderRootWatch* root : m_folderWatchRoots)
{
root->Start();
}
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "File Change Monitoring started.\n");
m_startedWatching = true;
}
void FileWatcher::StopWatching()
{
if (!m_startedWatching)
{
AZ_Warning("FileWatcher", false, "StartWatching() called when is not watching for file changes.");
return;
}
for (FolderRootWatch* root : m_folderWatchRoots)
{
root->Stop();
}
m_startedWatching = false;
}
#include "native/FileWatcher/moc_FileWatcher.cpp"
#include "native/FileWatcher/moc_FileWatcherAPI.cpp"
@@ -0,0 +1,99 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef FILEWATCHER_COMPONENT_H
#define FILEWATCHER_COMPONENT_H
//////////////////////////////////////////////////////////////////////////
#if !defined(Q_MOC_RUN)
#include "FileWatcherAPI.h"
#include <AzCore/std/containers/vector.h>
#include <QMap>
#include <QVector>
#include <QString>
#include <thread>
#endif
class FileWatcher;
//////////////////////////////////////////////////////////////////////////
//! FolderRootWatch
/*! Class used for holding a point in the files system from which file changes are tracked.
* */
class FolderRootWatch
: public QObject
{
Q_OBJECT
friend class FileWatcher;
public:
FolderRootWatch(const QString rootFolder);
virtual ~FolderRootWatch();
void ProcessNewFileEvent(const QString& file);
void ProcessDeleteFileEvent(const QString& file);
void ProcessModifyFileEvent(const QString& file);
void ProcessRenameFileEvent(const QString& fileOld, const QString& fileNew);
public Q_SLOTS:
bool Start();
void Stop();
private:
void WatchFolderLoop();
private:
std::thread m_thread;
QString m_root;
QMap<int, FolderWatchBase*> m_subFolderWatchesMap;
volatile bool m_shutdownThreadSignal;
FileWatcher* m_fileWatcher;
// Can't use unique_ptr because this is a QObject and Qt's magic sauce is
// unable to determine the size of the unique_ptr and so fails to compile
struct PlatformImplementation;
PlatformImplementation* m_platformImpl;
};
//////////////////////////////////////////////////////////////////////////
//! FileWatcher
/*! Class that handles creation and deletion of FolderRootWatches based on
*! the given FolderWatches, and forwards file change signals to them.
* */
class FileWatcher
: public QObject
{
Q_OBJECT
public:
FileWatcher();
virtual ~FileWatcher();
//////////////////////////////////////////////////////////////////////////
virtual int AddFolderWatch(FolderWatchBase* pFolderWatch);
virtual void RemoveFolderWatch(int handle);
//////////////////////////////////////////////////////////////////////////
void StartWatching();
void StopWatching();
Q_SIGNALS:
void AnyFileChange(FileChangeInfo info);
private:
int m_nextHandle;
AZStd::vector<FolderRootWatch*> m_folderWatchRoots;
bool m_startedWatching = false;
};
#endif//FILEWATCHER_COMPONENT_H
@@ -0,0 +1,226 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef FILEWATCHERAPI_H
#define FILEWATCHERAPI_H
#include <QString>
#include <QObject>
#include <QDir>
//////////////////////////////////////////////////////////////////////////
//! FileAction
/*! Enum for which file changes are tracked.
* */
enum FileAction
{
FileAction_None = 0x00,
FileAction_Added = 0x01,
FileAction_Removed = 0x02,
FileAction_Modified = 0x04,
FileAction_Any = 0xFF,
};
inline FileAction operator | (FileAction a, FileAction b)
{
return static_cast<FileAction>(static_cast<int>(a) | static_cast<int>(b));
}
inline FileAction operator & (FileAction a, FileAction b)
{
return static_cast<FileAction>(static_cast<int>(a) & static_cast<int>(b));
}
//////////////////////////////////////////////////////////////////////////
//! FileChangeInfo
/*! Struct for passing along information about file changes.
* */
struct FileChangeInfo
{
FileChangeInfo()
: m_action(FileAction::FileAction_None)
{}
FileChangeInfo(const FileChangeInfo& rhs)
: m_action(rhs.m_action)
, m_filePath(rhs.m_filePath)
, m_filePathOld(rhs.m_filePathOld)
{
}
FileAction m_action;
QString m_filePath;
QString m_filePathOld;
};
Q_DECLARE_METATYPE(FileChangeInfo)
//////////////////////////////////////////////////////////////////////////
//! FolderWatchBase
/*! Class for filtering file changes generated from a root watch. Define your own
*! custom filtering by deriving from this base class and implement your own
*! custom code for what to do when receiving a file change notification.
* */
class FolderWatchBase
: public QObject
{
Q_OBJECT
public:
FolderWatchBase(const QString strFolder, bool bWatchSubtree = true, FileAction fileAction = FileAction::FileAction_Any)
: m_folder(strFolder)
, m_watchSubtree(bWatchSubtree)
, m_fileAction(fileAction)
{
m_folder = QDir::toNativeSeparators(QDir::cleanPath(m_folder) + "/");
}
//! IsSubfolder(folderA, folderB)
//! returns whether folderA is a subfolder of folderB
//! assumptions: absolute paths, case insensitive
static bool IsSubfolder(const QString& folderA, const QString& folderB)
{
// lets avoid allocating or messing with memory - this is a MAJOR hotspot as it is called for any file change even in the cache!
int sizeB = folderB.length();
int sizeA = folderA.length();
if (sizeA <= sizeB)
{
return false;
}
QChar slash1 = QChar('\\');
QChar slash2 = QChar('/');
int posA = 0;
// A is going to be the longer one, so use B:
for (int idx = 0; idx < sizeB; ++idx)
{
QChar charAtA = folderA.at(posA);
QChar charAtB = folderB.at(idx);
if ((charAtB == slash1) || (charAtB == slash2))
{
if ((charAtA != slash1) && (charAtA != slash2))
{
return false;
}
++posA;
}
else
{
if (charAtA.toLower() != charAtB.toLower())
{
return false;
}
++posA;
}
}
return true;
}
QString m_folder;
bool m_watchSubtree;
FileAction m_fileAction;
public Q_SLOTS:
void OnAnyFileChange(FileChangeInfo info)
{
//if they set a file action then respect it by rejecting non matching file actions
if (info.m_action & m_fileAction)
{
//is the file is in the folder or subtree (if specified) then call OnFileChange
if (FolderWatchBase::IsSubfolder(info.m_filePath, m_folder))
{
OnFileChange(info);
}
}
}
virtual void OnFileChange(const FileChangeInfo& info) = 0;
};
//////////////////////////////////////////////////////////////////////////
//! FolderWatchCallbackEx
/*! Class implements a more complex filtering that can optionally filter for file
*! extension and call different callback for different kinds of file changes
*! generated from a root watch.
*! Notes:
*! - empty extension "" catches all file changes
*! - extension should not include the leading "."
* */
class FolderWatchCallbackEx
: public FolderWatchBase
{
Q_OBJECT
public:
FolderWatchCallbackEx(const QString strFolder, const QString extension, bool bWatchSubtree)
: FolderWatchBase(strFolder, bWatchSubtree)
, m_extension(extension)
{
}
QString m_extension;
//on file change call the change callback if passes extension then route
//to specific file action type callback
virtual void OnFileChange(const FileChangeInfo& info)
{
//if they set an extension to watch for only let matching extensions through
QFileInfo fileInfo(info.m_filePath);
if (!m_watchSubtree)
{
// filter out subtrees too.
QStringRef subRef = info.m_filePath.rightRef(info.m_filePath.length() - m_folder.length());
if ((subRef.indexOf('/') != -1) || (subRef.indexOf('\\') != -1))
{
return; // filter this out.
}
// we don't care about subdirs. IsDir is more expensive so we do it after the above filter.
if (fileInfo.isDir())
{
return;
}
}
if (m_extension.isEmpty() || fileInfo.completeSuffix().compare(m_extension, Qt::CaseInsensitive) == 0)
{
if (info.m_action & FileAction::FileAction_Any)
{
Q_EMIT fileChange(info);
}
if (info.m_action & FileAction::FileAction_Added)
{
Q_EMIT fileAdded(info.m_filePath);
}
if (info.m_action & FileAction::FileAction_Removed)
{
Q_EMIT fileRemoved(info.m_filePath);
}
if (info.m_action & FileAction::FileAction_Modified)
{
Q_EMIT fileModified(info.m_filePath);
}
}
}
Q_SIGNALS:
void fileChange(FileChangeInfo info);
void fileAdded(QString filePath);
void fileRemoved(QString filePath);
void fileModified(QString filePath);
};
#endif//FILEWATCHERAPI_H
@@ -0,0 +1,374 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <limits>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include <AzCore/Settings/SettingsRegistryImpl.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzFramework/Platform/PlatformDefaults.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <native/InternalBuilders/SettingsRegistryBuilder.h>
namespace AssetProcessor
{
void SettingsRegistryBuilder::SettingsExporter::WriteName(AZStd::string_view name)
{
if (m_includeName)
{
m_writer.Key(name.data(), aznumeric_caster(name.length()));
}
}
SettingsRegistryBuilder::SettingsExporter::SettingsExporter(
rapidjson::StringBuffer& buffer, const AZStd::vector<AZStd::string>& excludes)
: m_writer(rapidjson::Writer<rapidjson::StringBuffer>(buffer))
, m_excludes(excludes)
{
}
AZ::SettingsRegistryInterface::VisitResponse SettingsRegistryBuilder::SettingsExporter::Traverse(
AZStd::string_view path, AZStd::string_view valueName, AZ::SettingsRegistryInterface::VisitAction action,
AZ::SettingsRegistryInterface::Type type)
{
for (const AZStd::string& exclude : m_excludes)
{
if (exclude == path)
{
return AZ::SettingsRegistryInterface::VisitResponse::Skip;
}
}
if (action == AZ::SettingsRegistryInterface::VisitAction::Begin)
{
AZ_Assert(type == AZ::SettingsRegistryInterface::Type::Object || type == AZ::SettingsRegistryInterface::Type::Array,
"Unexpected type visited: %i.", type);
WriteName(valueName);
if (type == AZ::SettingsRegistryInterface::Type::Object)
{
m_result = m_result && m_writer.StartObject();
m_includeNameStack.push(true);
m_includeName = true;
}
else
{
m_result = m_result && m_writer.StartArray();
m_includeNameStack.push(false);
m_includeName = false;
}
}
else if (action == AZ::SettingsRegistryInterface::VisitAction::End)
{
if (type == AZ::SettingsRegistryInterface::Type::Object)
{
m_result = m_result && m_writer.EndObject();
}
else
{
m_result = m_result && m_writer.EndArray();
}
AZ_Assert(!m_includeNameStack.empty(), "Attempting to close a json array or object that wasn't started.");
m_includeNameStack.pop();
m_includeName = !m_includeNameStack.empty() ? m_includeNameStack.top() : true;
}
else if (type == AZ::SettingsRegistryInterface::Type::Null)
{
WriteName(valueName);
m_result = m_result && m_writer.Null();
}
return m_result ?
AZ::SettingsRegistryInterface::VisitResponse::Continue :
AZ::SettingsRegistryInterface::VisitResponse::Done;
}
void SettingsRegistryBuilder::SettingsExporter::Visit(
AZStd::string_view, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, bool value)
{
WriteName(valueName);
m_result = m_result && m_writer.Bool(value);
}
void SettingsRegistryBuilder::SettingsExporter::Visit(
AZStd::string_view, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, AZ::s64 value)
{
WriteName(valueName);
m_result = m_result && m_writer.Int64(value);
}
void SettingsRegistryBuilder::SettingsExporter::Visit(
AZStd::string_view, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, AZ::u64 value)
{
WriteName(valueName);
m_result = m_result && m_writer.Uint64(value);
}
void SettingsRegistryBuilder::SettingsExporter::Visit(
AZStd::string_view, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, double value)
{
WriteName(valueName);
m_result = m_result && m_writer.Double(value);
}
void SettingsRegistryBuilder::SettingsExporter::Visit(
AZStd::string_view, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, AZStd::string_view value)
{
WriteName(valueName);
m_result = m_result && m_writer.String(value.data(), aznumeric_caster(value.length()));
}
bool SettingsRegistryBuilder::SettingsExporter::Finalize()
{
if (!m_includeNameStack.empty())
{
AZ_Assert(false, "m_includeNameStack is expected to be empty. This means that there was an object or array what wasn't closed.");
return false;
}
return m_result;
}
void SettingsRegistryBuilder::SettingsExporter::Reset(rapidjson::StringBuffer& buffer)
{
m_writer.Reset(buffer);
m_includeName = false;
m_result = true;
}
SettingsRegistryBuilder::SettingsRegistryBuilder()
: m_builderId("{1BB18B28-2953-4922-A80B-E7375FCD7FC1}")
, m_assetType("{FEBB3C7B-9C8B-46C3-8AAF-3D132D811087}")
{
AssetBuilderSDK::AssetBuilderCommandBus::Handler::BusConnect(m_builderId);
}
bool SettingsRegistryBuilder::Initialize()
{
AssetBuilderSDK::AssetBuilderDesc builderDesc;
builderDesc.m_name = "Settings Registry Builder";
builderDesc.m_patterns.emplace_back("*/bootstrap.cfg", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
builderDesc.m_builderType = AssetBuilderSDK::AssetBuilderDesc::AssetBuilderType::Internal;
builderDesc.m_busId = m_builderId;
builderDesc.m_createJobFunction = AZStd::bind(&SettingsRegistryBuilder::CreateJobs, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
builderDesc.m_processJobFunction = AZStd::bind(&SettingsRegistryBuilder::ProcessJob, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBusTraits::RegisterBuilderInformation, builderDesc);
return true;
}
void SettingsRegistryBuilder::Uninitialize() {}
void SettingsRegistryBuilder::ShutDown()
{
m_isShuttingDown = true;
}
void SettingsRegistryBuilder::CreateJobs(
const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response)
{
if (m_isShuttingDown)
{
response.m_result = AssetBuilderSDK::CreateJobsResultCode::ShuttingDown;
return;
}
for (const AssetBuilderSDK::PlatformInfo& info : request.m_enabledPlatforms)
{
AssetBuilderSDK::JobDescriptor job;
job.m_jobKey = "Settings Registry";
// The settings are the very first thing the game reads so needs to available before anything else.
job.m_priority = std::numeric_limits<decltype(job.m_priority)>::max();
job.m_critical = true;
job.SetPlatformIdentifier(info.m_identifier.c_str());
response.m_createJobOutputs.push_back(AZStd::move(job));
}
response.m_sourceFileDependencyList.emplace_back("*.setreg", AZ::Uuid::CreateNull(),
AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards);
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
}
void SettingsRegistryBuilder::ProcessJob(
const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response)
{
if (m_isShuttingDown)
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
return;
}
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
AZStd::vector<AZStd::string> excludes = ReadExcludesFromRegistry();
AZStd::vector<char> scratchBuffer;
scratchBuffer.reserve(512 * 1024); // Reserve 512kb to avoid repeatedly resizing the buffer;
AZStd::fixed_vector<AZStd::string_view, AzFramework::MaxPlatformCodeNames> platformCodes;
AzFramework::PlatformHelper::AppendPlatformCodeNames(platformCodes, request.m_platformInfo.m_identifier);
const AZStd::string& assetPlatformIdentifier = request.m_jobDescription.GetPlatformIdentifier();
// Determines the suffix that will be used for the launcher based on processing server vs non-server assets
const char* launcherType = assetPlatformIdentifier != AzFramework::PlatformHelper::GetPlatformName(AzFramework::PlatformId::SERVER)
? "_GameLauncher" : "_ServerLauncher";
AZ::SettingsRegistryInterface::Specializations specializations[] =
{
{ AZStd::string_view{"release"}, AZStd::string_view{"game"} },
{ AZStd::string_view{"profile"}, AZStd::string_view{"game"} },
{ AZStd::string_view{"debug"}, AZStd::string_view{"game"} }
};
// Add the project specific specializations
if (auto settingsRegistry = AZ::Interface<AZ::SettingsRegistryInterface>::Get(); settingsRegistry)
{
auto projectKey = AZ::SettingsRegistryInterface::FixedValueString::format("%s/sys_game_folder", AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey);
if (AZ::SettingsRegistryInterface::FixedValueString projectName; settingsRegistry->Get(projectName, projectKey))
{
for (AZ::SettingsRegistryInterface::Specializations& specialization : specializations)
{
specialization.Append(projectName);
// The Game Launcher normally has a build target name of <ProjectName>Launcher
// Add that as a specialization to pick up the gem dependencies files that are specialized
// on a the Game Launcher target if the asset platform isn't "server"
specialization.Append(projectName + launcherType);
}
}
}
AZStd::string outputPath;
AzFramework::StringFunc::Path::Join(request.m_tempDirPath.c_str(), "bootstrap.game.", outputPath);
size_t extensionOffset = outputPath.length();
rapidjson::StringBuffer outputBuffer;
outputBuffer.Reserve(512 * 1024); // Reserve 512kb to avoid repeatedly resizing the buffer;
SettingsExporter exporter(outputBuffer, excludes);
for (AZStd::string_view platform : platformCodes)
{
AZ::u32 productSubID = static_cast<AZ::u32>(AZStd::hash<AZStd::string_view>{}(platform)); // Deliberately ignoring half the bits.
for (size_t i = 0; i < AZ_ARRAY_SIZE(specializations); ++i)
{
if (m_isShuttingDown)
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
return;
}
AZ::SettingsRegistryImpl registry;
// Seed the local settings registry using the AssetProcessor settings registry
if (auto settingsRegistry = AZ::Interface<AZ::SettingsRegistryInterface>::Get(); settingsRegistry != nullptr)
{
AZStd::array settingsToCopy{
AZStd::string::format("%s/sys_game_folder", AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey),
AZStd::string{AZ::SettingsRegistryMergeUtils::FilePathKey_BinaryFolder},
AZStd::string{AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder},
AZStd::string{AZ::SettingsRegistryMergeUtils::FilePathKey_SourceGameFolder},
AZStd::string{AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder},
AZStd::string{AZ::SettingsRegistryMergeUtils::FilePathKey_CacheGameFolder}
};
for (const auto& settingsKey : settingsToCopy)
{
AZ::SettingsRegistryInterface::FixedValueString settingsValue;
bool settingsCopied = settingsRegistry->Get(settingsValue, settingsKey)
&& registry.Set(settingsKey, settingsValue);
AZ_Warning("Settings Registry Builder", settingsCopied, "Unable to copy setting %s from AssetProcessor settings registry"
" to local settings registry", settingsKey.c_str());
}
}
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_Bootstrap(registry);
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_EngineRegistry(registry, platform, specializations[i], &scratchBuffer);
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_GemRegistries(registry, platform, specializations[i], &scratchBuffer);
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_ProjectRegistry(registry, platform, specializations[i], &scratchBuffer);
// Merge the Developer User settings registry only in non-release builds
if (!specializations->Contains("release"))
{
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_DevRegistry(registry, platform, specializations[i], &scratchBuffer);
}
AZ::ComponentApplicationBus::Broadcast([&registry](AZ::ComponentApplicationRequests* appRequests)
{
if (AZ::CommandLine* commandLine = appRequests->GetAzCommandLine(); commandLine != nullptr)
{
constexpr bool executeRegDumpCommands = false;
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_CommandLine(registry, *commandLine, executeRegDumpCommands);
}
});
if (registry.Visit(exporter, ""))
{
if (!exporter.Finalize())
{
return;
}
outputPath += specializations[i].GetSpecialization(0); // Append configuration
outputPath += '.';
outputPath += platform;
outputPath += ".setreg";
AZ::IO::SystemFile file;
if (!file.Open(outputPath.c_str(),
AZ::IO::SystemFile::OpenMode::SF_OPEN_CREATE | AZ::IO::SystemFile::OpenMode::SF_OPEN_WRITE_ONLY))
{
AZ_Error("Settings Registry Builder", false, R"(Failed to open file "%s" for writing.)", outputPath.c_str());
return;
}
if (file.Write(outputBuffer.GetString(), outputBuffer.GetSize()) != outputBuffer.GetSize())
{
AZ_Error("Settings Registry Builder", false, R"(Failed to write settings registry to file "%s".)", outputPath.c_str());
return;
}
file.Close();
response.m_outputProducts.emplace_back(outputPath, m_assetType, productSubID + aznumeric_cast<AZ::u32>(i));
outputPath.erase(extensionOffset);
}
outputBuffer.Clear();
exporter.Reset(outputBuffer);
}
}
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
}
AZStd::vector<AZStd::string> SettingsRegistryBuilder::ReadExcludesFromRegistry() const
{
AZStd::vector<AZStd::string> excludes;
auto builderRegistry = AZ::SettingsRegistry::Get();
AZStd::string path = "/Amazon/AssetBuilder/SettingsRegistry/Excludes/";
size_t offset = path.length();
size_t counter = 0;
do
{
path += AZStd::to_string(counter);
AZStd::string exclude;
if (builderRegistry->Get(exclude, path))
{
excludes.push_back(AZStd::move(exclude));
}
else
{
return excludes;
}
counter++;
path.erase(offset);
} while (true);
}
} // namespace AssetProcessor
@@ -0,0 +1,74 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AssetBuilderSDK/AssetBuilderBusses.h>
#include <AzCore/JSON/writer.h>
#include <AzCore/Settings/SettingsRegistry.h>
#include <AzCore/std/containers/stack.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/string/string.h>
namespace AssetProcessor
{
class SettingsRegistryBuilder
: public AssetBuilderSDK::AssetBuilderCommandBus::Handler
{
public:
class SettingsExporter : public AZ::SettingsRegistryInterface::Visitor
{
public:
SettingsExporter(rapidjson::StringBuffer& buffer, const AZStd::vector<AZStd::string>& excludes);
~SettingsExporter() override = default;
AZ::SettingsRegistryInterface::VisitResponse Traverse(AZStd::string_view path, AZStd::string_view valueName,
AZ::SettingsRegistryInterface::VisitAction action, AZ::SettingsRegistryInterface::Type type) override;
void Visit(AZStd::string_view, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, bool value) override;
void Visit(AZStd::string_view, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, AZ::s64 value) override;
void Visit(AZStd::string_view, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, AZ::u64 value) override;
void Visit(AZStd::string_view, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, double value) override;
void Visit(AZStd::string_view, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, AZStd::string_view value) override;
bool Finalize();
void Reset(rapidjson::StringBuffer& buffer);
private:
rapidjson::Writer<rapidjson::StringBuffer> m_writer;
const AZStd::vector<AZStd::string>& m_excludes;
AZStd::stack<bool> m_includeNameStack;
bool m_includeName{ false };
bool m_result{ true };
void WriteName(AZStd::string_view name);
};
SettingsRegistryBuilder();
~SettingsRegistryBuilder() override = default;
bool Initialize();
void Uninitialize();
void ShutDown() override;
void CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response);
void ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response);
protected:
AZStd::vector<AZStd::string> ReadExcludesFromRegistry() const;
private:
AZ::Uuid m_builderId;
AZ::Data::AssetType m_assetType;
bool m_isShuttingDown{ false };
};
} // namespace AssetProcessor
@@ -0,0 +1,356 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <QPair>
#include <QMetaType>
#include <AzCore/Math/Uuid.h>
#include <AzCore/Math/Crc.h>
#include <QString>
#include <QList>
#include <QSet>
#include <AssetBuilderSDK/AssetBuilderBusses.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/containers/map.h>
#include <AzCore/std/containers/set.h>
#include <AzCore/Asset/AssetCommon.h>
#include <AzFramework/Asset/AssetRegistry.h>
#include <AzCore/Math/Crc.h>
#include <native/AssetManager/assetScanFolderInfo.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace AssetProcessor
{
const char* const DebugChannel = "Debug"; //Use this channel name if you want to write the message to the log file only.
const char* const ConsoleChannel = "AssetProcessor";// Use this channel name if you want to write the message to both the console and the log file.
const char* const FENCE_FILE_EXTENSION = "fence"; //fence file extension
const char* const AutoFailReasonKey = "failreason"; // the key to look in for auto-fail reason.
const char* const AutoFailLogFile = "faillogfile"; // if this is provided, this is a complete log of the failure and will be added after the failreason.
const char* const AutoFailOmitFromDatabaseKey = "failreason_omitFromDatabase"; // if set in your job info hash, your job will not be tracked by the database.
const char* const JobWarningKey = "ap_warningmessage"; // key used to store a warning message to be shown in the job log
const char* const PlaceHolderFileName = "$missing_dependency$"; // Used as a placeholder in the dependency system, such as when a source file is deleted and a previously met dependency is broken.
const unsigned int g_RetriesForFenceFile = 5; // number of retries for fencing
const int RetriesForJobNetworkError = 1; // number of times to retry a job when a network error is determined to have caused a job failure
// Even though AP can handle files with path length greater than window's legacy path length limit, we have some 3rdparty sdk's
// which do not handle this case ,therefore we will make AP fail any jobs whose either source file or output file name exceeds the windows legacy path length limit
#define AP_MAX_PATH_LEN 260
//! a shared convenience typedef for requests that have come over the network
//! The first element is the connection id it came from and the second element is the serial number
//! which can be used to send a response.
typedef QPair<quint32, quint32> NetworkRequestID;
//! a shared convenience typedef for Escalating Jobs
//! The first element is the jobRunKey of the job and the second element is the escalation
typedef QList<QPair<AZ::s64, int> > JobIdEscalationList;
//! A map which is used to keep absolute paths --> Database Paths of source files.
//! This is intentionally a map (not unordered_map) in order to ensure order is stable, and to eliminate duplicates.
typedef AZStd::map<AZStd::string, AZStd::string> SourceFilesForFingerprintingContainer;
enum AssetScanningStatus
{
Unknown,
Started,
InProgress,
Completed,
Stopped
};
//! This enum stores all the different job escalation values
enum JobEscalation
{
ProcessAssetRequestSyncEscalation = 200,
ProcessAssetRequestStatusEscalation = 150,
AssetJobRequestEscalation = 100,
Default = 0
};
//! This enum stores all the different asset processor status values
enum AssetProcessorStatus
{
Initializing_Gems,
Initializing_Builders,
Scanning_Started,
Analyzing_Jobs,
Processing_Jobs,
};
enum AssetCatalogStatus
{
RequiresSaving,
UpToDate
};
//! AssetProcessorStatusEntry stores all the necessary information related to AssetProcessorStatus
struct AssetProcessorStatusEntry
{
AssetProcessorStatus m_status;
unsigned int m_count = 0;
QString m_extraInfo; //this can be used to send any other info like name etc
explicit AssetProcessorStatusEntry(AssetProcessorStatus status, unsigned int count = 0, QString extraInfo = QString())
: m_status(status)
, m_count(count)
, m_extraInfo(extraInfo)
{
}
AssetProcessorStatusEntry() = default;
};
struct AssetRecognizer;
//! JobEntry is an internal structure that is used to uniquely identify a specific job and keeps track of it as it flows through the AP system
//! It prevents us from having to copy the entire of JobDetails, which is a very heavy structure.
//! In general, communication ABOUT jobs will have the JobEntry as the key
class JobEntry
{
public:
// note that QStrings are ref-counted copy-on-write, so a move operation will not be beneficial unless this struct gains considerable heap allocated fields.
QString m_databaseSourceName; //! DATABASE "SourceName" Column, which includes the 'output prefix' if present, used for keying
QString m_watchFolderPath; //! contains the absolute path to the watch folder that the file was found in.
QString m_pathRelativeToWatchFolder; //! contains the relative path (from the above watch folder) that the file was found in.
AZ::Uuid m_builderGuid = AZ::Uuid::CreateNull(); //! the builder that will perform the job
AssetBuilderSDK::PlatformInfo m_platformInfo;
AZ::Uuid m_sourceFileUUID = AZ::Uuid::CreateNull(); ///< The actual UUID of the source being processed
QString m_jobKey; // JobKey is used when a single input file, for a single platform, for a single builder outputs many separate jobs
AZ::u32 m_computedFingerprint = 0; // what the fingerprint was at the time of job creation.
qint64 m_computedFingerprintTimeStamp = 0; // stores the number of milliseconds since the universal coordinated time when the fingerprint was computed.
AZ::u64 m_jobRunKey = 0;
bool m_checkExclusiveLock = false; ///< indicates whether we need to check the input file for exclusive lock before we process this job
bool m_addToDatabase = true; ///< If false, this is just a UI job, and should not affect the database.
QString GetAbsoluteSourcePath() const
{
if (!m_watchFolderPath.isEmpty())
{
return m_watchFolderPath + "/" + m_pathRelativeToWatchFolder;
}
return m_pathRelativeToWatchFolder;
}
AZ::u32 GetHash() const
{
AZ::Crc32 crc(m_databaseSourceName.toUtf8().constData());
crc.Add(m_platformInfo.m_identifier.c_str());
crc.Add(m_jobKey.toUtf8().constData());
crc.Add(m_builderGuid.ToString<AZStd::string>().c_str());
return crc;
}
JobEntry() = default;
JobEntry(QString watchFolderPath, QString relativePathToFile, QString databaseSourceName, const AZ::Uuid& builderGuid, const AssetBuilderSDK::PlatformInfo& platformInfo, QString jobKey, AZ::u32 computedFingerprint, AZ::u64 jobRunKey, const AZ::Uuid &sourceUuid, bool addToDatabase = true)
: m_watchFolderPath(watchFolderPath)
, m_pathRelativeToWatchFolder(relativePathToFile)
, m_databaseSourceName(databaseSourceName)
, m_builderGuid(builderGuid)
, m_platformInfo(platformInfo)
, m_jobKey(jobKey)
, m_computedFingerprint(computedFingerprint)
, m_jobRunKey(jobRunKey)
, m_addToDatabase(addToDatabase)
, m_sourceFileUUID(sourceUuid)
{
}
};
//! This is an internal structure that hold all the information related to source file Dependency
struct SourceFileDependencyInternal
{
AZStd::string m_sourceWatchFolder; // this is the absolute path to the watch folder.
AZStd::string m_relativeSourcePath; // this is a pure relative path, not a database path
AZ::Uuid m_sourceUUID;
AZ::Uuid m_builderId;
AssetBuilderSDK::SourceFileDependency m_sourceFileDependency; // this is the raw data captured from the builder.
AZStd::string ToString() const
{
return AZStd::string::format(" %s %s %s", m_sourceUUID.ToString<AZStd::string>().c_str(), m_builderId.ToString<AZStd::string>().c_str(), m_relativeSourcePath.c_str());
}
};
//! JobDependencyInternal is an internal structure that is used to store job dependency related info
//! for later processing once we have resolved all the job dependency.
struct JobDependencyInternal
{
JobDependencyInternal(const AssetBuilderSDK::JobDependency& jobDependency)
:m_jobDependency(jobDependency)
{
}
AZStd::set<AZ::Uuid> m_builderUuidList;// ordered set because we have to use dependent jobs fingerprint in some sorted order.
AssetBuilderSDK::JobDependency m_jobDependency;
AZStd::string ToString() const
{
return AZStd::string::format("%s %s %s", m_jobDependency.m_sourceFile.m_sourceFileDependencyPath.c_str(), m_jobDependency.m_jobKey.c_str(), m_jobDependency.m_platformIdentifier.c_str());
}
};
//! JobDetails is an internal structure that is used to store job related information by the Asset Processor
//! Its heavy, since it contains the parameter map and the builder desc so is expensive to copy and in general only used to create jobs
//! After which, the Job Entry is used to track and identify jobs.
class JobDetails
{
public:
JobEntry m_jobEntry;
AZStd::string m_extraInformationForFingerprinting;
const ScanFolderInfo* m_scanFolder; // the scan folder info the file was found in
QString m_destinationPath; // the final folder that will be where your products are placed if you give relative path names
// destinationPath will be a cache folder. If you tell it to emit something like "blah.dds"
// it will put it in (destinationPath)/blah.dds for example
AZStd::vector<JobDependencyInternal> m_jobDependencyList;
// which files to include in the fingerprinting. (Not including job dependencies)
SourceFilesForFingerprintingContainer m_fingerprintFiles;
bool m_critical = false;
int m_priority = -1;
// indicates whether we need to check the server first for the outputs of this job
// before we start processing locally
bool m_checkServer = false;
// Indicates whether this job needs to be processed irrespective of whether its fingerprint got modified or not.
bool m_autoProcessJob = false;
AssetBuilderSDK::AssetBuilderDesc m_assetBuilderDesc;
AssetBuilderSDK::JobParameterMap m_jobParam;
// autoFail makes jobs which are added to the list and will automatically fail, and are used
// to make sure that a "failure" shows up on the list so that the user can click to inspect the job and see why
// it has failed instead of having a job fail mysteriously or be hard to find out why.
// it is currently the only way for the job to be marked as a failure because of data integrity reasons after the builder
// has already succeeded in actually making the asset data.
// if you set a job to "auto fail" it will check the m_jobParam map for a AZ_CRC(AutoFailReasonKey) and use that, if present, for fail information
bool m_autoFail = false;
AZStd::string ToString() const
{
return QString("%1 %2 %3").arg(m_jobEntry.m_databaseSourceName, m_jobEntry.m_platformInfo.m_identifier.c_str(), m_jobEntry.m_jobKey).toUtf8().data();
}
bool operator==(const JobDetails& rhs) const
{
return ((m_jobEntry.m_databaseSourceName == rhs.m_jobEntry.m_databaseSourceName) &&
(m_jobEntry.m_platformInfo.m_identifier == rhs.m_jobEntry.m_platformInfo.m_identifier) &&
(m_jobEntry.m_jobKey == rhs.m_jobEntry.m_jobKey) &&
m_jobEntry.m_builderGuid == rhs.m_jobEntry.m_builderGuid);
}
JobDetails() = default;
};
//! JobDesc struct is used for identifying jobs that need to be processed again
//! because of job dependency declared on them by other jobs
struct JobDesc
{
AZStd::string m_databaseSourceName;
AZStd::string m_jobKey;
AZStd::string m_platformIdentifier;
bool operator==(const JobDesc& rhs) const
{
return AzFramework::StringFunc::Equal(m_databaseSourceName.c_str(), rhs.m_databaseSourceName.c_str())
&& m_platformIdentifier == rhs.m_platformIdentifier
&& m_jobKey == rhs.m_jobKey;
}
JobDesc(const AZStd::string& databaseSourceName, const AZStd::string& jobKey, const AZStd::string& platformIdentifier)
: m_databaseSourceName(databaseSourceName)
, m_jobKey(jobKey)
, m_platformIdentifier(platformIdentifier)
{
}
AZStd::string ToString() const
{
AZStd::string lowerSourceName = m_databaseSourceName;
AZStd::to_lower(lowerSourceName.begin(), lowerSourceName.end());
return AZStd::string::format("%s %s %s", lowerSourceName.c_str(), m_platformIdentifier.c_str(), m_jobKey.c_str());
}
};
//! JobIndentifier is an internal structure that store all the data that can uniquely identify a job
struct JobIndentifier
{
JobDesc m_jobDesc;
AZ::Uuid m_builderUuid = AZ::Uuid::CreateNull();
bool operator==(const JobIndentifier& rhs) const
{
return (m_jobDesc == rhs.m_jobDesc) && (m_builderUuid == rhs.m_builderUuid);
}
JobIndentifier(const JobDesc& jobDesc, const AZ::Uuid builderUuid)
: m_jobDesc(jobDesc)
, m_builderUuid(builderUuid)
{
}
};
} // namespace AssetProcessor
namespace AZStd
{
template<>
struct hash<AssetProcessor::JobDetails>
{
using argument_type = AssetProcessor::JobDetails;
using result_type = size_t;
result_type operator() (const argument_type& jobDetails) const
{
size_t h = 0;
hash_combine(h, jobDetails.ToString());
hash_combine(h, jobDetails.m_jobEntry.m_builderGuid);
return h;
}
};
template<>
struct hash<AssetProcessor::JobDesc>
{
using argument_type = AssetProcessor::JobDesc;
using result_type = size_t;
result_type operator() (const argument_type& jobDesc) const
{
size_t h = 0;
hash_combine(h, jobDesc.ToString());
return h;
}
};
template<>
struct hash<AssetProcessor::JobIndentifier>
{
using argument_type = AssetProcessor::JobIndentifier;
using result_type = size_t;
result_type operator() (const argument_type& jobIndentifier) const
{
size_t h = 0;
hash_combine(h, jobIndentifier.m_jobDesc);
hash_combine(h, jobIndentifier.m_builderUuid);
return h;
}
};
}
Q_DECLARE_METATYPE(AssetBuilderSDK::ProcessJobResponse)
Q_DECLARE_METATYPE(AssetProcessor::JobEntry)
Q_DECLARE_METATYPE(AssetProcessor::AssetProcessorStatusEntry)
Q_DECLARE_METATYPE(AssetProcessor::JobDetails)
Q_DECLARE_METATYPE(AssetProcessor::NetworkRequestID)
Q_DECLARE_METATYPE(AssetProcessor::AssetScanningStatus)
Q_DECLARE_METATYPE(AssetProcessor::AssetCatalogStatus)
@@ -0,0 +1,921 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "connection.h"
#include "native/connection/connectionworker.h"
#include "native/utilities/ByteArrayStream.h"
#include <QSettings>
Connection::Connection(qintptr socketDescriptor, QObject* parent)
: Connection(false, socketDescriptor, parent)
{
}
Connection::Connection(bool isUserCreatedConnection, qintptr socketDescriptor, QObject* parent)
: QObject(parent)
, m_userCreatedConnection(isUserCreatedConnection)
{
m_runElapsed = true;
//metrics
m_numOpenRequests = 0;
m_numCloseRequests = 0;
m_numOpened = 0;
m_numClosed = 0;
m_numReadRequests = 0;
m_numWriteRequests = 0;
m_numTellRequests = 0;
m_numSeekRequests = 0;
m_numEofRequests = 0;
m_numIsReadOnlyRequests = 0;
m_numIsDirectoryRequests = 0;
m_numSizeRequests = 0;
m_numModificationTimeRequests = 0;
m_numExistsRequests = 0;
m_numFlushRequests = 0;
m_numCreatePathRequests = 0;
m_numDestroyPathRequests = 0;
m_numRemoveRequests = 0;
m_numCopyRequests = 0;
m_numRenameRequests = 0;
m_numFindFileNamesRequests = 0;
m_bytesRead = 0;
m_bytesWritten = 0;
m_bytesSent = 0;
m_bytesReceived = 0;
m_numOpenFiles = 0;
//connection
m_identifier = "";//empty
m_ipAddress = "127.0.0.1";// default is loopback address
m_port = 22229;//default port number
m_status = Disconnected;//default status
m_autoConnect = false;//default status
m_connectionId = 0; //default
m_connectionWorker = new AssetProcessor::ConnectionWorker(socketDescriptor);
m_connectionWorker->moveToThread(&m_connectionWorkerThread);
m_connectionWorker->GetSocket().moveToThread(&m_connectionWorkerThread);
connect(this, &Connection::TerminateConnection, m_connectionWorker, &AssetProcessor::ConnectionWorker::RequestTerminate, Qt::DirectConnection);
connect(this, &Connection::NormalConnectionRequested, m_connectionWorker, &AssetProcessor::ConnectionWorker::ConnectToEngine);
connect(m_connectionWorker, &AssetProcessor::ConnectionWorker::Identifier, this, [this](QString identifier) {
// For user created connections, the id is user generated (either because they've manually entered some text
// this session, or because the id was loaded from a session previously saved where the user entered it).
// As such, when the connection worker reports a new id from after the connection occurs,
// we only pay attention to it when it is not a user created connection.
if (!m_userCreatedConnection)
{
SetIdentifier(identifier);
}
});
connect(m_connectionWorker, &AssetProcessor::ConnectionWorker::AssetPlatformsString, this, &Connection::SetAssetPlatformsString);
connect(m_connectionWorker, &AssetProcessor::ConnectionWorker::ConnectionDisconnected, this, &Connection::OnConnectionDisconnect, Qt::QueuedConnection);
// the blocking queued connection is here because the worker calls OnConnectionEstablished and then immediately starts emitting messages about
// data coming in. We want to immediately establish connectivity this way and we don't want it to proceed with message delivery until then.
connect(m_connectionWorker, &AssetProcessor::ConnectionWorker::ConnectionEstablished, this, &Connection::OnConnectionEstablished, Qt::BlockingQueuedConnection);
connect(m_connectionWorker, &AssetProcessor::ConnectionWorker::ErrorMessage, this, &Connection::ErrorMessage);
connect(m_connectionWorker, &AssetProcessor::ConnectionWorker::IsAddressWhiteListed, this, &Connection::IsAddressWhiteListed);
connect(this, &Connection::AddressIsWhiteListed, m_connectionWorker, &AssetProcessor::ConnectionWorker::AddressIsWhiteListed);
}
void Connection::Activate(qintptr socketDescriptor)
{
m_connectionWorkerThread.setObjectName("Connection Worker Thread");
m_connectionWorkerThread.start();
//if socketDescriptor is positive it means that it is an incoming connection
if (socketDescriptor >= 0)
{
SetStatus(Connecting);
// by invoking the ConnectSocket, we cause it to occur in the worker's thread
QMetaObject::invokeMethod(m_connectionWorker, "ConnectSocket", Q_ARG(qintptr, socketDescriptor));
}
}
Connection::~Connection()
{
Q_ASSERT(!m_connectionWorkerThread.isRunning());
Q_EMIT ConnectionDestroyed(m_connectionId);
}
QString Connection::Identifier() const
{
return m_identifier;
}
void Connection::SetIdentifier(QString identifier)
{
if (m_identifier == identifier)
{
return;
}
m_identifier = identifier;
Q_EMIT IdentifierChanged();
Q_EMIT DisplayNameChanged(); // regardless of whether the identifier is empty or not, this always affects the display name.
}
QString Connection::IpAddress() const
{
return m_ipAddress;
}
QStringList Connection::AssetPlatforms() const
{
return m_assetPlatforms;
}
QString Connection::AssetPlatformsString() const
{
return m_assetPlatforms.join(',');
}
void Connection::SetAssetPlatforms(QStringList assetPlatforms)
{
if (m_assetPlatforms == assetPlatforms)
{
return;
}
m_assetPlatforms = assetPlatforms;
Q_EMIT AssetPlatformChanged();
}
QString Connection::DisplayName() const
{
if (m_identifier.isEmpty())
{
return m_ipAddress;
}
return m_identifier;
}
QString Connection::Elapsed() const
{
return m_elapsedDisplay;
}
void Connection::SetIpAddress(QString ipAddress)
{
if (Status() == Connected)
{
AZ_Warning(AssetProcessor::ConsoleChannel, Status() == Connected, "You are not allowed to change the ip address of a connected connection.\n");
return;
}
if (ipAddress == m_ipAddress)
{
return;
}
m_ipAddress = ipAddress;
Q_EMIT IpAddressChanged();
if (m_identifier.isEmpty()) // if the identifier is empty, then the display name is the ip address
{
Q_EMIT DisplayNameChanged();
}
}
int Connection::Port() const
{
return m_port;
}
void Connection::SetPort(int port)
{
if (Status() == Connected)
{
AZ_Warning(AssetProcessor::ConsoleChannel, Status() == Connected, "You are not allowed to change the port of a connected connection.\n");
return;
}
if (port == m_port)
{
return;
}
m_port = aznumeric_cast<quint16>(port);
Q_EMIT PortChanged();
}
Connection::ConnectionStatus Connection::Status() const
{
return m_status;
}
void Connection::SaveConnection(QSettings& qSettings)
{
qSettings.setValue("identifier", Identifier());
qSettings.setValue("ipAddress", IpAddress());
qSettings.setValue("port", Port());
qSettings.setValue("assetplatform", AssetPlatforms());
qSettings.setValue("autoConnect", AutoConnect());
qSettings.setValue("userConnection", m_userCreatedConnection);
}
void Connection::LoadConnection(QSettings& qSettings)
{
SetIdentifier(qSettings.value("identifier").toString());
SetIpAddress(qSettings.value("ipAddress").toString());
SetPort(qSettings.value("port").toInt());
SetAssetPlatformsString(qSettings.value("assetplatform").toString());
SetAutoConnect(qSettings.value("autoConnect").toBool());
SetStatus(Disconnected);
m_userCreatedConnection = qSettings.value("userConnection", false).toBool();
}
void Connection::SetStatus(Connection::ConnectionStatus status)
{
if (status == m_status)
{
return;
}
m_status = status;
Q_EMIT StatusChanged(m_connectionId);
if (status == Connection::Connected)
{
AssetProcessor::ConnectionBus::Handler::BusConnect(m_connectionId);
}
else if (status == Connection::Disconnected)
{
AssetProcessor::ConnectionBus::Handler::BusDisconnect();
}
}
bool Connection::AutoConnect() const
{
return m_autoConnect;
}
void Connection::Connect()
{
m_queuedReconnect = false;
if (!m_connectionWorker)
{
// this can happen if you queued a connect but in the interim, we were deleteLater'd due to removal.
return;
}
m_connectionWorker->Reset();
Q_EMIT NormalConnectionRequested(m_ipAddress, m_port);
}
void Connection::Disconnect()
{
Q_EMIT DisconnectConnection(m_connectionId);
}
void Connection::Terminate()
{
Q_EMIT TerminateConnection();
if (m_connectionWorkerThread.isRunning())
{
m_connectionWorkerThread.quit();
m_connectionWorkerThread.wait();
}
deleteLater();
}
void Connection::SetAutoConnect(bool autoConnect)
{
if (autoConnect == m_autoConnect)
{
return;
}
m_autoConnect = autoConnect;
if (m_autoConnect)
{
SetStatus(Connecting);
Connect();
}
else
{
SetStatus(Disconnected);
Disconnect();
}
Q_EMIT AutoConnectChanged();
}
void Connection::OnConnectionDisconnect()
{
if (m_connectionWorker)
{
disconnect(this, &Connection::SendMessage, m_connectionWorker, &AssetProcessor::ConnectionWorker::SendMessage);
disconnect(m_connectionWorker, &AssetProcessor::ConnectionWorker::ReceiveMessage, this, &Connection::ReceiveMessage);
}
// For user created connections, the id is user generated (either because they've manually entered some text
// this session, or because the id was loaded from a session previously saved where the user entered it).
// As such, when a connection disconnects, we only want to clear the id when the connection was triggered
// from something other than the user (i.e. like when an automatic connection from Editor or a job worker
// disconnects).
if (!m_userCreatedConnection)
{
SetIdentifier(QString());
}
SetAssetPlatforms(QStringList());
if (m_autoConnect)
{
if (!m_queuedReconnect)
{
m_queuedReconnect = true;
SetStatus(Connecting);
QTimer::singleShot(500, this, SLOT(Connect()));
}
}
else
{
Disconnect();
SetStatus(Disconnected);
SetAssetPlatforms(QStringList());
// if we did not initiate the connection, we should erase it when it disappears.
if (!InitiatedConnection())
{
Terminate();
}
}
}
void Connection::OnConnectionEstablished(QString ipAddress, quint16 port)
{
connect(this, &Connection::SendMessage, m_connectionWorker, &AssetProcessor::ConnectionWorker::SendMessage, Qt::UniqueConnection);
connect(m_connectionWorker, &AssetProcessor::ConnectionWorker::ReceiveMessage, this, &Connection::ReceiveMessage, Qt::UniqueConnection);
m_elapsed = 0;
m_elapsedTimer.start();
m_runElapsed = true;
UpdateElapsed();
SetIpAddress(ipAddress);
SetPort(port);
SetStatus(Connected);
}
void Connection::ReceiveMessage(unsigned int type, unsigned int serial, QByteArray payload)
{
Q_EMIT DeliverMessage(m_connectionId, type, serial, payload);
}
void Connection::ErrorMessage(QString errorString)
{
Q_EMIT Error(m_connectionId, errorString);
}
void Connection::UpdateElapsed()
{
if (m_runElapsed)
{
m_elapsed += m_elapsedTimer.restart();
int seconds = aznumeric_cast<int>(m_elapsed / 1000);
int hours = seconds / (60 * 60);
seconds -= hours * (60 * 60);
int minutes = seconds / 60;
seconds -= minutes * 60;
m_elapsedDisplay.clear();
if (hours < 10)
{
m_elapsedDisplay = "0";
}
m_elapsedDisplay += QString::number(hours) + ":";
if (minutes < 10)
{
m_elapsedDisplay += "0";
}
m_elapsedDisplay += QString::number(minutes) + ":";
if (seconds < 10)
{
m_elapsedDisplay += "0";
}
m_elapsedDisplay += QString::number(seconds);
Q_EMIT ElapsedChanged();
QTimer::singleShot(1000, this, SLOT(UpdateElapsed()));
}
}
unsigned int Connection::ConnectionId() const
{
return m_connectionId;
}
void Connection::SetConnectionId(unsigned int connectionId)
{
m_connectionId = connectionId;
}
void Connection::SendMessageToWorker(unsigned int type, unsigned int serial, QByteArray payload)
{
Q_EMIT SendMessage(type, serial, payload);
}
void Connection::AddBytesReceived(qint64 add, bool update)
{
m_bytesReceived += add;
if (update)
{
Q_EMIT BytesReceivedChanged();
}
}
void Connection::AddBytesSent(qint64 add, bool update)
{
m_bytesSent += add;
if (update)
{
Q_EMIT BytesSentChanged();
}
}
void Connection::AddBytesRead(qint64 add, bool update)
{
m_bytesRead += add;
if (update)
{
Q_EMIT BytesReadChanged();
}
}
void Connection::AddBytesWritten(qint64 add, bool update)
{
m_bytesWritten += add;
if (update)
{
Q_EMIT BytesWrittenChanged();
}
}
void Connection::AddOpenRequest(bool update)
{
m_numOpenRequests++;
if (update)
{
Q_EMIT NumOpenRequestsChanged();
}
}
void Connection::AddCloseRequest(bool update)
{
m_numCloseRequests++;
if (update)
{
Q_EMIT NumCloseRequestsChanged();
}
}
void Connection::AddOpened(bool update)
{
m_numOpened++;
m_numOpenFiles = m_numOpened - m_numClosed;
if (update)
{
Q_EMIT NumOpenedChanged();
Q_EMIT NumOpenFilesChanged();
}
}
void Connection::AddClosed(bool update)
{
m_numClosed++;
m_numOpenFiles = m_numOpened - m_numClosed;
if (update)
{
Q_EMIT NumClosedChanged();
Q_EMIT NumOpenFilesChanged();
}
}
void Connection::AddReadRequest(bool update)
{
m_numReadRequests++;
if (update)
{
Q_EMIT NumReadRequestsChanged();
}
}
void Connection::AddWriteRequest(bool update)
{
m_numWriteRequests++;
if (update)
{
Q_EMIT NumWriteRequestsChanged();
}
}
void Connection::AddTellRequest(bool update)
{
m_numTellRequests++;
if (update)
{
Q_EMIT NumTellRequestsChanged();
}
}
void Connection::AddSeekRequest(bool update)
{
m_numSeekRequests++;
if (update)
{
Q_EMIT NumSeekRequestsChanged();
}
}
void Connection::AddEofRequest(bool update)
{
m_numEofRequests++;
if (update)
{
Q_EMIT NumEofRequestsChanged();
}
}
void Connection::AddIsReadOnlyRequest(bool update)
{
m_numIsReadOnlyRequests++;
if (update)
{
Q_EMIT NumIsReadOnlyRequestsChanged();
}
}
void Connection::AddIsDirectoryRequest(bool update)
{
m_numIsDirectoryRequests++;
if (update)
{
Q_EMIT NumIsDirectoryRequestsChanged();
}
}
void Connection::AddSizeRequest(bool update)
{
m_numSizeRequests++;
if (update)
{
Q_EMIT NumSizeRequestsChanged();
}
}
void Connection::AddModificationTimeRequest(bool update)
{
m_numModificationTimeRequests++;
if (update)
{
Q_EMIT NumModificationTimeRequestsChanged();
}
}
void Connection::AddExistsRequest(bool update)
{
m_numExistsRequests++;
if (update)
{
Q_EMIT NumExistsRequestsChanged();
}
}
void Connection::AddFlushRequest(bool update)
{
m_numFlushRequests++;
if (update)
{
Q_EMIT NumFlushRequestsChanged();
}
}
void Connection::AddCreatePathRequest(bool update)
{
m_numCreatePathRequests++;
if (update)
{
Q_EMIT NumCreatePathRequestsChanged();
}
}
void Connection::AddDestroyPathRequest(bool update)
{
m_numDestroyPathRequests++;
if (update)
{
Q_EMIT NumDestroyPathRequestsChanged();
}
}
void Connection::AddRemoveRequest(bool update)
{
m_numRemoveRequests++;
if (update)
{
Q_EMIT NumRemoveRequestsChanged();
}
}
void Connection::AddCopyRequest(bool update)
{
m_numCopyRequests++;
if (update)
{
Q_EMIT NumCopyRequestsChanged();
}
}
void Connection::AddRenameRequest(bool update)
{
m_numRenameRequests++;
if (update)
{
Q_EMIT NumRenameRequestsChanged();
}
}
void Connection::AddFindFileNamesRequest(bool update)
{
m_numFindFileNamesRequests++;
if (update)
{
Q_EMIT NumFindFileNamesRequestsChanged();
}
}
void Connection::UpdateBytesReceived()
{
Q_EMIT BytesReceivedChanged();
}
void Connection::UpdateBytesSent()
{
Q_EMIT BytesSentChanged();
}
void Connection::UpdateBytesRead()
{
Q_EMIT BytesReadChanged();
}
void Connection::UpdateBytesWritten()
{
Q_EMIT BytesWrittenChanged();
}
void Connection::UpdateOpenRequest()
{
Q_EMIT NumOpenRequestsChanged();
}
void Connection::UpdateCloseRequest()
{
Q_EMIT NumCloseRequestsChanged();
}
void Connection::UpdateOpened()
{
Q_EMIT NumOpenedChanged();
}
void Connection::UpdateClosed()
{
Q_EMIT NumClosedChanged();
}
void Connection::UpdateReadRequest()
{
Q_EMIT NumReadRequestsChanged();
}
void Connection::UpdateWriteRequest()
{
Q_EMIT NumWriteRequestsChanged();
}
void Connection::UpdateTellRequest()
{
Q_EMIT NumTellRequestsChanged();
}
void Connection::UpdateSeekRequest()
{
Q_EMIT NumSeekRequestsChanged();
}
void Connection::UpdateEofRequest()
{
Q_EMIT NumEofRequestsChanged();
}
void Connection::UpdateIsReadOnlyRequest()
{
Q_EMIT NumIsReadOnlyRequestsChanged();
}
void Connection::UpdateIsDirectoryRequest()
{
Q_EMIT NumIsDirectoryRequestsChanged();
}
void Connection::UpdateSizeRequest()
{
Q_EMIT NumSizeRequestsChanged();
}
void Connection::UpdateModificationTimeRequest()
{
Q_EMIT NumModificationTimeRequestsChanged();
}
void Connection::UpdateExistsRequest()
{
Q_EMIT NumExistsRequestsChanged();
}
void Connection::UpdateFlushRequest()
{
Q_EMIT NumFlushRequestsChanged();
}
void Connection::UpdateCreatePathRequest()
{
Q_EMIT NumCreatePathRequestsChanged();
}
void Connection::UpdateDestroyPathRequest()
{
Q_EMIT NumDestroyPathRequestsChanged();
}
void Connection::UpdateRemoveRequest()
{
Q_EMIT NumRemoveRequestsChanged();
}
void Connection::UpdateCopyRequest()
{
Q_EMIT NumCopyRequestsChanged();
}
void Connection::UpdateRenameRequest()
{
Q_EMIT NumRenameRequestsChanged();
}
void Connection::UpdateFindFileNamesRequest()
{
Q_EMIT NumFindFileNamesRequestsChanged();
}
void Connection::UpdateMetrics()
{
UpdateBytesReceived();
UpdateBytesSent();
UpdateBytesRead();
UpdateBytesWritten();
UpdateOpenRequest();
UpdateCloseRequest();
UpdateOpened();
UpdateClosed();
UpdateReadRequest();
UpdateWriteRequest();
UpdateTellRequest();
UpdateSeekRequest();
UpdateEofRequest();
UpdateIsReadOnlyRequest();
UpdateIsDirectoryRequest();
UpdateSizeRequest();
UpdateModificationTimeRequest();
UpdateExistsRequest();
UpdateFlushRequest();
UpdateCreatePathRequest();
UpdateDestroyPathRequest();
UpdateRemoveRequest();
UpdateCopyRequest();
UpdateRenameRequest();
UpdateFindFileNamesRequest();
}
size_t Connection::Send(unsigned int serial, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message)
{
QByteArray buffer;
bool wroteToStream = AssetProcessor::PackMessage(message, buffer);
AZ_Assert(wroteToStream, "Connection::Send: Could not serialize to stream (type=%u)", message.GetMessageType());
if (wroteToStream)
{
return SendRaw(message.GetMessageType(), serial, buffer);
}
return 0;
}
size_t Connection::SendRaw(unsigned int type, unsigned int serial, const QByteArray& data)
{
SendMessageToWorker(type, serial, data);
return data.size();
}
size_t Connection::SendPerPlatform(unsigned int serial, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message, const QString& platform)
{
if (m_assetPlatforms.contains(platform, Qt::CaseInsensitive))
{
return Send(serial, message);
}
return 0;
}
size_t Connection::SendRawPerPlatform(unsigned int type, unsigned int serial, const QByteArray& data, const QString& platform)
{
if (m_assetPlatforms.contains(platform, Qt::CaseInsensitive))
{
return SendRaw(type, serial, data);
}
return 0;
}
AZ::u32 Connection::GetNextSerial()
{
static AZStd::atomic_uint serial(AzFramework::AssetSystem::DEFAULT_SERIAL);
AZ::u32 nextSerial = ++serial;
// Avoid special-case serials
return (nextSerial & AzFramework::AssetSystem::RESPONSE_SERIAL_FLAG
|| nextSerial == AzFramework::AssetSystem::DEFAULT_SERIAL
|| nextSerial == AzFramework::AssetSystem::NEGOTIATION_SERIAL)
? GetNextSerial() // re-roll, we picked a special serial
: nextSerial;
}
unsigned int Connection::SendRequest(const AzFramework::AssetSystem::BaseAssetProcessorMessage& message, const AssetProcessor::ConnectionBusTraits::ResponseCallback& callback)
{
AZ::u32 serial = GetNextSerial();
{
AZStd::lock_guard<AZStd::mutex> lock(m_responseHandlerMutex);
m_responseHandlerMap.insert({ serial, callback });
}
Send(serial, message);
return serial;
}
size_t Connection::SendResponse(unsigned int serial, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message)
{
serial |= AzFramework::AssetSystem::RESPONSE_SERIAL_FLAG; // Set top bit to indicate this is a response
return Send(serial, message);
}
void Connection::InvokeResponseHandler(AZ::u32 serial, AZ::u32 type, QByteArray data)
{
AZStd::lock_guard<AZStd::mutex> lock(m_responseHandlerMutex);
auto itr = m_responseHandlerMap.find(serial);
if (itr != m_responseHandlerMap.end())
{
itr->second(type, data);
m_responseHandlerMap.erase(itr);
}
}
void Connection::RemoveResponseHandler(unsigned int serial)
{
AZStd::lock_guard<AZStd::mutex> lock(m_responseHandlerMutex);
m_responseHandlerMap.erase(serial);
}
bool Connection::InitiatedConnection() const
{
if (m_connectionWorker)
{
return m_connectionWorker->InitiatedConnection();
}
return false;
}
bool Connection::UserCreatedConnection() const
{
return m_userCreatedConnection;
}
void Connection::SetAssetPlatformsString(QString assetPlatforms)
{
SetAssetPlatforms(assetPlatforms.split(',', Qt::SkipEmptyParts));
}
@@ -0,0 +1,306 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef CONNECTION_H
#define CONNECTION_H
#if !defined(Q_MOC_RUN)
#include <QThread>
#include <QElapsedTimer>
#include "native/utilities/AssetUtilEBusHelper.h"
#include <QHostAddress>
#include <QTimer>
#include <QString>
#include <QPointer>
#endif
class QSettings;
namespace AssetProcessor
{
class ConnectionWorker;
class PlatformConfiguration;
}
#undef SendMessage
/** This Class contains all the information related to a single connecton
*/
class Connection
: public QObject
, public AssetProcessor::ConnectionBus::Handler
{
Q_OBJECT
Q_PROPERTY(QString identifier READ Identifier WRITE SetIdentifier NOTIFY IdentifierChanged)
Q_PROPERTY(QString ipAddress READ IpAddress WRITE SetIpAddress NOTIFY IpAddressChanged)
Q_PROPERTY(int port READ Port WRITE SetPort NOTIFY PortChanged)
Q_PROPERTY(ConnectionStatus status READ Status NOTIFY StatusChanged)
Q_PROPERTY(QStringList assetPlatform READ AssetPlatforms WRITE SetAssetPlatforms NOTIFY AssetPlatformChanged)
Q_PROPERTY(QString assetPlatformsString READ AssetPlatformsString WRITE SetAssetPlatformsString)
Q_PROPERTY(bool autoConnect READ AutoConnect WRITE SetAutoConnect NOTIFY AutoConnectChanged)
Q_PROPERTY(QString displayName READ DisplayName NOTIFY DisplayNameChanged)
Q_PROPERTY(QString elapsed READ Elapsed NOTIFY ElapsedChanged)
//metrics
Q_PROPERTY(qint64 numOpenRequests MEMBER m_numOpenRequests NOTIFY NumOpenRequestsChanged)
Q_PROPERTY(qint64 numCloseRequests MEMBER m_numCloseRequests NOTIFY NumCloseRequestsChanged)
Q_PROPERTY(qint64 numOpened MEMBER m_numOpened NOTIFY NumOpenedChanged)
Q_PROPERTY(qint64 numClosed MEMBER m_numClosed NOTIFY NumClosedChanged)
Q_PROPERTY(qint64 numReadRequests MEMBER m_numReadRequests NOTIFY NumReadRequestsChanged)
Q_PROPERTY(qint64 numWriteRequests MEMBER m_numWriteRequests NOTIFY NumWriteRequestsChanged)
Q_PROPERTY(qint64 numSeekRequests MEMBER m_numSeekRequests NOTIFY NumSeekRequestsChanged)
Q_PROPERTY(qint64 numTellRequests MEMBER m_numTellRequests NOTIFY NumTellRequestsChanged)
Q_PROPERTY(qint64 numEofRequests MEMBER m_numEofRequests NOTIFY NumEofRequestsChanged)
Q_PROPERTY(qint64 numIsReadOnlyRequests MEMBER m_numIsReadOnlyRequests NOTIFY NumIsReadOnlyRequestsChanged)
Q_PROPERTY(qint64 numIsDirectoryRequests MEMBER m_numIsDirectoryRequests NOTIFY NumIsDirectoryRequestsChanged)
Q_PROPERTY(qint64 numSizeRequests MEMBER m_numSizeRequests NOTIFY NumSizeRequestsChanged)
Q_PROPERTY(qint64 numModificationTimeRequests MEMBER m_numModificationTimeRequests NOTIFY NumModificationTimeRequestsChanged)
Q_PROPERTY(qint64 numExistsRequests MEMBER m_numExistsRequests NOTIFY NumExistsRequestsChanged)
Q_PROPERTY(qint64 numFlushRequests MEMBER m_numFlushRequests NOTIFY NumFlushRequestsChanged)
Q_PROPERTY(qint64 numCreatePathRequests MEMBER m_numCreatePathRequests NOTIFY NumCreatePathRequestsChanged)
Q_PROPERTY(qint64 numDestroyPathRequests MEMBER m_numDestroyPathRequests NOTIFY NumDestroyPathRequestsChanged)
Q_PROPERTY(qint64 numRemoveRequests MEMBER m_numRemoveRequests NOTIFY NumRemoveRequestsChanged)
Q_PROPERTY(qint64 numCopyRequests MEMBER m_numCopyRequests NOTIFY NumCopyRequestsChanged)
Q_PROPERTY(qint64 numRenameRequests MEMBER m_numRenameRequests NOTIFY NumRenameRequestsChanged)
Q_PROPERTY(qint64 numFindFileNamesRequests MEMBER m_numFindFileNamesRequests NOTIFY NumFindFileNamesRequestsChanged)
Q_PROPERTY(qint64 bytesRead MEMBER m_bytesRead NOTIFY BytesReadChanged)
Q_PROPERTY(qint64 bytesWritten MEMBER m_bytesWritten NOTIFY BytesWrittenChanged)
Q_PROPERTY(qint64 bytesSent MEMBER m_bytesSent NOTIFY BytesSentChanged)
Q_PROPERTY(qint64 bytesReceived MEMBER m_bytesReceived NOTIFY BytesReceivedChanged)
Q_PROPERTY(qint64 numOpenFiles MEMBER m_numOpenFiles NOTIFY NumOpenFilesChanged)
public:
explicit Connection(qintptr socketDescriptor = -1, QObject* parent = 0);
explicit Connection(bool isUserCreatedConnection, qintptr socketDescriptor = -1, QObject* parent = 0);
virtual ~Connection();
enum ConnectionStatus
{
Disconnected, Connected, Connecting
};
Q_ENUMS(ConnectionStatus)
void Activate(qintptr socketDescriptor);
QString Identifier() const;
QString IpAddress() const;
int Port() const;
ConnectionStatus Status() const;
QStringList AssetPlatforms() const;
QString AssetPlatformsString() const;
void SaveConnection(QSettings& qSettings);
void LoadConnection(QSettings& qSettings);
bool AutoConnect() const;
QString DisplayName() const;
QString Elapsed() const;
bool InitiatedConnection() const;
bool UserCreatedConnection() const;
void Disconnect();
unsigned int ConnectionId() const;
void SetConnectionId(unsigned int ConnectionId);
void Terminate();
void SendMessageToWorker(unsigned int type, unsigned int serial, QByteArray payload);
void AddBytesReceived(qint64 add, bool update);
void AddBytesSent(qint64 add, bool update);
void AddBytesRead(qint64 add, bool update);
void AddBytesWritten(qint64 add, bool update);
void AddOpenRequest(bool update);
void AddCloseRequest(bool update);
void AddOpened(bool update);
void AddClosed(bool update);
void AddReadRequest(bool update);
void AddWriteRequest(bool update);
void AddTellRequest(bool update);
void AddSeekRequest(bool update);
void AddEofRequest(bool update);
void AddIsReadOnlyRequest(bool update);
void AddIsDirectoryRequest(bool update);
void AddSizeRequest(bool update);
void AddModificationTimeRequest(bool update);
void AddExistsRequest(bool update);
void AddFlushRequest(bool update);
void AddCreatePathRequest(bool update);
void AddDestroyPathRequest(bool update);
void AddRemoveRequest(bool update);
void AddCopyRequest(bool update);
void AddRenameRequest(bool update);
void AddFindFileNamesRequest(bool update);
void UpdateBytesReceived();
void UpdateBytesSent();
void UpdateBytesRead();
void UpdateBytesWritten();
void UpdateOpenRequest();
void UpdateCloseRequest();
void UpdateOpened();
void UpdateClosed();
void UpdateReadRequest();
void UpdateWriteRequest();
void UpdateTellRequest();
void UpdateSeekRequest();
void UpdateEofRequest();
void UpdateIsReadOnlyRequest();
void UpdateIsDirectoryRequest();
void UpdateSizeRequest();
void UpdateModificationTimeRequest();
void UpdateExistsRequest();
void UpdateFlushRequest();
void UpdateCreatePathRequest();
void UpdateDestroyPathRequest();
void UpdateRemoveRequest();
void UpdateCopyRequest();
void UpdateRenameRequest();
void UpdateFindFileNamesRequest();
void UpdateMetrics();
// AssetProcessor::ConnectionBus interface
size_t Send(unsigned int serial, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message) override;
size_t SendRaw(unsigned int type, unsigned int serial, const QByteArray& data) override;
size_t SendPerPlatform(unsigned int serial, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message, const QString& platform) override;
size_t SendRawPerPlatform(unsigned int type, unsigned int serial, const QByteArray& data, const QString& platform) override;
//! callback runs on the main thread, be sure to keep the work to an absolute minimum
unsigned int SendRequest(const AzFramework::AssetSystem::BaseAssetProcessorMessage& message, const AssetProcessor::ConnectionBusTraits::ResponseCallback& callback) override;
size_t SendResponse(unsigned int serial, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message) override;
void RemoveResponseHandler(unsigned int serial) override;
void InvokeResponseHandler(AZ::u32 serial, AZ::u32 type, QByteArray data);
Q_SIGNALS:
void IdentifierChanged();
void IpAddressChanged();
void PortChanged();
void StatusChanged(unsigned int connId);
void AssetPlatformChanged();
void AutoConnectChanged();
void DisplayNameChanged();
void ElapsedChanged();
void NormalConnectionRequested(QString IpAddress, quint16 Port);
void connectionEnded();
void TerminateConnection();
void SendMessage(unsigned int type, unsigned int serial, QByteArray payload);
void DeliverMessage(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ConnectionDestroyed(unsigned int connId);
void DisconnectConnection(unsigned int connId);
void AddGameMessageToOutgoingQueue();
void Error(unsigned int connId, QString errorString);
// the token is just any identifier to identify a particular connection, potentially from the same host.
// the response (AddressIsWhiteListed) will have the same token as was sent.
void IsAddressWhiteListed(QHostAddress hostAddress, void* token);
void AddressIsWhiteListed(void* token, bool result);
//metrics
void NumOpenRequestsChanged();
void NumCloseRequestsChanged();
void NumOpenedChanged();
void NumClosedChanged();
void NumReadRequestsChanged();
void NumWriteRequestsChanged();
void NumSeekRequestsChanged();
void NumTellRequestsChanged();
void NumEofRequestsChanged();
void NumIsReadOnlyRequestsChanged();
void NumIsDirectoryRequestsChanged();
void NumSizeRequestsChanged();
void NumModificationTimeRequestsChanged();
void NumExistsRequestsChanged();
void NumFlushRequestsChanged();
void NumCreatePathRequestsChanged();
void NumDestroyPathRequestsChanged();
void NumRemoveRequestsChanged();
void NumCopyRequestsChanged();
void NumRenameRequestsChanged();
void NumFindFileNamesRequestsChanged();
void BytesReadChanged();
void BytesWrittenChanged();
void BytesSentChanged();
void BytesReceivedChanged();
void NumOpenFilesChanged();
public Q_SLOTS:
void SetIdentifier(QString Identifier);
void SetIpAddress(QString IpAddress);
void SetPort(int Port);
void SetStatus(ConnectionStatus Status);
void SetAssetPlatforms(QStringList assetPlatform);
void SetAssetPlatformsString(QString assetPlatforms);
void SetAutoConnect(bool AutoConnect);
void OnConnectionDisconnect();
void OnConnectionEstablished(QString ipAddress, quint16 port);
void ReceiveMessage(unsigned int type, unsigned int serial, QByteArray payload);
void ErrorMessage(QString negotiateFailure);
void UpdateElapsed();
void Connect();
private:
AZ::u32 GetNextSerial();
unsigned int m_connectionId;
QString m_identifier;
QString m_ipAddress;
quint16 m_port;
ConnectionStatus m_status;
QStringList m_assetPlatforms;
bool m_autoConnect;
QThread m_connectionWorkerThread;
QPointer<AssetProcessor::ConnectionWorker> m_connectionWorker;
bool m_runElapsed;
QElapsedTimer m_elapsedTimer;
qint64 m_elapsed;
QString m_elapsedDisplay;
bool m_queuedReconnect = false;
bool m_userCreatedConnection = false;
AZStd::mutex m_responseHandlerMutex;
AZStd::unordered_map<AZ::u32, AssetProcessor::ConnectionBusTraits::ResponseCallback> m_responseHandlerMap;
//metrics
qint64 m_numOpenRequests;
qint64 m_numCloseRequests;
qint64 m_numOpened;
qint64 m_numClosed;
qint64 m_numReadRequests;
qint64 m_numWriteRequests;
qint64 m_numTellRequests;
qint64 m_numSeekRequests;
qint64 m_numEofRequests;
qint64 m_numIsReadOnlyRequests;
qint64 m_numIsDirectoryRequests;
qint64 m_numSizeRequests;
qint64 m_numModificationTimeRequests;
qint64 m_numExistsRequests;
qint64 m_numFlushRequests;
qint64 m_numCreatePathRequests;
qint64 m_numDestroyPathRequests;
qint64 m_numRemoveRequests;
qint64 m_numCopyRequests;
qint64 m_numRenameRequests;
qint64 m_numFindFileNamesRequests;
qint64 m_bytesRead;
qint64 m_bytesWritten;
qint64 m_bytesSent;
qint64 m_bytesReceived;
qint64 m_numOpenFiles;
Q_DISABLE_COPY(Connection)
};
#endif // CONNECTION_H
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,217 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef CONNECTIONMANAGER_H
#define CONNECTIONMANAGER_H
#if !defined(Q_MOC_RUN)
#include <AzCore/std/function/function_fwd.h> // <functional> complains about exception handling and is not okay to mix with azcore/etc stuff.
#include <QReadWriteLock>
#include <QMap>
#include <QMultiMap>
#include <QObject>
#include <QString>
#include <QHostAddress>
#include <QStringListModel>
#include "native/utilities/AssetUtilEBusHelper.h"
#include <QAbstractItemModel>
#endif
class Connection;
typedef AZStd::function<void(unsigned int, unsigned int, unsigned int, QByteArray, QString)> regFunc;
typedef QMap<unsigned int, Connection*> ConnectionMap;
typedef QMultiMap<unsigned int, regFunc> RouteMultiMap;
class ConnectionManagerRequests
: public AZ::EBusTraits
{
public:
virtual void RegisterService(unsigned int messageType, regFunc func) = 0;
};
using ConnectionManagerRequestBus = AZ::EBus<ConnectionManagerRequests>;
namespace AssetProcessor
{
class PlatformConfiguration;
}
/** This is a container class for connection
*/
class ConnectionManager
: public QAbstractItemModel,
public ConnectionManagerRequestBus::Handler
{
Q_OBJECT
public:
enum Column
{
StatusColumn,
IdColumn,
IpColumn,
PortColumn,
PlatformColumn,
AutoConnectColumn,
Max
};
enum Roles
{
UserConnectionRole = Qt::UserRole + 1,
};
explicit ConnectionManager(QObject* parent = 0);
virtual ~ConnectionManager();
// Singleton pattern:
static ConnectionManager* Get();
Q_INVOKABLE int getCount() const;
Q_INVOKABLE Connection* getConnection(unsigned int connectionId);
Q_INVOKABLE ConnectionMap& getConnectionMap();
Q_INVOKABLE unsigned int addConnection(qintptr socketDescriptor = -1);
Q_INVOKABLE unsigned int addUserConnection();
Q_INVOKABLE void removeConnection(unsigned int connectionId);
unsigned int GetConnectionId(QString ipaddress, int port);
void SaveConnections(QString settingPrefix = ""); // settingPrefix allowed for testing purposes.
void LoadConnections(QString settingPrefix = ""); // settingPrefix allowed for testing purposes.
void RegisterService(unsigned int type, regFunc func) override;
//QAbstractItemListModel
QVariant data(const QModelIndex& index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QModelIndex index(int row, int column, const QModelIndex&) const override;
QModelIndex parent(const QModelIndex&) const override;
int columnCount(const QModelIndex& parent) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
void removeConnection(const QModelIndex& index);
Q_SIGNALS:
void connectionAdded(unsigned int connectionId, Connection* connection);
void beforeConnectionRemoved(unsigned int connectionId);
void ConnectionDisconnected(unsigned int connectionId);
void ConnectionRemoved(unsigned int connectionId);
void ConnectionError(unsigned int connId, QString error);
void ReadyToQuit(QObject* source);
void SyncWhiteListAndRejectedList(QStringList whiteList, QStringList rejectedList);
// this is a response to the whitelist request with that same token.
void AddressIsWhiteListed(void* token, bool result);
void FirstTimeAddedToRejctedList(QString ipAddress);
public Q_SLOTS:
void SendMessageToService(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void QuitRequested();
void RemoveConnectionFromMap(unsigned int connectionId);
void MakeSureConnectionMapEmpty();
void NewConnection(qintptr socketDescriptor);
void WhiteListingEnabled(bool enabled);
void IsAddressWhiteListed(QHostAddress hostAddress, void* token);
void AddWhiteListedAddress(QString address);
void RemoveWhiteListedAddress(QString address);
void AddRejectedAddress(QString address, bool surpressWarning = false);
void RemoveRejectedAddress(QString address);
//metrics
void AddBytesReceived(unsigned int connId, qint64 add, bool update);
void AddBytesSent(unsigned int connId, qint64 add, bool update);
void AddBytesRead(unsigned int connId, qint64 add, bool update);
void AddBytesWritten(unsigned int connId, qint64 add, bool update);
void AddOpenRequest(unsigned int connId, bool update);
void AddCloseRequest(unsigned int connId, bool update);
void AddOpened(unsigned int connId, bool update);
void AddClosed(unsigned int connId, bool update);
void AddReadRequest(unsigned int connId, bool update);
void AddWriteRequest(unsigned int connId, bool update);
void AddTellRequest(unsigned int connId, bool update);
void AddSeekRequest(unsigned int connId, bool update);
void AddIsReadOnlyRequest(unsigned int connId, bool update);
void AddIsDirectoryRequest(unsigned int connId, bool update);
void AddSizeRequest(unsigned int connId, bool update);
void AddModificationTimeRequest(unsigned int connId, bool update);
void AddExistsRequest(unsigned int connId, bool update);
void AddFlushRequest(unsigned int connId, bool update);
void AddCreatePathRequest(unsigned int connId, bool update);
void AddDestroyPathRequest(unsigned int connId, bool update);
void AddRemoveRequest(unsigned int connId, bool update);
void AddCopyRequest(unsigned int connId, bool update);
void AddRenameRequest(unsigned int connId, bool update);
void AddFindFileNamesRequest(unsigned int connId, bool update);
void UpdateBytesReceived(unsigned int connId);
void UpdateBytesSent(unsigned int connId);
void UpdateBytesRead(unsigned int connId);
void UpdateBytesWritten(unsigned int connId);
void UpdateOpenRequest(unsigned int connId);
void UpdateCloseRequest(unsigned int connId);
void UpdateOpened(unsigned int connId);
void UpdateClosed(unsigned int connId);
void UpdateReadRequest(unsigned int connId);
void UpdateWriteRequest(unsigned int connId);
void UpdateTellRequest(unsigned int connId);
void UpdateSeekRequest(unsigned int connId);
void UpdateIsReadOnlyRequest(unsigned int connId);
void UpdateIsDirectoryRequest(unsigned int connId);
void UpdateSizeRequest(unsigned int connId);
void UpdateModificationTimeRequest(unsigned int connId);
void UpdateExistsRequest(unsigned int connId);
void UpdateFlushRequest(unsigned int connId);
void UpdateCreatePathRequest(unsigned int connId);
void UpdateDestroyPathRequest(unsigned int connId);
void UpdateRemoveRequest(unsigned int connId);
void UpdateCopyRequest(unsigned int connId);
void UpdateRenameRequest(unsigned int connId);
void UpdateFindFileNamesRequest(unsigned int connId);
void UpdateConnectionMetrics();
void OnStatusChanged(unsigned int connId);
void UpdateWhiteListFromBootStrap();
private:
unsigned int internalAddConnection(bool isUserConnection, qintptr socketDescriptor = -1);
bool IsResponse(unsigned int serial);
void RouteIncomingMessage(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
Connection* FindConnection(const QModelIndex& index) const;
unsigned int m_nextConnectionId;
ConnectionMap m_connectionMap;
RouteMultiMap m_messageRoute;
QHostAddress m_lastHostAddress = QHostAddress::Null;
AZ::u64 m_lastConnectionTimeInUTCMilliSecs = 0;
// keeps track of how many platforms are connected of a given type
// the key is the name of the platform, and the value is the number of those kind of platforms.
QHash<QString, int> m_platformsConnected;
//white listing
bool m_whiteListingEnabled = true;
//these lists are just caches, only used for updating
QStringList m_whiteListedAddresses;
QStringList m_rejectedAddresses;
};
#endif // CONNECTIONMANAGER_H
@@ -0,0 +1,37 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef ASSETBUILDER_CONNECTIONMESSAGE_H
#define ASSETBUILDER_CONNECTIONMESSAGE_H
#include <QByteArray>
namespace AssetProcessor
{
struct MessageHeader
{
unsigned int type;
unsigned int size;
unsigned int serial;
};
// This is the framing for all packets sent to/from the AssetProcessor
struct Message
{
MessageHeader header;
QByteArray payload;
Message() = default;
Message(const Message&) = default;
};
}
#endif // ASSETBUILDER_CONNECTIONMESSAGE_H
@@ -0,0 +1,486 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "connectionworker.h"
#include "native/utilities/assetUtils.h"
#include <native/utilities/ByteArrayStream.h>
#include <QThread>
#include <QTimer>
#include <QCoreApplication>
#include <QThread>
#include <AzFramework/API/ApplicationAPI.h>
// enable this to debug negotiation - it enables a huge delay so that when a debugger attaches we don't fail.
//#define DEBUG_NEGOTIATION
#undef SendMessage
namespace AssetProcessor {
ConnectionWorker::ConnectionWorker(qintptr /*socketDescriptor*/, QObject* parent)
: QObject(parent)
, m_terminate(false)
{
#ifdef DEBUG_NEGOTIATION
m_waitDelay = 60 * 10 * 1000; // 10 min in debug, in ms
#endif
connect(&m_engineSocket, &QTcpSocket::stateChanged, this, &ConnectionWorker::EngineSocketStateChanged, Qt::QueuedConnection);
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, "Connection::ConnectionWorker created for socket %p: %p", socketDescriptor, this);
#endif
}
ConnectionWorker::~ConnectionWorker()
{
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, "ConnectionWorker::~: %p", this);
#endif
thread()->quit();
}
bool ConnectionWorker::ReadMessage(QTcpSocket& socket, AssetProcessor::Message& message)
{
const qint64 sizeOfHeader = static_cast<qint64>(sizeof(AssetProcessor::MessageHeader));
qint64 bytesAvailable = socket.bytesAvailable();
if (bytesAvailable == 0 || bytesAvailable < sizeOfHeader)
{
return false;
}
// read header
if (!ReadData(socket, (char*)&message.header, sizeOfHeader))
{
DisconnectSockets();
return false;
}
// Prepare the payload buffer
message.payload.resize(message.header.size);
// read payload
if (!ReadData(socket, message.payload.data(), message.header.size))
{
DisconnectSockets();
return false;
}
return true;
}
bool ConnectionWorker::ReadData(QTcpSocket& socket, char* buffer, qint64 size)
{
qint64 bytesRemaining = size;
while (bytesRemaining > 0)
{
// check first, or Qt will throw a warning if we try to do this on an already-disconnected-socket
if (socket.state() != QAbstractSocket::ConnectedState)
{
return false;
}
qint64 bytesRead = socket.read(buffer, bytesRemaining);
if (bytesRead == -1)
{
return false;
}
buffer += bytesRead;
bytesRemaining -= bytesRead;
if (bytesRemaining > 0)
{
socket.waitForReadyRead();
}
}
return true;
}
bool ConnectionWorker::WriteMessage(QTcpSocket& socket, const AssetProcessor::Message& message)
{
const qint64 sizeOfHeader = static_cast<qint64>(sizeof(AssetProcessor::MessageHeader));
AZ_Assert(message.header.size == aznumeric_cast<decltype(message.header.size)>(message.payload.size()), "Message header size does not match payload size");
// Write header
if (!WriteData(socket, (char*)&message.header, sizeOfHeader))
{
DisconnectSockets();
return false;
}
// write payload
if (!WriteData(socket, message.payload.data(), message.payload.size()))
{
DisconnectSockets();
return false;
}
return true;
}
bool ConnectionWorker::WriteData(QTcpSocket& socket, const char* buffer, qint64 size)
{
qint64 bytesRemaining = size;
while (bytesRemaining > 0)
{
// check first, or Qt will throw a warning if we try to do this on an already-disconnected-socket
if (socket.state() != QAbstractSocket::ConnectedState)
{
return false;
}
qint64 bytesWritten = socket.write(buffer, bytesRemaining);
if (bytesWritten == -1)
{
return false;
}
buffer += bytesWritten;
bytesRemaining -= bytesWritten;
}
return true;
}
void ConnectionWorker::EngineSocketHasData()
{
if (m_terminate)
{
return;
}
while (m_engineSocket.bytesAvailable() > 0)
{
AssetProcessor::Message message;
if (ReadMessage(m_engineSocket, message))
{
Q_EMIT ReceiveMessage(message.header.type, message.header.serial, message.payload);
}
else
{
break;
}
}
}
void ConnectionWorker::SendMessage(unsigned int type, unsigned int serial, QByteArray payload)
{
AssetProcessor::Message message;
message.header.type = type;
message.header.serial = serial;
message.header.size = payload.size();
message.payload = payload;
WriteMessage(m_engineSocket, message);
}
namespace Detail
{
template <class N>
bool WriteNegotiation(ConnectionWorker* worker, QTcpSocket& socket, const N& negotiation, unsigned int serial = AzFramework::AssetSystem::NEGOTIATION_SERIAL)
{
AssetProcessor::Message message;
bool packed = AssetProcessor::PackMessage(negotiation, message.payload);
if (packed)
{
message.header.type = negotiation.GetMessageType();
message.header.serial = serial;
message.header.size = message.payload.size();
return worker->WriteMessage(socket, message);
}
return false;
}
template <class N>
bool ReadNegotiation(ConnectionWorker* worker, int waitDelay, QTcpSocket& socket, N& negotiation, unsigned int* serial = nullptr)
{
if (socket.bytesAvailable() == 0)
{
socket.waitForReadyRead(waitDelay);
}
AssetProcessor::Message message;
if (!worker->ReadMessage(socket, message))
{
return false;
}
if (serial)
{
*serial = message.header.serial;
}
return AssetProcessor::UnpackMessage(message.payload, negotiation);
}
}
// Negotiation directly with a game or downstream AssetProcessor:
// if the connection is initiated from this end:
// 1) Send AP Info to downstream engine
// 2) Get downstream engine info
// if there is an incoming connection
// 1) Get downstream engine info
// 2) Send AP Info
bool ConnectionWorker::NegotiateDirect(bool initiate)
{
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, "ConnectionWorker::NegotiateDirect: %p", this);
#endif
using Detail::ReadNegotiation;
using Detail::WriteNegotiation;
using namespace AzFramework::AssetSystem;
AZStd::string azBranchToken;
AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::CalculateBranchTokenForAppRoot, azBranchToken);
QString branchToken(azBranchToken.c_str());
QString projectName = AssetUtilities::ComputeGameName();
NegotiationMessage myInfo;
char processId[20];
azsnprintf(processId, 20, "%lld", QCoreApplication::applicationPid());
myInfo.m_identifier = "ASSETPROCESSOR";
myInfo.m_negotiationInfoMap.insert(AZStd::make_pair(NegotiationInfo_ProcessId, AZ::OSString(processId)));
myInfo.m_negotiationInfoMap.insert(AZStd::make_pair(NegotiationInfo_BranchIndentifier, AZ::OSString(azBranchToken.c_str())));
myInfo.m_negotiationInfoMap.insert(AZStd::make_pair(NegotiationInfo_ProjectName, AZ::OSString(projectName.toUtf8().constData())));
NegotiationMessage engineInfo;
if (initiate)
{
if (!WriteNegotiation(this, m_engineSocket, myInfo))
{
Q_EMIT ErrorMessage("Unable to send negotiation message");
QTimer::singleShot(0, this, SLOT(DisconnectSockets()));
return false;
}
if (!ReadNegotiation(this, m_waitDelay, m_engineSocket, engineInfo))
{
Q_EMIT ErrorMessage("Unable to read negotiation message");
QTimer::singleShot(0, this, SLOT(DisconnectSockets()));
return false;
}
}
else
{
unsigned int serial = 0;
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, "ConnectionWorker::NegotiateDirect: Reading negotiation from engine socket %p", this);
#endif
if (!ReadNegotiation(this, m_waitDelay, m_engineSocket, engineInfo, &serial))
{
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, "ConnectionWorker::NegotiateDirect: no negotation arrived %p", this);
#endif
Q_EMIT ErrorMessage("Unable to read engine negotiation message");
QTimer::singleShot(0, this, SLOT(DisconnectSockets()));
return false;
}
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, "ConnectionWorker::NegotiateDirect: writing negotiation to engine socket %p", this);
#endif
if (!WriteNegotiation(this, m_engineSocket, myInfo, serial))
{
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, "ConnectionWorker::NegotiateDirect: no negotation sent %p", this);
#endif
Q_EMIT ErrorMessage("Unable to send negotiation message");
QTimer::singleShot(0, this, SLOT(DisconnectSockets()));
return false;
}
}
// Skip the process Id validation during negotiation if the identifier is UNITTEST
if (engineInfo.m_identifier != "UNITTEST")
{
if (strncmp(engineInfo.m_negotiationInfoMap[NegotiationInfo_ProcessId].c_str(), processId, strlen(processId)) == 0)
{
Q_EMIT ErrorMessage("Attempted to negotiate with self");
QTimer::singleShot(0, this, SLOT(DisconnectSockets()));
return false;
}
}
if (engineInfo.m_apiVersion != myInfo.m_apiVersion)
{
Q_EMIT ErrorMessage("Negotiation Failed.Version Mismatch.");
QTimer::singleShot(0, this, SLOT(DisconnectSockets()));
return false;
}
QString incomingBranchToken(engineInfo.m_negotiationInfoMap[NegotiationInfo_BranchIndentifier].c_str());
if (QString::compare(incomingBranchToken, branchToken, Qt::CaseInsensitive) != 0)
{
//if we are here it means that the editor/game which is negotiating is running on a different branch
// note that it could have just read nothing from the engine or a repeat packet, in that case, discard it silently and try again.
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "ConnectionWorker::NegotiateDirect: branch token mismatch from %s - %p - %s vs %s\n", engineInfo.m_identifier.c_str(), this, incomingBranchToken.toUtf8().data(), branchToken.toUtf8().data());
AssetProcessor::MessageInfoBus::Broadcast(&AssetProcessor::MessageInfoBus::Events::NegotiationFailed);
QTimer::singleShot(0, this, SLOT(DisconnectSockets()));
return false;
}
QString incomingProjectName(engineInfo.m_negotiationInfoMap[NegotiationInfo_ProjectName].c_str());
// Do a case-insensitive compare for the project name because some (case-sensitive) platforms will blower-case the incoming project name
if(QString::compare(incomingProjectName, projectName, Qt::CaseInsensitive) != 0)
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "ConnectionWorker::NegotiateDirect: project name mismatch from %s - %p - %s vs %s\n", engineInfo.m_identifier.c_str(), this, incomingProjectName.toUtf8().constData(), projectName.toUtf8().constData());
AssetProcessor::MessageInfoBus::Broadcast(&AssetProcessor::MessageInfoBus::Events::NegotiationFailed);
QTimer::singleShot(0, this, SLOT(DisconnectSockets()));
return false;
}
Q_EMIT Identifier(engineInfo.m_identifier.c_str());
Q_EMIT AssetPlatformsString(engineInfo.m_negotiationInfoMap[NegotiationInfo_Platform].c_str());
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, "ConnectionWorker::NegotiateDirect: negotation complete %p", this);
#endif
Q_EMIT ConnectionEstablished(m_engineSocket.peerAddress().toString(), m_engineSocket.peerPort());
connect(&m_engineSocket, &QTcpSocket::readyRead, this, &ConnectionWorker::EngineSocketHasData);
// force the socket to evaluate any data recv'd between negotiation and now
QTimer::singleShot(0, this, SLOT(EngineSocketHasData()));
return true;
}
// RequestTerminate can be called from anywhere, so we queue the actual
// termination to ensure it happens in the worker's thread
void ConnectionWorker::RequestTerminate()
{
if (!m_alreadySentTermination)
{
m_terminate = true;
m_alreadySentTermination = true;
QMetaObject::invokeMethod(this, "TerminateConnection", Qt::BlockingQueuedConnection);
}
}
void ConnectionWorker::TerminateConnection()
{
disconnect(&m_engineSocket, &QTcpSocket::stateChanged, this, &ConnectionWorker::EngineSocketStateChanged);
DisconnectSockets();
deleteLater();
}
void ConnectionWorker::ConnectSocket(qintptr socketDescriptor)
{
AZ_Assert(socketDescriptor != -1, "ConectionWorker::ConnectSocket: Supplied socket is invalid");
if (socketDescriptor != -1)
{
// calling setSocketDescriptor will cause it to invoke EngineSocketStateChanged instantly, which we don't want, so disconnect it temporarily.
disconnect(&m_engineSocket, &QTcpSocket::stateChanged, this, &ConnectionWorker::EngineSocketStateChanged);
m_engineSocket.setSocketDescriptor(socketDescriptor, QAbstractSocket::ConnectedState, QIODevice::ReadWrite);
Q_EMIT IsAddressWhiteListed(m_engineSocket.peerAddress(), reinterpret_cast<void*>(this));
}
}
void ConnectionWorker::AddressIsWhiteListed(void* token, bool result)
{
if (reinterpret_cast<void*>(this) == token)
{
if (result)
{
// this address has been approved, connect and proceed
connect(&m_engineSocket, &QTcpSocket::stateChanged, this, &ConnectionWorker::EngineSocketStateChanged);
EngineSocketStateChanged(QAbstractSocket::ConnectedState);
}
else
{
// this address has been rejected, disconnect immediately!!!
AZ_TracePrintf(AssetProcessor::ConsoleChannel, " A connection attempt was ignored because it is not whitelisted. Please consider adding white_list=(IP ADDRESS),localhost to the bootstrap.cfg");
disconnect(&m_engineSocket, &QTcpSocket::readyRead, this, &ConnectionWorker::EngineSocketHasData);
DisconnectSockets();
}
}
}
void ConnectionWorker::ConnectToEngine(QString ipAddress, quint16 port)
{
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, " ConnectionWorker::ConnectToEngine");
#endif
m_terminate = false;
if (m_engineSocket.state() == QAbstractSocket::UnconnectedState)
{
m_initiatedConnection = true;
m_engineSocket.connectToHost(ipAddress, port, QIODevice::ReadWrite);
}
}
void ConnectionWorker::EngineSocketStateChanged(QAbstractSocket::SocketState socketState)
{
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, " ConnectionWorker::EngineSocketStateChanged to %i", (int)socketState);
#endif
if (m_terminate)
{
return;
}
if (socketState == QAbstractSocket::ConnectedState)
{
m_engineSocket.setSocketOption(QAbstractSocket::KeepAliveOption, 1);
m_engineSocket.setSocketOption(QAbstractSocket::LowDelayOption, 1); //disable nagles algorithm
m_engineSocketIsConnected = true;
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, "ConnectionWorker::EngineSocketStateChanged: %p connected now (%s)", this, m_engineSocketIsConnected ? "True" : "False");
#endif
QMetaObject::invokeMethod(this, "NegotiateDirect", Qt::QueuedConnection, Q_ARG(bool, m_initiatedConnection));
}
else if (socketState == QAbstractSocket::UnconnectedState)
{
m_engineSocketIsConnected = false;
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, "ConnectionWorker::EngineSocketStateChanged: %p unconnected, now (%s)", this, m_engineSocketIsConnected ? "True" : "False");
#endif
disconnect(&m_engineSocket, &QTcpSocket::readyRead, 0, 0);
DisconnectSockets();
}
}
void ConnectionWorker::DisconnectSockets()
{
#if defined(DEBUG_NEGOTIATION)
AZ_TracePrintf(AssetProcessor::DebugChannel, " ConnectionWorker::DisconnectSockets");
#endif
m_engineSocket.abort();
m_engineSocket.close();
Q_EMIT ConnectionDisconnected();
}
void ConnectionWorker::Reset()
{
m_terminate = false;
}
bool ConnectionWorker::Terminate()
{
return m_terminate;
}
QTcpSocket& ConnectionWorker::GetSocket()
{
return m_engineSocket;
}
bool ConnectionWorker::InitiatedConnection() const
{
return m_initiatedConnection;
}
} // namespace AssetProcessor
@@ -0,0 +1,88 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QTcpSocket>
#include <QHostAddress>
#include "native/connection/connectionMessages.h"
#include <AzCore/Memory/Memory.h>
#include <AzCore/Memory/SystemAllocator.h>
#endif
/** This Class is responsible for connecting to the client
*/
#undef SendMessage
namespace AssetProcessor
{
class ConnectionWorker
: public QObject
{
Q_OBJECT
public:
explicit ConnectionWorker(qintptr socketDescriptor = -1, QObject* parent = 0);
virtual ~ConnectionWorker();
QTcpSocket& GetSocket();
void Reset();
bool Terminate();
bool ReadMessage(QTcpSocket& socket, AssetProcessor::Message& message);
bool ReadData(QTcpSocket& socket, char* buffer, qint64 size);
bool WriteMessage(QTcpSocket& socket, const AssetProcessor::Message& message);
bool WriteData(QTcpSocket& socket, const char* buffer, qint64 size);
//! True if we initiated the connection, false if someone connected to us.
bool InitiatedConnection() const;
Q_SIGNALS:
void ReceiveMessage(unsigned int type, unsigned int serial, QByteArray payload);
void SocketIPAddress(QString ipAddress);
void SocketPort(int port);
void Identifier(QString identifier);
void AssetPlatformsString(QString platform);
void ConnectionDisconnected();
void ConnectionEstablished(QString ipAddress, quint16 port);
void ErrorMessage(QString msg);
// the token identifies the unique connection instance, since multiple may have the same address
void IsAddressWhiteListed(QHostAddress hostAddress, void* token);
public Q_SLOTS:
void ConnectSocket(qintptr socketDescriptor);
void ConnectToEngine(QString ipAddress, quint16 port);
void EngineSocketHasData();
void EngineSocketStateChanged(QAbstractSocket::SocketState socketState);
void SendMessage(unsigned int type, unsigned int serial, QByteArray payload);
void DisconnectSockets();
void RequestTerminate();
bool NegotiateDirect(bool initiate);
// the token will be the same token sent in the whitelisting request.
void AddressIsWhiteListed(void* token, bool result);
private Q_SLOTS:
void TerminateConnection();
private:
QTcpSocket m_engineSocket;
volatile bool m_terminate;
volatile bool m_alreadySentTermination = false;
bool m_initiatedConnection = false;
bool m_engineSocketIsConnected = false;
int m_waitDelay = 10000; //increased to 10000 as 5000 milliseconds was enough in the unloaded general case but when the computer is loaded we need more time to negotiate a connection or we only get connection failures
};
} // namespace AssetProcessor
@@ -0,0 +1,41 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "utilities/BatchApplicationManager.h"
int main(int argc, char* argv[])
{
qputenv("QT_MAC_DISABLE_FOREGROUND_APPLICATION_TRANSFORM", "1");
BatchApplicationManager applicationManager(&argc, &argv);
setvbuf(stdout, NULL, _IONBF, 0); // Disabling output buffering to fix test failures due to incomplete logs
ApplicationManager::BeforeRunStatus status = applicationManager.BeforeRun();
if (status != ApplicationManager::BeforeRunStatus::Status_Success)
{
if (status == ApplicationManager::BeforeRunStatus::Status_Restarting)
{
//AssetProcessor will restart
return 0;
}
else
{
//Initialization failed
return 1;
}
}
return applicationManager.Run() ? 0 : 1;
}
@@ -0,0 +1,42 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "utilities/GUIApplicationManager.h"
#include <AzQtComponents/Utilities/HandleDpiAwareness.h>
int main(int argc, char* argv[])
{
qputenv("QT_MAC_DISABLE_FOREGROUND_APPLICATION_TRANSFORM", "1");
AzQtComponents::Utilities::HandleDpiAwareness(AzQtComponents::Utilities::PerScreenDpiAware);
GUIApplicationManager applicationManager(&argc, &argv);
ApplicationManager::BeforeRunStatus status = applicationManager.BeforeRun();
if (status != ApplicationManager::BeforeRunStatus::Status_Success)
{
if (status == ApplicationManager::BeforeRunStatus::Status_Restarting)
{
//AssetProcessor will restart
return 0;
}
else
{
//Initialization failed
return 1;
}
}
return applicationManager.Run() ? 0 : 1;
}
@@ -0,0 +1,43 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef ASSETPROCESSOR_PRECOMPILED_H
#define ASSETPROCESSOR_PRECOMPILED_H
#if 1 // change to 0 to temporarily turn off pch to make sure includes are ok
#include <QObject>
#include <QMetaObject>
#include <QFile>
#include <QString>
#include <QStringList>
#include <QList>
#if !defined(BATCH_MODE)
#include <QApplication>
#endif
#include <QHash>
#include <QByteArray>
#include <QAbstractListModel>
#include <QSet>
#include <QDir>
#include <QDebug>
#include <QPair>
#include <QTimer>
#include <QQueue>
#include <QTime>
#include <QVector>
#include <QThread>
#include <QProcess>
#include <QCoreApplication>
#endif
#endif // ASSETPROCESSOR_PRECOMPILED_H
@@ -0,0 +1,522 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <native/resourcecompiler/JobsModel.h>
#include <AzToolsFramework/API/AssetDatabaseBus.h>
#include <native/utilities/assetUtils.h>
#include <native/AssetDatabase/AssetDatabase.h>
#include <native/resourcecompiler/RCJobSortFilterProxyModel.h>
#include <native/utilities/JobDiagnosticTracker.h>
namespace AssetProcessor
{
JobsModel::JobsModel(QObject* parent)
: QAbstractItemModel(parent)
, m_pendingIcon(QStringLiteral(":/stylesheet/img/logging/pending.svg"))
, m_errorIcon(QStringLiteral(":/stylesheet/img/logging/error.svg"))
, m_warningIcon(QStringLiteral(":/stylesheet/img/logging/warning-yellow.svg"))
, m_okIcon(QStringLiteral(":/stylesheet/img/logging/valid.svg"))
, m_processingIcon(QStringLiteral(":/stylesheet/img/logging/processing.svg"))
{
}
JobsModel::~JobsModel()
{
for (int idx = 0; idx < m_cachedJobs.size(); idx++)
{
delete m_cachedJobs[idx];
}
m_cachedJobs.clear();
m_cachedJobsLookup.clear();
}
QModelIndex JobsModel::parent(const QModelIndex& index) const
{
AZ_UNUSED(index);
return QModelIndex();
}
QModelIndex JobsModel::index(int row, int column, const QModelIndex& parent) const
{
if (row >= rowCount(parent) || column >= columnCount(parent))
{
return QModelIndex();
}
return createIndex(row, column);
}
int JobsModel::rowCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : itemCount();
}
int JobsModel::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : Column::Max;
}
QVariant JobsModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Horizontal)
{
return QAbstractItemModel::headerData(section, orientation, role);
}
switch (role)
{
case Qt::DisplayRole:
{
switch (section)
{
case ColumnStatus:
return tr("Status");
case ColumnSource:
return tr("Source");
case ColumnPlatform:
return tr("Platform");
case ColumnJobKey:
return tr("Job Key");
case ColumnCompleted:
return tr("Completed");
default:
break;
}
}
case Qt::TextAlignmentRole:
{
return Qt::AlignLeft + Qt::AlignVCenter;
}
default:
break;
}
return QAbstractItemModel::headerData(section, orientation, role);
}
int JobsModel::itemCount() const
{
return aznumeric_caster(m_cachedJobs.size());
}
QVariant JobsModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
{
return QVariant();
}
if (index.row() >= itemCount())
{
return QVariant();
}
switch (role)
{
case Qt::DecorationRole:
{
if (index.column() == ColumnStatus) {
using namespace AzToolsFramework::AssetSystem;
switch (getItem(index.row())->m_jobState) {
case JobStatus::Queued:
return m_pendingIcon;
case JobStatus::Failed_InvalidSourceNameExceedsMaxLimit: // fall through intentional
case JobStatus::Failed:
return m_errorIcon;
case JobStatus::Completed:
{
CachedJobInfo* jobInfo = getItem(index.row());
if(jobInfo->m_warningCount > 0 || jobInfo->m_errorCount > 0)
{
// Warning icon is used for both warnings and errors.
return m_warningIcon;
}
return m_okIcon;
}
case JobStatus::InProgress:
return m_processingIcon;
}
}
break;
}
case Qt::DisplayRole:
case SortRole:
switch (index.column())
{
case ColumnStatus:
{
CachedJobInfo* jobInfo = getItem(index.row());
return GetStatusInString(jobInfo->m_jobState, jobInfo->m_warningCount, jobInfo->m_errorCount);
}
case ColumnSource:
return getItem(index.row())->m_elementId.GetInputAssetName();
case ColumnPlatform:
return getItem(index.row())->m_elementId.GetPlatform();
case ColumnJobKey:
return getItem(index.row())->m_elementId.GetJobDescriptor();
case ColumnCompleted:
if (role == SortRole)
{
return getItem(index.row())->m_completedTime;
}
else
{
return getItem(index.row())->m_completedTime.toString("hh:mm:ss.zzz MMM dd, yyyy");
}
default:
break;
}
case logRole:
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetUtilities;
JobInfo jobInfo;
AssetJobLogResponse jobLogResponse;
auto* cachedJobInfo = getItem(index.row());
jobInfo.m_sourceFile = cachedJobInfo->m_elementId.GetInputAssetName().toUtf8().data();
jobInfo.m_platform = cachedJobInfo->m_elementId.GetPlatform().toUtf8().data();
jobInfo.m_jobKey = cachedJobInfo->m_elementId.GetJobDescriptor().toUtf8().data();
jobInfo.m_builderGuid = cachedJobInfo->m_builderGuid;
jobInfo.m_jobRunKey = cachedJobInfo->m_jobRunKey;
jobInfo.m_warningCount = cachedJobInfo->m_warningCount;
jobInfo.m_errorCount = cachedJobInfo->m_errorCount;
ReadJobLogResult readJobLogResult = ReadJobLog(jobInfo, jobLogResponse);
const char* jobLogData = jobLogResponse.m_jobLog.c_str();
// ReadJobLog prepends the result with Error: if it can't find the file, even if the job was
// completed successfully or is still pending, so we detect that and give a less panicky response
// to the end user.
if (readJobLogResult == ReadJobLogResult::MissingLogFile)
{
switch (cachedJobInfo->m_jobState)
{
case JobStatus::Completed:
jobLogData = "The log file from the last (successful) run of this job could not be found.\nLogs are not always generated for successful jobs and this does not indicate an error.";
break;
case JobStatus::InProgress:
case JobStatus::Queued:
jobLogData = "The job is still processing and the log file has not yet been created";
break;
default:
// leave the job log as it is
break;
}
}
return QVariant(jobLogData);
}
case Qt::TextAlignmentRole:
{
return Qt::AlignLeft + Qt::AlignVCenter;
}
case statusRole:
{
CachedJobInfo* jobInfo = getItem(index.row());
return QVariant::fromValue(JobStatusInfo{ jobInfo->m_jobState, jobInfo->m_warningCount, jobInfo->m_errorCount });
}
case logFileRole:
{
AzToolsFramework::AssetSystem::JobInfo jobInfo;
CachedJobInfo* cachedJobInfo = getItem(index.row());
jobInfo.m_sourceFile = cachedJobInfo->m_elementId.GetInputAssetName().toUtf8().data();
jobInfo.m_platform = cachedJobInfo->m_elementId.GetPlatform().toUtf8().data();
jobInfo.m_jobKey = cachedJobInfo->m_elementId.GetJobDescriptor().toUtf8().data();
jobInfo.m_builderGuid = cachedJobInfo->m_builderGuid;
jobInfo.m_jobRunKey = cachedJobInfo->m_jobRunKey;
jobInfo.m_warningCount = cachedJobInfo->m_warningCount;
jobInfo.m_errorCount = cachedJobInfo->m_errorCount;
AZStd::string logFile = AssetUtilities::ComputeJobLogFolder() + "/" + AssetUtilities::ComputeJobLogFileName(jobInfo);
return QVariant(logFile.c_str());
}
default:
break;
}
return QVariant();
}
CachedJobInfo* JobsModel::getItem(int index) const
{
if (index >= 0 && index < m_cachedJobs.size())
{
return m_cachedJobs[index];
}
return nullptr; //invalid index
}
void Append(QString& base, const QString& input, const QString& seperator = ", ")
{
if(input.isEmpty())
{
return;
}
if(!base.isEmpty())
{
base.append(seperator);
}
base.append(input);
}
QString JobsModel::GetStatusInString(const AzToolsFramework::AssetSystem::JobStatus& state, AZ::u32 warningCount, AZ::u32 errorCount)
{
using namespace AzToolsFramework::AssetSystem;
switch (state)
{
case JobStatus::Queued:
return tr("Pending");
case JobStatus::Failed_InvalidSourceNameExceedsMaxLimit: // fall through intentional
case JobStatus::Failed:
return tr("Failed");
case JobStatus::Completed:
{
QString message = tr("Completed");
QString extra;
if (warningCount > 0)
{
extra.append(QString("%1 %2").arg(warningCount).arg( warningCount == 1 ? tr("warning") : tr("warnings") ));
}
if (errorCount > 0)
{
Append(extra, QString("%1 %2").arg(errorCount).arg( errorCount == 1 ? tr("error") : tr("errors") ));
}
Append(message, extra, ": ");
return message;
}
case JobStatus::InProgress:
return tr("InProgress");
}
return QString();
}
void JobsModel::PopulateJobsFromDatabase()
{
beginResetModel();
AZStd::string databaseLocation;
AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Broadcast(&AzToolsFramework::AssetDatabase::AssetDatabaseRequests::GetAssetDatabaseLocation, databaseLocation);
if (!databaseLocation.empty())
{
AssetProcessor::AssetDatabaseConnection assetDatabaseConnection;
assetDatabaseConnection.OpenDatabase();
auto jobsFunction = [this, &assetDatabaseConnection](AzToolsFramework::AssetDatabase::JobDatabaseEntry& entry)
{
AzToolsFramework::AssetDatabase::SourceDatabaseEntry source;
assetDatabaseConnection.GetSourceBySourceID(entry.m_sourcePK, source);
CachedJobInfo* jobInfo = new CachedJobInfo();
jobInfo->m_elementId.SetInputAssetName(source.m_sourceName.c_str());
jobInfo->m_elementId.SetPlatform(entry.m_platform.c_str());
jobInfo->m_elementId.SetJobDescriptor(entry.m_jobKey.c_str());
jobInfo->m_jobState = entry.m_status;
jobInfo->m_jobRunKey = aznumeric_cast<uint32_t>(entry.m_jobRunKey);
jobInfo->m_builderGuid = entry.m_builderGuid;
jobInfo->m_completedTime = QDateTime::fromMSecsSinceEpoch(entry.m_lastLogTime);
jobInfo->m_warningCount = entry.m_warningCount;
jobInfo->m_errorCount = entry.m_errorCount;
m_cachedJobs.push_back(jobInfo);
m_cachedJobsLookup.insert(jobInfo->m_elementId, aznumeric_caster(m_cachedJobs.size() - 1));
return true;
};
assetDatabaseConnection.QueryJobsTable(jobsFunction);
}
endResetModel();
}
QModelIndex JobsModel::GetJobFromProduct(const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& productEntry, AzToolsFramework::AssetDatabase::AssetDatabaseConnection& assetDatabaseConnection)
{
AZStd::string sourceForProduct;
assetDatabaseConnection.QuerySourceByProductID(
productEntry.m_productID,
[&](AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry)
{
sourceForProduct = sourceEntry.m_sourceName;
return false;
});
if (sourceForProduct.empty())
{
return QModelIndex();
}
AzToolsFramework::AssetDatabase::JobDatabaseEntry foundJobEntry;
assetDatabaseConnection.QueryJobByProductID(
productEntry.m_productID,
[&](AzToolsFramework::AssetDatabase::JobDatabaseEntry& jobEntry)
{
foundJobEntry = jobEntry;
return false;
});
if (foundJobEntry.m_jobID == AzToolsFramework::AssetDatabase::InvalidEntryId)
{
return QModelIndex();
}
return GetJobFromSourceAndJobInfo(sourceForProduct, foundJobEntry.m_platform, foundJobEntry.m_jobKey);
}
QModelIndex JobsModel::GetJobFromSourceAndJobInfo(const AZStd::string& source, const AZStd::string& platform, const AZStd::string& jobKey)
{
QueueElementID elementId(source.c_str(), platform.c_str(), jobKey.c_str());
auto iter = m_cachedJobsLookup.find(elementId);
if (iter == m_cachedJobsLookup.end())
{
return QModelIndex();
}
return index(iter.value(), 0, QModelIndex());
}
void JobsModel::OnJobStatusChanged(JobEntry entry, AzToolsFramework::AssetSystem::JobStatus status)
{
QueueElementID elementId(entry.m_databaseSourceName, entry.m_platformInfo.m_identifier.c_str(), entry.m_jobKey);
CachedJobInfo* jobInfo = nullptr;
unsigned int jobIndex = 0;
JobDiagnosticInfo jobDiagnosticInfo{};
JobDiagnosticRequestBus::BroadcastResult(jobDiagnosticInfo, &JobDiagnosticRequestBus::Events::GetDiagnosticInfo, entry.m_jobRunKey);
auto iter = m_cachedJobsLookup.find(elementId);
if (iter == m_cachedJobsLookup.end())
{
jobInfo = new CachedJobInfo();
jobInfo->m_elementId.SetInputAssetName(entry.m_databaseSourceName.toUtf8().data());
jobInfo->m_elementId.SetPlatform(entry.m_platformInfo.m_identifier.c_str());
jobInfo->m_elementId.SetJobDescriptor(entry.m_jobKey.toUtf8().data());
jobInfo->m_jobRunKey = aznumeric_cast<uint32_t>(entry.m_jobRunKey);
jobInfo->m_builderGuid = entry.m_builderGuid;
jobInfo->m_jobState = status;
jobInfo->m_warningCount = jobDiagnosticInfo.m_warningCount;
jobInfo->m_errorCount = jobDiagnosticInfo.m_errorCount;
jobIndex = aznumeric_caster(m_cachedJobs.size());
beginInsertRows(QModelIndex(), jobIndex, jobIndex);
m_cachedJobs.push_back(jobInfo);
m_cachedJobsLookup.insert(jobInfo->m_elementId, jobIndex);
endInsertRows();
}
else
{
jobIndex = iter.value();
jobInfo = m_cachedJobs[jobIndex];
jobInfo->m_jobState = status;
jobInfo->m_jobRunKey = aznumeric_cast<uint32_t>(entry.m_jobRunKey);
jobInfo->m_builderGuid = entry.m_builderGuid;
jobInfo->m_warningCount = jobDiagnosticInfo.m_warningCount;
jobInfo->m_errorCount = jobDiagnosticInfo.m_errorCount;
if (jobInfo->m_jobState == AzToolsFramework::AssetSystem::JobStatus::Completed || jobInfo->m_jobState == AzToolsFramework::AssetSystem::JobStatus::Failed)
{
jobInfo->m_completedTime = QDateTime::currentDateTime();
}
else
{
jobInfo->m_completedTime = QDateTime();
}
Q_EMIT dataChanged(index(jobIndex, 0, QModelIndex()), index(jobIndex, columnCount() - 1, QModelIndex()));
}
}
void JobsModel::OnSourceRemoved(QString sourceDatabasePath)
{
// when a source is removed, we need to eliminate all job entries for that source regardless of all other details of it.
QList<AssetProcessor::QueueElementID> elementsToRemove;
for (int index = 0; index < m_cachedJobs.size(); ++index)
{
if (QString::compare(m_cachedJobs[index]->m_elementId.GetInputAssetName(), sourceDatabasePath, Qt::CaseSensitive) == 0)
{
elementsToRemove.push_back(m_cachedJobs[index]->m_elementId);
}
}
// now that we've collected all the elements to remove, we can remove them.
// Doing it this way avoids problems with mutating these cache structures while iterating them.
for (const AssetProcessor::QueueElementID& removal : elementsToRemove)
{
RemoveJob(removal);
}
}
void JobsModel::OnFolderRemoved(QString folderPath)
{
QList<AssetProcessor::QueueElementID> elementsToRemove;
for (int index = 0; index < m_cachedJobs.size(); ++index)
{
if (m_cachedJobs[index]->m_elementId.GetInputAssetName().startsWith(folderPath, Qt::CaseSensitive))
{
elementsToRemove.push_back(m_cachedJobs[index]->m_elementId);
}
}
// now that we've collected all the elements to remove, we can remove them.
// Doing it this way avoids problems with mutating these cache structures while iterating them.
for (const AssetProcessor::QueueElementID& removal : elementsToRemove)
{
RemoveJob(removal);
}
}
void JobsModel::OnJobRemoved(AzToolsFramework::AssetSystem::JobInfo jobInfo)
{
RemoveJob(QueueElementID(jobInfo.m_sourceFile.c_str(), jobInfo.m_platform.c_str(), jobInfo.m_jobKey.c_str()));
}
void JobsModel::RemoveJob(const AssetProcessor::QueueElementID& elementId)
{
auto iter = m_cachedJobsLookup.find(elementId);
if (iter != m_cachedJobsLookup.end())
{
unsigned int jobIndex = iter.value();
CachedJobInfo* jobInfo = m_cachedJobs[jobIndex];
beginRemoveRows(QModelIndex(), jobIndex, jobIndex);
m_cachedJobs.erase(m_cachedJobs.begin() + jobIndex);
delete jobInfo;
m_cachedJobsLookup.erase(iter);
// Since we are storing the jobIndex for each job for faster lookup therefore
// we need to update the jobIndex for jobs that were after the removed job.
for (int idx = jobIndex; idx < m_cachedJobs.size(); idx++)
{
jobInfo = m_cachedJobs[idx];
auto iterator = m_cachedJobsLookup.find(jobInfo->m_elementId);
if (iterator != m_cachedJobsLookup.end())
{
unsigned int index = iterator.value();
m_cachedJobsLookup[jobInfo->m_elementId] = --index;
}
}
endRemoveRows();
}
}
} //namespace AssetProcessor
@@ -0,0 +1,113 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <native/resourcecompiler/RCCommon.h>
#include <QAbstractItemModel>
#include <QDateTime>
#include <QHash>
#include <QIcon>
#include <QVector>
// Don't reorder above RCCommon
#include <native/assetprocessor.h>
#endif
// Do this here, rather than EditorAssetSystemAPI.h so that we don't have to link against Qt5Core to
// use EditorAssetSystemAPI.h
Q_DECLARE_METATYPE(AzToolsFramework::AssetSystem::JobStatus);
namespace AzToolsFramework
{
namespace AssetDatabase
{
class ProductDatabaseEntry;
class AssetDatabaseConnection;
}
}
namespace AssetProcessor
{
//! CachedJobInfo stores all the necessary information needed for showing a particular job including its log
struct CachedJobInfo
{
QueueElementID m_elementId;
QDateTime m_completedTime;
AzToolsFramework::AssetSystem::JobStatus m_jobState;
AZ::u32 m_warningCount;
AZ::u32 m_errorCount;
unsigned int m_jobRunKey;
AZ::Uuid m_builderGuid;
};
/**
* The JobsModel class contains list of jobs from both the Database and the RCController
*/
class JobsModel
: public QAbstractItemModel
{
Q_OBJECT
public:
enum DataRoles
{
logRole = Qt::UserRole + 1,
statusRole,
logFileRole,
SortRole,
};
enum Column
{
ColumnStatus,
ColumnSource,
ColumnCompleted,
ColumnPlatform,
ColumnJobKey,
Max
};
explicit JobsModel(QObject* parent = nullptr);
virtual ~JobsModel();
QModelIndex parent(const QModelIndex& index) const override;
QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QVariant data(const QModelIndex& index, int role) const override;
int itemCount() const;
CachedJobInfo* getItem(int index) const;
static QString GetStatusInString(const AzToolsFramework::AssetSystem::JobStatus& state, AZ::u32 warningCount, AZ::u32 errorCount);
void PopulateJobsFromDatabase();
QModelIndex GetJobFromProduct(const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& productEntry, AzToolsFramework::AssetDatabase::AssetDatabaseConnection& assetDatabaseConnection);
QModelIndex GetJobFromSourceAndJobInfo(const AZStd::string& source, const AZStd::string& platform, const AZStd::string& jobKey);
public Q_SLOTS:
void OnJobStatusChanged(JobEntry entry, AzToolsFramework::AssetSystem::JobStatus status);
void OnJobRemoved(AzToolsFramework::AssetSystem::JobInfo jobInfo);
void OnSourceRemoved(QString sourceDatabasePath);
void OnFolderRemoved(QString folderPath);
protected:
QIcon m_pendingIcon;
QIcon m_errorIcon;
QIcon m_warningIcon;
QIcon m_okIcon;
QIcon m_processingIcon;
AZStd::vector<CachedJobInfo*> m_cachedJobs;
QHash<AssetProcessor::QueueElementID, int> m_cachedJobsLookup; // QVector uses int as type of index.
void RemoveJob(const AssetProcessor::QueueElementID& elementId);
};
} //namespace AssetProcessor
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,211 @@
/*
* All or portions of this file Copyright(c) Amazon.com, Inc.or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
*or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include "../utilities/ApplicationManagerAPI.h"
#include "native/AssetManager/assetProcessorManager.h"
#include "native/utilities/PlatformConfiguration.h"
namespace AssetProcessor
{
struct RCCompiler
{
//! RC.exe execution result
struct Result
{
Result(int exitCode, bool crashed, const QString& outputDir);
Result() = default;
int m_exitCode = 1;
bool m_crashed = false;
QString m_outputDir;
};
virtual ~RCCompiler() = default;
virtual bool Initialize(const QString& systemRoot, const QString& rcExecutableFullPath) = 0;
virtual bool Execute(const QString& inputFile, const QString& watchFolder, const QString& platformIdentifier, const QString& params,
const QString& dest, const AssetBuilderSDK::JobCancelListener* jobCancelListener, Result& result) const = 0;
virtual void RequestQuit() = 0;
};
//! Worker class to handle shell execution of the legacy rc.exe compiler
class NativeLegacyRCCompiler
: public RCCompiler
{
public:
NativeLegacyRCCompiler();
bool Initialize(const QString& systemRoot, const QString& rcExecutableFullPath) override;
bool Execute(const QString& inputFile, const QString& watchFolder, const QString& platformIdentifier, const QString& params, const QString& dest,
const AssetBuilderSDK::JobCancelListener* jobCancelListener, Result& result) const override;
static QString BuildCommand(const QString& inputFile, const QString& watchFolder, const QString& platformIdentifier, const QString& params, const QString& dest);
void RequestQuit() override;
private:
static const int s_maxSleepTime;
static const unsigned int s_jobMaximumWaitTime;
bool m_resourceCompilerInitialized;
QDir m_systemRoot;
QString m_rcExecutableFullPath;
volatile bool m_requestedQuit;
};
//! Internal structure that consolidates a internal builder id, its name, and its custom fixed rc param match
class BuilderIdAndName
{
public:
enum class Type
{
REGISTERED_BUILDER,
UNREGISTERED_BUILDER
};
BuilderIdAndName() = default;
BuilderIdAndName(QString builderName, QString builderId, Type type, QString rcParam = QString(""));
BuilderIdAndName(const BuilderIdAndName& src) = default;
BuilderIdAndName& operator=(const AssetProcessor::BuilderIdAndName& src);
const QString& GetName() const;
bool GetUuid(AZ::Uuid& builderUuid) const;
const QString& GetRcParam() const;
const QString& GetId() const;
const Type GetType() const;
private:
Type m_type = Type::UNREGISTERED_BUILDER;
QString m_builderName;
QString m_builderId;
QString m_rcParam;
};
extern const BuilderIdAndName BUILDER_ID_COPY;
extern const BuilderIdAndName BUILDER_ID_RC;
extern const BuilderIdAndName BUILDER_ID_SKIP;
extern const QHash<QString, BuilderIdAndName> INTERNAL_BUILDER_BY_ID;
//! Internal Builder version of the asset recognizer structure that is read in from the platform configuration class
struct InternalAssetRecognizer
: public AssetRecognizer
{
InternalAssetRecognizer(const AssetRecognizer& src, const QString& builderId, const QHash<QString, AssetPlatformSpec>& assetPlatformSpecByPlatform);
InternalAssetRecognizer(const InternalAssetRecognizer& src) = default;
AZ::u32 CalculateCRC() const;
//! Map of platform specs based on the identifier of the platform
QHash<QString, AssetPlatformSpec> m_platformSpecsByPlatform;
//! unique id that is generated for each unique internal asset recognizer
//! which can be used as the key for the job parameter map (see AssetBuilderSDK::JobParameterMap)
AZ::u32 m_paramID;
//! Keep track which internal builder type this recognizer is for
const QString m_builderId;
};
typedef QHash<AZ::u32, InternalAssetRecognizer*> InternalRecognizerContainer;
typedef QList<const InternalAssetRecognizer*> InternalRecognizerPointerContainer;
typedef AZStd::function<void(const AssetBuilderSDK::AssetBuilderDesc& builderDesc)> RegisterBuilderDescCallback;
typedef AZStd::list<InternalAssetRecognizer*> InternalAssetRecognizerList;
class InternalRecognizerBasedBuilder
: public AssetBuilderSDK::AssetBuilderCommandBus::Handler
{
public:
//! Constructor to initialize the internal based builder to a present set of internal builders and fixed bus id
InternalRecognizerBasedBuilder();
virtual ~InternalRecognizerBasedBuilder();
AssetBuilderSDK::AssetBuilderDesc CreateBuilderDesc(const QString& builderId, const AZStd::vector<AssetBuilderSDK::AssetBuilderPattern>& builderPatterns);
void ShutDown() override;
virtual bool Initialize(const RecognizerConfiguration& recognizerConfig);
virtual void InitializeAssetRecognizers(const RecognizerContainer& assetRecognizers);
virtual void UnInitialize();
//! Returns false if there were no matches, otherwise returns true
virtual bool GetMatchingRecognizers(const AZStd::vector<AssetBuilderSDK::PlatformInfo>& platformInfos, const QString& fileName, InternalRecognizerPointerContainer& output) const;
void CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response);
void ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response);
static bool MatchTempFileToSkip(const QString& outputFilename);
static void RegisterInternalAssetRecognizerToMap(
const AssetRecognizer& assetRecognizer,
const QString& builderId,
QHash<QString, AssetPlatformSpec>& sourceAssetPlatformSpecs,
QHash<QString, InternalAssetRecognizerList>& internalRecognizerListByType);
//! Split all of the asset recognizers from a container into buckets based on their specific builder action type
static void BuildInternalAssetRecognizersByType(
const RecognizerContainer& assetRecognizers,
QHash<QString, InternalAssetRecognizerList>& internalRecognizerListByType);
protected:
//! Constructor to initialize the internal builders and a general internal builder uuid that is used for bus
//! registration. This constructor is helpful for deriving other classes from this builder for purposes like
//! unit testing.
InternalRecognizerBasedBuilder(QHash<QString, BuilderIdAndName> inputBuilderByIdMap, AZ::Uuid internalBuilderUuid);
// overridden in unit tests. Searches for RC.EXE
virtual bool FindRC(QString& rcAbsolutePathOut);
void CreateLegacyRCJob(
const AssetBuilderSDK::CreateJobsRequest& request,
QString rcParam,
AssetBuilderSDK::CreateJobsResponse& response);
void ProcessLegacyRCJob(
const AssetBuilderSDK::ProcessJobRequest& request,
QString rcParam,
AZ::Uuid productAssetType,
const AssetBuilderSDK::JobCancelListener& jobCancelListener,
AssetBuilderSDK::ProcessJobResponse& response);
//! Given a folder (dest) containing the aftermath of a RC process, generate a response structure.
//! if responseFromRCCompiler is true it means that the response is an already-populated response struct
//! that was loaded from a response file and we should just append legacy SubIDs to it (responses are used INSTEAD of
//! heuristics).
//! otherwise it means that we know nothing about the files that were produced and should perform heuristics to determine.
//! productAssetType is what uuid type (or nulls) to apply to the generated products for when responseFromRCCompiler is false.
void ProcessRCResultFolder(const QString &dest, const AZ::Uuid& productAssetType, bool responseFromRCCompiler, AssetBuilderSDK::ProcessJobResponse &response);
void ProcessCopyJob(
const AssetBuilderSDK::ProcessJobRequest& request,
AZ::Uuid productAssetType,
bool outputProductDependencies,
const AssetBuilderSDK::JobCancelListener& jobCancelListener,
AssetBuilderSDK::ProcessJobResponse& response);
// overridable so we can unit-test override it.
virtual QFileInfoList GetFilesInDirectory(const QString& directoryPath);
// overridable so we can unit-test override it.
virtual bool SaveProcessJobRequestFile(const char* requestFileDir, const char* requestFileName, const AssetBuilderSDK::ProcessJobRequest& request);
// returns false only if there is a critical failure.
virtual bool LoadProcessJobResponseFile(const char* responseFileDir, const char* responseFileName, AssetBuilderSDK::ProcessJobResponse& response, bool& responseLoaded);
AZStd::unique_ptr<RCCompiler> m_rcCompiler;
volatile bool m_isShuttingDown;
InternalRecognizerContainer m_assetRecognizerDictionary;
QHash<QString, BuilderIdAndName> m_builderById;
//! UUid for the internal recognizer for logging purposes since the this
//! class manages multiple internal build uuids
AZ::Uuid m_internalRecognizerBuilderUuid;
};
} // namespace AssetProcessor
@@ -0,0 +1,94 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "RCCommon.h"
#include <QHash>
namespace AssetProcessor
{
QueueElementID::QueueElementID(QString inputAssetName, QString platform, QString jobDescriptor)
: m_inputAssetName(inputAssetName)
, m_platform(platform)
, m_jobDescriptor(jobDescriptor)
{
}
QString QueueElementID::GetInputAssetName() const
{
return m_inputAssetName;
}
QString QueueElementID::GetPlatform() const
{
return m_platform;
}
QString QueueElementID::GetJobDescriptor() const
{
return m_jobDescriptor;
}
void QueueElementID::SetInputAssetName(QString inputAssetName)
{
m_inputAssetName = inputAssetName;
}
void QueueElementID::SetPlatform(QString platform)
{
m_platform = platform;
}
void QueueElementID::SetJobDescriptor(QString jobDescriptor)
{
m_jobDescriptor = jobDescriptor;
}
bool QueueElementID::operator==(const QueueElementID& other) const
{
// if this becomes a hotspot in profile, we could use CRCs or other boost to comparison here. These classes are constructed rarely
// compared to how commonly they are compared with each other.
return (
(QString::compare(m_inputAssetName, other.m_inputAssetName, Qt::CaseSensitive) == 0) &&
(QString::compare(m_platform, other.m_platform, Qt::CaseInsensitive) == 0) &&
(QString::compare(m_jobDescriptor, other.m_jobDescriptor, Qt::CaseInsensitive) == 0)
);
}
bool QueueElementID::operator<(const QueueElementID& other) const
{
int compare = QString::compare(m_inputAssetName, other.m_inputAssetName, Qt::CaseSensitive);
if (compare != 0)
{
return (compare < 0);
}
compare = QString::compare(m_platform, other.m_platform, Qt::CaseInsensitive);
if (compare != 0)
{
return (compare < 0);
}
compare = QString::compare(m_jobDescriptor, other.m_jobDescriptor, Qt::CaseInsensitive);
if (compare != 0)
{
return (compare < 0);
}
// all three are equal, other is not less than this.
return false;
}
uint qHash(const AssetProcessor::QueueElementID& key, uint seed)
{
return qHash(key.GetInputAssetName().toLower() + key.GetPlatform().toLower() + key.GetJobDescriptor().toLower(), seed);
}
} // end namespace AssetProcessor
@@ -0,0 +1,65 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef ASSETPROCESSOR_RCCOMMON_H
#define ASSETPROCESSOR_RCCOMMON_H
#include <QString>
namespace AssetProcessor
{
// RCCommon contains common structs used by the RC Job System
//! Job Exit codes.
// note that this is NOT a complete list of return codes, and RC.EXE itself may return unknown return codes here.
enum JobExitCodes : int
{
JobExitCode_Success = 0,
JobExitCode_Failed = -10,
JobExitCode_InvalidParams = -9,
JobExitCode_UnableToCreateTempDir = -8,
JobExitCode_CopyFailed = -6,
JobExitCode_Unknown = -1,
JobExitCode_RCNotFound = -4,
JobExitCode_RCCouldNotBeLaunched = -5,
JobExitCode_JobCancelled = -7,
};
//! Identifies a queued job uniquely. Not a case sensitive compare
class QueueElementID
{
public:
QueueElementID() = default;
///! note that inputAssetName is a database name (so includes outputprefix), not a relative path.
QueueElementID(QString inputAssetName, QString platform, QString jobDescriptor);
QString GetInputAssetName() const; ///< This is the database name, with output prefix.
QString GetPlatform() const;
QString GetJobDescriptor() const;
void SetInputAssetName(QString inputAssetName);
void SetPlatform(QString platform);
void SetJobDescriptor(QString jobDescriptor);
bool operator==(const QueueElementID& other) const;
bool operator<(const QueueElementID& other) const;
protected:
QString m_inputAssetName;
QString m_platform;
QString m_jobDescriptor;
};
uint qHash(const AssetProcessor::QueueElementID& key, uint seed = 0);
} // namespace AssetProcessor
#endif //ASSETPROCESSOR_RCQUEUESORTMODEL_H
@@ -0,0 +1,106 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <native/resourcecompiler/RCJobSortFilterProxyModel.h>
#include <native/resourcecompiler/JobsModel.h> //for jobsModel column enum
#include <AzQtComponents/Components/FilteredSearchWidget.h>
namespace AssetProcessor
{
JobSortFilterProxyModel::JobSortFilterProxyModel(QObject* parent)
: QSortFilterProxyModel(parent)
{
setSortCaseSensitivity(Qt::CaseInsensitive);
setFilterCaseSensitivity(Qt::CaseInsensitive);
}
bool JobSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
using namespace AzToolsFramework::AssetSystem;
const QModelIndex jobStateIndex = sourceModel()->index(sourceRow,
AssetProcessor::JobsModel::Column::ColumnStatus, sourceParent);
JobStatusInfo jobStatus = sourceModel()->data(jobStateIndex, AssetProcessor::JobsModel::statusRole)
.value<JobStatusInfo>();
if (jobStatus.m_status == JobStatus::Failed_InvalidSourceNameExceedsMaxLimit)
{
jobStatus.m_status = JobStatus::Failed;
}
// Checks our custom filters.
// true = passed our filter, send on to the default filter
// false = reject the row
auto filterFunc = [&]()
{
if (m_completedWithWarningsFilter && jobStatus.m_status == JobStatus::Completed && (jobStatus.m_errorCount > 0 || jobStatus.m_warningCount > 0))
{
return true;
}
if (!m_activeTypeFilters.isEmpty() && m_activeTypeFilters.contains(jobStatus.m_status))
{
return true;
}
return false;
};
bool hasFilters = !m_activeTypeFilters.isEmpty() || m_completedWithWarningsFilter == true;
bool useDefaultFilter = hasFilters ? filterFunc() : true;
return useDefaultFilter ? QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent) : false;
}
void JobSortFilterProxyModel::OnJobStatusFilterChanged(const AzQtComponents::SearchTypeFilterList& activeTypeFilters)
{
m_completedWithWarningsFilter = false;
m_activeTypeFilters.clear();
for (const auto& filter : activeTypeFilters)
{
if (filter.metadata.canConvert<AzToolsFramework::AssetSystem::JobStatus>())
{
m_activeTypeFilters << filter.metadata.value<AzToolsFramework::AssetSystem::JobStatus>();
}
else if (filter.metadata.canConvert<AssetProcessor::CustomJobStatusFilter>())
{
m_completedWithWarningsFilter = filter.metadata.value<AssetProcessor::CustomJobStatusFilter>().m_completedWithWarnings;
}
}
invalidateFilter();
}
bool JobSortFilterProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
// Only the completed column has an override, because it displays time in a different format
// than what works best to sort.
if (left.column() != JobsModel::ColumnCompleted || right.column() != JobsModel::ColumnCompleted)
{
return QSortFilterProxyModel::lessThan(left, right);
}
QVariant leftTime = sourceModel()->data(left, JobsModel::SortRole);
QVariant rightTime = sourceModel()->data(right, JobsModel::SortRole);
if (leftTime.type() != QVariant::DateTime || rightTime.type() != QVariant::DateTime)
{
return QSortFilterProxyModel::lessThan(left, right);
}
return leftTime.toDateTime() < rightTime.toDateTime();
}
} //namespace AssetProcessor
@@ -0,0 +1,74 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QSortFilterProxyModel>
#include <AzCore/base.h>
#endif
namespace AzQtComponents
{
struct SearchTypeFilter;
using SearchTypeFilterList = QVector<SearchTypeFilter>;
}
namespace AzToolsFramework
{
namespace AssetSystem
{
enum class JobStatus;
}
}
namespace AssetProcessor
{
struct CustomJobStatusFilter
{
CustomJobStatusFilter() = default;
CustomJobStatusFilter(bool completeWithWarnings)
: m_completedWithWarnings(completeWithWarnings)
{
}
bool m_completedWithWarnings = false;
};
struct JobStatusInfo
{
AzToolsFramework::AssetSystem::JobStatus m_status;
AZ::u32 m_warningCount;
AZ::u32 m_errorCount;
};
class JobSortFilterProxyModel
: public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit JobSortFilterProxyModel(QObject* parent = nullptr);
void OnJobStatusFilterChanged(const AzQtComponents::SearchTypeFilterList& activeTypeFilters);
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
private:
QList<AzToolsFramework::AssetSystem::JobStatus> m_activeTypeFilters = {};
bool m_completedWithWarningsFilter = false;
};
} // namespace AssetProcessor
Q_DECLARE_METATYPE(AssetProcessor::CustomJobStatusFilter);
Q_DECLARE_METATYPE(AssetProcessor::JobStatusInfo);
@@ -0,0 +1,258 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <native/resourcecompiler/RCQueueSortModel.h>
#include "rcjoblistmodel.h"
namespace AssetProcessor
{
RCQueueSortModel::RCQueueSortModel(QObject* parent)
: QSortFilterProxyModel(parent)
{
// jobs assigned to "all" platforms are always active.
m_currentlyConnectedPlatforms.insert(QString("all"));
}
void RCQueueSortModel::AttachToModel(RCJobListModel* target)
{
if (target)
{
setDynamicSortFilter(true);
BusConnect();
m_sourceModel = target;
setSourceModel(target);
setSortRole(RCJobListModel::jobIndexRole);
sort(0);
}
else
{
BusDisconnect();
setSourceModel(nullptr);
m_sourceModel = nullptr;
}
}
RCJob* RCQueueSortModel::GetNextPendingJob()
{
if (m_dirtyNeedsResort)
{
setDynamicSortFilter(false);
QSortFilterProxyModel::sort(0);
setDynamicSortFilter(true);
m_dirtyNeedsResort = false;
}
RCJob* anyPendingJob = nullptr;
bool waitingOnCatalog = false; // If we find an asset thats waiting on the catalog, don't assume there's a cyclic dependency. We'll wait until the catalog is updated and then check again.
for (int idx = 0; idx < rowCount(); ++idx)
{
QModelIndex parentIndex = mapToSource(index(idx, 0));
RCJob* actualJob = m_sourceModel->getItem(parentIndex.row());
if ((actualJob) && (actualJob->GetState() == RCJob::pending))
{
bool canProcessJob = true;
for (const JobDependencyInternal& jobDepedencyInternal : actualJob->GetJobDependencies())
{
if (jobDepedencyInternal.m_jobDependency.m_type == AssetBuilderSDK::JobDependencyType::Order || jobDepedencyInternal.m_jobDependency.m_type == AssetBuilderSDK::JobDependencyType::OrderOnce)
{
const AssetBuilderSDK::JobDependency& jobDependency = jobDepedencyInternal.m_jobDependency;
QueueElementID elementId(jobDependency.m_sourceFile.m_sourceFileDependencyPath.c_str(), jobDependency.m_platformIdentifier.c_str(), jobDependency.m_jobKey.c_str());
if (m_sourceModel->isInFlight(elementId) || m_sourceModel->isInQueue(elementId))
{
canProcessJob = false;
if (!anyPendingJob)
{
anyPendingJob = actualJob;
}
}
else if(m_sourceModel->isWaitingOnCatalog(elementId))
{
canProcessJob = false;
waitingOnCatalog = true;
}
}
}
if (canProcessJob)
{
return actualJob;
}
}
}
// Either there are no jobs to do or there is a cyclic order job dependency.
if (anyPendingJob && m_sourceModel->jobsInFlight() == 0 && !waitingOnCatalog)
{
AZ_Warning(AssetProcessor::DebugChannel, false, " Cyclic job order dependency detected. Processing job (%s, %s, %s, %s) to unblock.",
anyPendingJob->GetJobEntry().m_pathRelativeToWatchFolder.toUtf8().data(), anyPendingJob->GetJobKey().toUtf8().data(),
anyPendingJob->GetJobEntry().m_platformInfo.m_identifier.c_str(), anyPendingJob->GetBuilderGuid().ToString<AZStd::string>().c_str());
return anyPendingJob;
}
else
{
return nullptr;
}
}
bool RCQueueSortModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
(void)source_parent;
RCJob* actualJob = m_sourceModel->getItem(source_row);
if (!actualJob)
{
return false;
}
if (actualJob->GetState() != RCJob::pending)
{
return false;
}
return true;
}
bool RCQueueSortModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
RCJob* leftJob = m_sourceModel->getItem(left.row());
RCJob* rightJob = m_sourceModel->getItem(right.row());
// auto fail jobs always take priority to give user feedback asap.
bool autoFailLeft = leftJob->IsAutoFail();
bool autoFailRight = rightJob->IsAutoFail();
if (autoFailLeft)
{
if (!autoFailRight)
{
return true; // left before right
}
}
else if (autoFailRight)
{
return false; // right before left.
}
// first thing to check is in platform.
bool leftActive = m_currentlyConnectedPlatforms.contains(leftJob->GetPlatformInfo().m_identifier.c_str());
bool rightActive = m_currentlyConnectedPlatforms.contains(rightJob->GetPlatformInfo().m_identifier.c_str());
if (leftActive)
{
if (!rightActive)
{
return true; // left before right
}
}
else if (rightActive)
{
return false; // right before left.
}
// critical jobs take priority
if (leftJob->IsCritical())
{
if (!rightJob->IsCritical())
{
return true; // left wins.
}
}
else if (rightJob->IsCritical())
{
return false; // right wins
}
int leftJobEscalation = leftJob->JobEscalation();
int rightJobEscalation = rightJob->JobEscalation();
// This function, even though its called lessThan(), really is asking, does LEFT come before RIGHT
// The higher the escalation, the more important the request, and thus the sooner we want to process the job
// Which means if left has a higher escalation number than right, its LESS THAN right.
if (leftJobEscalation != rightJobEscalation)
{
return leftJobEscalation > rightJobEscalation;
}
// arbitrarily, lets have PC get done first since pc-format assets are what the editor uses.
if (leftJob->GetPlatformInfo().m_identifier != rightJob->GetPlatformInfo().m_identifier)
{
if (leftJob->GetPlatformInfo().m_identifier == AzToolsFramework::AssetSystem::GetHostAssetPlatform())
{
return true; // left wins.
}
if (rightJob->GetPlatformInfo().m_identifier == AzToolsFramework::AssetSystem::GetHostAssetPlatform())
{
return false; // right wins
}
}
int priorityLeft = leftJob->GetPriority();
int priorityRight = rightJob->GetPriority();
if (priorityLeft != priorityRight)
{
return priorityLeft > priorityRight;
}
// if we get all the way down here it means we're dealing with two assets which are not
// in any compile groups, not a priority platform, not a priority type, priority platform, etc.
// we can arrange these any way we want, but must pick at least a stable order.
return leftJob->GetJobEntry().m_jobRunKey < rightJob->GetJobEntry().m_jobRunKey;
}
void RCQueueSortModel::AssetProcessorPlatformConnected(const AZStd::string platform)
{
QMetaObject::invokeMethod(this, "ProcessPlatformChangeMessage", Qt::QueuedConnection, Q_ARG(QString, QString::fromUtf8(platform.c_str())), Q_ARG(bool, true));
}
void RCQueueSortModel::AssetProcessorPlatformDisconnected(const AZStd::string platform)
{
QMetaObject::invokeMethod(this, "ProcessPlatformChangeMessage", Qt::QueuedConnection, Q_ARG(QString, QString::fromUtf8(platform.c_str())), Q_ARG(bool, false));
}
void RCQueueSortModel::ProcessPlatformChangeMessage(QString platformName, bool connected)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "RCQueueSortModel: Platform %s has %s.", platformName.toUtf8().data(), connected ? "connected" : "disconnected");
m_dirtyNeedsResort = true;
if (connected)
{
m_currentlyConnectedPlatforms.insert(platformName);
}
else
{
m_currentlyConnectedPlatforms.remove(platformName);
}
}
void RCQueueSortModel::AddJobIdEntry(AssetProcessor::RCJob* rcJob)
{
m_currentJobRunKeyToJobEntries[rcJob->GetJobEntry().m_jobRunKey] = rcJob;
}
void RCQueueSortModel::RemoveJobIdEntry(AssetProcessor::RCJob* rcJob)
{
m_currentJobRunKeyToJobEntries.erase(rcJob->GetJobEntry().m_jobRunKey);
}
void RCQueueSortModel::OnEscalateJobs(AssetProcessor::JobIdEscalationList jobIdEscalationList)
{
for (const auto& jobIdEscalationPair : jobIdEscalationList)
{
auto found = m_currentJobRunKeyToJobEntries.find(jobIdEscalationPair.first);
if (found != m_currentJobRunKeyToJobEntries.end())
{
m_sourceModel->UpdateJobEscalation(found->second, jobIdEscalationPair.second);
}
}
}
} // end namespace AssetProcessor
@@ -0,0 +1,88 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef ASSETPROCESSOR_RCQUEUESORTMODEL_H
#define ASSETPROCESSOR_RCQUEUESORTMODEL_H
#if !defined(Q_MOC_RUN)
#include <QSortFilterProxyModel>
#include <QSet>
#include <QString>
#include "native/utilities/AssetUtilEBusHelper.h"
#include <AzCore/std/containers/unordered_map.h>
#include "native/assetprocessor.h"
#endif
class RCcontrollerUnitTests;
namespace AssetProcessor
{
class QueueElementID;
class RCJobListModel;
class RCJob;
//! This sort and filtering proxy model attaches to the raw RC job list
//! And presents it in the optimal order for processing rather than display.
//! The current desired order is
//! * Critical (currently Copy) jobs for currently connected platforms
//! * Jobs in Sync Compile Requests for currently connected platforms (with most recent requests first)
//! * Jobs in Async Compile Lists for currently connected platforms
//! * Remaining jobs in currently connected platforms, in priority order
//! (The same, repeated, for unconnected platforms).
class RCQueueSortModel
: public QSortFilterProxyModel
, protected AssetProcessorPlatformBus::Handler
{
Q_OBJECT
friend class ::RCcontrollerUnitTests;
public:
explicit RCQueueSortModel(QObject* parent = 0);
void AttachToModel(RCJobListModel* target);
RCJob* GetNextPendingJob();
void AddJobIdEntry(AssetProcessor::RCJob* rcJob);
void RemoveJobIdEntry(AssetProcessor::RCJob* rcJob);
// implement QSortFilteRProxyModel:
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
public Q_SLOTS:
void OnEscalateJobs(AssetProcessor::JobIdEscalationList jobIdEscalationList);
protected:
typedef AZStd::unordered_map<AZ::s64, AssetProcessor::RCJob*> JobRunKeyToRCJobMap;
JobRunKeyToRCJobMap m_currentJobRunKeyToJobEntries;
QSet<QString> m_currentlyConnectedPlatforms;
bool m_dirtyNeedsResort = false; // instead of constantly resorting, we resort only when someone wants to pull an element from us
// ---------------------------------------------------------
// AssetProcessorPlatformBus::Handler
void AssetProcessorPlatformConnected(const AZStd::string platform) override;
void AssetProcessorPlatformDisconnected(const AZStd::string platform) override;
// -----------
RCJobListModel* m_sourceModel;
private Q_SLOTS:
void ProcessPlatformChangeMessage(QString platformName, bool connected);
};
} // namespace AssetProcessor
#endif //ASSETPROCESSOR_RCQUEUESORTMODEL_H
@@ -0,0 +1,431 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "rccontroller.h"
#include <native/resourcecompiler/RCCommon.h>
#include <QTimer>
#include <QThreadPool>
namespace AssetProcessor
{
RCController::RCController(int cfg_minJobs, int cfg_maxJobs, QObject* parent)
: QObject(parent)
, m_dispatchingJobs(false)
, m_shuttingDown(false)
{
AssetProcessorPlatformBus::Handler::BusConnect();
// Determine a good starting value for max jobs
int maxJobs = QThread::idealThreadCount();
if (maxJobs == -1)
{
maxJobs = 3;
}
maxJobs = qMax<int>(maxJobs - 1, 1);
// if the user has specified max jobs in the cfg file, then we obey their request
// regardless of whether they have chosen something bad or not - they would have had to explicitly
// pick this value (we ship with default 0 meaning auto), so if they've changed it, they intend it that way
m_maxJobs = cfg_maxJobs ? qMax(cfg_minJobs, cfg_maxJobs) : maxJobs;
m_RCQueueSortModel.AttachToModel(&m_RCJobListModel);
// make sure that the global thread pool has enough slots to accomidate your request though, since
// by default, the global thread pool has idealThreadCount() slots only.
// leave an extra slot for non-job work.
int currentMaxThreadCount = QThreadPool::globalInstance()->maxThreadCount();
int newMaxThreadCount = qMax<int>(currentMaxThreadCount, m_maxJobs + 1);
QThreadPool::globalInstance()->setMaxThreadCount(newMaxThreadCount);
QObject::connect(this, &RCController::EscalateJobs, &m_RCQueueSortModel, &AssetProcessor::RCQueueSortModel::OnEscalateJobs);
}
RCController::~RCController()
{
AssetProcessorPlatformBus::Handler::BusDisconnect();
m_RCQueueSortModel.AttachToModel(nullptr);
}
RCJobListModel* RCController::GetQueueModel()
{
return &m_RCJobListModel;
}
void RCController::StartJob(RCJob* rcJob)
{
Q_ASSERT(rcJob);
// request to be notified when job is done
QObject::connect(rcJob, &RCJob::Finished, this, [this, rcJob]()
{
FinishJob(rcJob);
}, Qt::QueuedConnection);
// Mark as "being processed" by moving to Processing list
m_RCJobListModel.markAsProcessing(rcJob);
m_RCJobListModel.markAsStarted(rcJob);
Q_EMIT JobStatusChanged(rcJob->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::InProgress);
rcJob->Start();
Q_EMIT JobStarted(rcJob->GetJobEntry().m_pathRelativeToWatchFolder, QString::fromUtf8(rcJob->GetPlatformInfo().m_identifier.c_str()));
}
void RCController::QuitRequested()
{
m_shuttingDown = true;
if (m_RCJobListModel.jobsInFlight() == 0)
{
Q_EMIT ReadyToQuit(this);
return;
}
QTimer::singleShot(10, this, SLOT(QuitRequested()));
}
int RCController::NumberOfPendingCriticalJobsPerPlatform(QString platform)
{
return m_pendingCriticalJobsPerPlatform[platform.toLower()];
}
int RCController::NumberOfPendingJobsPerPlatform(QString platform)
{
return m_jobsCountPerPlatform[platform.toLower()];
}
void RCController::FinishJob(RCJob* rcJob)
{
m_RCQueueSortModel.RemoveJobIdEntry(rcJob);
QString platform = rcJob->GetPlatformInfo().m_identifier.c_str();
auto found = m_jobsCountPerPlatform.find(platform);
if (found != m_jobsCountPerPlatform.end())
{
int prevCount = found.value();
if (prevCount > 0)
{
int newCount = prevCount - 1;
m_jobsCountPerPlatform[platform] = newCount;
Q_EMIT JobsInQueuePerPlatform(platform, newCount);
}
}
if (rcJob->IsCritical())
{
int criticalJobsCount = m_pendingCriticalJobsPerPlatform[platform.toLower()] - 1;
m_pendingCriticalJobsPerPlatform[platform.toLower()] = criticalJobsCount;
}
if (rcJob->GetState() == RCJob::cancelled)
{
Q_EMIT FileCancelled(rcJob->GetJobEntry());
}
else if (rcJob->GetState() != RCJob::completed)
{
Q_EMIT FileFailed(rcJob->GetJobEntry());
Q_EMIT JobStatusChanged(rcJob->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Failed);
}
else
{
Q_EMIT FileCompiled(rcJob->GetJobEntry(), AZStd::move(rcJob->GetProcessJobResponse()));
Q_EMIT JobStatusChanged(rcJob->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
}
// Move to Completed list which will mark as "completed"
// unless a different state has been set.
m_RCJobListModel.markAsCompleted(rcJob);
if (!m_dispatchingPaused)
{
Q_EMIT ActiveJobsCountChanged(aznumeric_cast<unsigned int>(m_RCJobListModel.itemCount()));
}
if (!m_shuttingDown)
{
// Start next job only if we are not shutting down
DispatchJobs();
// if there is no next job, and nothing is in flight, we are done.
if (IsIdle())
{
Q_EMIT BecameIdle();
}
}
}
bool RCController::IsIdle()
{
return ((!m_RCQueueSortModel.GetNextPendingJob()) && (m_RCJobListModel.jobsInFlight() == 0));
}
void RCController::JobSubmitted(JobDetails details)
{
AssetProcessor::QueueElementID checkFile(details.m_jobEntry.m_databaseSourceName, details.m_jobEntry.m_platformInfo.m_identifier.c_str(), details.m_jobEntry.m_jobKey);
if (m_RCJobListModel.isInQueue(checkFile))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job is already in queue and has not started yet - ignored [%s, %s, %s]\n", checkFile.GetInputAssetName().toUtf8().data(), checkFile.GetPlatform().toUtf8().data(), checkFile.GetJobDescriptor().toUtf8().data());
return;
}
if (m_RCJobListModel.isInFlight(checkFile))
{
// if the computed fingerprint is the same as the fingerprint of the in-flight job, this is okay.
int existingJobIndex = m_RCJobListModel.GetIndexOfProcessingJob(checkFile);
if (existingJobIndex != -1)
{
RCJob* job = m_RCJobListModel.getItem(existingJobIndex);
bool cancelJob = false;
if (job->GetJobEntry().m_computedFingerprint != details.m_jobEntry.m_computedFingerprint)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Cancelling Job [%s, %s, %s] with old FP %u, replacing with new FP %u \n", checkFile.GetInputAssetName().toUtf8().data(), checkFile.GetPlatform().toUtf8().data(), checkFile.GetJobDescriptor().toUtf8().data(), job->GetJobEntry().m_computedFingerprint, details.m_jobEntry.m_computedFingerprint);
cancelJob = true;
}
else if(!job->GetJobDependencies().empty())
{
// If a job has dependencies, it's very likely it was re-queued as a result of a dependency being changed
// The in-flight job is probably going to fail at best, or use old data at worst, so cancel the in-flight job
AZ_TracePrintf(AssetProcessor::DebugChannel, "Cancelling Job with dependencies [%s, %s, %s], replacing with re-queued job\n",
checkFile.GetInputAssetName().toUtf8().data(), checkFile.GetPlatform().toUtf8().data(), checkFile.GetJobDescriptor().toUtf8().data());
cancelJob = true;
}
else
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job is already in progress but has the same computed fingerprint (%u) - ignored [%s, %s, %s]\n", details.m_jobEntry.m_computedFingerprint, checkFile.GetInputAssetName().toUtf8().data(), checkFile.GetPlatform().toUtf8().data(), checkFile.GetJobDescriptor().toUtf8().data());
return;
}
if(cancelJob)
{
job->SetState(RCJob::JobState::cancelled);
AssetBuilderSDK::JobCommandBus::Event(job->GetJobEntry().m_jobRunKey, &AssetBuilderSDK::JobCommandBus::Events::Cancel);
m_RCJobListModel.UpdateRow(existingJobIndex);
}
}
}
RCJob* rcJob = new RCJob(&m_RCJobListModel);
rcJob->Init(details); // note - move operation. From this point on you must use the job details to refer to it.
m_RCQueueSortModel.AddJobIdEntry(rcJob);
m_RCJobListModel.addNewJob(rcJob);
QString platformName = rcJob->GetPlatformInfo().m_identifier.c_str();// we need to get the actual platform from the rcJob
if (rcJob->IsCritical())
{
int criticalJobsCount = m_pendingCriticalJobsPerPlatform[platformName.toLower()] + 1;
m_pendingCriticalJobsPerPlatform[platformName.toLower()] = criticalJobsCount;
}
auto found = m_jobsCountPerPlatform.find(platformName);
if (found != m_jobsCountPerPlatform.end())
{
int newCount = found.value() + 1;
m_jobsCountPerPlatform[platformName] = newCount;
}
else
{
m_jobsCountPerPlatform[platformName] = 1;
}
Q_EMIT JobsInQueuePerPlatform(platformName, m_jobsCountPerPlatform[platformName]);
Q_EMIT JobStatusChanged(rcJob->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Queued);
if (!m_dispatchingPaused)
{
Q_EMIT ActiveJobsCountChanged(aznumeric_cast<unsigned int>(m_RCJobListModel.itemCount()));
}
// Start the job we just received if no job currently running
if ((!m_shuttingDown) && (!m_dispatchingJobs))
{
DispatchJobs();
}
}
void RCController::SetDispatchPaused(bool pause)
{
if (m_dispatchingPaused != pause)
{
m_dispatchingPaused = pause;
if (!pause)
{
if ((!m_shuttingDown) && (!m_dispatchingJobs))
{
DispatchJobs();
Q_EMIT ActiveJobsCountChanged(aznumeric_cast<unsigned int>(m_RCJobListModel.itemCount()));
}
}
}
}
void RCController::DispatchJobsImpl()
{
m_dispatchJobsQueued = false;
if (!m_dispatchingJobs)
{
m_dispatchingJobs = true;
RCJob* rcJob = m_RCQueueSortModel.GetNextPendingJob();
while (m_RCJobListModel.jobsInFlight() < m_maxJobs && rcJob && !m_shuttingDown)
{
if (m_dispatchingPaused)
{
// note, even if dispatching is "paused" we start all "auto fail jobs" so that user gets instant feedback on failure.
if (!rcJob->IsAutoFail())
{
break;
}
}
StartJob(rcJob);
rcJob = m_RCQueueSortModel.GetNextPendingJob();
}
m_dispatchingJobs = false;
}
}
void RCController::DispatchJobs()
{
if (!m_dispatchJobsQueued)
{
m_dispatchJobsQueued = true;
QMetaObject::invokeMethod(this, "DispatchJobsImpl", Qt::QueuedConnection);
}
}
void RCController::OnRequestCompileGroup(AssetProcessor::NetworkRequestID groupID, QString platform, QString searchTerm, AZ::Data::AssetId assetId, bool isStatusRequest, int searchType)
{
// someone has asked for a compile group to be created that conforms to that search term.
// the goal here is to use a heuristic to find any assets that match the search term and place them in a new group
// then respond with the appropriate response.
// lets do some minimal processing on the search term
AssetProcessor::JobIdEscalationList escalationList;
QSet<AssetProcessor::QueueElementID> results;
if (assetId.IsValid())
{
m_RCJobListModel.PerformUUIDSearch(assetId.m_guid, platform, results, escalationList, isStatusRequest);
}
else
{
m_RCJobListModel.PerformHeuristicSearch(AssetUtilities::NormalizeAndRemoveAlias(searchTerm), platform, results, escalationList, isStatusRequest, searchType);
}
if (results.isEmpty())
{
// nothing found
Q_EMIT CompileGroupCreated(groupID, AzFramework::AssetSystem::AssetStatus_Unknown);
AZ_TracePrintf(AssetProcessor::DebugChannel, "OnRequestCompileGroup: %s - %s requested, but no matching source assets found.\n", searchTerm.toUtf8().constData(), assetId.ToString<AZStd::string>().c_str());
}
else
{
// it is not necessary to denote the search terms or list of results here because
// PerformHeursticSearch already prints out the results.
m_RCQueueSortModel.OnEscalateJobs(escalationList);
m_activeCompileGroups.push_back(AssetCompileGroup());
m_activeCompileGroups.back().m_groupMembers.swap(results);
m_activeCompileGroups.back().m_requestID = groupID;
Q_EMIT CompileGroupCreated(groupID, AzFramework::AssetSystem::AssetStatus_Queued);
}
}
void RCController::OnEscalateJobsBySearchTerm(QString platform, QString searchTerm)
{
AssetProcessor::JobIdEscalationList escalationList;
QSet<AssetProcessor::QueueElementID> results;
m_RCJobListModel.PerformHeuristicSearch(AssetUtilities::NormalizeAndRemoveAlias(searchTerm), platform, results, escalationList, true);
if (!results.isEmpty())
{
// it is not necessary to denote the search terms or list of results here because
// PerformHeursticSearch already prints out the results.
m_RCQueueSortModel.OnEscalateJobs(escalationList);
}
// do not print a warning out when this fails, its fine for things to escalate jobs as a matter of course just to "make sure" they are escalated
// and its fine if none are in the build queue.
}
void RCController::OnEscalateJobsBySourceUUID(QString platform, AZ::Uuid sourceUuid)
{
AssetProcessor::JobIdEscalationList escalationList;
QSet<AssetProcessor::QueueElementID> results;
m_RCJobListModel.PerformUUIDSearch(sourceUuid, platform, results, escalationList, true);
if (!results.isEmpty())
{
for (const AssetProcessor::QueueElementID& result : results)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "OnEscalateJobsBySourceUUID: %s --> %s\n", sourceUuid.ToString<AZStd::string>().c_str(), result.GetInputAssetName().toUtf8().constData());
}
m_RCQueueSortModel.OnEscalateJobs(escalationList);
}
// do not print a warning out when this fails, its fine for things to escalate jobs as a matter of course just to "make sure" they are escalated
// and its fine if none are in the build queue.
}
void RCController::OnJobComplete(JobEntry completeEntry, AzToolsFramework::AssetSystem::JobStatus state)
{
if (m_activeCompileGroups.empty())
{
return;
}
QueueElementID jobQueueId(completeEntry.m_databaseSourceName, completeEntry.m_platformInfo.m_identifier.c_str(), completeEntry.m_jobKey);
// only the 'completed' status means success:
bool statusSucceeded = (state == AzToolsFramework::AssetSystem::JobStatus::Completed);
// start at the end so that we can actually erase the compile groups and not skip any:
for (int groupIdx = m_activeCompileGroups.size() - 1; groupIdx >= 0; --groupIdx)
{
AssetCompileGroup& compileGroup = m_activeCompileGroups[groupIdx];
auto it = compileGroup.m_groupMembers.find(jobQueueId);
if (it != compileGroup.m_groupMembers.end())
{
compileGroup.m_groupMembers.erase(it);
if ((compileGroup.m_groupMembers.isEmpty()) || (!statusSucceeded))
{
// if we get here, we're either empty (and succeeded) or we failed one and have now failed
Q_EMIT CompileGroupFinished(compileGroup.m_requestID, statusSucceeded ? AzFramework::AssetSystem::AssetStatus_Compiled: AzFramework::AssetSystem::AssetStatus_Failed);
m_activeCompileGroups.removeAt(groupIdx);
}
}
}
}
void RCController::RemoveJobsBySource(QString relSourceFileDatabaseName)
{
// some jobs may have not been started yet, these need to be removed manually
AZStd::vector<RCJob*> pendingJobs;
m_RCJobListModel.EraseJobs(relSourceFileDatabaseName, pendingJobs);
// force finish all pending jobs
for (auto* rcJob : pendingJobs)
{
FinishJob(rcJob);
}
}
void RCController::OnAddedToCatalog(JobEntry jobEntry)
{
AssetProcessor::QueueElementID checkFile(jobEntry.m_databaseSourceName, jobEntry.m_platformInfo.m_identifier.c_str(), jobEntry.m_jobKey);
m_RCJobListModel.markAsCataloged(checkFile);
DispatchJobs();
}
} // Namespace AssetProcessor
@@ -0,0 +1,144 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef RCCONTROLLER_H
#define RCCONTROLLER_H
#if !defined(Q_MOC_RUN)
#include "RCCommon.h"
#include <QObject>
#include <QProcess>
#include <QDir>
#include <QList>
#include "native/utilities/AssetUtilEBusHelper.h"
#include "rcjoblistmodel.h"
#include "RCQueueSortModel.h"
#include <AzFramework/Asset/AssetProcessorMessages.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#endif
class RCcontrollerUnitTests;
namespace AssetProcessor
{
/**
* The RCController class controls the receiving of job requests, adding them to the model,
* running RC and sending of responses
*/
class RCController
: public QObject
, public AssetProcessorPlatformBus::Handler
{
friend class ::RCcontrollerUnitTests;
Q_OBJECT
public:
enum CommandType
{
cmdUnknown = 0,
cmdExecute,
cmdTerminate
};
RCController() = default;
explicit RCController(int minJobs, int maxJobs, QObject* parent = 0);
virtual ~RCController();
AssetProcessor::RCJobListModel* GetQueueModel();
void StartJob(AssetProcessor::RCJob* rcJob);
int NumberOfPendingCriticalJobsPerPlatform(QString platform);
void SetSystemRoot(const QDir& systemRoot);
int NumberOfPendingJobsPerPlatform(QString platform);
bool IsIdle();
bool IsPriorityCopyJob(AssetProcessor::RCJob* rcJob);
Q_SIGNALS:
void FileCompiled(JobEntry entry, AssetBuilderSDK::ProcessJobResponse response);
void FileFailed(JobEntry entry);
void FileCancelled(JobEntry entry);
//void AssetStatus(JobEntry jobEntry, AzFramework::AssetProcessor::AssetStatus status);
void RcError(QString error);
void ReadyToQuit(QObject* source); //After receiving QuitRequested, you must send this when its safe
///! JobStarted will notify with a path name relative to the watch folder it was found in (not the database sourcename column)
void JobStarted(QString inputFile, QString platform);
void JobStatusChanged(JobEntry entry, AzToolsFramework::AssetSystem::JobStatus status);
void JobsInQueuePerPlatform(QString platform, int jobs);
void ActiveJobsCountChanged(unsigned int jobs); // This is the count of jobs which are either queued or inflight
void BecameIdle();
//! This will be signalled upon compile group creation - or failure to do so (in which case status will be unknown)
void CompileGroupCreated(AssetProcessor::NetworkRequestID groupID, AzFramework::AssetSystem::AssetStatus status);
//! Once a compile group has an error or finished, this will be invoked.
void CompileGroupFinished(AssetProcessor::NetworkRequestID groupID, AzFramework::AssetSystem::AssetStatus status);
void EscalateJobs(AssetProcessor::JobIdEscalationList jobIdEscalationList);
public Q_SLOTS:
void JobSubmitted(JobDetails details);
void QuitRequested();
//! This will be called in order to create a compile group and start tracking it.
void OnRequestCompileGroup(AssetProcessor::NetworkRequestID groupID, QString platform, QString searchTerm, AZ::Data::AssetId assetId, bool isStatusRequest = true, int searchType = 0);
void OnEscalateJobsBySearchTerm(QString platform, QString searchTerm);
void OnEscalateJobsBySourceUUID(QString platform, AZ::Uuid sourceUuid);
void DispatchJobs();
void DispatchJobsImpl();
//! Pause or unpause dispatching, only necessary on startup to avoid thrashing and make sure no jobs jump the gun.
void SetDispatchPaused(bool pause);
//! All jobs which match this source will be cancelled or removed. Note that relSourceFile should have any applicable output prefixes!
void RemoveJobsBySource(QString relSourceFileDatabaseName);
// when the AP is truly done with a particular job and its going to be deleted and nothing more cares about it,
// this function is called. this allows us to synchronize the various threads (catalog, queue, etc) to know that
// its completely done.
void OnJobComplete(JobEntry completeEntry, AzToolsFramework::AssetSystem::JobStatus status);
void OnAddedToCatalog(JobEntry jobEntry);
private:
void FinishJob(AssetProcessor::RCJob* rcJob);
unsigned int m_maxJobs;
bool m_dispatchingJobs = false;
bool m_shuttingDown = false;
bool m_dispatchingPaused = true;// dispatching starts out paused.
bool m_dispatchJobsQueued = false;
QMap<QString, int> m_jobsCountPerPlatform;// This stores the count of jobs per platform in the RC Queue
QMap<QString, int> m_pendingCriticalJobsPerPlatform;// This stores the count of pending critical jobs per platform in the RC Queue
AssetProcessor::RCJobListModel m_RCJobListModel;
AssetProcessor::RCQueueSortModel m_RCQueueSortModel;
//! An Asset Compile Group is a set of assets that we're tracking the compilation of
//! It consists of a whole bunch of assets and is considered to be "complete" when either one of the assets in the group fails
//! Or all assets in the group have finished.
class AssetCompileGroup
{
public:
AssetProcessor::NetworkRequestID m_requestID;
QSet<AssetProcessor::QueueElementID> m_groupMembers;
};
QList<AssetCompileGroup> m_activeCompileGroups;
};
} // namespace AssetProcessor
#endif // RCCONTROLLER_H
@@ -0,0 +1,965 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "rcjob.h"
#include <AzToolsFramework/UI/Logging/LogLine.h>
#include <native/utilities/BuilderManager.h>
#include <native/utilities/ThreadHelper.h>
#include <QtConcurrent/QtConcurrentRun>
#include <QElapsedTimer>
#include "native/utilities/JobDiagnosticTracker.h"
namespace
{
unsigned long s_jobSerial = 1;
bool s_typesRegistered = false;
// You have up to 60 minutes to finish processing an asset.
// This was increased from 10 to account for PVRTC compression
// taking up to an hour for large normal map textures, and should
// be reduced again once we move to the ASTC compression format, or
// find another solution to reduce processing times to be reasonable.
const unsigned int g_jobMaximumWaitTime = 1000 * 60 * 60;
const unsigned int g_sleepDurationForLockingAndFingerprintChecking = 100;
const unsigned int g_graceTimeBeforeLockingAndFingerprintChecking = 300;
const unsigned int g_timeoutInSecsForRetryingCopy = 30;
const char* const s_tempString = "%TEMP%";
const char* const s_jobLogFileName = "jobLog.xml";
bool MoveCopyFile(QString sourceFile, QString productFile, bool isCopyJob = false)
{
if (!isCopyJob && (AssetUtilities::MoveFileWithTimeout(sourceFile, productFile, g_timeoutInSecsForRetryingCopy)))
{
//We do not want to rename the file if it is a copy job
return true;
}
else if (AssetUtilities::CopyFileWithTimeout(sourceFile, productFile, g_timeoutInSecsForRetryingCopy))
{
// try to copy instead
return true;
}
AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Failed to move OR copy file from Source directory: %s to Destination Directory: %s", sourceFile.toUtf8().data(), productFile.toUtf8().data());
return false;
}
}
using namespace AssetProcessor;
bool Params::IsValidParams() const
{
return (!m_finalOutputDir.isEmpty());
}
bool RCParams::IsValidParams() const
{
return (
(!m_rcExe.isEmpty()) &&
(!m_rootDir.isEmpty()) &&
(!m_inputFile.isEmpty()) &&
Params::IsValidParams()
);
}
namespace AssetProcessor
{
RCJob::RCJob(QObject* parent)
: QObject(parent)
, m_timeCreated(QDateTime::currentDateTime())
, m_scanFolderID(0)
{
m_jobState = RCJob::pending;
if (!s_typesRegistered)
{
qRegisterMetaType<RCParams>("RCParams");
qRegisterMetaType<BuilderParams>("BuilderParams");
qRegisterMetaType<JobOutputInfo>("JobOutputInfo");
s_typesRegistered = true;
}
}
RCJob::~RCJob()
{
}
void RCJob::Init(JobDetails& details)
{
m_jobDetails = AZStd::move(details);
m_queueElementID = QueueElementID(GetJobEntry().m_databaseSourceName, GetPlatformInfo().m_identifier.c_str(), GetJobKey());
}
const JobEntry& RCJob::GetJobEntry() const
{
return m_jobDetails.m_jobEntry;
}
QDateTime RCJob::GetTimeCreated() const
{
return m_timeCreated;
}
void RCJob::SetTimeCreated(const QDateTime& timeCreated)
{
m_timeCreated = timeCreated;
}
QDateTime RCJob::GetTimeLaunched() const
{
return m_timeLaunched;
}
void RCJob::SetTimeLaunched(const QDateTime& timeLaunched)
{
m_timeLaunched = timeLaunched;
}
QDateTime RCJob::GetTimeCompleted() const
{
return m_timeCompleted;
}
void RCJob::SetTimeCompleted(const QDateTime& timeCompleted)
{
m_timeCompleted = timeCompleted;
}
AZ::u32 RCJob::GetOriginalFingerprint() const
{
return m_jobDetails.m_jobEntry.m_computedFingerprint;
}
void RCJob::SetOriginalFingerprint(unsigned int fingerprint)
{
m_jobDetails.m_jobEntry.m_computedFingerprint = fingerprint;
}
RCJob::JobState RCJob::GetState() const
{
return m_jobState;
}
void RCJob::SetState(const JobState& state)
{
bool wasPending = (m_jobState == pending);
m_jobState = state;
if ((wasPending)&&(m_jobState == cancelled))
{
// if we were pending (had not started yet) and we are now canceled, we still have to emit the finished signal
// so that all the various systems waiting for us can do their housekeeping.
Q_EMIT Finished();
}
}
void RCJob::SetJobEscalation(int jobEscalation)
{
m_JobEscalation = jobEscalation;
}
void RCJob::SetCheckExclusiveLock(bool value)
{
m_jobDetails.m_jobEntry.m_checkExclusiveLock = value;
}
QString RCJob::GetStateDescription(const RCJob::JobState& state)
{
switch (state)
{
case RCJob::pending:
return tr("Pending");
case RCJob::processing:
return tr("Processing");
case RCJob::completed:
return tr("Completed");
case RCJob::crashed:
return tr("Crashed");
case RCJob::terminated:
return tr("Terminated");
case RCJob::failed:
return tr("Failed");
case RCJob::cancelled:
return tr("Cancelled");
}
return QString();
}
const AZ::Uuid& RCJob::GetInputFileUuid() const
{
return m_jobDetails.m_jobEntry.m_sourceFileUUID;
}
QString RCJob::GetFinalOutputPath() const
{
return m_jobDetails.m_destinationPath;
}
const AssetBuilderSDK::PlatformInfo& RCJob::GetPlatformInfo() const
{
return m_jobDetails.m_jobEntry.m_platformInfo;
}
AssetBuilderSDK::ProcessJobResponse& RCJob::GetProcessJobResponse()
{
return m_processJobResponse;
}
void RCJob::PopulateProcessJobRequest(AssetBuilderSDK::ProcessJobRequest& processJobRequest)
{
processJobRequest.m_jobDescription.m_critical = IsCritical();
processJobRequest.m_jobDescription.m_additionalFingerprintInfo = m_jobDetails.m_extraInformationForFingerprinting;
processJobRequest.m_jobDescription.m_jobKey = GetJobKey().toUtf8().data();
processJobRequest.m_jobDescription.m_jobParameters = AZStd::move(m_jobDetails.m_jobParam);
processJobRequest.m_jobDescription.SetPlatformIdentifier(GetPlatformInfo().m_identifier.c_str());
processJobRequest.m_jobDescription.m_priority = GetPriority();
processJobRequest.m_platformInfo = GetPlatformInfo();
processJobRequest.m_builderGuid = GetBuilderGuid();
processJobRequest.m_sourceFile = GetJobEntry().m_pathRelativeToWatchFolder.toUtf8().data();
processJobRequest.m_sourceFileUUID = GetInputFileUuid();
processJobRequest.m_watchFolder = GetJobEntry().m_watchFolderPath.toUtf8().data();
processJobRequest.m_fullPath = GetJobEntry().GetAbsoluteSourcePath().toUtf8().data();
processJobRequest.m_jobId = GetJobEntry().m_jobRunKey;
}
QString RCJob::GetJobKey() const
{
return m_jobDetails.m_jobEntry.m_jobKey;
}
AZ::Uuid RCJob::GetBuilderGuid() const
{
return m_jobDetails.m_jobEntry.m_builderGuid;
}
bool RCJob::IsCritical() const
{
return m_jobDetails.m_critical;
}
bool RCJob::IsAutoFail() const
{
return m_jobDetails.m_autoFail;
}
int RCJob::GetPriority() const
{
return m_jobDetails.m_priority;
}
const AZStd::vector<AssetProcessor::JobDependencyInternal>& RCJob::GetJobDependencies()
{
return m_jobDetails.m_jobDependencyList;
}
void RCJob::Start()
{
// the following trace can be uncommented if there is a need to deeply inspect job running.
//AZ_TracePrintf(AssetProcessor::DebugChannel, "JobTrace Start(%i %s,%s,%s)\n", this, GetInputFileAbsolutePath().toUtf8().data(), GetPlatform().toUtf8().data(), GetJobKey().toUtf8().data());
AssetUtilities::QuitListener listener;
listener.BusConnect();
RCParams rc(this);
BuilderParams builderParams(this);
//Create the process job request
AssetBuilderSDK::ProcessJobRequest processJobRequest;
PopulateProcessJobRequest(processJobRequest);
builderParams.m_processJobRequest = processJobRequest;
builderParams.m_finalOutputDir = GetFinalOutputPath();
builderParams.m_assetBuilderDesc = m_jobDetails.m_assetBuilderDesc;
// when the job finishes, record the results and emit Finished()
connect(this, &RCJob::JobFinished, this, [this](AssetBuilderSDK::ProcessJobResponse result)
{
m_processJobResponse = AZStd::move(result);
switch (m_processJobResponse.m_resultCode)
{
case AssetBuilderSDK::ProcessJobResult_Crashed:
{
SetState(crashed);
}
break;
case AssetBuilderSDK::ProcessJobResult_Success:
{
SetState(completed);
}
break;
case AssetBuilderSDK::ProcessJobResult_Cancelled:
{
SetState(cancelled);
}
break;
default:
{
SetState(failed);
}
break;
}
Q_EMIT Finished();
});
if (!listener.WasQuitRequested())
{
QtConcurrent::run(&RCJob::ExecuteBuilderCommand, builderParams);
}
else
{
AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Job canceled due to quit being requested.");
SetState(terminated);
Q_EMIT Finished();
}
listener.BusDisconnect();
}
void RCJob::ExecuteBuilderCommand(BuilderParams builderParams)
{
// Note: this occurs inside a worker thread.
// listen for the user quitting (CTRL-C or otherwise)
AssetUtilities::QuitListener listener;
listener.BusConnect();
QElapsedTimer ticker;
ticker.start();
AssetBuilderSDK::ProcessJobResponse result;
if (builderParams.m_rcJob->m_jobDetails.m_autoFail)
{
// if this is an auto-fail job, we should avoid doing any additional work besides the work required to fail the job and
// write the details into its log. This is because Auto-fail jobs have 'incomplete' job descriptors, and only exist to
// force a job to fail with a reasonable log file stating the reason for failure. An example of where it is useful to
// use auto-fail jobs is when, after compilation was successful, something goes wrong integrating the result into the
// cache. (For example, files collide, or the product file name would be too long). The job will have at that point
// already completed, the thread long gone, so we can 'append' to the log in this manner post-build by creating a new
// job that will automatically fail and ingest the old (success) log along with additional fail reasons and then fail.
AutoFailJob(builderParams);
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
Q_EMIT builderParams.m_rcJob->JobFinished(result);
return;
}
// We are adding a grace time before we check exclusive lock and validate the fingerprint of the file.
// This grace time should prevent multiple jobs from getting added to the queue if the source file is still updating.
qint64 milliSecsDiff = QDateTime::currentMSecsSinceEpoch() - builderParams.m_rcJob->GetJobEntry().m_computedFingerprintTimeStamp;
if (milliSecsDiff < g_graceTimeBeforeLockingAndFingerprintChecking)
{
QThread::msleep(aznumeric_cast<unsigned long>(g_graceTimeBeforeLockingAndFingerprintChecking - milliSecsDiff));
}
// Lock and unlock the source file to ensure it is not still open by another process.
// This prevents premature processing of some source files that are opened for writing, but are zero bytes for longer than the modification threshhold
QString inputFile = builderParams.m_rcJob->GetJobEntry().GetAbsoluteSourcePath();
if (builderParams.m_rcJob->GetJobEntry().m_checkExclusiveLock && QFile::exists(inputFile))
{
// We will only continue once we get exclusive lock on the source file
while (!AssetUtilities::CheckCanLock(inputFile))
{
QThread::msleep(g_sleepDurationForLockingAndFingerprintChecking);
if (listener.WasQuitRequested() || (ticker.elapsed() > g_jobMaximumWaitTime))
{
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
Q_EMIT builderParams.m_rcJob->JobFinished(result);
return;
}
}
}
// We will only continue once the fingerprint of the file stops changing
unsigned int fingerprint = AssetUtilities::GenerateFingerprint(builderParams.m_rcJob->m_jobDetails);
while (fingerprint != builderParams.m_rcJob->GetOriginalFingerprint())
{
builderParams.m_rcJob->SetOriginalFingerprint(fingerprint);
QThread::msleep(g_sleepDurationForLockingAndFingerprintChecking);
if (listener.WasQuitRequested() || (ticker.elapsed() > g_jobMaximumWaitTime))
{
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
Q_EMIT builderParams.m_rcJob->JobFinished(result);
return;
}
fingerprint = AssetUtilities::GenerateFingerprint(builderParams.m_rcJob->m_jobDetails);
}
Q_EMIT builderParams.m_rcJob->BeginWork();
// We will actually start working on the job after this point and even if RcController gets the same job again, we will put it in the queue for processing
builderParams.m_rcJob->DoWork(result, builderParams, listener);
Q_EMIT builderParams.m_rcJob->JobFinished(result);
}
void RCJob::AutoFailJob(BuilderParams& builderParams)
{
// force the fail data to be captured to the log file.
// because this is being executed in a thread worker, this won't stomp the main thread's job id.
AssetProcessor::SetThreadLocalJobId(builderParams.m_rcJob->GetJobEntry().m_jobRunKey);
AssetUtilities::JobLogTraceListener jobLogTraceListener(builderParams.m_rcJob->m_jobDetails.m_jobEntry);
QString sourceFullPath(builderParams.m_processJobRequest.m_fullPath.c_str());
auto failReason = builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.find(AZ_CRC(AssetProcessor::AutoFailReasonKey));
if (failReason != builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.end())
{
// you are allowed to have many lines in your fail reason.
AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Failed processing %s", sourceFullPath.toUtf8().data());
AZStd::vector<AZStd::string> delimited;
AzFramework::StringFunc::Tokenize(failReason->second.c_str(), delimited, "\n");
for (const AZStd::string& token : delimited)
{
AZ_Error(AssetBuilderSDK::ErrorWindow, false, "%s", token.c_str());
}
}
else
{
// since we didn't have a custom auto-fail reason, add a token to the log file that will help with
// forensic debugging to differentiate auto-fails from regular fails (although it should also be
// obvious from the output in other ways)
AZ_TracePrintf("Debug", "(auto-failed)\n");
}
auto failLogFile = builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.find(AZ_CRC(AssetProcessor::AutoFailLogFile));
if (failLogFile != builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.end())
{
AzToolsFramework::Logging::LogLine::ParseLog(failLogFile->second.c_str(), failLogFile->second.size(),
[](AzToolsFramework::Logging::LogLine& target)
{
switch (target.GetLogType())
{
case AzToolsFramework::Logging::LogLine::TYPE_DEBUG:
AZ_TracePrintf(target.GetLogWindow().c_str(), "%s", target.GetLogMessage().c_str());
break;
case AzToolsFramework::Logging::LogLine::TYPE_MESSAGE:
AZ_TracePrintf(target.GetLogWindow().c_str(), "%s", target.GetLogMessage().c_str());
break;
case AzToolsFramework::Logging::LogLine::TYPE_WARNING:
AZ_Warning(target.GetLogWindow().c_str(), false, "%s", target.GetLogMessage().c_str());
break;
case AzToolsFramework::Logging::LogLine::TYPE_ERROR:
AZ_Error(target.GetLogWindow().c_str(), false, "%s", target.GetLogMessage().c_str());
break;
case AzToolsFramework::Logging::LogLine::TYPE_CONTEXT:
AZ_TracePrintf(target.GetLogWindow().c_str(), " %s", target.GetLogMessage().c_str());
break;
}
});
}
// note that this line below is printed out to be consistent with the output from a job that normally failed, so
// applications reading log file will find it.
AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Builder indicated that the job has failed.\n");
if (builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.find(AZ_CRC(AssetProcessor::AutoFailOmitFromDatabaseKey)) != builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.end())
{
// we don't add Auto-fail jobs to the database if they have asked to be emitted.
builderParams.m_rcJob->m_jobDetails.m_jobEntry.m_addToDatabase = false;
}
AssetProcessor::SetThreadLocalJobId(0);
}
void RCJob::DoWork(AssetBuilderSDK::ProcessJobResponse& result, BuilderParams& builderParams, AssetUtilities::QuitListener& listener)
{
// Setting job id for logging purposes
AssetProcessor::SetThreadLocalJobId(builderParams.m_rcJob->GetJobEntry().m_jobRunKey);
AssetUtilities::JobLogTraceListener jobLogTraceListener(builderParams.m_rcJob->m_jobDetails.m_jobEntry);
{
AssetBuilderSDK::JobCancelListener JobCancelListener(builderParams.m_rcJob->m_jobDetails.m_jobEntry.m_jobRunKey);
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed; // failed by default
auto warningMessage = builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.find(AZ_CRC(AssetProcessor::JobWarningKey));
if (warningMessage != builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.end())
{
// you are allowed to have many lines in your warning message.
AZStd::vector<AZStd::string> delimited;
AzFramework::StringFunc::Tokenize(warningMessage->second.c_str(), delimited, "\n");
for (const AZStd::string& token : delimited)
{
AZ_Warning(AssetBuilderSDK::WarningWindow, false, "%s", token.c_str());
}
}
// create a temporary directory for Builder to work in.
// lets make it as a subdir of a known temp dir
QString workFolder;
if (!AssetUtilities::CreateTempWorkspace(workFolder))
{
AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Could not create temporary directory for Builder!\n");
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
Q_EMIT builderParams.m_rcJob->JobFinished(result);
return;
}
builderParams.m_processJobRequest.m_tempDirPath = AZStd::string(workFolder.toUtf8().data());
QString sourceFullPath(builderParams.m_processJobRequest.m_fullPath.c_str());
if (sourceFullPath.length() >= AP_MAX_PATH_LEN)
{
AZ_Warning(AssetBuilderSDK::WarningWindow, false, "Source Asset: %s filepath length %d exceeds the maximum path length (%d) allowed.\n", sourceFullPath.toUtf8().data(), sourceFullPath.length(), AP_MAX_PATH_LEN);
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
}
else
{
if (!JobCancelListener.IsCancelled())
{
bool runProcessJob = true;
if (m_jobDetails.m_checkServer)
{
QFileInfo fileInfo(builderParams.m_processJobRequest.m_sourceFile.c_str());
builderParams.m_serverKey = QString("%1_%2_%3_%4").arg(fileInfo.completeBaseName(), builderParams.m_processJobRequest.m_jobDescription.m_jobKey.c_str(), builderParams.m_processJobRequest.m_platformInfo.m_identifier.c_str()).arg(builderParams.m_rcJob->GetOriginalFingerprint());
bool operationResult = false;
if (AssetUtilities::InServerMode())
{
// sending process job command to the builder
builderParams.m_assetBuilderDesc.m_processJobFunction(builderParams.m_processJobRequest, result);
runProcessJob = false;
if (result.m_resultCode == AssetBuilderSDK::ProcessJobResult_Success)
{
auto beforeStoreResult = BeforeStoringJobResult(builderParams, result);
if (beforeStoreResult.IsSuccess())
{
AssetProcessor::AssetServerBus::BroadcastResult(operationResult, &AssetProcessor::AssetServerBusTraits::StoreJobResult, builderParams, beforeStoreResult.GetValue());
}
else
{
AZ_Warning(AssetBuilderSDK::WarningWindow, false, "Failed preparing store result for %s", builderParams.m_processJobRequest.m_sourceFile.c_str());
}
if (!operationResult)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Unable to save job (%s, %s, %s) with fingerprint (%u) to the server.\n",
builderParams.m_rcJob->GetJobEntry().m_pathRelativeToWatchFolder.toUtf8().data(), builderParams.m_rcJob->GetJobKey().toUtf8().data(),
builderParams.m_rcJob->GetPlatformInfo().m_identifier.c_str(), builderParams.m_rcJob->GetOriginalFingerprint());
}
}
}
else
{
// running as client, check with the server whether it has already
// processed this asset, if not or if the operation fails then process locally
AssetProcessor::AssetServerBus::BroadcastResult(operationResult, &AssetProcessor::AssetServerBusTraits::RetrieveJobResult, builderParams);
if (operationResult)
{
operationResult = AfterRetrievingJobResult(builderParams, jobLogTraceListener, result);
}
else
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Unable to get job (%s, %s, %s) with fingerprint (%u) from the server. Processing locally.\n",
builderParams.m_rcJob->GetJobEntry().m_pathRelativeToWatchFolder.toUtf8().data(), builderParams.m_rcJob->GetJobKey().toUtf8().data(),
builderParams.m_rcJob->GetPlatformInfo().m_identifier.c_str(), builderParams.m_rcJob->GetOriginalFingerprint());
}
runProcessJob = !operationResult;
}
}
if(runProcessJob)
{
result.m_outputProducts.clear();
// sending process job command to the builder
builderParams.m_assetBuilderDesc.m_processJobFunction(builderParams.m_processJobRequest, result);
}
}
}
if (JobCancelListener.IsCancelled())
{
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
}
}
bool shouldRemoveTempFolder = true;
if (result.m_resultCode == AssetBuilderSDK::ProcessJobResult_Success)
{
// do a final check of this job to make sure its not making colliding subIds.
AZStd::unordered_set<AZ::u32> subIdsFound;
for (const AssetBuilderSDK::JobProduct& product : result.m_outputProducts)
{
if (!subIdsFound.insert(product.m_productSubID).second)
{
// if this happens the element was already in the set.
AZ_Error(AssetBuilderSDK::ErrorWindow, false, "The builder created more than one asset with the same subID (%u) when emitting product %s\n Builders should set a unique m_productSubID value for each product, as this is used as part of the address of the asset.", product.m_productSubID, product.m_productFileName.c_str());
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
break;
}
}
}
if(result.m_resultCode == AssetBuilderSDK::ProcessJobResult_Success)
{
bool handledDependencies = true; // True in case there are no outputs
for (const AssetBuilderSDK::JobProduct& jobProduct : result.m_outputProducts)
{
handledDependencies = false; // False by default since there are outputs
if(jobProduct.m_dependenciesHandled)
{
handledDependencies = true;
break;
}
}
if(!handledDependencies)
{
AZ_Warning(AssetBuilderSDK::WarningWindow, false, "The builder (%s) has not indicated it handled outputting product dependencies for file %s. This is a programmer error.", builderParams.m_assetBuilderDesc.m_name.c_str(), builderParams.m_processJobRequest.m_sourceFile.c_str());
AZ_Warning(AssetBuilderSDK::WarningWindow, false, "For builders that output AZ serialized types, it is recommended to use AssetBuilderSDK::OutputObject which will handle outputting product depenedencies and creating the JobProduct. This is fine to use even if your builder never has product dependencies.");
AZ_Warning(AssetBuilderSDK::WarningWindow, false, "For builders that need custom depenedency parsing that cannot be handled by AssetBuilderSDK::OutputObject or ones that output non-AZ serialized types, add the dependencies to m_dependencies and m_pathDependencies on the JobProduct and then set m_dependenciesHandled to true.");
jobLogTraceListener.AddWarning();
}
WarningLevel warningLevel = WarningLevel::Default;
JobDiagnosticRequestBus::BroadcastResult(warningLevel, &JobDiagnosticRequestBus::Events::GetWarningLevel);
const bool hasErrors = jobLogTraceListener.GetErrorCount() > 0;
const bool hasWarnings = jobLogTraceListener.GetWarningCount() > 0;
if(warningLevel == WarningLevel::FatalErrors && hasErrors)
{
AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Failing job, fatal errors setting is enabled");
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
}
else if(warningLevel == WarningLevel::FatalErrorsAndWarnings && (hasErrors || hasWarnings))
{
AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Failing job, fatal errors and warnings setting is enabled");
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
}
}
switch (result.m_resultCode)
{
case AssetBuilderSDK::ProcessJobResult_Success:
// make sure there's no subid collision inside a job.
{
if (!CopyCompiledAssets(builderParams, result))
{
result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
shouldRemoveTempFolder = false;
}
shouldRemoveTempFolder = shouldRemoveTempFolder && !s_createRequestFileForSuccessfulJob;
}
break;
case AssetBuilderSDK::ProcessJobResult_Crashed:
AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Builder indicated that its process crashed!");
break;
case AssetBuilderSDK::ProcessJobResult_Cancelled:
AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Builder indicates that the job was cancelled.");
break;
case AssetBuilderSDK::ProcessJobResult_Failed:
AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Builder indicated that the job has failed.");
shouldRemoveTempFolder = false;
break;
}
if ((shouldRemoveTempFolder) || (listener.WasQuitRequested()))
{
QDir workingDir(QString(builderParams.m_processJobRequest.m_tempDirPath.c_str()));
workingDir.removeRecursively();
}
// Setting the job id back to zero for error detection
AssetProcessor::SetThreadLocalJobId(0);
listener.BusDisconnect();
JobDiagnosticRequestBus::Broadcast(&JobDiagnosticRequestBus::Events::RecordDiagnosticInfo, builderParams.m_rcJob->GetJobEntry().m_jobRunKey, JobDiagnosticInfo(aznumeric_cast<AZ::u32>(jobLogTraceListener.GetWarningCount()), aznumeric_cast<AZ::u32>(jobLogTraceListener.GetErrorCount())));
}
bool RCJob::CopyCompiledAssets(BuilderParams& params, AssetBuilderSDK::ProcessJobResponse& response)
{
if (response.m_outputProducts.empty())
{
// early out here for performance - no need to do anything at all here so don't waste time with IsDir or Exists or anything.
return true;
}
QDir outputDirectory(params.m_finalOutputDir);
QString tempFolder = params.m_processJobRequest.m_tempDirPath.c_str();
QDir tempDir(tempFolder);
if (params.m_finalOutputDir.isEmpty())
{
AZ_Assert(false, "CopyCompiledAssets: params.m_finalOutputDir is empty for an asset processor job. This should not happen and is because of a recent code change. Check history of any new builders or rcjob.cpp\n");
return false;
}
if (!tempDir.exists())
{
AZ_Assert(false, "PCopyCompiledAssets: params.m_processJobRequest.m_tempDirPath is empty for an asset processor job. This should not happen and is because of a recent code change! Check history of RCJob.cpp and any new builder code changes.\n");
return false;
}
// if outputDirectory does not exist then create it
unsigned int waitTimeInSecs = 3;
if (!AssetUtilities::CreateDirectoryWithTimeout(outputDirectory, waitTimeInSecs))
{
AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Failed to create output directory: %s\n", outputDirectory.absolutePath().toUtf8().data());
return false;
}
// copy the built products into the appropriate location in the real cache and update the job status accordingly.
// note that we go to the trouble of first doing all the checking for disk space and existence of the source files
// before we notify the AP or start moving any of the files so that failures cause the least amount of damage possible.
// this vector is a set of pairs where the first of each pair is the source file (absolute) we intend to copy
// and the second is the product destination we intend to copy it to.
QList< QPair<QString, QString> > outputsToCopy;
outputsToCopy.reserve(static_cast<int>(response.m_outputProducts.size()));
qint64 totalFileSizeRequired = 0;
for (AssetBuilderSDK::JobProduct& product : response.m_outputProducts)
{
// each Output Product communicated by the builder will either be
// * a relative path, which means we assume its relative to the temp folder, and we attempt to move the file
// * an absolute path in the temp folder, and we attempt to move also
// * an absolute path outside the temp folder, in which we assume you'd like to just copy a file somewhere.
QString outputProduct = QString::fromUtf8(product.m_productFileName.c_str()); // could be a relative path.
QFileInfo fileInfo(outputProduct);
if (fileInfo.isRelative())
{
// we assume that its relative to the TEMP folder.
fileInfo = QFileInfo(tempDir.absoluteFilePath(outputProduct));
}
QString absolutePathOfSource = fileInfo.absoluteFilePath();
QString outputFilename = fileInfo.fileName();
QString productFile = AssetUtilities::NormalizeFilePath(outputDirectory.filePath(outputFilename.toLower()));
// Don't make productFile all lowercase for case-insensitive as this
// breaks macOS. The case is already setup properly when the job
// was created.
if (productFile.length() >= AP_MAX_PATH_LEN)
{
AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Cannot copy file: Product '%s' path length (%d) exceeds the max path length (%d) allowed on disk\n", productFile.toUtf8().data(), productFile.length(), AP_MAX_PATH_LEN);
return false;
}
QFileInfo inFile(absolutePathOfSource);
if (!inFile.exists())
{
AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Cannot copy file - product file with absolute path '%s' attempting to save into cache could not be found", absolutePathOfSource.toUtf8().constData());
return false;
}
totalFileSizeRequired += inFile.size();
outputsToCopy.push_back(qMakePair(absolutePathOfSource, productFile));
// also update the product file name to be the final resting place of this product in the cache (normalized!)
product.m_productFileName = AssetUtilities::NormalizeFilePath(productFile).toUtf8().constData();
}
// now we can check if there's enough space for ALL the files before we copy any.
bool hasSpace = false;
AssetProcessor::DiskSpaceInfoBus::BroadcastResult(hasSpace, &AssetProcessor::DiskSpaceInfoBusTraits::CheckSufficientDiskSpace, outputDirectory.absolutePath(), totalFileSizeRequired, false);
if (!hasSpace)
{
AZ_Error(AssetProcessor::ConsoleChannel, false, "Cannot save file to cache, not enough disk space to save all the products of %s. Total needed: %lli bytes", params.m_processJobRequest.m_sourceFile.c_str(), totalFileSizeRequired);
return false;
}
// if we get here, we are good to go in terms of disk space and sources existing, so we make the best attempt we can.
// first, we broadcast the name of ALL of the outputs we are about to change:
for (const QPair<QString, QString>& filePair : outputsToCopy)
{
const QString& productAbsolutePath = filePair.second;
// note that this absolute path is a real file system path, and the following API requires normalized paths:
QString normalized = AssetUtilities::NormalizeFilePath(productAbsolutePath);
AssetProcessor::ProcessingJobInfoBus::Broadcast(&AssetProcessor::ProcessingJobInfoBus::Events::BeginCacheFileUpdate, normalized.toUtf8().constData());
}
// after we do the above notify its important that we do not early exit this function without undoing those locks.
bool anyFileFailed = false;
for (const QPair<QString, QString>& filePair : outputsToCopy)
{
const QString& sourceAbsolutePath = filePair.first;
const QString& productAbsolutePath = filePair.second;
bool isCopyJob = !(sourceAbsolutePath.startsWith(tempFolder, Qt::CaseInsensitive));
if (!MoveCopyFile(sourceAbsolutePath, productAbsolutePath, isCopyJob)) // this has its own traceprintf for failure
{
// MoveCopyFile will have output to the log. No need to double output here.
anyFileFailed = true;
continue;
}
//we now ensure that the file is writable - this is just a warning if it fails, not a complete failure.
if (!AssetUtilities::MakeFileWritable(productAbsolutePath))
{
AZ_TracePrintf(AssetBuilderSDK::WarningWindow, "Unable to change permission for the file: %s.\n", productAbsolutePath.toUtf8().data());
}
}
// once we're done, regardless of success or failure, we 'unlock' those files for further process.
// if we failed, also re-trigger them to rebuild (the bool param at the end of the ebus call)
for (const QPair<QString, QString>& filePair : outputsToCopy)
{
const QString& productAbsolutePath = filePair.second;
// note that this absolute path is a real file system path, and the following API requires normalized paths:
QString normalized = AssetUtilities::NormalizeFilePath(productAbsolutePath);
AssetProcessor::ProcessingJobInfoBus::Broadcast(&AssetProcessor::ProcessingJobInfoBus::Events::EndCacheFileUpdate, normalized.toUtf8().constData(), anyFileFailed);
}
return !anyFileFailed;
}
AZ::Outcome<AZStd::vector<AZStd::string>> RCJob::BeforeStoringJobResult(const BuilderParams& builderParams, AssetBuilderSDK::ProcessJobResponse jobResponse)
{
AZStd::string normalizedTempFolderPath = builderParams.m_processJobRequest.m_tempDirPath;
AzFramework::StringFunc::Path::Normalize(normalizedTempFolderPath);
AZStd::vector<AZStd::string> sourceFiles;
for (AssetBuilderSDK::JobProduct& product : jobResponse.m_outputProducts)
{
// Try to handle Absolute paths within the temp folder
if (!AzFramework::StringFunc::Replace(product.m_productFileName, normalizedTempFolderPath.c_str(), s_tempString))
{
// From CopyCompiledAssets:
// each Output Product communicated by the builder will either be
// * a relative path, which means we assume its relative to the temp folder, and we attempt to move the file
// * an absolute path in the temp folder, and we attempt to move also
// * an absolute path outside the temp folder, in which we assume you'd like to just copy a file somewhere.
// We need to handle case 3 here (Case 2 was above, case 1 is treated as relative within temp)
// If the path was not absolute within the temp folder and not relative it should be an absolute path beneath our source (Including the source)
// meaning a copy job which needs to be added to our archive.
if (!AzFramework::StringFunc::Path::IsRelative(product.m_productFileName.c_str()))
{
AZStd::string sourceFile{ builderParams.m_rcJob->GetJobEntry().GetAbsoluteSourcePath().toUtf8().data() };
AzFramework::StringFunc::Path::Normalize(sourceFile);
AzFramework::StringFunc::Path::StripFullName(sourceFile);
AzFramework::StringFunc::Path::Normalize(product.m_productFileName);
size_t sourcePathPos = product.m_productFileName.find(sourceFile.c_str());
if(sourcePathPos != AZStd::string::npos)
{
sourceFiles.push_back(product.m_productFileName.substr(sourceFile.size()).c_str());
AzFramework::StringFunc::Path::Join(s_tempString, product.m_productFileName.substr(sourceFile.size()).c_str(), product.m_productFileName);
}
else
{
AZ_Warning(AssetBuilderSDK::WarningWindow, false, "Failed to find source path %s or temp path %s in non relative path in %s", sourceFile.c_str(), normalizedTempFolderPath.c_str(), product.m_productFileName.c_str());
}
}
}
}
AZStd::string responseFilePath;
AzFramework::StringFunc::Path::ConstructFull(builderParams.m_processJobRequest.m_tempDirPath.c_str(), AssetBuilderSDK::s_processJobResponseFileName, responseFilePath, true);
//Save ProcessJobResponse to disk
if (!AZ::Utils::SaveObjectToFile(responseFilePath, AZ::DataStream::StreamType::ST_XML, &jobResponse))
{
return AZ::Failure();
}
AzToolsFramework::AssetSystem::JobInfo jobInfo;
AzToolsFramework::AssetSystem::AssetJobLogResponse jobLogResponse;
jobInfo.m_sourceFile = builderParams.m_rcJob->GetJobEntry().m_databaseSourceName.toUtf8().data();
jobInfo.m_platform = builderParams.m_rcJob->GetPlatformInfo().m_identifier.c_str();
jobInfo.m_jobKey = builderParams.m_rcJob->GetJobKey().toUtf8().data();
jobInfo.m_builderGuid = builderParams.m_rcJob->GetBuilderGuid();
jobInfo.m_jobRunKey = builderParams.m_rcJob->GetJobEntry().m_jobRunKey;
jobInfo.m_watchFolder = builderParams.m_processJobRequest.m_watchFolder;
AssetUtilities::ReadJobLog(jobInfo, jobLogResponse);
//Save joblog to disk
AZStd::string jobLogFilePath;
AzFramework::StringFunc::Path::ConstructFull(builderParams.m_processJobRequest.m_tempDirPath.c_str(), s_jobLogFileName, jobLogFilePath, true);
if (!AZ::Utils::SaveObjectToFile(jobLogFilePath, AZ::DataStream::StreamType::ST_XML, &jobLogResponse))
{
return AZ::Failure();
}
return AZ::Success(sourceFiles);
}
bool RCJob::AfterRetrievingJobResult(const BuilderParams& builderParams, AssetUtilities::JobLogTraceListener& jobLogTraceListener, AssetBuilderSDK::ProcessJobResponse& jobResponse)
{
AZStd::string responseFilePath;
AzFramework::StringFunc::Path::ConstructFull(builderParams.m_processJobRequest.m_tempDirPath.c_str(), AssetBuilderSDK::s_processJobResponseFileName, responseFilePath, true);
if (!AZ::Utils::LoadObjectFromFileInPlace(responseFilePath.c_str(), jobResponse))
{
return false;
}
//Ensure that ProcessJobResponse have the correct absolute paths
for (AssetBuilderSDK::JobProduct& product : jobResponse.m_outputProducts)
{
AzFramework::StringFunc::Replace(product.m_productFileName, s_tempString, builderParams.m_processJobRequest.m_tempDirPath.c_str(), s_tempString);
}
AZStd::string jobLogFilePath;
AzFramework::StringFunc::Path::ConstructFull(builderParams.m_processJobRequest.m_tempDirPath.c_str(), s_jobLogFileName, jobLogFilePath, true);
AzToolsFramework::AssetSystem::AssetJobLogResponse jobLogResponse;
if (!AZ::Utils::LoadObjectFromFileInPlace(jobLogFilePath.c_str(), jobLogResponse))
{
return false;
}
if (!jobLogResponse.m_isSuccess)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job log request was unsuccessful for job (%s, %s, %s) from the server.\n",
builderParams.m_rcJob->GetJobEntry().m_pathRelativeToWatchFolder.toUtf8().data(), builderParams.m_rcJob->GetJobKey().toUtf8().data(),
builderParams.m_rcJob->GetPlatformInfo().m_identifier.c_str());
if(jobLogResponse.m_jobLog.find("No log file found") != AZStd::string::npos)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Unable to find job log from the server. This could happen if you are trying to use the server cache with a copy job,\
please check the assetprocessorplatformconfig.ini file and ensure that server cache is disabled for the job.\n");
}
return false;
}
// writing server logs
AZ_TracePrintf(AssetProcessor::DebugChannel, "------------SERVER BEGIN----------\n");
AzToolsFramework::Logging::LogLine::ParseLog(jobLogResponse.m_jobLog.c_str(), jobLogResponse.m_jobLog.size(),
[&jobLogTraceListener](AzToolsFramework::Logging::LogLine& line)
{
jobLogTraceListener.AppendLog(line);
});
AZ_TracePrintf(AssetProcessor::DebugChannel, "------------SERVER END----------\n");
return true;
}
AZStd::string BuilderParams::GetTempJobDirectory() const
{
return m_processJobRequest.m_tempDirPath;
}
QString BuilderParams::GetServerKey() const
{
return m_serverKey;
}
} // namespace AssetProcessor
//////////////////////////////////////////////////////////////////////////
@@ -0,0 +1,238 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef RCJOB_H
#define RCJOB_H
#if !defined(Q_MOC_RUN)
#include <QObject>
#include <QString>
#include <QDateTime>
#include <QStringList>
#include <AzCore/base.h>
#include "RCCommon.h"
#include "native/utilities/PlatformConfiguration.h"
#include <AzCore/Math/Uuid.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include "native/assetprocessor.h"
#include <AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h>
#include <QFileInfoList>
#endif
namespace AssetProcessor
{
struct AssetRecognizer;
class RCJob;
//! Params Base class
struct Params
{
Params(AssetProcessor::RCJob* job = nullptr)
: m_rcJob(job)
{}
virtual ~Params() = default;
AssetProcessor::RCJob* m_rcJob;
QString m_finalOutputDir;
Params(const Params&) = default;
virtual bool IsValidParams() const;
};
//! RCParams contains info that is required by the rc
struct RCParams
: public Params
{
QString m_rootDir;
QString m_rcExe;
QString m_inputFile;
QString m_platformIdentifier;
QString m_params;
RCParams(AssetProcessor::RCJob* job = nullptr)
: Params(job)
{}
RCParams(const RCParams&) = default;
bool IsValidParams() const override;
};
//! BuilderParams contains info that is required by the builders
struct BuilderParams
: public Params
{
AssetBuilderSDK::ProcessJobRequest m_processJobRequest;
AssetBuilderSDK::AssetBuilderDesc m_assetBuilderDesc;
QString m_serverKey;
BuilderParams(AssetProcessor::RCJob* job = nullptr)
: Params(job)
{}
BuilderParams(const BuilderParams&) = default;
AZStd::string GetTempJobDirectory() const;
QString GetServerKey() const;
};
//! JobOutputInfo is used to store job related messages.
//! Messages can be an error or just some information.
struct JobOutputInfo
{
QString m_windowName; // window name is used to specify whether it is an error or not
QString m_message;// the actual message
JobOutputInfo() = default;
JobOutputInfo(QString window, QString message)
: m_windowName(window)
, m_message(message)
{
}
};
/**
* The RCJob class contains all the necessary information about a single RC job
*/
class RCJob
: public QObject
{
Q_OBJECT
public:
enum JobState
{
pending,
processing,
completed,
crashed,
terminated,
cancelled,
failed,
};
explicit RCJob(QObject* parent = 0);
virtual ~RCJob();
void Init(JobDetails& details);
QString Params() const;
QString CommandLine() const;
QDateTime GetTimeCreated() const;
void SetTimeCreated(const QDateTime& timeCreated);
QDateTime GetTimeLaunched() const;
void SetTimeLaunched(const QDateTime& timeLaunched);
QDateTime GetTimeCompleted() const;
void SetTimeCompleted(const QDateTime& timeCompleted);
void SetOriginalFingerprint(AZ::u32 originalFingerprint);
AZ::u32 GetOriginalFingerprint() const;
QString GetConsoleOutput() const;
void SetConsoleOutput(QString rcOut);
JobState GetState() const;
void SetState(const JobState& state);
const AZ::Uuid& GetInputFileUuid() const;
QString GetDestination() const;
//! the final output path is where the actual outputs are copied when processing succeeds
//! this will be in the asset cache, in the gamename / platform / gamename folder.
QString GetFinalOutputPath() const;
const AssetProcessor::AssetRecognizer* GetRecognizer() const;
void SetRecognizer(const AssetProcessor::AssetRecognizer* value);
const AssetBuilderSDK::PlatformInfo& GetPlatformInfo() const;
// intentionally non-const to move.
AssetBuilderSDK::ProcessJobResponse& GetProcessJobResponse();
const JobEntry& GetJobEntry() const;
void Start();
const QueueElementID& GetElementID() const { return m_queueElementID; }
const int JobEscalation() { return m_JobEscalation; }
void SetJobEscalation(int jobEscalation);
void SetCheckExclusiveLock(bool value);
Q_SIGNALS:
//! This signal will be emitted when we make sure that no other application has a lock on the source file
//! and also that the fingerprint of the source file is stable and not changing.
//! This will basically indicate that we are starting to perform work on the current job
void BeginWork();
void Finished();
void JobFinished(AssetBuilderSDK::ProcessJobResponse result);
public:
static QString GetStateDescription(const JobState& state);
static void ExecuteBuilderCommand(BuilderParams builderParams);
static void AutoFailJob(BuilderParams& builderParams);
static bool CopyCompiledAssets(BuilderParams& params, AssetBuilderSDK::ProcessJobResponse& response);
//! This method will save the processJobResponse and the job log to the temp directory as xml files.
//! We will be modifying absolute paths in processJobResponse before saving it to the disk.
static AZ::Outcome<AZStd::vector<AZStd::string>> BeforeStoringJobResult(const BuilderParams& builderParams, AssetBuilderSDK::ProcessJobResponse jobResponse);
//! This method will retrieve the processJobResponse and the job log from the temp directory.
//! This method is also responsible for emitting the server job logs to the local job log file.
static bool AfterRetrievingJobResult(const BuilderParams& builderParams, AssetUtilities::JobLogTraceListener& jobLogTraceListener, AssetBuilderSDK::ProcessJobResponse& jobResponse);
QString GetJobKey() const;
AZ::Uuid GetBuilderGuid() const;
bool IsCritical() const;
bool IsAutoFail() const;
int GetPriority() const;
const AZStd::vector<JobDependencyInternal>& GetJobDependencies();
protected:
//! DoWork ensure that the job is ready for being processing and than makes the actual builder call
virtual void DoWork(AssetBuilderSDK::ProcessJobResponse& result, BuilderParams& builderParams, AssetUtilities::QuitListener& listener);
void PopulateProcessJobRequest(AssetBuilderSDK::ProcessJobRequest& processJobRequest);
private:
JobDetails m_jobDetails;
JobState m_jobState;
QueueElementID m_queueElementID; // cached to prevent lots of construction of this all over the place
int m_JobEscalation = AssetProcessor::JobEscalation::Default; // Escalation indicates how important the job is and how soon it needs processing, the greater the number the greater the escalation
QDateTime m_timeCreated;
QDateTime m_timeLaunched;
QDateTime m_timeCompleted;
unsigned int m_exitCode = 0;
AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer m_products;
AssetBuilderSDK::ProcessJobResponse m_processJobResponse;
AZ::u32 m_scanFolderID;
};
} // namespace AssetProcessor
Q_DECLARE_METATYPE(AssetProcessor::BuilderParams);
Q_DECLARE_METATYPE(AssetProcessor::JobOutputInfo);
Q_DECLARE_METATYPE(AssetProcessor::RCParams);
#endif // RCJOB_H
@@ -0,0 +1,543 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "rcjoblistmodel.h"
// uncomment this in order to add additional verbose log output for this class.
// can drastically slow down function since this class is a hotspot
// #define DEBUG_RCJOB_MODEL
namespace AssetProcessor
{
RCJobListModel::RCJobListModel(QObject* parent)
: QAbstractItemModel(parent)
{
}
int RCJobListModel::rowCount(const QModelIndex& parent) const
{
if (parent.isValid())
{
return 0;
}
return itemCount();
}
QModelIndex RCJobListModel::parent(const QModelIndex& /*index*/) const
{
return QModelIndex();
}
QModelIndex RCJobListModel::index(int row, int column, const QModelIndex& parent) const
{
if (row >= rowCount(parent) || column >= columnCount(parent))
{
return QModelIndex();
}
return createIndex(row, column);
}
int RCJobListModel::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : Column::Max;
}
QVariant RCJobListModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
{
switch (section)
{
case ColumnState:
return tr("State");
case ColumnJobId:
return tr("Job Id");
case ColumnCommand:
return tr("Asset");
case ColumnCompleted:
return tr("Completed");
case ColumnPlatform:
return tr("Platform");
default:
break;
}
}
return QAbstractItemModel::headerData(section, orientation, role);
}
unsigned int RCJobListModel::jobsInFlight() const
{
return m_jobsInFlight.size();
}
void RCJobListModel::UpdateJobEscalation(AssetProcessor::RCJob* rcJob, int jobEscalation)
{
for (int idx = 0; idx < rowCount(); ++idx)
{
RCJob* job = getItem(idx);
if (job == rcJob)
{
rcJob->SetJobEscalation(jobEscalation);
Q_EMIT dataChanged(index(idx, 0), index(idx, columnCount() - 1));
break;
}
}
}
void RCJobListModel::UpdateRow(int jobIndex)
{
Q_EMIT dataChanged(index(jobIndex, 0), index(jobIndex, columnCount() - 1));
}
QVariant RCJobListModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
{
return QVariant();
}
if (index.row() >= itemCount())
{
return QVariant();
}
switch (role)
{
case jobIndexRole:
return getItem(index.row())->GetJobEntry().m_jobRunKey;
case stateRole:
return RCJob::GetStateDescription(getItem(index.row())->GetState());
case displayNameRole:
return getItem(index.row())->GetJobEntry().m_pathRelativeToWatchFolder;
case timeCreatedRole:
return getItem(index.row())->GetTimeCreated().toString("hh:mm:ss.zzz");
case timeLaunchedRole:
return getItem(index.row())->GetTimeLaunched().toString("hh:mm:ss.zzz");
case timeCompletedRole:
return getItem(index.row())->GetTimeCompleted().toString("hh:mm:ss.zzz");
case Qt::DisplayRole:
switch (index.column())
{
case ColumnJobId:
return getItem(index.row())->GetJobEntry().m_jobRunKey;
case ColumnState:
return RCJob::GetStateDescription(getItem(index.row())->GetState());
case ColumnCommand:
return getItem(index.row())->GetJobEntry().m_pathRelativeToWatchFolder;
case ColumnCompleted:
return getItem(index.row())->GetTimeCompleted().toString("hh:mm:ss.zzz");
case ColumnPlatform:
return QString::fromUtf8(getItem(index.row())->GetPlatformInfo().m_identifier.c_str());
default:
break;
}
default:
break;
}
return QVariant();
}
int RCJobListModel::itemCount() const
{
return aznumeric_caster(m_jobs.size());
}
RCJob* RCJobListModel::getItem(int index) const
{
if (index >= 0 && index < m_jobs.size())
{
return m_jobs[index];
}
return nullptr; //invalid index
}
bool RCJobListModel::isEmpty()
{
return m_jobs.empty();
}
void RCJobListModel::addNewJob(RCJob* rcJob)
{
int posForInsert = aznumeric_caster(m_jobs.size());
beginInsertRows(QModelIndex(), posForInsert, posForInsert);
m_jobs.push_back(rcJob);
#if defined(DEBUG_RCJOB_MODEL)
AZ_TracePrintf(AssetProcessor::DebugChannel, "JobTrace AddNewJob(%i %s,%s,%s)\n", rcJob, rcJob->GetInputFileAbsolutePath().toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
#endif
bool isPending = false;
if (rcJob->GetState() == RCJob::pending)
{
m_jobsInQueueLookup.insert(rcJob->GetElementID(), rcJob);
isPending = true;
}
endInsertRows();
}
void RCJobListModel::markAsProcessing(RCJob* rcJob)
{
#if defined(DEBUG_RCJOB_MODEL)
AZ_TracePrintf(AssetProcessor::DebugChannel, "JobTrace markAsProcessing(%i %s,%s,%s)\n", rcJob, rcJob->GetJobEntry().m_databaseSourceName.toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
#endif
rcJob->SetState(RCJob::processing);
rcJob->SetTimeLaunched(QDateTime::currentDateTime());
m_jobsInFlight.insert(rcJob);
for(size_t jobIndex = m_jobs.size() - 1; jobIndex >= 0; --jobIndex)
{
if(m_jobs[jobIndex] == rcJob)
{
Q_EMIT dataChanged(index(aznumeric_caster(jobIndex), 0, QModelIndex()), index(aznumeric_caster(jobIndex), 0, QModelIndex()));
return;
}
}
AZ_TracePrintf(AssetProcessor::DebugChannel, "JobTrace jobIndex == -1!!! (%i %s,%s,%s)\n",
rcJob, rcJob->GetJobEntry().m_databaseSourceName.toUtf8().constData(),
rcJob->GetPlatformInfo().m_identifier.c_str(),
rcJob->GetJobKey().toUtf8().constData());
AZ_Assert(false, "Job not found!!!");
}
void RCJobListModel::markAsStarted(RCJob* rcJob)
{
#if defined(DEBUG_RCJOB_MODEL)
AZ_TracePrintf(AssetProcessor::DebugChannel, "JobTrace markAsStarted(%i %s,%s,%s)\n", rcJob, rcJob->GetInputFileAbsolutePath().toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
#endif
auto foundInQueue = m_jobsInQueueLookup.find(rcJob->GetElementID());
while ((foundInQueue != m_jobsInQueueLookup.end()) && (foundInQueue.value() == rcJob))
{
foundInQueue = m_jobsInQueueLookup.erase(foundInQueue);
}
}
void RCJobListModel::markAsCompleted(RCJob* rcJob)
{
#if defined(DEBUG_RCJOB_MODEL)
AZ_TracePrintf(AssetProcessor::DebugChannel, "JobTrace markAsCompleted(%i %s,%s,%s)\n", rcJob, rcJob->GetInputFileAbsolutePath().toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
#endif
rcJob->SetTimeCompleted(QDateTime::currentDateTime());
auto foundInQueue = m_jobsInQueueLookup.find(rcJob->GetElementID());
while ((foundInQueue != m_jobsInQueueLookup.end()) && (foundInQueue.value() == rcJob))
{
foundInQueue = m_jobsInQueueLookup.erase(foundInQueue);
}
for (size_t jobIndex = m_jobs.size() - 1; jobIndex >= 0; --jobIndex)
{
if(m_jobs[jobIndex] == rcJob)
{
m_jobsInFlight.remove(rcJob);
// remove it from the list and delete it - there is a separate model that keeps track for the GUI so no need to keep jobs around.
{
#if defined(DEBUG_RCJOB_MODEL)
AZ_TracePrintf(AssetProcessor::DebugChannel, "JobTrace =>JobCompleted(%i %s,%s,%s)\n", rcJob, rcJob->GetJobEntry().m_databaseSourceName.toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
#endif
beginRemoveRows(QModelIndex(), aznumeric_caster(jobIndex), aznumeric_caster(jobIndex));
m_jobs.erase(m_jobs.begin() + jobIndex);
endRemoveRows();
// Only completed jobs need to wait on a catalog write
if (rcJob->GetState() == RCJob::completed)
{
const auto& id = rcJob->GetElementID();
auto itr = m_finishedJobsNotInCatalog.find(id);
if (itr != m_finishedJobsNotInCatalog.end())
{
itr.value()++;
}
else
{
m_finishedJobsNotInCatalog.insert(id, 1);
}
}
rcJob->deleteLater();
}
return;
}
}
AZ_TracePrintf(AssetProcessor::DebugChannel, "JobTrace jobIndex == -1!!! (%i %s,%s,%s)\n", rcJob, rcJob->GetJobEntry().m_databaseSourceName.toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
AZ_Assert(false, "Job not found!!!");
}
void RCJobListModel::markAsCataloged(const AssetProcessor::QueueElementID& check)
{
auto itr = m_finishedJobsNotInCatalog.find(check);
if(itr == m_finishedJobsNotInCatalog.end())
{
AZ_Assert(false, "Attempting to mark a job as written to the catalog before the job has been put in the waiting queue! %s", check.GetInputAssetName().toUtf8().constData());
return;
}
itr.value()--;
if (itr.value() == 0)
{
m_finishedJobsNotInCatalog.erase(itr);
}
}
bool RCJobListModel::isInFlight(const AssetProcessor::QueueElementID& check) const
{
for (auto rcJob : m_jobsInFlight)
{
if (check == rcJob->GetElementID())
{
return true;
}
}
return false;
}
int RCJobListModel::GetIndexOfProcessingJob(const QueueElementID& elementId)
{
for (int idx = 0; idx < rowCount(); ++idx)
{
RCJob* job = getItem(idx);
if (job->GetState() == RCJob::processing && job->GetElementID() == elementId)
{
return idx;
break;
}
}
return -1; // invalid index
}
void RCJobListModel::EraseJobs(QString sourceFileDatabaseName, AZStd::vector<RCJob*>& pendingJobs)
{
for (int jobIdx = 0; jobIdx < rowCount(); ++jobIdx)
{
RCJob* job = getItem(jobIdx);
if (QString::compare(job->GetJobEntry().m_databaseSourceName, sourceFileDatabaseName, Qt::CaseInsensitive) == 0)
{
const QueueElementID& target = job->GetElementID();
if ((isInQueue(target)) || (isInFlight(target)))
{
// Its important that this still follows the 'cancelled' flow, so that other parts of the code can update their "in progress" and other maps.
AZ_TracePrintf(AssetProcessor::DebugChannel, "Cancelling Job [%s, %s, %s] because the source file no longer exists.\n", target.GetInputAssetName().toUtf8().data(), target.GetPlatform().toUtf8().data(), target.GetJobDescriptor().toUtf8().data());
// if a job is pending, it was never started and thus will never enter Finished state,
// so simply changing its state to cancelled is not enough, collect them and return to rccontroller to process manually
if (job->GetState() == RCJob::JobState::pending)
{
pendingJobs.push_back(job);
}
job->SetState(RCJob::JobState::cancelled);
AssetBuilderSDK::JobCommandBus::Event(job->GetJobEntry().m_jobRunKey, &AssetBuilderSDK::JobCommandBus::Events::Cancel);
UpdateRow(jobIdx);
}
}
}
}
bool RCJobListModel::isInQueue(const AssetProcessor::QueueElementID& check) const
{
return m_jobsInQueueLookup.contains(check);
}
bool RCJobListModel::isWaitingOnCatalog(const QueueElementID& check) const
{
return m_finishedJobsNotInCatalog.contains(check);
}
void RCJobListModel::PerformHeuristicSearch(QString searchTerm, QString platform, QSet<QueueElementID>& found, AssetProcessor::JobIdEscalationList& escalationList, bool isStatusRequest, int searchRules)
{
int escalationValue = 0;
if (isStatusRequest)
{
escalationValue = AssetProcessor::JobEscalation::ProcessAssetRequestStatusEscalation;
}
else
{
escalationValue = AssetProcessor::JobEscalation::ProcessAssetRequestSyncEscalation;
}
// try to narrowly exact-match the search term in case the search term refers to a specific actual source file:
for (const RCJob* rcJob : m_jobs)
{
if ((platform != rcJob->GetPlatformInfo().m_identifier.c_str()) || (rcJob->GetState() != RCJob::pending))
{
continue;
}
QString input = rcJob->GetJobEntry().m_pathRelativeToWatchFolder;
if (input.endsWith(searchTerm, Qt::CaseInsensitive))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job Queue: Heuristic search found exact match (%s,%s,%s).\n", rcJob->GetJobEntry().m_databaseSourceName.toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
found.insert(QueueElementID(rcJob->GetJobEntry().m_databaseSourceName, platform, rcJob->GetJobKey()));
escalationList.append(qMakePair(rcJob->GetJobEntry().m_jobRunKey, escalationValue));
}
}
for (const RCJob* rcJob : m_jobsInFlight)
{
if (platform != rcJob->GetPlatformInfo().m_identifier.c_str())
{
continue;
}
QString input = rcJob->GetJobEntry().m_pathRelativeToWatchFolder;
if (input.endsWith(searchTerm, Qt::CaseInsensitive))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job Queue: Heuristic search found exact match (%s,%s,%s).\n", rcJob->GetJobEntry().m_databaseSourceName.toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
found.insert(QueueElementID(rcJob->GetJobEntry().m_databaseSourceName, platform, rcJob->GetJobKey()));
}
}
if (!found.isEmpty() || searchRules == AzFramework::AssetSystem::RequestAssetStatus::SearchType::Exact)
{
return;
}
// broaden the heuristic. Try without extensions - that is, ignore everything after the dot.
// if there are dashes, ignore them also. This is how you match "blah.dds" to actually mean "blah.tif"
// since we have no idea what products will be generated by a source still in the queue until it runs
int dotIndex = searchTerm.lastIndexOf('.');
QStringRef searchTermWithNoExtension = searchTerm.midRef(0, dotIndex);
if (dotIndex != -1)
{
for (const auto& rcJob : m_jobs)
{
if ((platform != rcJob->GetPlatformInfo().m_identifier.c_str()) || (rcJob->GetState() != RCJob::pending))
{
continue;
}
QString input = rcJob->GetJobEntry().m_pathRelativeToWatchFolder;
dotIndex = input.lastIndexOf('.');
if (dotIndex != -1)
{
QStringRef testref = input.midRef(0, dotIndex);
if (testref.endsWith(searchTermWithNoExtension, Qt::CaseInsensitive))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job Queue: Heuristic search found broad match (%s,%s,%s).\n", rcJob->GetJobEntry().m_pathRelativeToWatchFolder.toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
found.insert(QueueElementID(rcJob->GetJobEntry().m_databaseSourceName, platform, rcJob->GetJobKey()));
escalationList.append(qMakePair(rcJob->GetJobEntry().m_jobRunKey, escalationValue));
}
}
}
for (const RCJob* rcJob : m_jobsInFlight)
{
if (platform != rcJob->GetPlatformInfo().m_identifier.c_str())
{
continue;
}
QString input = rcJob->GetJobEntry().m_pathRelativeToWatchFolder;
dotIndex = input.lastIndexOf('.');
if (dotIndex != -1)
{
QStringRef testref = input.midRef(0, dotIndex);
if (testref.endsWith(searchTermWithNoExtension, Qt::CaseInsensitive))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job Queue: Heuristic search found broad match (%s,%s,%s).\n", rcJob->GetJobEntry().m_databaseSourceName.toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
found.insert(QueueElementID(rcJob->GetJobEntry().m_databaseSourceName, platform, rcJob->GetJobKey()));
}
}
}
}
if (!found.isEmpty())
{
return;
}
// broaden the heuristic further. Eliminate anything after the last underscore in the file name
// (so blahblah_diff.dds just becomes blahblah) and then allow anything which has that file somewhere in it.
int slashIndex = searchTerm.lastIndexOf('/');
int dashIndex = searchTerm.lastIndexOf('_');
QStringRef searchTermWithNoSuffix = searchTermWithNoExtension;
if ((dashIndex != -1) && (slashIndex == -1) || (dashIndex > slashIndex))
{
searchTermWithNoSuffix = searchTermWithNoSuffix.mid(0, dashIndex);
}
for (const auto& rcJob : m_jobs)
{
if ((platform != rcJob->GetPlatformInfo().m_identifier.c_str()) || (rcJob->GetState() != RCJob::pending))
{
continue;
}
QString input = rcJob->GetJobEntry().m_pathRelativeToWatchFolder;
if (input.contains(searchTermWithNoSuffix, Qt::CaseInsensitive)) //notice here that we use simply CONTAINS instead of endswith - this can potentially be very broad!
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job Queue: Heuristic search found ultra-broad match (%s,%s,%s).\n", rcJob->GetJobEntry().m_databaseSourceName.toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
found.insert(QueueElementID(rcJob->GetJobEntry().m_databaseSourceName, platform, rcJob->GetJobKey()));
escalationList.append(qMakePair(rcJob->GetJobEntry().m_jobRunKey, escalationValue));
}
}
for (const RCJob* rcJob : m_jobsInFlight)
{
if (platform != rcJob->GetPlatformInfo().m_identifier.c_str())
{
continue;
}
QString input = rcJob->GetJobEntry().m_pathRelativeToWatchFolder;
if (input.contains(searchTermWithNoSuffix, Qt::CaseInsensitive)) //notice here that we use simply CONTAINS instead of endswith - this can potentially be very broad!
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job Queue: Heuristic search found ultra-broad match (%s,%s,%s).\n", rcJob->GetJobEntry().m_databaseSourceName.toUtf8().constData(), rcJob->GetPlatformInfo().m_identifier.c_str(), rcJob->GetJobKey().toUtf8().constData());
found.insert(QueueElementID(rcJob->GetJobEntry().m_databaseSourceName, platform, rcJob->GetJobKey()));
}
}
}
void RCJobListModel::PerformUUIDSearch(AZ::Uuid searchUuid, QString platform, QSet<QueueElementID>& found, AssetProcessor::JobIdEscalationList& escalationList, bool isStatusRequest)
{
int escalationValue = 0;
if (isStatusRequest)
{
escalationValue = AssetProcessor::JobEscalation::ProcessAssetRequestStatusEscalation;
}
else
{
escalationValue = AssetProcessor::JobEscalation::ProcessAssetRequestSyncEscalation;
}
for (const RCJob* rcJob : m_jobs)
{
if ((platform != rcJob->GetPlatformInfo().m_identifier.c_str()) || (rcJob->GetState() != RCJob::pending))
{
continue;
}
if (rcJob->GetJobEntry().m_sourceFileUUID == searchUuid)
{
found.insert(QueueElementID(rcJob->GetJobEntry().m_databaseSourceName, platform, rcJob->GetJobKey()));
escalationList.append(qMakePair(rcJob->GetJobEntry().m_jobRunKey, escalationValue));
}
}
}
}// namespace AssetProcessor
@@ -0,0 +1,118 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef RCQUEUEMODEL_H
#define RCQUEUEMODEL_H
#if !defined(Q_MOC_RUN)
#include "RCCommon.h"
#include <QAbstractItemModel>
#include <QObject>
#include <QQueue>
#include <QMultiMap>
#include "rcjob.h"
#endif
namespace AssetProcessor
{
class QueueElementID;
}
class RCcontrollerUnitTests;
namespace AssetProcessor
{
/**
* The RCJobListModel class contains lists of RC jobs
*/
class RCJobListModel
: public QAbstractItemModel
{
friend class ::RCcontrollerUnitTests;
Q_OBJECT
public:
enum DataRoles
{
jobIndexRole = Qt::UserRole + 1,
stateRole,
displayNameRole,
timeCreatedRole,
timeLaunchedRole,
timeCompletedRole,
jobDataRole,
};
enum Column
{
ColumnState,
ColumnJobId,
ColumnCommand,
ColumnCompleted,
ColumnPlatform,
Max
};
explicit RCJobListModel(QObject* parent = 0);
QModelIndex parent(const QModelIndex&) const override;
QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QVariant data(const QModelIndex& index, int role) const;
void markAsProcessing(RCJob* rcJob);
void markAsStarted(RCJob* rcJob);
void markAsCompleted(RCJob* rcJob);
void markAsCataloged(const AssetProcessor::QueueElementID& check);
unsigned int jobsInFlight() const;
void UpdateJobEscalation(AssetProcessor::RCJob* rcJob, int jobPrioririty);
void UpdateRow(int jobIndex);
bool isEmpty();
void addNewJob(RCJob* rcJob);
bool isInFlight(const QueueElementID& check) const;
bool isInQueue(const QueueElementID& check) const;
bool isWaitingOnCatalog(const QueueElementID& check) const;
void PerformHeuristicSearch(QString searchTerm, QString platform, QSet<QueueElementID>& found, AssetProcessor::JobIdEscalationList& escalationList, bool isStatusRequest, int searchRules = 0);
void PerformUUIDSearch(AZ::Uuid searchUuid, QString platform, QSet<QueueElementID>& found, AssetProcessor::JobIdEscalationList& escalationList, bool isStatusRequest);
int itemCount() const;
RCJob* getItem(int index) const;
int GetIndexOfProcessingJob(const QueueElementID& elementId);
///! EraseJobs expects the database name of the source file. (So with outputprefix)
void EraseJobs(QString sourceFileDatabaseName, AZStd::vector<RCJob*>& pendingJobs);
private:
AZStd::vector<RCJob*> m_jobs;
QSet<RCJob*> m_jobsInFlight;
// Keeps track of jobs waiting on the APM thread to finish writing out to the catalog
// This prevents job dependencies from starting before the dependent job is actually done
// Since the jobs aren't uniquely identified, and the APM thread can fall behind, we keep track of how many have finished
QHash<QueueElementID, int> m_finishedJobsNotInCatalog;
// profiler showed much of our time was spent in IsInQueue.
QMultiMap<QueueElementID, RCJob*> m_jobsInQueueLookup;
};
} // namespace AssetProcessor
#endif // RCQUEUEMODEL_H
@@ -0,0 +1,166 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "shadercompilerManager.h"
#include "shadercompilerjob.h"
#include <QThreadPool>
#include "native/utilities/assetUtils.h"
ShaderCompilerManager::ShaderCompilerManager(QObject* parent)
: QObject(parent)
, m_isUnitTesting(false)
, m_numberOfJobsStarted(0)
, m_numberOfJobsEnded(0)
, m_numberOfErrors(0)
{
}
ShaderCompilerManager::~ShaderCompilerManager()
{
}
void ShaderCompilerManager::process(unsigned int connID, unsigned int type, unsigned int serial, QByteArray payload)
{
(void)type;
(void)serial;
Q_ASSERT(AssetUtilities::ComputeCRC32Lowercase("ShaderCompilerProxyRequest") == type);
decodeShaderCompilerRequest(connID, payload);
}
void ShaderCompilerManager::decodeShaderCompilerRequest(unsigned int connID, QByteArray payload)
{
if (payload.length() < sizeof(unsigned int) + sizeof(unsigned int) + 2 + sizeof(unsigned short))
{
QString error = "Payload size is too small";
AZ_Warning(AssetProcessor::ConsoleChannel, false, error.toUtf8().data());
emit sendErrorMessage(error);
return;
}
unsigned char* data_end = reinterpret_cast<unsigned char*>(payload.data() + payload.size());
unsigned int* requestId = reinterpret_cast<unsigned int*>(data_end - sizeof(unsigned int));
unsigned int* serverListSizePtr = reinterpret_cast<unsigned int*>(data_end - sizeof(unsigned int) - sizeof(unsigned int));
unsigned short* serverPortPtr = reinterpret_cast<unsigned short*>(data_end - sizeof(unsigned int) - sizeof(unsigned int) - sizeof(unsigned short));
ShaderCompilerRequestMessage msg;
QString error;
msg.requestId = *requestId;
msg.serverListSize = *serverListSizePtr;
msg.serverPort = *serverPortPtr;
if ((msg.serverListSize <= 0) || (msg.serverListSize > 100000))
{
error = "Shader Compiler Server List is wrong";
AZ_Warning(AssetProcessor::ConsoleChannel, false, error.toUtf8().data());
emit sendErrorMessage(error);
return;
}
if (msg.serverPort == 0)
{
error = "Shader Compiler port is wrong";
AZ_Warning(AssetProcessor::ConsoleChannel, false, error.toUtf8().data());
emit sendErrorMessage(error);
return;
}
char* position_of_first_null = reinterpret_cast<char*>(serverPortPtr) - 1;// -1 for null
if ((*position_of_first_null) != '\0')
{
error = "Shader Compiler payload is corrupt,position is not null";
AZ_Warning(AssetProcessor::ConsoleChannel, false, error.toUtf8().data());
emit sendErrorMessage(error);
return;
}
char* beginning_of_serverList = position_of_first_null - msg.serverListSize;
char* position_of_second_null = beginning_of_serverList - 1;//-1 for null
if ((*position_of_second_null) != '\0')
{
error = "Shader Compiler payload is corrupt,position is not null";
AZ_Warning(AssetProcessor::ConsoleChannel, false, error.toUtf8().data());
emit sendErrorMessage(error);
return;
}
unsigned int originalPayloadSize = static_cast<unsigned int>(payload.size()) - sizeof(unsigned int) - sizeof(unsigned int) - sizeof(unsigned short) - static_cast<unsigned int>(msg.serverListSize) - 2;
msg.serverList = beginning_of_serverList;
msg.originalPayload.insert(0, payload.data(), static_cast<unsigned int>(originalPayloadSize));
ShaderCompilerJob* shaderCompilerJob = new ShaderCompilerJob();
shaderCompilerJob->initialize(this, msg);
shaderCompilerJob->setIsUnitTesting(m_isUnitTesting);
m_shaderCompilerJobMap[msg.requestId] = connID;
shaderCompilerJob->setAutoDelete(true);
QThreadPool* threadPool = QThreadPool::globalInstance();
threadPool->start(shaderCompilerJob);
}
void ShaderCompilerManager::OnShaderCompilerJobComplete(QByteArray payload, unsigned int requestId)
{
auto iterator = m_shaderCompilerJobMap.find(requestId);
if (iterator != m_shaderCompilerJobMap.end())
{
sendResponse(iterator.value(), AssetUtilities::ComputeCRC32Lowercase("ShaderCompilerProxyResponse"), 0, payload);
}
else
{
QString error = "Shader Compiler cannot find the connection id";
AZ_Warning(AssetProcessor::ConsoleChannel, false, error.toUtf8().data());
emit sendErrorMessage(error);
}
}
void ShaderCompilerManager::sendResponse(unsigned int connId, unsigned int /*type*/, unsigned int /*serial*/, QByteArray payload)
{
EBUS_EVENT_ID(connId, AssetProcessor::ConnectionBus, SendRaw, AssetUtilities::ComputeCRC32Lowercase("ShaderCompilerProxyResponse"), 0, payload);
}
void ShaderCompilerManager::shaderCompilerError(QString errorMessage, QString server, QString timestamp, QString payload)
{
m_numberOfErrors++;
emit numberOfErrorsChanged();
emit sendErrorMessageFromShaderJob(errorMessage, server, timestamp, payload);
}
void ShaderCompilerManager::jobStarted()
{
m_numberOfJobsStarted++;
emit numberOfJobsStartedChanged();
}
void ShaderCompilerManager::jobEnded()
{
m_numberOfJobsEnded++;
numberOfJobsEndedChanged();
}
void ShaderCompilerManager::setIsUnitTesting(bool isUnitTesting)
{
m_isUnitTesting = isUnitTesting;
}
int ShaderCompilerManager::numberOfJobsStarted()
{
return m_numberOfJobsStarted;
}
int ShaderCompilerManager::numberOfJobsEnded()
{
return m_numberOfJobsEnded;
}
int ShaderCompilerManager::numberOfErrors()
{
return m_numberOfErrors;
}
@@ -0,0 +1,71 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef SHADERCOMPILERMANAGER_H
#define SHADERCOMPILERMANAGER_H
#if !defined(Q_MOC_RUN)
#include <QObject>
#include <QHash>
#include <QString>
#include <QByteArray>
#endif
typedef QHash<unsigned int, unsigned int> ShaderCompilerJobMap;
/**
* The Shader Compiler Manager class receive a shader compile request
* and starts a shader compiler job for it
*/
class ShaderCompilerManager
: public QObject
{
Q_OBJECT
Q_PROPERTY(int numberOfJobsStarted READ numberOfJobsStarted NOTIFY numberOfJobsStartedChanged)
Q_PROPERTY(int numberOfJobsEnded READ numberOfJobsEnded NOTIFY numberOfJobsEndedChanged)
Q_PROPERTY(int numberOfErrors READ numberOfErrors NOTIFY numberOfErrorsChanged)
public:
explicit ShaderCompilerManager(QObject* parent = 0);
virtual ~ShaderCompilerManager();
void process(unsigned int connID, unsigned int type, unsigned int serial, QByteArray payload);
void decodeShaderCompilerRequest(unsigned int connID, QByteArray payload);
void setIsUnitTesting(bool isUnitTesting);
int numberOfJobsStarted();
int numberOfJobsEnded();
int numberOfErrors();
virtual void sendResponse(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
signals:
void sendErrorMessage(QString errorMessage);
void sendErrorMessageFromShaderJob(QString errorMessage, QString server, QString timestamp, QString payload);
void numberOfJobsStartedChanged();
void numberOfJobsEndedChanged();
void numberOfErrorsChanged();
public slots:
void OnShaderCompilerJobComplete(QByteArray payload, unsigned int requestId);
void shaderCompilerError(QString errorMessage, QString server, QString timestamp, QString payload);
void jobStarted();
void jobEnded();
private:
ShaderCompilerJobMap m_shaderCompilerJobMap;
bool m_isUnitTesting;
int m_numberOfJobsStarted;
int m_numberOfJobsEnded;
int m_numberOfErrors;
};
#endif // SHADERCOMPILERMANAGER_H
@@ -0,0 +1,28 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef SHADERCOMPILERMESSAGES_H
#define SHADERCOMPILERMESSAGES_H
#include <QByteArray>
#include <QString>
struct ShaderCompilerRequestMessage
{
QByteArray originalPayload;
QString serverList;
unsigned short serverPort;
unsigned int serverListSize;
unsigned int requestId;
};
#endif //SHADERCOMPILERMESSAGES_H
@@ -0,0 +1,154 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "shadercompilerModel.h"
namespace
{
ShaderCompilerModel* s_singleton = nullptr;
}
ShaderCompilerModel::ShaderCompilerModel(QObject* parent)
: QAbstractItemModel(parent)
{
Q_ASSERT(s_singleton == nullptr);
s_singleton = this;
}
ShaderCompilerModel::~ShaderCompilerModel()
{
s_singleton = nullptr;
}
ShaderCompilerModel* ShaderCompilerModel::Get()
{
return s_singleton;
}
QVariant ShaderCompilerModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
{
return QVariant();
}
int row = index.row();
if (row < 0)
{
return QVariant();
}
if (row >= m_shaderErrorInfoList.count())
{
return QVariant();
}
switch (role)
{
case TimeStampRole:
return m_shaderErrorInfoList[row].m_shaderTimestamp;
case ServerRole:
return m_shaderErrorInfoList[row].m_shaderServerName;
case ErrorRole:
return m_shaderErrorInfoList[row].m_shaderError;
case OriginalRequestRole:
return m_shaderErrorInfoList[row].m_shaderOriginalPayload;
case Qt::DisplayRole:
switch (index.column())
{
case ColumnTimeStamp:
return m_shaderErrorInfoList[row].m_shaderTimestamp;
case ColumnServer:
return m_shaderErrorInfoList[row].m_shaderServerName;
case ColumnError:
return m_shaderErrorInfoList[row].m_shaderServerName;
}
}
return QVariant();
}
Qt::ItemFlags ShaderCompilerModel::flags(const QModelIndex& index) const
{
(void)index;
return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
}
int ShaderCompilerModel::rowCount(const QModelIndex& parent) const
{
(void)parent;
return m_shaderErrorInfoList.count();
}
QModelIndex ShaderCompilerModel::parent(const QModelIndex&) const
{
return QModelIndex();
}
QModelIndex ShaderCompilerModel::index(int row, int column, const QModelIndex& parent) const
{
if (row >= rowCount(parent) || column >= columnCount(parent))
{
return QModelIndex();
}
return createIndex(row, column);
}
int ShaderCompilerModel::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : Column::Max;
}
QVariant ShaderCompilerModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
{
switch (section)
{
case ColumnTimeStamp:
return tr("Time Stamp");
case ColumnServer:
return tr("Server");
case ColumnError:
return tr("Error");
default:
break;
}
}
return QAbstractItemModel::headerData(section, orientation, role);
}
QHash<int, QByteArray> ShaderCompilerModel::roleNames() const
{
QHash<int, QByteArray> result;
result[TimeStampRole] = "timestamp";
result[ServerRole] = "server";
result[ErrorRole] = "error";
result[OriginalRequestRole] = "originalRequest";
return result;
}
void ShaderCompilerModel::addShaderErrorInfoEntry(QString errorMessage, QString timestamp, QString payload, QString server)
{
ShaderCompilerErrorInfo shaderCompileErrorInfo(errorMessage, timestamp, payload, server);
beginInsertRows(QModelIndex(), m_shaderErrorInfoList.size(), m_shaderErrorInfoList.size());
m_shaderErrorInfoList.append(shaderCompileErrorInfo);
endInsertRows();
}
@@ -0,0 +1,97 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef SHADERCOMPILERMODEL_H
#define SHADERCOMPILERMODEL_H
#if !defined(Q_MOC_RUN)
#include <QAbstractItemModel>
#include <QList>
#include <QVariant>
#include <QHash>
#include <QByteArray>
#include <QString>
#endif
class QModelIndex;
class QObject;
struct ShaderCompilerErrorInfo
{
QString m_shaderError;
QString m_shaderTimestamp;
QString m_shaderOriginalPayload;
QString m_shaderServerName;
ShaderCompilerErrorInfo(QString shaderError, QString shaderTimestamp, QString shaderOriginalPayload, QString shaderServerName)
: m_shaderError(shaderError)
, m_shaderTimestamp(shaderTimestamp)
, m_shaderOriginalPayload(shaderOriginalPayload)
, m_shaderServerName(shaderServerName)
{
}
};
/** The Shader Compiler model is responsible for capturing error requests
*/
class ShaderCompilerModel
: public QAbstractItemModel
{
Q_OBJECT
public:
enum DataRoles
{
TimeStampRole = Qt::UserRole + 1,
ServerRole,
ErrorRole,
OriginalRequestRole,
};
enum Column
{
ColumnTimeStamp,
ColumnServer,
ColumnError,
Max
};
/// standard Qt constructor
explicit ShaderCompilerModel(QObject* parent = 0);
virtual ~ShaderCompilerModel();
// singleton pattern
static ShaderCompilerModel* Get();
/// QAbstractListModel interface
QModelIndex parent(const QModelIndex&) const override;
QModelIndex index(int row, int column, const QModelIndex& parent) const override;
int columnCount(const QModelIndex&) const override;
virtual int rowCount(const QModelIndex& parent) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
virtual QVariant data(const QModelIndex& index, int role) const override;
virtual QHash<int, QByteArray> roleNames() const override;
virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
public slots:
void addShaderErrorInfoEntry(QString errorMessage, QString timestamp, QString payload, QString server);
private:
QList<ShaderCompilerErrorInfo> m_shaderErrorInfoList;
};
#endif // SHADERCOMPILERMODEL_H
@@ -0,0 +1,198 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "shadercompilerjob.h"
#include "native/assetprocessor.h"
#include <QTcpSocket>
ShaderCompilerJob::ShaderCompilerJob()
: m_isUnitTesting(false)
, m_manager(nullptr)
{
}
ShaderCompilerJob::~ShaderCompilerJob()
{
m_manager = nullptr;
}
ShaderCompilerRequestMessage ShaderCompilerJob::ShaderCompilerMessage() const
{
return m_ShaderCompilerMessage;
}
void ShaderCompilerJob::initialize(QObject* pManager, const ShaderCompilerRequestMessage& ShaderCompilerMessage)
{
m_manager = pManager;
m_ShaderCompilerMessage = ShaderCompilerMessage;
}
QString ShaderCompilerJob::getServerAddress()
{
if (isServerListEmpty())
{
return QString();
}
QString serverAddress;
if (!m_ShaderCompilerMessage.serverList.contains(","))
{
serverAddress = m_ShaderCompilerMessage.serverList;
m_ShaderCompilerMessage.serverList.clear();
return serverAddress;
}
QStringList serverList = m_ShaderCompilerMessage.serverList.split(",");
serverAddress = serverList.takeAt(0);
m_ShaderCompilerMessage.serverList = serverList.join(",");
return serverAddress;
}
bool ShaderCompilerJob::isServerListEmpty()
{
return m_ShaderCompilerMessage.serverList.isEmpty();
}
bool ShaderCompilerJob::attemptDelivery(QString serverAddress, QByteArray& payload)
{
QTcpSocket socket;
QString error;
int waitingTime = 8000; // 8 sec timeout for sending.
int jobCompileMaxTime = 1000 * 60; // 60 sec timeout for compilation
if (m_isUnitTesting)
{
waitingTime = 500;
jobCompileMaxTime = 500;
}
socket.connectToHost(serverAddress, m_ShaderCompilerMessage.serverPort, QIODevice::ReadWrite);
if (socket.waitForConnected(waitingTime))
{
qint64 bytesWritten = 0;
qint64 payloadSize = static_cast<qint64>(m_ShaderCompilerMessage.originalPayload.size());
// send payload size to server
while (bytesWritten != sizeof(qint64))
{
qint64 currentWrite = socket.write(reinterpret_cast<char*>(&payloadSize) + bytesWritten,
sizeof(qint64) - bytesWritten);
if (currentWrite == -1)
{
//It is important to note that we are only outputting the error to debugchannel only here because
//we are forwarding these error messages upstream to the manager,who will take the appropriate action
error = "Connection Lost:Unable to send data";
AZ_TracePrintf(AssetProcessor::DebugChannel, error.toUtf8().data());
QMetaObject::invokeMethod(m_manager, "shaderCompilerError", Qt::QueuedConnection, Q_ARG(QString, error), Q_ARG(QString, QDateTime::currentDateTime().toString()), Q_ARG(QString, QString(m_ShaderCompilerMessage.originalPayload)), Q_ARG(QString, serverAddress));
return false;
}
socket.flush();
bytesWritten += currentWrite;
}
bytesWritten = 0;
//send actual payload to server
while (bytesWritten != m_ShaderCompilerMessage.originalPayload.size())
{
qint64 currentWrite = socket.write(m_ShaderCompilerMessage.originalPayload.data() + bytesWritten,
m_ShaderCompilerMessage.originalPayload.size() - bytesWritten);
if (currentWrite == -1)
{
error = "Connection Lost:Unable to send data";
AZ_TracePrintf(AssetProcessor::DebugChannel, error.toUtf8().data());
QMetaObject::invokeMethod(m_manager, "shaderCompilerError", Qt::QueuedConnection, Q_ARG(QString, error), Q_ARG(QString, QDateTime::currentDateTime().toString()), Q_ARG(QString, QString(m_ShaderCompilerMessage.originalPayload)), Q_ARG(QString, serverAddress));
}
socket.flush();
bytesWritten += currentWrite;
}
}
else
{
error = "Unable to connect to IP Address " + serverAddress;
AZ_TracePrintf(AssetProcessor::DebugChannel, error.toUtf8().data());
QMetaObject::invokeMethod(m_manager, "shaderCompilerError", Qt::QueuedConnection, Q_ARG(QString, error), Q_ARG(QString, QDateTime::currentDateTime().toString()), Q_ARG(QString, QString(m_ShaderCompilerMessage.originalPayload)), Q_ARG(QString, serverAddress));
return false;
}
unsigned int expectedBytes = sizeof(unsigned int) + sizeof(qint8);
unsigned int bytesReadTotal = 0;
unsigned int messageSize = 0;
bool isMessageSizeKnown = false;
//read the entire payload
while ((bytesReadTotal < expectedBytes + messageSize))
{
if (socket.bytesAvailable() == 0)
{
if (!socket.waitForReadyRead(jobCompileMaxTime))
{
error = "Remote IP is taking too long to respond: " + serverAddress;
AZ_TracePrintf(AssetProcessor::DebugChannel, error.toUtf8().data());
QMetaObject::invokeMethod(m_manager, "shaderCompilerError", Qt::QueuedConnection, Q_ARG(QString, error), Q_ARG(QString, QDateTime::currentDateTime().toString()), Q_ARG(QString, QString(m_ShaderCompilerMessage.originalPayload)), Q_ARG(QString, serverAddress));
payload.clear();
return false;
}
}
qint64 bytesAvailable = socket.bytesAvailable();
if (bytesAvailable >= expectedBytes && !isMessageSizeKnown)
{
socket.peek(reinterpret_cast<char*>(&messageSize), sizeof(unsigned int));
payload.resize(expectedBytes + messageSize);
isMessageSizeKnown = true;
}
if (bytesAvailable > 0)
{
qint64 bytesRead = socket.read(payload.data() + bytesReadTotal, bytesAvailable);
if (bytesRead <= 0)
{
error = "Connection closed by remote IP Address " + serverAddress;
AZ_TracePrintf(AssetProcessor::DebugChannel, error.toUtf8().data());
QMetaObject::invokeMethod(m_manager, "shaderCompilerError", Qt::QueuedConnection, Q_ARG(QString, error), Q_ARG(QString, QDateTime::currentDateTime().toString()), Q_ARG(QString, QString(m_ShaderCompilerMessage.originalPayload)), Q_ARG(QString, serverAddress));
payload.clear();
return false;
}
bytesReadTotal = aznumeric_cast<uint32_t>(bytesReadTotal + bytesRead);
}
}
return true; // payload successfully send
}
void ShaderCompilerJob::run()
{
QMetaObject::invokeMethod(m_manager, "jobStarted", Qt::QueuedConnection);
QByteArray payload;
//until server list is empty, keep trying
while (!isServerListEmpty())
{
QString serverAddress = getServerAddress();
//attempt to send payload
if (attemptDelivery(serverAddress, payload))
{
break;
}
}
//we are appending request id at the end of every payload,
//therefore in the case of any errors also
//we will be sending atleast four bytes to the game
payload.append(reinterpret_cast<char*>(&m_ShaderCompilerMessage.requestId), sizeof(unsigned int));
QMetaObject::invokeMethod(m_manager, "OnShaderCompilerJobComplete", Qt::QueuedConnection, Q_ARG(QByteArray, payload), Q_ARG(unsigned int, m_ShaderCompilerMessage.requestId));
QMetaObject::invokeMethod(m_manager, "jobEnded", Qt::QueuedConnection);
}
void ShaderCompilerJob::setIsUnitTesting(bool isUnitTesting)
{
m_isUnitTesting = isUnitTesting;
}
@@ -0,0 +1,48 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef SHADERCOMPILERJOB_H
#define SHADERCOMPILERJOB_H
#include <QRunnable>
#include "shadercompilerMessages.h"
class QByteArray;
class QObject;
/**
* This class is responsible for connecting to the shader compiler server
* and getting back the response to the shader compiler manager
*/
class ShaderCompilerJob
: public QRunnable
{
public:
explicit ShaderCompilerJob();
virtual ~ShaderCompilerJob();
ShaderCompilerRequestMessage ShaderCompilerMessage() const;
void initialize(QObject* pManager, const ShaderCompilerRequestMessage& ShaderCompilerMessage);
QString getServerAddress();
bool isServerListEmpty();
virtual void run() override;
void setIsUnitTesting(bool isUnitTesting);
bool attemptDelivery(QString serverAddress, QByteArray& payload);
private:
ShaderCompilerRequestMessage m_ShaderCompilerMessage;
QObject* m_manager;
bool m_isUnitTesting;
};
#endif // SHADERCOMPILERJOB_H
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,359 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzTest/AzTest.h>
#include <utilities/BatchApplicationManager.h>
#include <utilities/ApplicationServer.h>
#include <AzFramework/Asset/AssetSystemComponent.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <connection/connectionManager.h>
#include <QCoreApplication>
#include <QTemporaryDir>
#include <AzFramework/Network/AssetProcessorConnection.h>
namespace AssetProcessorMessagesTests
{
using namespace testing;
using ::testing::NiceMock;
using namespace AssetProcessor;
using namespace AssetBuilderSDK;
static constexpr unsigned short AssetProcessorPort = static_cast<unsigned short>(888u);
class AssetProcessorMessages;
struct UnitTestBatchApplicationManager
: BatchApplicationManager
{
UnitTestBatchApplicationManager(int* argc, char*** argv, QObject* parent)
: BatchApplicationManager(argc, argv, parent)
{
}
friend class AssetProcessorMessages;
};
class AssetProcessorMessagesTestsMockDatabaseLocationListener : public AzToolsFramework::AssetDatabase::AssetDatabaseRequests::Bus::Handler
{
public:
MOCK_METHOD1(GetAssetDatabaseLocation, bool(AZStd::string&));
};
struct MockAssetCatalog : AssetProcessor::AssetCatalog
{
MockAssetCatalog(QObject* parent, AssetProcessor::PlatformConfiguration* platformConfiguration)
: AssetCatalog(parent, platformConfiguration)
{
}
AzFramework::AssetSystem::GetUnresolvedDependencyCountsResponse HandleGetUnresolvedDependencyCountsRequest(MessageData<AzFramework::AssetSystem::GetUnresolvedDependencyCountsRequest> messageData) override
{
m_called = true;
return AssetCatalog::HandleGetUnresolvedDependencyCountsRequest(messageData);
}
bool m_called = false;
};
struct MockAssetRequestHandler : AssetRequestHandler
{
bool InvokeHandler(MessageData<AzFramework::AssetSystem::BaseAssetProcessorMessage> message) override
{
m_invoked = true;
return AssetRequestHandler::InvokeHandler(message);
}
AZStd::atomic_bool m_invoked = false;
};
class AssetProcessorMessages
: public ::UnitTest::ScopedAllocatorSetupFixture
{
public:
void SetUp() override
{
AssetUtilities::ResetGameName();
m_temporarySourceDir = QDir(m_temporaryDir.path());
m_databaseLocation = m_temporarySourceDir.absoluteFilePath("test_database.sqlite").toUtf8().constData();
ON_CALL(m_databaseLocationListener, GetAssetDatabaseLocation(_))
.WillByDefault(
DoAll( // set the 0th argument ref (string) to the database location and return true.
SetArgReferee<0>(m_databaseLocation.c_str()),
Return(true)));
m_databaseLocationListener.BusConnect();
m_dbConn.OpenDatabase();
int argC = 0;
m_batchApplicationManager = AZStd::make_unique<UnitTestBatchApplicationManager>(&argC, nullptr, nullptr);
m_batchApplicationManager->BeforeRun();
// Override Game Name to be "SamplesProject"
AssetUtilities::ComputeGameName("SamplesProject", true);
m_batchApplicationManager->m_platformConfiguration = new PlatformConfiguration();
m_batchApplicationManager->InitAssetProcessorManager();
m_assetCatalog = AZStd::make_unique<MockAssetCatalog>(nullptr, m_batchApplicationManager->m_platformConfiguration);
m_batchApplicationManager->m_assetCatalog = m_assetCatalog.get();
m_batchApplicationManager->InitRCController();
m_batchApplicationManager->InitFileStateCache();
m_batchApplicationManager->InitFileMonitor();
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();
QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ConnectionError, [](unsigned /*connId*/, QString error)
{
AZ_Error("ConnectionManager", false, "%s", error.toUtf8().constData());
});
ASSERT_TRUE(m_batchApplicationManager->m_applicationServer->startListening(AssetProcessorPort));
using namespace AzFramework;
m_assetSystemComponent = AZStd::make_unique<AssetSystem::AssetSystemComponent>();
m_assetSystemComponent->Init();
m_assetSystemComponent->Activate();
QCoreApplication::processEvents();
RunNetworkRequest([]()
{
AZStd::string appBranchToken;
AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::CalculateBranchTokenForAppRoot, appBranchToken);
AzFramework::AssetSystem::ConnectionSettings connectionSettings;
connectionSettings.m_assetProcessorIp = "127.0.0.1";
connectionSettings.m_assetProcessorPort = AssetProcessorPort;
connectionSettings.m_branchToken = appBranchToken;
connectionSettings.m_projectName = "SamplesProject";
connectionSettings.m_assetPlatform = "pc";
connectionSettings.m_connectionIdentifier = "UNITTEST";
connectionSettings.m_connectTimeout = AZStd::chrono::seconds(15);
connectionSettings.m_connectionDirection = AzFramework::AssetSystem::ConnectionSettings::ConnectionDirection::ConnectToAssetProcessor;
connectionSettings.m_waitUntilAssetProcessorIsReady = false;
connectionSettings.m_launchAssetProcessorOnFailedConnection = false;
bool result = false;
AzFramework::AssetSystemRequestBus::BroadcastResult(result,
&AzFramework::AssetSystemRequestBus::Events::EstablishAssetProcessorConnection, connectionSettings);
ASSERT_TRUE(result);
});
}
void TearDown() override
{
QEventLoop eventLoop;
QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit);
m_batchApplicationManager->m_connectionManager->QuitRequested();
eventLoop.exec();
m_assetSystemComponent->Deactivate();
m_batchApplicationManager->Destroy();
}
void RunNetworkRequest(AZStd::function<void()> func) const
{
AZStd::atomic_bool finished = false;
auto start = AZStd::chrono::monotonic_clock::now();
auto thread = AZStd::thread([&finished, &func]()
{
func();
finished = true;
}
);
constexpr int MaxWaitTime = 5;
while (!finished && AZStd::chrono::monotonic_clock::now() - start < AZStd::chrono::seconds(MaxWaitTime))
{
QCoreApplication::processEvents();
}
ASSERT_TRUE(finished) << "Timeout";
thread.join();
}
protected:
MockAssetRequestHandler* m_assetRequestHandler{}; // Not owned, AP will delete this pointer
QTemporaryDir m_temporaryDir;
AZStd::unique_ptr<UnitTestBatchApplicationManager> m_batchApplicationManager;
AZStd::unique_ptr<AzFramework::AssetSystem::AssetSystemComponent> m_assetSystemComponent;
NiceMock<AssetProcessorMessagesTestsMockDatabaseLocationListener> m_databaseLocationListener;
AZStd::unique_ptr<MockAssetCatalog> m_assetCatalog = nullptr;
QDir m_temporarySourceDir;
AZStd::string m_databaseLocation;
AssetDatabaseConnection m_dbConn;
};
struct MessagePair
{
AZStd::unique_ptr<AzFramework::AssetSystem::BaseAssetProcessorMessage> m_request;
AZStd::unique_ptr<AzFramework::AssetSystem::BaseAssetProcessorMessage> m_response;
};
TEST_F(AssetProcessorMessages, All)
{
// Test that we can successfully send network messages and have them arrive for processing
// For messages that have a response, it also verifies the response comes back
// Note that several harmless warnings will be triggered due to the messages not having any data set
using namespace AzFramework::AssetSystem;
using namespace AzToolsFramework::AssetSystem;
AZStd::vector<MessagePair> testMessages;
AZStd::unordered_map<int, AZStd::string> nameMap; // This is just for debugging, so we can output the name of failed messages
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
auto addPairFunc = [&testMessages, &nameMap, serializeContext](auto* request, auto* response)
{
testMessages.emplace_back(MessagePair{
AZStd::unique_ptr<AZStd::remove_pointer_t<decltype(request)>>(request),
AZStd::unique_ptr<AZStd::remove_pointer_t<decltype(response)>>(response)
});
auto data = serializeContext->FindClassData(request->RTTI_GetType());
nameMap[request->GetMessageType()] = data->m_name;
};
auto addRequestFunc = [&testMessages, &nameMap, serializeContext](auto* request)
{
testMessages.emplace_back(MessagePair{AZStd::unique_ptr<AZStd::remove_pointer_t<decltype(request)>>(request), nullptr });
auto data = serializeContext->FindClassData(request->RTTI_GetType());
nameMap[request->GetMessageType()] = data->m_name;
};
addPairFunc(new GetFullSourcePathFromRelativeProductPathRequest(), new GetFullSourcePathFromRelativeProductPathResponse());
addPairFunc(new GetRelativeProductPathFromFullSourceOrProductPathRequest(), new GetRelativeProductPathFromFullSourceOrProductPathResponse());
addPairFunc(new SourceAssetInfoRequest(), new SourceAssetInfoResponse());
addPairFunc(new SourceAssetProductsInfoRequest(), new SourceAssetProductsInfoResponse());
addPairFunc(new GetScanFoldersRequest(), new GetScanFoldersResponse());
addPairFunc(new GetAssetSafeFoldersRequest(), new GetAssetSafeFoldersResponse());
addRequestFunc(new RegisterSourceAssetRequest());
addRequestFunc(new UnregisterSourceAssetRequest());
addPairFunc(new AssetInfoRequest(), new AssetInfoResponse());
addPairFunc(new AssetDependencyInfoRequest(), new AssetDependencyInfoResponse());
addRequestFunc(new RequestEscalateAsset());
addPairFunc(new RequestAssetStatus(), new ResponseAssetStatus());
RunNetworkRequest([&testMessages, &nameMap, this]()
{
for(auto&& pair : testMessages)
{
AZStd::string messageName = nameMap[pair.m_request->GetMessageType()];
m_assetRequestHandler->m_invoked = false;
if(pair.m_response)
{
EXPECT_TRUE(SendRequest(*pair.m_request.get(), *pair.m_response.get())) << "Message " << messageName.c_str() << " failed to send";
}
else
{
EXPECT_TRUE(SendRequest(*pair.m_request.get())) << "Message " << messageName.c_str() << " failed to send";
// Since there's no response, the above line will finish immediately, so we need to wait a little bit so the message can actually be sent
// before we check if it was received
// We'll wait a maximum of 5 seconds, checking periodically if the message was received, to avoid failing due to slow running test servers
constexpr int MaxWaitTimeSeconds = 5;
auto start = AZStd::chrono::monotonic_clock::now();
while (!m_assetRequestHandler->m_invoked && AZStd::chrono::monotonic_clock::now() - start < AZStd::chrono::seconds(MaxWaitTimeSeconds))
{
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(10));
}
}
EXPECT_TRUE(m_assetRequestHandler->m_invoked) << "Message " << messageName.c_str() << " was not received";
}
});
}
TEST_F(AssetProcessorMessages, GetUnresolvedProductReferences_Succeeds)
{
using namespace AzToolsFramework::AssetDatabase;
// Setup the database with all needed info
ScanFolderDatabaseEntry scanfolder1("scanfolder1", "scanfolder1", "scanfolder1", "");
ASSERT_TRUE(m_dbConn.SetScanFolder(scanfolder1));
SourceDatabaseEntry source1(scanfolder1.m_scanFolderID, "source1.png", AZ::Uuid::CreateRandom(), "Fingerprint");
SourceDatabaseEntry source2(scanfolder1.m_scanFolderID, "source2.png", AZ::Uuid::CreateRandom(), "Fingerprint");
ASSERT_TRUE(m_dbConn.SetSource(source1));
ASSERT_TRUE(m_dbConn.SetSource(source2));
JobDatabaseEntry job1(source1.m_sourceID, "jobkey", 1234, "pc", AZ::Uuid::CreateRandom(), AzToolsFramework::AssetSystem::JobStatus::Completed, 1111);
JobDatabaseEntry job2(source2.m_sourceID, "jobkey", 1234, "pc", AZ::Uuid::CreateRandom(), AzToolsFramework::AssetSystem::JobStatus::Completed, 2222);
ASSERT_TRUE(m_dbConn.SetJob(job1));
ASSERT_TRUE(m_dbConn.SetJob(job2));
ProductDatabaseEntry product1(job1.m_jobID, 5, "source1.product", AZ::Data::AssetType::CreateRandom());
ProductDatabaseEntry product2(job2.m_jobID, 15, "source2.product", AZ::Data::AssetType::CreateRandom());
ASSERT_TRUE(m_dbConn.SetProduct(product1));
ASSERT_TRUE(m_dbConn.SetProduct(product2));
ProductDependencyDatabaseEntry dependency1(product1.m_productID, AZ::Uuid::CreateNull(), 0, {}, "pc", 0, "somefileA.txt", ProductDependencyDatabaseEntry::DependencyType::ProductDep_SourceFile);
ProductDependencyDatabaseEntry dependency2(product1.m_productID, AZ::Uuid::CreateNull(), 0, {}, "pc", 0, "somefileB.txt", ProductDependencyDatabaseEntry::DependencyType::ProductDep_ProductFile);
ProductDependencyDatabaseEntry dependency3(product1.m_productID, AZ::Uuid::CreateNull(), 0, {}, "pc", 0, "somefileC.txt");
ProductDependencyDatabaseEntry dependency4(product1.m_productID, AZ::Uuid::CreateNull(), 0, {}, "pc", 0, ":somefileD.txt"); // Exclusion
ProductDependencyDatabaseEntry dependency5(product1.m_productID, AZ::Uuid::CreateNull(), 0, {}, "pc", 0, "somefileE*.txt"); // Wildcard
ASSERT_TRUE(m_dbConn.SetProductDependency(dependency1));
ASSERT_TRUE(m_dbConn.SetProductDependency(dependency2));
ASSERT_TRUE(m_dbConn.SetProductDependency(dependency3));
ASSERT_TRUE(m_dbConn.SetProductDependency(dependency4));
ASSERT_TRUE(m_dbConn.SetProductDependency(dependency5));
// Setup the asset catalog
AzFramework::AssetSystem::AssetNotificationMessage assetNotificationMessage("source1.product", AzFramework::AssetSystem::AssetNotificationMessage::NotificationType::AssetChanged, AZ::Data::AssetType::CreateRandom(), "pc");
assetNotificationMessage.m_assetId = AZ::Data::AssetId(source1.m_sourceGuid, product1.m_subID);
assetNotificationMessage.m_dependencies.push_back(AZ::Data::ProductDependency(AZ::Data::AssetId(source2.m_sourceGuid, product2.m_subID), {}));
m_assetCatalog->OnAssetMessage(assetNotificationMessage);
// Run the actual test
RunNetworkRequest([&source1, &product1]()
{
using namespace AzFramework;
AZ::u32 assetReferenceCount, pathReferenceCount;
AZ::Data::AssetId assetId = AZ::Data::AssetId(source1.m_sourceGuid, product1.m_subID);
AssetSystemRequestBus::Broadcast(&AssetSystemRequestBus::Events::GetUnresolvedProductReferences, assetId, assetReferenceCount, pathReferenceCount);
ASSERT_EQ(assetReferenceCount, 1);
ASSERT_EQ(pathReferenceCount, 3);
});
ASSERT_TRUE(m_assetCatalog->m_called);
}
}
@@ -0,0 +1,167 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "AssetProcessorTest.h"
#include <AzCore/UserSettings/UserSettingsComponent.h>
#include "BaseAssetProcessorTest.h"
#include <native/utilities/BatchApplicationManager.h>
#include <native/connection/connectionManager.h>
#include <QCoreApplication>
AZ_UNIT_TEST_HOOK(new BaseAssetProcessorTestEnvironment)
namespace AssetProcessor
{
class UnitTestAppManager : public BatchApplicationManager
{
public:
explicit UnitTestAppManager(int* argc, char*** argv)
: BatchApplicationManager(argc, argv)
{}
bool PrepareForTests()
{
if (!ApplicationManager::Activate())
{
return false;
}
// tests which use the builder bus plug in their own mock version, so disconnect ours.
AssetProcessor::AssetBuilderInfoBus::Handler::BusDisconnect();
// Disable saving global user settings to prevent failure due to detecting file updates
AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
m_platformConfig.reset(new AssetProcessor::PlatformConfiguration);
m_connectionManager.reset(new ConnectionManager(m_platformConfig.get()));
RegisterObjectForQuit(m_connectionManager.get());
return true;
}
AZStd::unique_ptr<AssetProcessor::PlatformConfiguration> m_platformConfig;
AZStd::unique_ptr<ConnectionManager> m_connectionManager;
};
class LegacyTestAdapter : public AssetProcessorTest,
public ::testing::WithParamInterface<std::string>
{
void SetUp() override
{
AssetProcessorTest::SetUp();
static int numParams = 1;
static char processName[] = {"AssetProcessorBatch"};
static char* namePtr = &processName[0];
static char** paramStringArray = &namePtr;
m_application.reset(new UnitTestAppManager(&numParams, &paramStringArray));
ASSERT_EQ(m_application->BeforeRun(), ApplicationManager::Status_Success);
ASSERT_TRUE(m_application->PrepareForTests());
}
void TearDown() override
{
m_application.reset();
AssetProcessorTest::TearDown();
}
AZStd::unique_ptr<UnitTestAppManager> m_application;
};
// use the list of registered legacy unit tests to generate the list of test parameters:
std::vector<std::string> GenerateTestCases()
{
std::vector<std::string> names;
UnitTestRegistry* currentTest = UnitTestRegistry::first();
while (currentTest)
{
names.push_back(currentTest->getName());
currentTest = currentTest->next();
}
return names;
}
// use the above generator function to decide what the name of the test is
// instead of just showing "0" "1" etc
std::string GenerateTestName(const ::testing::TestParamInfo<std::string>& info)
{
return info.param;
}
TEST_P(LegacyTestAdapter, AllTests)
{
// this is a generator test function. This will be called repeatedly based on the above
// generator function. Each time, it will set GetParam() to be the generated value.
// doing just one at a time per setup and teardown makes sure each one works on its own and doesn't
// interfere with the others.
UnitTestRegistry* currentTest = UnitTestRegistry::first();
while (currentTest)
{
if (azstricmp(currentTest->getName(), GetParam().c_str()) == 0)
{
UnitTestRun* actualTest = currentTest->create();
volatile bool testIsComplete = false;
QString failMessage;
QObject::connect(actualTest, &UnitTestRun::UnitTestPassed, [&testIsComplete]()
{
testIsComplete = true;
});
QObject::connect(actualTest, &UnitTestRun::UnitTestFailed, [&testIsComplete, &failMessage](QString message)
{
testIsComplete = true;
failMessage = message;
});
QElapsedTimer time;
time.start();
actualTest->StartTest();
while (!testIsComplete)
{
QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete);
QCoreApplication::processEvents();
// operation
if (time.elapsed() > 120 * 1000) // (ms) no test, even in debug, takes longer than two minutes
{
testIsComplete = true;
failMessage = QString("Legacy test deadlocked or timed out.");
}
}
// Explanation of below: EXPECT_TRUE returns an object that can be used with the stream operator
// to add additional information when it fails, for display to the user.
EXPECT_TRUE(failMessage.isEmpty()) << failMessage.toUtf8().constData();
delete actualTest;
}
currentTest = currentTest->next();
}
}
INSTANTIATE_TEST_CASE_P(
Test,
LegacyTestAdapter,
testing::ValuesIn(GenerateTestCases()),
GenerateTestName);
};
@@ -0,0 +1,74 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzTest/AzTest.h>
#include <AzCore/Memory/Memory.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzFramework/Application/Application.h>
#include <native/utilities/assetUtils.h>
#include <native/unittests/UnitTestRunner.h> // for the assert absorber.
#include <AssetManager/FileStateCache.h>
namespace AssetProcessor
{
// This is an utility class for Asset Processor Tests
// Any gmock based fixture class can derived from this class and this will automatically do system allocation and teardown for you
// It is important to note that if you are overriding Setup and Teardown functions of your fixture class than please call the base class functions.
class AssetProcessorTest
: public ::testing::Test
{
protected:
UnitTestUtils::AssertAbsorber* m_errorAbsorber;
FileStatePassthrough m_fileStateCache;
void SetUp() override
{
if (!AZ::AllocatorInstance<AZ::OSAllocator>::IsReady())
{
m_ownsOSAllocator = true;
AZ::AllocatorInstance<AZ::OSAllocator>::Create();
}
if (!AZ::AllocatorInstance<AZ::SystemAllocator>::IsReady())
{
m_ownsSysAllocator = true;
AZ::AllocatorInstance<AZ::SystemAllocator>::Create();
}
m_errorAbsorber = new UnitTestUtils::AssertAbsorber();
m_application = AZStd::make_unique<AzFramework::Application>();
}
void TearDown() override
{
AssetUtilities::ResetAssetRoot();
m_application.reset();
delete m_errorAbsorber;
m_errorAbsorber = nullptr;
if (m_ownsSysAllocator)
{
AZ::AllocatorInstance<AZ::SystemAllocator>::Destroy();
m_ownsSysAllocator = false;
}
if (m_ownsOSAllocator)
{
AZ::AllocatorInstance<AZ::OSAllocator>::Destroy();
m_ownsOSAllocator = false;
}
}
bool m_ownsOSAllocator = false;
bool m_ownsSysAllocator = false;
AZStd::unique_ptr<AzFramework::Application> m_application;
};
}
@@ -0,0 +1,59 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <qlogging.h>
#include <QString>
// Environments subclass from AZ::Test::ITestEnvironment
class BaseAssetProcessorTestEnvironment : public AZ::Test::ITestEnvironment
{
public:
virtual ~BaseAssetProcessorTestEnvironment() {}
protected:
// Any time Qt emits a warning, critical, or fatal, consider the test to have failed!
static void UnitTestMessageHandler(QtMsgType type, const QMessageLogContext& /*context*/, const QString& msg)
{
switch (type)
{
case QtDebugMsg:
break;
case QtWarningMsg:
EXPECT_FALSE("QtWarningMsg") << msg.toUtf8().constData();
break;
case QtCriticalMsg:
EXPECT_FALSE("QtCriticalMsg") << msg.toUtf8().constData();
break;
case QtFatalMsg:
EXPECT_FALSE("QtFatalMsg") << msg.toUtf8().constData();
break;
}
}
// There are two pure-virtual functions to implement, setup and teardown
void SetupEnvironment() override
{
// Setup code
qInstallMessageHandler(UnitTestMessageHandler);
}
void TeardownEnvironment() override
{
qInstallMessageHandler(nullptr);
}
private:
// Put members that need to be maintained throughout testing lifecycle here
// Don't declare them in the setup/teardown functions!
};
@@ -0,0 +1,148 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "native/utilities/BuilderConfigurationManager.h"
#include <native/unittests/UnitTestRunner.h>
#include <AzCore/UnitTest/TestTypes.h>
class BuilderConfigurationTests
: public ::UnitTest::ScopedAllocatorSetupFixture
{
public:
BuilderConfigurationTests()
{
}
virtual ~BuilderConfigurationTests()
{
}
void SetUp() override
{
}
void TearDown() override
{
}
void CreateTestConfig(QString iniStr, AssetProcessor::BuilderConfigurationManager& configurationManager)
{
QDir tempPath(m_tempDir.path());
UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath(AssetProcessor::BuilderConfigFile).toUtf8().data(), iniStr);
configurationManager.LoadConfiguration(tempPath.absoluteFilePath(AssetProcessor::BuilderConfigFile).toUtf8().data());
}
QTemporaryDir m_tempDir;
};
const char SampleConfig[] =
"[Job PNG Compile]\n"
"checkServer=true\n"
"priority=3\n"
"critical=true\n"
"checkExclusiveLock=true\n"
"fingerprint=finger\n"
"jobFingerprint=somejob7\n"
"params=something=true,otherthing,somethingelse=7\n"
"[Builder Image Worker Builder]\n"
"fingerprint=fingerprint11\n"
"version=7\n"
"patterns=*.png\n"
"[Job TIFF Compile]\n"
"checkServer=false\n"
"priority=9\n"
"critical=false\n"
"checkExclusiveLock=true\n"
"fingerprint=fingerprint1\n"
"params=something=false,otheing,somethingelse=6\n";
TEST_F(BuilderConfigurationTests, TestBuilderConfig_LoadConfig_Success)
{
AssetProcessor::BuilderConfigurationManager builderConfig;
CreateTestConfig(SampleConfig, builderConfig);
ASSERT_TRUE(builderConfig.IsLoaded());
}
TEST_F(BuilderConfigurationTests, TestBuilderConfig_InvalidKey_NoUpdate)
{
AssetProcessor::BuilderConfigurationManager builderConfig;
CreateTestConfig(SampleConfig, builderConfig);
AssetBuilderSDK::JobDescriptor baseDescriptor;
AssetBuilderSDK::JobDescriptor testDescriptor;
// Verify an undefined key does not update our data
ASSERT_FALSE(builderConfig.UpdateJobDescriptor("False Key", testDescriptor));
ASSERT_EQ(testDescriptor.m_checkServer, baseDescriptor.m_checkServer);
ASSERT_EQ(testDescriptor.m_critical, baseDescriptor.m_critical);
ASSERT_EQ(testDescriptor.m_priority, baseDescriptor.m_priority);
ASSERT_EQ(testDescriptor.m_checkExclusiveLock, baseDescriptor.m_checkExclusiveLock);
ASSERT_EQ(testDescriptor.m_additionalFingerprintInfo, baseDescriptor.m_additionalFingerprintInfo);
ASSERT_EQ(testDescriptor.m_jobParameters, baseDescriptor.m_jobParameters);
}
TEST_F(BuilderConfigurationTests, TestBuilderConfig_JobEntry_Success)
{
AssetProcessor::BuilderConfigurationManager builderConfig;
CreateTestConfig(SampleConfig, builderConfig);
AssetBuilderSDK::JobDescriptor testDescriptor;
// Verify a JobEntry makes the expected updates from data
ASSERT_TRUE(builderConfig.UpdateJobDescriptor("PNG Compile", testDescriptor));
ASSERT_EQ(testDescriptor.m_checkServer, true);
ASSERT_EQ(testDescriptor.m_critical, true);
ASSERT_EQ(testDescriptor.m_priority, 3);
ASSERT_EQ(testDescriptor.m_checkExclusiveLock, true);
ASSERT_EQ(testDescriptor.m_additionalFingerprintInfo, "finger");
ASSERT_EQ(testDescriptor.m_jobParameters[AZ_CRC("something", 0x09da31fb)], "true");
ASSERT_EQ(testDescriptor.m_jobParameters[AZ_CRC("somethingelse", 0x237edebb)], "7");
ASSERT_NE(testDescriptor.m_jobParameters.find(AZ_CRC("otherthing", 0x6f2d0a4a)), testDescriptor.m_jobParameters.end());
}
TEST_F(BuilderConfigurationTests, TestBuilderConfig_SecondJobEntry_Success)
{
AssetProcessor::BuilderConfigurationManager builderConfig;
CreateTestConfig(SampleConfig, builderConfig);
AssetBuilderSDK::JobDescriptor testDescriptor;
// Verify a second JobEntry defined in an .ini file makes the expected updates from data
ASSERT_TRUE(builderConfig.UpdateJobDescriptor("TIFF Compile", testDescriptor));
ASSERT_EQ(testDescriptor.m_checkServer, false);
ASSERT_EQ(testDescriptor.m_critical, false);
ASSERT_EQ(testDescriptor.m_priority, 9);
ASSERT_EQ(testDescriptor.m_checkExclusiveLock, true);
ASSERT_EQ(testDescriptor.m_additionalFingerprintInfo, "fingerprint1");
ASSERT_EQ(testDescriptor.m_jobParameters[AZ_CRC("something", 0x09da31fb)], "false");
ASSERT_EQ(testDescriptor.m_jobParameters[AZ_CRC("somethingelse", 0x237edebb)], "6");
ASSERT_NE(testDescriptor.m_jobParameters.find(AZ_CRC("otheing", 0xba35d565)), testDescriptor.m_jobParameters.end());
}
TEST_F(BuilderConfigurationTests, TestBuilderConfig_BuilderEntry_Success)
{
AssetProcessor::BuilderConfigurationManager builderConfig;
CreateTestConfig(SampleConfig, builderConfig);
// Verify a Builder makes the expected updates from data
AssetBuilderSDK::AssetBuilderDesc testBuilder;
ASSERT_TRUE(builderConfig.UpdateBuilderDescriptor("Image Worker Builder", testBuilder));
ASSERT_EQ(testBuilder.m_analysisFingerprint, "fingerprint11");
ASSERT_EQ(testBuilder.m_version, 7);
ASSERT_EQ(testBuilder.m_patterns.size(), 1);
ASSERT_EQ(testBuilder.m_patterns[0].m_pattern, "*.png");
}
@@ -0,0 +1,181 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "FileProcessorTests.h"
namespace UnitTests
{
constexpr int ConnectionBusId = 0;
void FileProcessorTests::SetUp()
{
AssetProcessorTest::SetUp();
ConnectionBus::Handler::BusConnect(ConnectionBusId);
m_data.reset(new StaticData());
m_data->m_databaseLocationListener.BusConnect();
m_data->m_temporarySourceDir = QDir(m_data->m_temporaryDir.path());
// in other unit tests we may open the database called ":memory:" to use an in-memory database instead of one on disk.
// in this test, however, we use a real database, because the file processor shares it and opens its own connection to it.
// ":memory:" databases are one-instance-only, and even if another connection is opened to ":memory:" it would
// not share with others created using ":memory:" and get a unique database instead.
m_data->m_databaseLocation = m_data->m_temporarySourceDir.absoluteFilePath("test_database.sqlite").toUtf8().constData();
ON_CALL(m_data->m_databaseLocationListener, GetAssetDatabaseLocation(_))
.WillByDefault(
DoAll( // set the 0th argument ref (string) to the database location and return true.
SetArgReferee<0>(m_data->m_databaseLocation),
Return(true)));
// Initialize the database:
m_data->m_connection.ClearData(); // this is expected to reset/clear/reopen
m_data->m_config = AZStd::make_unique<AssetProcessor::PlatformConfiguration>();
m_data->m_config->EnablePlatform({ "pc", { "host", "renderer", "desktop" } }, true);
m_data->m_fileProcessor = AZStd::make_unique<FileProcessor>(m_data->m_config.get());
m_data->m_scanFolder = { m_data->m_temporarySourceDir.absolutePath().toUtf8().constData(), "dev", "rootportkey", "" };
ASSERT_TRUE(m_data->m_connection.SetScanFolder(m_data->m_scanFolder));
m_data->m_config->AddScanFolder(ScanFolderInfo(m_data->m_temporarySourceDir.absolutePath(), "dev", "rootportkey", "", false, true, m_data->m_config->GetEnabledPlatforms(), 0, m_data->m_scanFolder.m_scanFolderID));
for (int index = 0; index < 10; ++index)
{
FileDatabaseEntry entry;
entry.m_fileName = AZStd::string::format("somefile_%d.tif", index);
entry.m_isFolder = false;
entry.m_modTime = 0;
entry.m_scanFolderPK = m_data->m_scanFolder.m_scanFolderID;
m_data->m_fileEntries.push_back(entry);
}
}
void FileProcessorTests::TearDown()
{
m_data->m_databaseLocationListener.BusDisconnect();
m_data.reset();
ConnectionBus::Handler::BusDisconnect(ConnectionBusId);
AssetProcessorTest::TearDown();
}
size_t FileProcessorTests::Send([[maybe_unused]] unsigned int serial, [[maybe_unused]] const AzFramework::AssetSystem::BaseAssetProcessorMessage& message)
{
m_data->m_messagesSent++;
return 0;
}
TEST_F(FileProcessorTests, FilesAdded_WhenSentMultipleAdds_ShouldEmitOnlyOneAdd)
{
QSet<AssetFileInfo> scannerFiles;
m_data->m_fileProcessor->AssessAddedFile(m_data->m_temporarySourceDir.absoluteFilePath(m_data->m_fileEntries[0].m_fileName.c_str()));
m_data->m_fileProcessor->AssessAddedFile(m_data->m_temporarySourceDir.absoluteFilePath(m_data->m_fileEntries[0].m_fileName.c_str()));
ASSERT_EQ(m_data->m_messagesSent, 1);
}
TEST_F(FileProcessorTests, FilesFromScanner_ShouldSaveToDatabaseWithoutCreatingDuplicates)
{
QSet<AssetFileInfo> scannerFiles;
auto* scanFolder = m_data->m_config->GetScanFolderByPath(m_data->m_scanFolder.m_scanFolder.c_str());
ASSERT_NE(scanFolder, nullptr);
for (const auto& file : m_data->m_fileEntries)
{
scannerFiles.insert(AssetFileInfo(m_data->m_temporarySourceDir.absoluteFilePath(file.m_fileName.c_str()), QDateTime::fromMSecsSinceEpoch(file.m_modTime), 1234, scanFolder, file.m_isFolder));
}
m_data->m_fileProcessor->AssessFilesFromScanner(scannerFiles);
m_data->m_fileProcessor->Sync();
// Run again to make sure we don't get duplicate entries
m_data->m_fileProcessor->AssessFilesFromScanner(scannerFiles);
m_data->m_fileProcessor->Sync();
FileDatabaseEntryContainer actualEntries;
auto filesFunction = [&actualEntries](AzToolsFramework::AssetDatabase::FileDatabaseEntry& entry)
{
actualEntries.push_back(entry);
return true;
};
ASSERT_TRUE(m_data->m_connection.QueryFilesTable(filesFunction));
ASSERT_THAT(m_data->m_fileEntries, testing::UnorderedElementsAreArray(actualEntries));
}
TEST_F(FileProcessorTests, FilesFromScanner_ShouldHandleChangesBetweenSyncs)
{
QSet<AssetFileInfo> scannerFiles;
auto* scanFolder = m_data->m_config->GetScanFolderByPath(m_data->m_scanFolder.m_scanFolder.c_str());
ASSERT_NE(scanFolder, nullptr);
for (const auto& file : m_data->m_fileEntries)
{
scannerFiles.insert(AssetFileInfo(m_data->m_temporarySourceDir.absoluteFilePath(file.m_fileName.c_str()), QDateTime::fromMSecsSinceEpoch(file.m_modTime), 1234, scanFolder, file.m_isFolder));
}
m_data->m_fileProcessor->AssessFilesFromScanner(scannerFiles);
m_data->m_fileProcessor->Sync();
FileDatabaseEntryContainer actualEntries;
auto filesFunction = [&actualEntries](AzToolsFramework::AssetDatabase::FileDatabaseEntry& entry)
{
actualEntries.push_back(entry);
return true;
};
ASSERT_TRUE(m_data->m_connection.QueryFilesTable(filesFunction));
ASSERT_THAT(m_data->m_fileEntries, testing::UnorderedElementsAreArray(actualEntries));
// Clear the db (we don't have the file IDs in m_fileEntries to remove 1 by 1 so its easier to just remove them all)
for (const auto& file : actualEntries)
{
m_data->m_connection.RemoveFile(file.m_fileID);
}
// Remove two files
m_data->m_fileEntries.erase(m_data->m_fileEntries.begin());
m_data->m_fileEntries.erase(m_data->m_fileEntries.begin());
// Add a file
FileDatabaseEntry entry;
entry.m_fileName = AZStd::string::format("somefile_%d.tif", 11);
entry.m_isFolder = false;
entry.m_modTime = 0;
entry.m_scanFolderPK = m_data->m_scanFolder.m_scanFolderID;
m_data->m_fileEntries.push_back(entry);
scannerFiles.clear();
for (const auto& file : m_data->m_fileEntries)
{
scannerFiles.insert(AssetFileInfo(m_data->m_temporarySourceDir.absoluteFilePath(file.m_fileName.c_str()), QDateTime::fromMSecsSinceEpoch(file.m_modTime), 1234, scanFolder, file.m_isFolder));
}
// Sync again
m_data->m_fileProcessor->AssessFilesFromScanner(scannerFiles);
m_data->m_fileProcessor->Sync();
actualEntries.clear();
ASSERT_TRUE(m_data->m_connection.QueryFilesTable(filesFunction));
ASSERT_THAT(m_data->m_fileEntries, testing::UnorderedElementsAreArray(actualEntries));
}
}
@@ -0,0 +1,114 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzTest/AzTest.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include "native/tests/AssetProcessorTest.h"
#include "AzToolsFramework/API/AssetDatabaseBus.h"
#include "AssetDatabase/AssetDatabase.h"
#include "FileProcessor/FileProcessor.h"
#include "utilities/PlatformConfiguration.h"
#include <QCoreApplication>
#include <utilities/AssetUtilEBusHelper.h>
namespace UnitTests
{
using namespace testing;
using ::testing::NiceMock;
using namespace AssetProcessor;
using AzToolsFramework::AssetDatabase::ProductDatabaseEntry;
using AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry;
using AzToolsFramework::AssetDatabase::SourceDatabaseEntry;
using AzToolsFramework::AssetDatabase::SourceFileDependencyEntry;
using AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer;
using AzToolsFramework::AssetDatabase::JobDatabaseEntry;
using AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer;
using AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry;
using AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer;
using AzToolsFramework::AssetDatabase::AssetDatabaseConnection;
using AzToolsFramework::AssetDatabase::FileDatabaseEntry;
using AzToolsFramework::AssetDatabase::FileDatabaseEntryContainer;
class FileProcessorTestsMockDatabaseLocationListener : public AzToolsFramework::AssetDatabase::AssetDatabaseRequests::Bus::Handler
{
public:
MOCK_METHOD1(GetAssetDatabaseLocation, bool(AZStd::string&));
};
class FileProcessorTests
: public AssetProcessorTest,
public ConnectionBus::Handler
{
public:
void SetUp() override;
void TearDown() override;
//////////////////////////////////////////////////////////////////////////
// Sends an unsolicited message to the connection
size_t Send(unsigned int serial, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message) override;
// Sends a raw buffer to the connection
size_t SendRaw([[maybe_unused]] unsigned int type, [[maybe_unused]] unsigned int serial, [[maybe_unused]] const QByteArray& data) override { return 0; };
// Sends a message to the connection if the platform match
size_t SendPerPlatform([[maybe_unused]] unsigned int serial, [[maybe_unused]] const AzFramework::AssetSystem::BaseAssetProcessorMessage& message, [[maybe_unused]] const QString& platform) override { return 0; };
// Sends a raw buffer to the connection if the platform match
size_t SendRawPerPlatform([[maybe_unused]] unsigned int type, [[maybe_unused]] unsigned int serial, [[maybe_unused]] const QByteArray& data, [[maybe_unused]] const QString& platform) override { return 0; };
// Sends a message to the connection which expects a response.
unsigned int SendRequest([[maybe_unused]] const AzFramework::AssetSystem::BaseAssetProcessorMessage& message, [[maybe_unused]] const ResponseCallback& callback) override { return 0; };
// Sends a response to the connection
size_t SendResponse([[maybe_unused]] unsigned int serial, [[maybe_unused]] const AzFramework::AssetSystem::BaseAssetProcessorMessage& message) override { return 0; };
// Removes a response handler that is no longer needed
void RemoveResponseHandler([[maybe_unused]] unsigned int serial) override {};
protected:
struct StaticData
{
QTemporaryDir m_temporaryDir;
QDir m_temporarySourceDir;
// these variables are created during SetUp() and destroyed during TearDown() and thus are always available during tests using this fixture:
AZStd::string m_databaseLocation;
NiceMock<FileProcessorTestsMockDatabaseLocationListener> m_databaseLocationListener;
AssetProcessor::AssetDatabaseConnection m_connection;
AZStd::unique_ptr<AssetProcessor::PlatformConfiguration> m_config;
// The following database entry variables are initialized only when you call coverage test data CreateCoverageTestData().
// Tests which don't need or want a pre-made database should not call CreateCoverageTestData() but note that in that case
// these entries will be empty and their identifiers will be -1.
ScanFolderDatabaseEntry m_scanFolder;
AZStd::unique_ptr<FileProcessor> m_fileProcessor;
FileDatabaseEntryContainer m_fileEntries;
QCoreApplication m_coreApp;
int m_argc = 0;
int m_messagesSent = 0;
StaticData() : m_coreApp(m_argc, nullptr)
{
}
};
// we store the above data in a unique_ptr so that its memory can be cleared during TearDown() in one call, before we destroy the memory
// allocator, reducing the chance of missing or forgetting to destroy one in the future.
AZStd::unique_ptr<StaticData> m_data;
};
}
@@ -0,0 +1,171 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "FileStateCacheTests.h"
#include <native/utilities/assetUtils.h>
#include <native/unittests/UnitTestRunner.h>
namespace UnitTests
{
void FileStateCacheTests::SetUp()
{
m_temporarySourceDir = QDir(m_temporaryDir.path());
m_fileStateCache = AZStd::make_unique<FileStateCache>();
}
void FileStateCacheTests::TearDown()
{
m_fileStateCache = nullptr;
}
void FileStateCacheTests::CheckForFile(QString path, bool shouldExist)
{
bool exists = false;
FileStateInfo fileInfo;
auto* fileStateInterface = AZ::Interface<IFileStateRequests>::Get();
ASSERT_NE(fileStateInterface, nullptr);
exists = fileStateInterface->Exists(path);
ASSERT_EQ(exists, shouldExist);
exists = fileStateInterface->GetFileInfo(path, &fileInfo);
ASSERT_EQ(exists, shouldExist);
if (exists)
{
ASSERT_EQ(AssetUtilities::NormalizeFilePath(fileInfo.m_absolutePath), AssetUtilities::NormalizeFilePath(path));
ASSERT_FALSE(fileInfo.m_isDirectory);
ASSERT_EQ(fileInfo.m_fileSize, 0);
}
}
TEST_F(FileStateCacheTests, QueryFile_ShouldNotExist)
{
QString testPath = m_temporarySourceDir.absoluteFilePath("test.txt");
// Make the file but don't tell the cache about it
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(testPath));
CheckForFile(testPath, false);
}
TEST_F(FileStateCacheTests, QueryAddedFile_ShouldExist)
{
QString testPath = m_temporarySourceDir.absoluteFilePath("test.txt");
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(testPath));
m_fileStateCache->AddFile(testPath);
CheckForFile(testPath, true);
}
TEST_F(FileStateCacheTests, QueryBulkAddedFile_ShouldExist)
{
QString testPath = m_temporarySourceDir.absoluteFilePath("test.txt");
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(testPath));
QSet<AssetFileInfo> infoSet;
AssetFileInfo fileInfo;
fileInfo.m_filePath = testPath;
fileInfo.m_isDirectory = false;
fileInfo.m_fileSize = 0;
fileInfo.m_modTime = QFileInfo(testPath).lastModified();
infoSet.insert(fileInfo);
m_fileStateCache->AddInfoSet(infoSet);
CheckForFile(testPath, true);
}
TEST_F(FileStateCacheTests, QueryRemovedFile_ShouldNotExist)
{
QString testPath = m_temporarySourceDir.absoluteFilePath("test.txt");
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(testPath));
m_fileStateCache->AddFile(testPath);
m_fileStateCache->RemoveFile(testPath);
CheckForFile(testPath, false);
}
TEST_F(FileStateCacheTests, AddAndRemoveFolder_ShouldAddAndRemoveSubFiles)
{
QDir testFolder = m_temporarySourceDir.absoluteFilePath("subfolder");
QString testPath1 = testFolder.absoluteFilePath("test1.txt");
QString testPath2 = testFolder.absoluteFilePath("test2.txt");
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(testPath1));
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(testPath2));
m_fileStateCache->AddFile(testFolder.absolutePath());
CheckForFile(testPath1, true);
CheckForFile(testPath2, true);
m_fileStateCache->RemoveFile(testFolder.absolutePath());
CheckForFile(testPath1, false);
CheckForFile(testPath2, false);
}
TEST_F(FileStateCacheTests, UpdateFileAndQuery_ShouldExist)
{
QString testPath = m_temporarySourceDir.absoluteFilePath("test.txt");
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(testPath));
QSet<AssetFileInfo> infoSet;
AssetFileInfo fileInfo;
fileInfo.m_filePath = testPath;
fileInfo.m_isDirectory = false;
fileInfo.m_fileSize = 1234; // Setting the file size to non-zero (even though the actual file is 0), UpdateFile should update this to 0 and allow CheckForFile to pass as a result
fileInfo.m_modTime = QFileInfo(testPath).lastModified();
infoSet.insert(fileInfo);
m_fileStateCache->AddInfoSet(infoSet);
m_fileStateCache->UpdateFile(testPath);
CheckForFile(testPath, true);
}
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>();
QString testPath = m_temporarySourceDir.absoluteFilePath("test.txt");
CheckForFile(testPath, false);
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(testPath));
CheckForFile(testPath, true);
}
TEST_F(FileStateCacheTests, HandlesMixedSeperators)
{
QSet<AssetFileInfo> infoSet;
AssetFileInfo fileInfo;
fileInfo.m_filePath = R"(c:\some/test\file.txt)";
infoSet.insert(fileInfo);
m_fileStateCache->AddInfoSet(infoSet);
CheckForFile(R"(c:\some\test\file.txt)", true);
CheckForFile(R"(c:/some/test/file.txt)", true);
}
}
@@ -0,0 +1,38 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzTest/AzTest.h>
#include <AssetManager/FileStateCache.h>
#include <QTemporaryDir>
namespace UnitTests
{
using namespace testing;
using ::testing::NiceMock;
using namespace AssetProcessor;
class FileStateCacheTests : public ::testing::Test
{
public:
void SetUp() override;
void TearDown() override;
void CheckForFile(QString path, bool shouldExist);
protected:
QTemporaryDir m_temporaryDir;
QDir m_temporarySourceDir;
AZStd::unique_ptr<FileStateBase> m_fileStateCache;
};
}
@@ -0,0 +1,158 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/JSON/rapidjson.h>
#include <AzCore/Settings/SettingsRegistryImpl.h>
#include <native/InternalBuilders/SettingsRegistryBuilder.h>
#include <native/tests/AssetProcessorTest.h>
namespace AssetProcessor
{
class SettingsRegistryBuilderTest
: public AssetProcessorTest
{
};
// These tests are done relative to "TestValues" because the Settings Registry adds runtime information for
// anything that is merged in.
TEST_F(SettingsRegistryBuilderTest, SettingsExporter_ExportRegistryToJson_ProducesIdenticalJsonToRegularWriter)
{
static constexpr char json[] =
R"( {
"TestValues":
{
"BoolTrue": true,
"BoolFalse": false,
"Integer": 42,
"Double": 42.0,
"String": "hello",
"Array": [ null, true, false, 42, 42.0, "hello", { "Field": 42 }, [ 42, 42.0 ] ]
}
})";
rapidjson::Document document;
document.Parse(json);
ASSERT_FALSE(document.HasParseError());
rapidjson::StringBuffer jsonOutputBuffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(jsonOutputBuffer);
document.FindMember("TestValues")->value.Accept(writer);
AZ::SettingsRegistryImpl registry;
ASSERT_TRUE(registry.MergeSettings(json, AZ::SettingsRegistryInterface::Format::JsonMergePatch));
rapidjson::StringBuffer registryOutputBuffer;
AZStd::vector<AZStd::string> excludes;
SettingsRegistryBuilder::SettingsExporter exporter(registryOutputBuffer, excludes);
registry.Visit(exporter, "/TestValues");
ASSERT_TRUE(exporter.Finalize());
EXPECT_EQ(jsonOutputBuffer.GetLength(), registryOutputBuffer.GetLength());
EXPECT_STREQ(jsonOutputBuffer.GetString(), registryOutputBuffer.GetString());
}
TEST_F(SettingsRegistryBuilderTest, SettingsExporter_FilterOutSection_FieldNotInOutput)
{
static constexpr char json[] =
R"( {
"TestValues":
{
"A":
{
"B":
{
"X": 42
},
"C": true
}
}
})";
AZ::SettingsRegistryImpl registry;
ASSERT_TRUE(registry.MergeSettings(json, AZ::SettingsRegistryInterface::Format::JsonMergePatch));
rapidjson::StringBuffer registryOutputBuffer;
AZStd::vector<AZStd::string> excludes;
excludes.push_back("/TestValues/A/B");
SettingsRegistryBuilder::SettingsExporter exporter(registryOutputBuffer, excludes);
registry.Visit(exporter, "/TestValues");
ASSERT_TRUE(exporter.Finalize());
rapidjson::Document document;
document.Parse(registryOutputBuffer.GetString(), registryOutputBuffer.GetLength());
ASSERT_FALSE(document.HasParseError());
auto it = document.FindMember("A");
ASSERT_NE(document.MemberEnd(), it);
EXPECT_EQ(it->value.MemberEnd(), it->value.FindMember("B"));
EXPECT_NE(it->value.MemberEnd(), it->value.FindMember("C"));
}
TEST_F(SettingsRegistryBuilderTest, SettingsExporter_ExportRegistryWithNull_NullIsSerialized)
{
static constexpr char json[] =
R"( [
{ "op": "add", "path": "/TestValues", "value": { "Null": null } }
])";
AZ::SettingsRegistryImpl registry;
ASSERT_TRUE(registry.MergeSettings(json, AZ::SettingsRegistryInterface::Format::JsonPatch));
rapidjson::StringBuffer registryOutputBuffer;
AZStd::vector<AZStd::string> excludes;
SettingsRegistryBuilder::SettingsExporter exporter(registryOutputBuffer, excludes);
registry.Visit(exporter, "/TestValues");
ASSERT_TRUE(exporter.Finalize());
rapidjson::Document document;
document.Parse(registryOutputBuffer.GetString(), registryOutputBuffer.GetLength());
ASSERT_FALSE(document.HasParseError());
auto it = document.FindMember("Null");
ASSERT_NE(document.MemberEnd(), it);
EXPECT_TRUE(it->value.IsNull());
}
TEST_F(SettingsRegistryBuilderTest, SettingsExporter_ExportCanBeReused_SecondExportWorksCorrectly)
{
static constexpr char jsonFirst[] =
R"( {
"TestValues": { "FirstPass" : 1 }
})";
static constexpr char jsonSecond[] =
R"( {
"TestValues": { "SecondPass" : 1 }
})";
AZ::SettingsRegistryImpl registryFirst;
ASSERT_TRUE(registryFirst.MergeSettings(jsonFirst, AZ::SettingsRegistryInterface::Format::JsonMergePatch));
AZ::SettingsRegistryImpl registrySecond;
ASSERT_TRUE(registrySecond.MergeSettings(jsonSecond, AZ::SettingsRegistryInterface::Format::JsonMergePatch));
rapidjson::StringBuffer registryOutputBuffer;
AZStd::vector<AZStd::string> excludes;
SettingsRegistryBuilder::SettingsExporter exporter(registryOutputBuffer, excludes);
registryFirst.Visit(exporter, "/TestValues");
ASSERT_TRUE(exporter.Finalize());
registryOutputBuffer.Clear();
exporter.Reset(registryOutputBuffer);
registrySecond.Visit(exporter, "/TestValues");
rapidjson::Document document;
document.Parse(jsonSecond);
ASSERT_FALSE(document.HasParseError());
rapidjson::StringBuffer jsonOutputBuffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(jsonOutputBuffer);
document.FindMember("TestValues")->value.Accept(writer);
EXPECT_EQ(jsonOutputBuffer.GetLength(), registryOutputBuffer.GetLength());
EXPECT_STREQ(jsonOutputBuffer.GetString(), registryOutputBuffer.GetString());
}
} // namespace AssetProcessor
@@ -0,0 +1,261 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <native/tests/AssetProcessorTest.h>
#include <AzCore/Outcome/Outcome.h>
#include <AzToolsFramework/API/AssetDatabaseBus.h>
#include <native/utilities/MissingDependencyScanner.h>
#include <AssetDatabase/AssetDatabase.h>
namespace AssetProcessor
{
class MissingDependencyScanner_Test
: public MissingDependencyScanner
{
public:
AZStd::unordered_map<AZStd::string, AZStd::vector<AZStd::string>>& GetDependenciesRulesMap()
{
return m_dependenciesRulesMap;
}
};
class MissingDependencyScannerTestsMockDatabaseLocationListener : public AzToolsFramework::AssetDatabase::AssetDatabaseRequests::Bus::Handler
{
public:
MOCK_METHOD1(GetAssetDatabaseLocation, bool(AZStd::string&));
};
class MissingDependencyScannerTest
: public AssetProcessorTest
{
public:
MissingDependencyScannerTest()
{
}
protected:
void SetUp() override
{
using namespace testing;
using ::testing::NiceMock;
AssetProcessorTest::SetUp();
m_errorAbsorber = nullptr;
m_data = AZStd::make_unique<StaticData>();
QDir tempPath(m_data->m_tempDir.path());
m_data->m_databaseLocationListener.BusConnect();
m_data->m_databaseLocation = tempPath.absoluteFilePath("test_database.sqlite").toUtf8().constData();
ON_CALL(m_data->m_databaseLocationListener, GetAssetDatabaseLocation(_))
.WillByDefault(
DoAll( // set the 0th argument ref (string) to the database location and return true.
SetArgReferee<0>(m_data->m_databaseLocation),
Return(true)));
m_data->m_dbConn = AZStd::shared_ptr<AssetDatabaseConnection>(aznew AssetDatabaseConnection());
m_data->m_dbConn->OpenDatabase();
m_data->m_scopedDir.Setup(tempPath.absolutePath());
}
void TearDown() override
{
m_data = nullptr;
AssetProcessorTest::TearDown();
}
AZ::Outcome<AZ::s64, AZStd::string> CreateScanFolder(const AZStd::string& scanFolderName, const AZStd::string& scanFolderPath)
{
AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanFolder;
scanFolder.m_displayName = scanFolderName;
scanFolder.m_portableKey = scanFolderName;
scanFolder.m_scanFolder = scanFolderPath;
if (!m_data->m_dbConn->SetScanFolder(scanFolder))
{
return AZ::Failure(AZStd::string::format("Could not set create scan folder %s", scanFolderName.c_str()));
}
return AZ::Success(scanFolder.m_scanFolderID);
}
struct SourceAndProductInfo
{
AZ::Uuid m_uuid;
AZ::s64 m_productId;
};
AZ::Outcome<SourceAndProductInfo, AZStd::string> CreateSourceAndProductAsset(AZ::s64 scanFolderPK, const AZStd::string& sourceName, const AZStd::string& platform, const AZStd::string& productName)
{
using namespace AzToolsFramework::AssetDatabase;
SourceDatabaseEntry sourceEntry;
sourceEntry.m_sourceName = sourceName;
sourceEntry.m_sourceGuid = AssetUtilities::CreateSafeSourceUUIDFromName(sourceEntry.m_sourceName.c_str());
sourceEntry.m_scanFolderPK = scanFolderPK;
if (!m_data->m_dbConn->SetSource(sourceEntry))
{
return AZ::Failure(AZStd::string::format("Could not set source in the asset database for %s", sourceName.c_str()));
}
SourceAndProductInfo result;
result.m_uuid = sourceEntry.m_sourceGuid;
JobDatabaseEntry jobEntry;
jobEntry.m_sourcePK = sourceEntry.m_sourceID;
jobEntry.m_platform = platform;
jobEntry.m_jobRunKey = 1;
if(!m_data->m_dbConn->SetJob(jobEntry))
{
return AZ::Failure(AZStd::string::format("Could not set job in the asset database for %s", sourceName.c_str()));
}
ProductDatabaseEntry productEntry;
productEntry.m_jobPK = jobEntry.m_jobID;
productEntry.m_productName = AZStd::string::format("%s/%s", platform.c_str(), productName.c_str());
if(!m_data->m_dbConn->SetProduct(productEntry))
{
return AZ::Failure(AZStd::string::format("Could not set product in the asset database for %s", sourceName.c_str()));
}
result.m_productId = productEntry.m_productID;
return AZ::Success(result);
}
void CreateAndValidateMissingProductDependency(const AZStd::string& missingProductName)
{
using namespace AzToolsFramework::AssetDatabase;
QDir tempPath(m_data->m_tempDir.path());
QString testFilePath = tempPath.absoluteFilePath("subfolder1/assetProcessorManagerTest.txt");
AZStd::string testPlatform("pc");
AZStd::string missingProductPath(AZStd::string::format("test/%s", missingProductName.c_str()));
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(testFilePath, missingProductName.c_str()));
// Create the referenced product
AZ::Outcome<AZ::s64, AZStd::string> scanResult = CreateScanFolder("Test", tempPath.absoluteFilePath("subfolder1").toUtf8().constData());
ASSERT_TRUE(scanResult.IsSuccess());
AZ::s64 scanFolderIndex(scanResult.GetValue());
AZ::Outcome<SourceAndProductInfo, AZStd::string> firstAsset = CreateSourceAndProductAsset(scanFolderIndex, "tests/1", testPlatform, missingProductPath);
ASSERT_TRUE(firstAsset.IsSuccess());
AZ::Uuid actualTestGuid(firstAsset.GetValue().m_uuid);
// Create the product that references the product above. This represents the dummy file we created up above
AZ::Outcome<SourceAndProductInfo, AZStd::string> secondAsset = CreateSourceAndProductAsset(scanFolderIndex, "tests/2", testPlatform, "test/tests/2.product");
ASSERT_TRUE(secondAsset.IsSuccess());
AZ::s64 productId = secondAsset.GetValue().m_productId;
AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer container;
m_data->m_scanner.ScanFile(testFilePath.toUtf8().constData(), AssetProcessor::MissingDependencyScanner::DefaultMaxScanIteration, productId, container, m_data->m_dbConn, false, [](AZStd::string /*dependencyFile*/) {});
MissingProductDependencyDatabaseEntryContainer missingDeps;
ASSERT_TRUE(m_data->m_dbConn->GetMissingProductDependenciesByProductId(productId, missingDeps));
ASSERT_EQ(missingDeps.size(), 1);
ASSERT_EQ(missingDeps[0].m_productPK, productId);
ASSERT_EQ(missingDeps[0].m_dependencySourceGuid, actualTestGuid);
}
struct StaticData
{
QTemporaryDir m_tempDir;
AZStd::string m_databaseLocation;
::testing::NiceMock<MissingDependencyScannerTestsMockDatabaseLocationListener> m_databaseLocationListener;
AZStd::shared_ptr<AssetDatabaseConnection> m_dbConn;
MissingDependencyScanner_Test m_scanner;
UnitTestUtils::ScopedDir m_scopedDir; // Sets up FileIO instance
};
AZStd::unique_ptr<StaticData> m_data;
};
TEST_F(MissingDependencyScannerTest, ScanFile_FindsValidReferenceToProduct)
{
CreateAndValidateMissingProductDependency("tests/1.product");
}
TEST_F(MissingDependencyScannerTest, ScanFile_ValidReferenceToFileWithDash_FindsMissingReference)
{
CreateAndValidateMissingProductDependency("tests/1-withdash.product");
}
TEST_F(MissingDependencyScannerTest, ScanFile_CPP_File_FindsValidReferenceToProduct)
{
using namespace AzToolsFramework::AssetDatabase;
QDir tempPath(m_data->m_tempDir.path());
// Create the referenced product
ScanFolderDatabaseEntry scanFolder;
scanFolder.m_displayName = "Test";
scanFolder.m_portableKey = "Test";
scanFolder.m_scanFolder = tempPath.absoluteFilePath("subfolder1").toUtf8().constData();
ASSERT_TRUE(m_data->m_dbConn->SetScanFolder(scanFolder));
SourceDatabaseEntry sourceEntry;
sourceEntry.m_sourceName = "tests/1.source";
sourceEntry.m_sourceGuid = AssetUtilities::CreateSafeSourceUUIDFromName(sourceEntry.m_sourceName.c_str());
sourceEntry.m_scanFolderPK = 1;
ASSERT_TRUE(m_data->m_dbConn->SetSource(sourceEntry));
JobDatabaseEntry jobEntry;
jobEntry.m_sourcePK = sourceEntry.m_sourceID;
jobEntry.m_platform = "pc";
jobEntry.m_jobRunKey = 1;
ASSERT_TRUE(m_data->m_dbConn->SetJob(jobEntry));
ProductDatabaseEntry productEntry;
productEntry.m_jobPK = jobEntry.m_jobID;
productEntry.m_productName = "pc/test/tests/1.product";
ASSERT_TRUE(m_data->m_dbConn->SetProduct(productEntry));
AZStd::string productReference("tests/1.product");
// Create a cpp file that references the product above.
QString sourceFilePath = tempPath.absoluteFilePath("subfolder1/TestFile.cpp");
AZStd::string codeSourceCode = AZStd::string::format(R"(#include <Dummy/Dummy.h>;
#define PRODUCT_REFERENCE "%s")", productReference.c_str());
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(sourceFilePath, codeSourceCode.c_str()));
AZStd::string productDependency;
auto missingDependencyCallback = [&](AZStd::string relativeDependencyFilePath)
{
productDependency = relativeDependencyFilePath;
};
AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer container;
AZStd::string dependencyToken = "dummy";
// Since dependency rule map is empty this should show a missing dependency
m_data->m_scanner.ScanFile(sourceFilePath.toUtf8().constData(), AssetProcessor::MissingDependencyScanner::DefaultMaxScanIteration, m_data->m_dbConn, dependencyToken, false, missingDependencyCallback);
ASSERT_EQ(productDependency, productReference);
productDependency.clear();
QString anotherSourceFilePath = tempPath.absoluteFilePath("subfolder1/TestFile.cpp");
codeSourceCode = AZStd::string::format(R"(#include <Dummy/Dummy.h>;
AZStd::string filePath("%s")", productReference.c_str());
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(anotherSourceFilePath, codeSourceCode.c_str()));
m_data->m_scanner.ScanFile(anotherSourceFilePath.toUtf8().constData(), AssetProcessor::MissingDependencyScanner::DefaultMaxScanIteration, m_data->m_dbConn, dependencyToken, false, missingDependencyCallback);
ASSERT_EQ(productDependency, productReference);
AZStd::vector<AZStd::string> rulesMap;
rulesMap.emplace_back("*.product");
m_data->m_scanner.GetDependenciesRulesMap()[dependencyToken] = rulesMap;
productDependency.clear();
m_data->m_scanner.ScanFile(sourceFilePath.toUtf8().constData(), AssetProcessor::MissingDependencyScanner::DefaultMaxScanIteration, m_data->m_dbConn, dependencyToken, false, missingDependencyCallback);
ASSERT_TRUE(productDependency.empty());
}
}
@@ -0,0 +1,233 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <QTemporaryDir>
#include <AzTest/AzTest.h>
#include <AzCore/UnitTest/TestTypes.h>
#include "AzToolsFramework/API/AssetDatabaseBus.h"
#include "AssetDatabase/AssetDatabase.h"
#include <AssetManager/PathDependencyManager.h>
namespace UnitTests
{
class MockDatabaseLocationListener : public AzToolsFramework::AssetDatabase::AssetDatabaseRequests::Bus::Handler
{
public:
MOCK_METHOD1(GetAssetDatabaseLocation, bool(AZStd::string&));
};
namespace Util
{
using namespace AzToolsFramework::AssetDatabase;
void CreateSourceJobAndProduct(AssetProcessor::AssetDatabaseConnection* stateData, AZ::s64 scanfolderPk, SourceDatabaseEntry& source, JobDatabaseEntry& job, ProductDatabaseEntry& product, const char* sourceName, const char* productName)
{
source = SourceDatabaseEntry(scanfolderPk, sourceName, AZ::Uuid::CreateRandom(), "fingerprint");
EXPECT_TRUE(stateData->SetSource(source));
job = JobDatabaseEntry(source.m_sourceID, "jobkey", 1111, "pc", AZ::Uuid::CreateRandom(), AzToolsFramework::AssetSystem::JobStatus::Completed, 4444);
EXPECT_TRUE(stateData->SetJob(job));
product = ProductDatabaseEntry(job.m_jobID, 0, productName, AZ::Data::AssetType::CreateRandom());
EXPECT_TRUE(stateData->SetProduct(product));
}
}
struct PathDependencyDeletionTest
: UnitTest::ScopedAllocatorSetupFixture
, UnitTest::TraceBusRedirector
{
void SetUp() override;
void TearDown() override;
QTemporaryDir m_tempDir;
AZStd::string m_databaseLocation;
::testing::NiceMock<MockDatabaseLocationListener> m_databaseLocationListener;
AZStd::shared_ptr<AssetProcessor::AssetDatabaseConnection> m_stateData;
AZStd::unique_ptr<AssetProcessor::PlatformConfiguration> m_platformConfig;
};
void PathDependencyDeletionTest::SetUp()
{
using namespace ::testing;
using namespace AzToolsFramework::AssetDatabase;
BusConnect();
QDir tempPath(m_tempDir.path());
m_databaseLocationListener.BusConnect();
// in other unit tests we may open the database called ":memory:" to use an in-memory database instead of one on disk.
// in this test, however, we use a real database, because the file processor shares it and opens its own connection to it.
// ":memory:" databases are one-instance-only, and even if another connection is opened to ":memory:" it would
// not share with others created using ":memory:" and get a unique database instead.
m_databaseLocation = tempPath.absoluteFilePath("test_database.sqlite").toUtf8().constData();
ON_CALL(m_databaseLocationListener, GetAssetDatabaseLocation(_))
.WillByDefault(
DoAll( // set the 0th argument ref (string) to the database location and return true.
SetArgReferee<0>(m_databaseLocation),
Return(true)));
m_stateData = AZStd::shared_ptr<AssetProcessor::AssetDatabaseConnection>(new AssetProcessor::AssetDatabaseConnection());
m_stateData->OpenDatabase();
m_platformConfig = AZStd::make_unique<AssetProcessor::PlatformConfiguration>();
}
void PathDependencyDeletionTest::TearDown()
{
BusDisconnect();
}
TEST_F(PathDependencyDeletionTest, ExistingSourceWithUnmetDependency_RemovedFromDB_DependentSourceCreatedWithoutError)
{
using namespace AzToolsFramework::AssetDatabase;
// Add a product to the db with an unmet dependency
ScanFolderDatabaseEntry scanFolder("folder", "test", "test", "");
m_stateData->SetScanFolder(scanFolder);
SourceDatabaseEntry source1, source2;
JobDatabaseEntry job1, job2;
ProductDatabaseEntry product1, product2;
Util::CreateSourceJobAndProduct(m_stateData.get(), scanFolder.m_scanFolderID, source1, job1, product1, "source1.txt", "product1.jpg");
ProductDependencyDatabaseEntry dependency(product1.m_productID, AZ::Uuid::CreateRandom(), 0, 0, "pc", 0, "source2.txt", ProductDependencyDatabaseEntry::DependencyType::ProductDep_SourceFile);
m_stateData->SetProductDependency(dependency);
AssetProcessor::PathDependencyManager manager(m_stateData, m_platformConfig.get());
// Delete the data from the database
m_stateData->RemoveSource(source1.m_sourceID);
Util::CreateSourceJobAndProduct(m_stateData.get(), scanFolder.m_scanFolderID, source2, job2, product2, "source2.txt", "product2.jpg");
manager.RetryDeferredDependencies(source2);
}
TEST_F(PathDependencyDeletionTest, ExistingSourceWithUnmetDependency_RemovedFromDB_DependentProductCreatedWithoutError)
{
using namespace AzToolsFramework::AssetDatabase;
// Add a product to the db with an unmet dependency
ScanFolderDatabaseEntry scanFolder("folder", "test", "test", "");
m_stateData->SetScanFolder(scanFolder);
SourceDatabaseEntry source1, source2;
JobDatabaseEntry job1, job2;
ProductDatabaseEntry product1, product2;
Util::CreateSourceJobAndProduct(m_stateData.get(), scanFolder.m_scanFolderID, source1, job1, product1, "source1.txt", "product1.jpg");
ProductDependencyDatabaseEntry dependency(product1.m_productID, AZ::Uuid::CreateRandom(), 0, 0, "pc", 0, "product2.jpg", ProductDependencyDatabaseEntry::DependencyType::ProductDep_ProductFile);
m_stateData->SetProductDependency(dependency);
AssetProcessor::PathDependencyManager manager(m_stateData, m_platformConfig.get());
// Delete the data from the database
m_stateData->RemoveSource(source1.m_sourceID);
Util::CreateSourceJobAndProduct(m_stateData.get(), scanFolder.m_scanFolderID, source2, job2, product2, "source2.txt", "product2.jpg");
manager.RetryDeferredDependencies(source2);
}
TEST_F(PathDependencyDeletionTest, NewSourceWithUnmetDependency_RemovedFromDB_DependentSourceCreatedWithoutError)
{
using namespace AzToolsFramework::AssetDatabase;
AssetProcessor::PathDependencyManager manager(m_stateData, m_platformConfig.get());
// Add a product to the db with an unmet dependency
ScanFolderDatabaseEntry scanFolder("folder", "test", "test", "");
m_stateData->SetScanFolder(scanFolder);
SourceDatabaseEntry source1, source2;
JobDatabaseEntry job1, job2;
ProductDatabaseEntry product1, product2;
Util::CreateSourceJobAndProduct(m_stateData.get(), scanFolder.m_scanFolderID, source1, job1, product1, "source1.txt", "product1.jpg");
AssetBuilderSDK::ProductPathDependencySet set;
set.insert(AssetBuilderSDK::ProductPathDependency("source2.txt", AssetBuilderSDK::ProductPathDependencyType::SourceFile));
manager.SaveUnresolvedDependenciesToDatabase(set, product1, "pc");
// Delete the data from the database
m_stateData->RemoveSource(source1.m_sourceID);
Util::CreateSourceJobAndProduct(m_stateData.get(), scanFolder.m_scanFolderID, source2, job2, product2, "source2.txt", "product2.jpg");
manager.RetryDeferredDependencies(source2);
}
TEST_F(PathDependencyDeletionTest, NewSourceWithUnmetDependency_RemovedFromDB_DependentProductCreatedWithoutError)
{
using namespace AzToolsFramework::AssetDatabase;
AssetProcessor::PathDependencyManager manager(m_stateData, m_platformConfig.get());
// Add a product to the db with an unmet dependency
ScanFolderDatabaseEntry scanFolder("folder", "test", "test", "");
m_stateData->SetScanFolder(scanFolder);
SourceDatabaseEntry source1, source2;
JobDatabaseEntry job1, job2;
ProductDatabaseEntry product1, product2;
Util::CreateSourceJobAndProduct(m_stateData.get(), scanFolder.m_scanFolderID, source1, job1, product1, "source1.txt", "product1.jpg");
AssetBuilderSDK::ProductPathDependencySet set;
set.insert(AssetBuilderSDK::ProductPathDependency("product2.jpg", AssetBuilderSDK::ProductPathDependencyType::ProductFile));
manager.SaveUnresolvedDependenciesToDatabase(set, product1, "pc");
// Delete the data from the database
m_stateData->RemoveSource(source1.m_sourceID);
Util::CreateSourceJobAndProduct(m_stateData.get(), scanFolder.m_scanFolderID, source2, job2, product2, "source2.txt", "product2.jpg");
manager.RetryDeferredDependencies(source2);
}
TEST_F(PathDependencyDeletionTest, NewSourceWithUnmetDependency_Wildcard_RemovedFromDB_DependentSourceCreatedWithoutError)
{
using namespace AzToolsFramework::AssetDatabase;
AssetProcessor::PathDependencyManager manager(m_stateData, m_platformConfig.get());
// Add a product to the db with an unmet dependency
ScanFolderDatabaseEntry scanFolder("folder", "test", "test", "");
m_stateData->SetScanFolder(scanFolder);
SourceDatabaseEntry source1, source2;
JobDatabaseEntry job1, job2;
ProductDatabaseEntry product1, product2;
Util::CreateSourceJobAndProduct(m_stateData.get(), scanFolder.m_scanFolderID, source1, job1, product1, "source1.txt", "product1.jpg");
AssetBuilderSDK::ProductPathDependencySet set;
set.insert(AssetBuilderSDK::ProductPathDependency("sou*ce2.txt", AssetBuilderSDK::ProductPathDependencyType::SourceFile));
manager.SaveUnresolvedDependenciesToDatabase(set, product1, "pc");
// Delete the data from the database
m_stateData->RemoveSource(source1.m_sourceID);
Util::CreateSourceJobAndProduct(m_stateData.get(), scanFolder.m_scanFolderID, source2, job2, product2, "source2.txt", "product2.jpg");
manager.RetryDeferredDependencies(source2);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,443 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "assetBuilderSDKTest.h"
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include <AzCore/Component/ComponentApplication.h>
#include <AzCore/RTTI/AttributeReader.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Script/ScriptContextAttributes.h>
#include <AzCore/UnitTest/TestTypes.h>
#include "native/tests/BaseAssetProcessorTest.h"
#include "native/unittests/UnitTestRunner.h"
namespace AssetProcessor
{
struct AssetBehaviorContextTest
: public ::testing::Test
{
struct DataMembers
{
UnitTestUtils::AssertAbsorber m_absorber;
DataMembers() = default;
};
// the component application creates and returns a system entity, but doesn't keep track of it
AZ::Entity* m_systemEntity = nullptr;
// store all data we create here so that it can be destroyed on shutdown before we remove allocators
DataMembers* m_data = nullptr;
// the app is created separately so that we can control its lifetime.
AZStd::unique_ptr<AZ::ComponentApplication> m_app;
void SetUp() override
{
AZ::AllocatorInstance<AZ::SystemAllocator>::Create();
m_app.reset(aznew AZ::ComponentApplication());
AZ::ComponentApplication::Descriptor desc;
m_systemEntity = m_app->Create(desc);
AssetBuilderSDK::InitializeSerializationContext();
AssetBuilderSDK::InitializeBehaviorContext();
m_data = azcreate(DataMembers, ());
}
void TearDown() override
{
EXPECT_EQ(0, m_data->m_absorber.m_numAssertsAbsorbed);
EXPECT_EQ(0, m_data->m_absorber.m_numErrorsAbsorbed);
EXPECT_EQ(0, m_data->m_absorber.m_numWarningsAbsorbed);
azdestroy(m_data);
delete m_systemEntity;
m_systemEntity = nullptr;
m_app->Destroy();
m_app.reset();
AZ::AllocatorInstance<AZ::SystemAllocator>::Destroy();
}
bool IsBehaviorFlaggedForEditor(const AZ::AttributeArray& attributes)
{
AZ::Script::Attributes::ScopeFlags scopeType = AZ::Script::Attributes::ScopeFlags::Launcher;
AZ::Attribute* scopeAttribute = AZ::FindAttribute(AZ::Script::Attributes::Scope, attributes);
if (scopeAttribute)
{
AZ::AttributeReader scopeAttributeReader(nullptr, scopeAttribute);
scopeAttributeReader.Read<AZ::Script::Attributes::ScopeFlags>(scopeType);
}
return (scopeType == AZ::Script::Attributes::ScopeFlags::Automation ||
scopeType == AZ::Script::Attributes::ScopeFlags::Common);
}
};
TEST_F(AssetBehaviorContextTest, DetectBehaviorAssetBuilderPattern)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("AssetBuilderPattern");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("type"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("pattern"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("Regex"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("Wildcard"));
EXPECT_EQ(1, behaviorClass->m_constructors.size());
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorContext->m_classes.find("AZStd::vector<AssetBuilderPattern, allocator>"));
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorJobDescriptor)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("JobDescriptor");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_methods.end() != behaviorClass->m_methods.find("set_platform_identifier"));
EXPECT_TRUE(behaviorClass->m_methods.end() != behaviorClass->m_methods.find("get_platform_identifier"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("jobParameters"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("additionalFingerprintInfo"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("priority"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("checkExclusiveLock"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("checkServer"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("jobDependencyList"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("failOnError"));
EXPECT_EQ(2, behaviorClass->m_constructors.size());
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorContext->m_classes.find("AZStd::vector<JobDescriptor, allocator>"));
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorProductDependency)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("ProductDependency");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("dependencyId"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("flags"));
EXPECT_EQ(1, behaviorClass->m_constructors.size());
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorContext->m_classes.find("AZStd::vector<ProductDependency, allocator>"));
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorJobProduct)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("JobProduct");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("productFileName"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("productAssetType"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("productSubID"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("productDependencies"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("pathDependencies"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("dependenciesHandled"));
EXPECT_EQ(2, behaviorClass->m_constructors.size());
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorContext->m_classes.find("AZStd::vector<JobProduct, allocator>"));
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorProcessJobRequest)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("ProcessJobRequest");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourceFile"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("watchFolder"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("fullPath"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("builderGuid"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("jobDescription"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("tempDirPath"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("platformInfo"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourceFileDependencyList"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourceFileUUID"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("jobId"));
EXPECT_EQ(0, behaviorClass->m_constructors.size());
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorContext->m_classes.find("AZStd::vector<SourceFileDependency, allocator>"));
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorSourceFileDependency)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("SourceFileDependency");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourceFileDependencyPath"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourceFileDependencyUUID"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourceDependencyType"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("Absolute"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("Wildcards"));
EXPECT_EQ(2, behaviorClass->m_constructors.size());
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorAssetBuilderDesc)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("AssetBuilderDesc");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("analysisFingerprint"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("busId"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("flags"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("name"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("patterns"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("version"));
EXPECT_EQ(1, behaviorClass->m_constructors.size());
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorCreateJobsResponse)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("CreateJobsResponse");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("result"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourceFileDependencyList"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("createJobOutputs"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("ResultFailed"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("ResultShuttingDown"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("ResultSuccess"));
EXPECT_EQ(0, behaviorClass->m_constructors.size());
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorCreateJobsRequest)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("CreateJobsRequest");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("builderId"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("watchFolder"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourceFile"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourceFileUUID"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("enabledPlatforms"));
EXPECT_EQ(0, behaviorClass->m_constructors.size());
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorProductPathDependency)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("ProductPathDependency");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("dependencyPath"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("dependencyType"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("ProductFile"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("SourceFile"));
EXPECT_EQ(0, behaviorClass->m_constructors.size());
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorContext->m_classes.find("AZStd::vector<ProductPathDependency, allocator>"));
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorProcessJobResponse)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("ProcessJobResponse");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("outputProducts"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("resultCode"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("requiresSubIdGeneration"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourcesToReprocess"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("Success"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("Failed"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("Crashed"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("Cancelled"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("NetworkIssue"));
EXPECT_EQ(0, behaviorClass->m_constructors.size());
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorRegisterBuilderResponse)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("RegisterBuilderResponse");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("assetBuilderDescList"));
EXPECT_EQ(1, behaviorClass->m_constructors.size());
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorContext->m_classes.find("AZStd::vector<AssetBuilderDesc, allocator>"));
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorRegisterBuilderRequest)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("RegisterBuilderRequest");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("filePath"));
EXPECT_EQ(0, behaviorClass->m_constructors.size());
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorJobDependency)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("JobDependency");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("sourceFile"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("jobKey"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("platformIdentifier"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("type"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("Fingerprint"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("Order"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("OrderOnce"));
EXPECT_EQ(0, behaviorClass->m_constructors.size());
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorContext->m_classes.find("AZStd::vector<JobDependency, allocator>"));
}
TEST_F(AssetBehaviorContextTest, DetectBehaviorPlatformInfo)
{
auto&& behaviorContext = m_app->GetBehaviorContext();
auto behaviorClassEntry = behaviorContext->m_classes.find("PlatformInfo");
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorClassEntry);
AZ::BehaviorClass* behaviorClass = behaviorClassEntry->second;
EXPECT_TRUE(IsBehaviorFlaggedForEditor(behaviorClass->m_attributes));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("identifier"));
EXPECT_TRUE(behaviorClass->m_properties.end() != behaviorClass->m_properties.find("tags"));
EXPECT_EQ(0, behaviorClass->m_constructors.size());
EXPECT_TRUE(behaviorContext->m_classes.end() != behaviorContext->m_classes.find("AZStd::vector<PlatformInfo, allocator>"));
}
template <typename T>
bool EnumClassReadUpdateTest(AZ::BehaviorProperty* behaviorProperty, AZ::BehaviorObject& instance, T value)
{
T enumClassTypeValue = {};
EXPECT_TRUE(behaviorProperty->m_setter->Invoke(instance, value));
EXPECT_TRUE(behaviorProperty->m_getter->InvokeResult(enumClassTypeValue, instance));
EXPECT_EQ(value, enumClassTypeValue);
return value == enumClassTypeValue;
}
TEST_F(AssetBehaviorContextTest, DISABLED_EnumClass_ProductPathDependencyType_Accessible)
{
using namespace AssetBuilderSDK;
auto&& behaviorContext = m_app->GetBehaviorContext();
auto&& behaviorClassEntry = behaviorContext->m_classes.find("ProductPathDependency");
auto&& behaviorClass = behaviorClassEntry->second;
auto&& behaviorProperty = behaviorClass->m_properties["dependencyType"];
auto instance = behaviorClass->Create();
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, ProductPathDependencyType::ProductFile));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, ProductPathDependencyType::SourceFile));
behaviorClass->Destroy(instance);
}
TEST_F(AssetBehaviorContextTest, DISABLED_EnumClass_AssetBuilderPatternPatternType_Accessible)
{
using namespace AssetBuilderSDK;
auto&& behaviorContext = m_app->GetBehaviorContext();
auto&& behaviorClassEntry = behaviorContext->m_classes.find("AssetBuilderPattern");
auto&& behaviorClass = behaviorClassEntry->second;
auto&& behaviorProperty = behaviorClass->m_properties["type"];
auto instance = behaviorClass->Create();
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, AssetBuilderPattern::PatternType::Wildcard));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, AssetBuilderPattern::PatternType::Regex));
behaviorClass->Destroy(instance);
}
TEST_F(AssetBehaviorContextTest, DISABLED_EnumClass_ProcessJobResponse_Accessible)
{
using namespace AssetBuilderSDK;
auto&& behaviorContext = m_app->GetBehaviorContext();
auto&& behaviorClassEntry = behaviorContext->m_classes.find("ProcessJobResponse");
auto&& behaviorClass = behaviorClassEntry->second;
auto&& behaviorProperty = behaviorClass->m_properties["resultCode"];
auto instance = behaviorClass->Create();
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, ProcessJobResultCode::ProcessJobResult_Success));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, ProcessJobResultCode::ProcessJobResult_Failed));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, ProcessJobResultCode::ProcessJobResult_Crashed));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, ProcessJobResultCode::ProcessJobResult_Cancelled));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, ProcessJobResultCode::ProcessJobResult_NetworkIssue));
behaviorClass->Destroy(instance);
}
TEST_F(AssetBehaviorContextTest, DISABLED_EnumClass_JobDependencyType_Accessible)
{
using namespace AssetBuilderSDK;
auto&& behaviorContext = m_app->GetBehaviorContext();
auto&& behaviorClassEntry = behaviorContext->m_classes.find("JobDependency");
auto&& behaviorClass = behaviorClassEntry->second;
auto&& behaviorProperty = behaviorClass->m_properties["type"];
auto instance = behaviorClass->Create();
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, JobDependencyType::Fingerprint));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, JobDependencyType::Order));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, JobDependencyType::OrderOnce));
behaviorClass->Destroy(instance);
}
TEST_F(AssetBehaviorContextTest, DISABLED_EnumClass_CreateJobsResultCode_Accessible)
{
using namespace AssetBuilderSDK;
auto&& behaviorContext = m_app->GetBehaviorContext();
auto&& behaviorClassEntry = behaviorContext->m_classes.find("CreateJobsResponse");
auto&& behaviorClass = behaviorClassEntry->second;
auto&& behaviorProperty = behaviorClass->m_properties["result"];
auto instance = behaviorClass->Create();
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, CreateJobsResultCode::Failed));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, CreateJobsResultCode::ShuttingDown));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, CreateJobsResultCode::Success));
behaviorClass->Destroy(instance);
}
TEST_F(AssetBehaviorContextTest, DISABLED_EnumClass_SourceFileDependency_Accessible)
{
using namespace AssetBuilderSDK;
auto&& behaviorContext = m_app->GetBehaviorContext();
auto&& behaviorClassEntry = behaviorContext->m_classes.find("SourceFileDependency");
auto&& behaviorClass = behaviorClassEntry->second;
auto&& behaviorProperty = behaviorClass->m_properties["sourceDependencyType"];
auto instance = behaviorClass->Create();
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, SourceFileDependency::SourceFileDependencyType::Absolute));
EXPECT_TRUE(EnumClassReadUpdateTest(behaviorProperty, instance, SourceFileDependency::SourceFileDependencyType::Wildcards));
behaviorClass->Destroy(instance);
}
};
@@ -0,0 +1,295 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzTest/AzTest.h>
#include <AssetBuilderSDK/SerializationDependencies.h>
#include <Tests/SerializeContextFixture.h>
namespace SerializationDependencyTests
{
class ClassWithAssetId
{
public:
AZ_RTTI(ClassWithAssetId, "{F6970E05-890B-4E5D-A944-1F58E9751922}");
virtual ~ClassWithAssetId() {}
static void Reflect(AZ::ReflectContext* reflection)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection);
if (serializeContext)
{
serializeContext->Class<ClassWithAssetId>()
->Field("m_assetId", &ClassWithAssetId::m_assetId);
}
}
AZ::Data::AssetId m_assetId;
};
class ClassWithAsset
{
public:
AZ_RTTI(ClassWithAsset, "{D2BCF9BF-3E64-4942-8AFB-BD3E8453CB52}");
virtual ~ClassWithAsset() {}
static void Reflect(AZ::ReflectContext* reflection)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection);
if (serializeContext)
{
serializeContext->Class<ClassWithAsset>()
->Field("m_asset", &ClassWithAsset::m_asset);
}
}
AZ::Data::Asset<AZ::Data::AssetData> m_asset;
};
class ClassWithNoLoadAsset
{
public:
AZ_RTTI(ClassWithNoLoadAsset, "{C38D0DFA-A19E-48EF-BC0E-2BE4E320F65A}");
ClassWithNoLoadAsset() : m_asset(AZ::Data::AssetLoadBehavior::NoLoad)
{
}
virtual ~ClassWithNoLoadAsset() {}
static void Reflect(AZ::ReflectContext* reflection)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection);
if (serializeContext)
{
serializeContext->Class<ClassWithNoLoadAsset>()
->Field("m_asset", &ClassWithNoLoadAsset::m_asset);
}
}
AZ::Data::Asset<AZ::Data::AssetData> m_asset;
};
class SimpleAssetMock : public AzFramework::SimpleAssetReferenceBase
{
public:
AZ_RTTI(SimpleAssetMock, "{AA2CDA39-A357-441D-BABA-B1AD3C3A8083}", AzFramework::SimpleAssetReferenceBase);
static void Reflect(AZ::ReflectContext* reflection)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection);
if (serializeContext)
{
serializeContext->Class<SimpleAssetMock, AzFramework::SimpleAssetReferenceBase>();
}
}
AZ::Data::AssetType GetAssetType() const override
{
// Use an arbitrary ID for the asset type.
return AZ::Data::AssetType("{03FD33E2-DA2F-4021-A266-0DC9714FF84D}");
}
virtual const char* GetFileFilter() const
{
return nullptr;
}
};
class ClassWithSimpleAsset
{
public:
AZ_RTTI(ClassWithSimpleAsset, "{F4F50653-692C-46F8-A9B0-73C19523E56A}");
virtual ~ClassWithSimpleAsset() {}
static void Reflect(AZ::ReflectContext* reflection)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection);
if (serializeContext)
{
serializeContext->Class<ClassWithSimpleAsset>()
->Field("m_simpleAsset", &ClassWithSimpleAsset::m_simpleAsset);
}
}
SimpleAssetMock m_simpleAsset;
};
class SerializationDependenciesTests
: public UnitTest::SerializeContextFixture
, public UnitTest::TraceBusRedirector
{
protected:
void SetUp() override
{
SerializeContextFixture::SetUp();
AZ::Debug::TraceMessageBus::Handler::BusConnect();
AZ::Data::AssetId::Reflect(m_serializeContext);
AZ::Data::AssetData::Reflect(m_serializeContext);
AzFramework::SimpleAssetReferenceBase::Reflect(m_serializeContext);
ClassWithAssetId::Reflect(m_serializeContext);
ClassWithAsset::Reflect(m_serializeContext);
SimpleAssetMock::Reflect(m_serializeContext);
ClassWithSimpleAsset::Reflect(m_serializeContext);
ClassWithNoLoadAsset::Reflect(m_serializeContext);
}
void TearDown() override
{
AZ::Debug::TraceMessageBus::Handler::BusDisconnect();
UnitTest::SerializeContextFixture::TearDown();
}
};
int GetProductDependencySlot(const AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencies, const AZ::Data::AssetId& assetId)
{
for (int productDependencySlot = 0; productDependencySlot < aznumeric_cast<int>(productDependencies.size()); ++productDependencySlot)
{
if (productDependencies[productDependencySlot].m_dependencyId == assetId)
{
return productDependencySlot;
}
}
return false;
}
bool FindAssetIdInProductDependencies(const AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencies, const AZ::Data::AssetId& assetId)
{
return (GetProductDependencySlot(productDependencies, assetId) != -1);
}
TEST_F(SerializationDependenciesTests, GatherProductDependencies_NullData_NoCrash)
{
AZStd::vector<AssetBuilderSDK::ProductDependency> productDependencies;
AssetBuilderSDK::ProductPathDependencySet productPathDependencySet;
// Using a known type for the nullptr instead of a void* so the template resolves properly for the call.
ClassWithAssetId* nullClass = nullptr;
AZ_TEST_START_TRACE_SUPPRESSION;
bool gatherResult = AssetBuilderSDK::GatherProductDependencies(*m_serializeContext, nullClass, productDependencies, productPathDependencySet);
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
ASSERT_FALSE(gatherResult);
ASSERT_EQ(productDependencies.size(), 0);
ASSERT_EQ(productPathDependencySet.size(), 0);
}
TEST_F(SerializationDependenciesTests, GatherProductDependencies_HasValidAssetId_AssetIdFound)
{
AZStd::vector<AssetBuilderSDK::ProductDependency> productDependencies;
AssetBuilderSDK::ProductPathDependencySet productPathDependencySet;
ClassWithAssetId classWithAssetId;
classWithAssetId.m_assetId = AZ::Data::AssetId("{3008D6F9-1E56-4699-95F9-91A3758A964E}", 33);
bool gatherResult = AssetBuilderSDK::GatherProductDependencies(*m_serializeContext, &classWithAssetId, productDependencies, productPathDependencySet);
ASSERT_TRUE(gatherResult);
ASSERT_EQ(productDependencies.size(), 1);
ASSERT_TRUE(FindAssetIdInProductDependencies(productDependencies, classWithAssetId.m_assetId));
ASSERT_EQ(productPathDependencySet.size(), 0);
}
TEST_F(SerializationDependenciesTests, GatherProductDependencies_HasNullAssetId_NoDependencyEmitted)
{
AZStd::vector<AssetBuilderSDK::ProductDependency> productDependencies;
AssetBuilderSDK::ProductPathDependencySet productPathDependencySet;
ClassWithAssetId classWithAssetId;
bool gatherResult = AssetBuilderSDK::GatherProductDependencies(*m_serializeContext, &classWithAssetId, productDependencies, productPathDependencySet);
ASSERT_TRUE(gatherResult);
ASSERT_EQ(productDependencies.size(), 0);
ASSERT_EQ(productPathDependencySet.size(), 0);
}
TEST_F(SerializationDependenciesTests, GatherProductDependencies_HasValidAsset_AssetIdFound)
{
AZStd::vector<AssetBuilderSDK::ProductDependency> productDependencies;
AssetBuilderSDK::ProductPathDependencySet productPathDependencySet;
ClassWithAsset classWithAsset;
AZ::Data::AssetId testAssetId("{CAAC5458-0738-43F6-A2BD-4E315C64BFD3}", 71);
classWithAsset.m_asset = AZ::Data::Asset<AZ::Data::AssetData>(
testAssetId,
azrtti_typeid<AZ::Data::AssetData>());
bool gatherResult = AssetBuilderSDK::GatherProductDependencies(*m_serializeContext, &classWithAsset, productDependencies, productPathDependencySet);
ASSERT_TRUE(gatherResult);
ASSERT_EQ(productDependencies.size(), 1);
ASSERT_TRUE(FindAssetIdInProductDependencies(productDependencies, testAssetId));
ASSERT_EQ(productPathDependencySet.size(), 0);
}
TEST_F(SerializationDependenciesTests, GatherProductDependencies_HasNullAsset_NoDependencyEmitted)
{
AZStd::vector<AssetBuilderSDK::ProductDependency> productDependencies;
AssetBuilderSDK::ProductPathDependencySet productPathDependencySet;
ClassWithAsset classWithAsset;
AZ::Data::AssetId testAssetId;
testAssetId.SetInvalid(); // Make it clear that this is an invalid ID.
classWithAsset.m_asset = AZ::Data::Asset<AZ::Data::AssetData>(
testAssetId,
azrtti_typeid<AZ::Data::AssetData>());
bool gatherResult = AssetBuilderSDK::GatherProductDependencies(*m_serializeContext, &classWithAsset, productDependencies, productPathDependencySet);
ASSERT_TRUE(gatherResult);
ASSERT_EQ(productDependencies.size(), 0);
ASSERT_EQ(productPathDependencySet.size(), 0);
}
TEST_F(SerializationDependenciesTests, GatherProductDependencies_HasValidSimpleAsset_AssetPathFound)
{
AZStd::vector<AssetBuilderSDK::ProductDependency> productDependencies;
AssetBuilderSDK::ProductPathDependencySet productPathDependencySet;
ClassWithSimpleAsset classWithSimpleAsset;
const AZStd::string expectedAssetPath("TestAssetPathString.txt");
classWithSimpleAsset.m_simpleAsset.SetAssetPath(expectedAssetPath.c_str());
bool gatherResult = AssetBuilderSDK::GatherProductDependencies(*m_serializeContext, &classWithSimpleAsset, productDependencies, productPathDependencySet);
ASSERT_TRUE(gatherResult);
ASSERT_EQ(productDependencies.size(), 0);
ASSERT_EQ(productPathDependencySet.size(), 1);
ASSERT_TRUE(productPathDependencySet.begin()->m_dependencyPath.compare(expectedAssetPath) == 0);
ASSERT_TRUE(productPathDependencySet.begin()->m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::ProductFile);
}
TEST_F(SerializationDependenciesTests, GatherProductDependencies_DependencyFlagsSerialization_Success)
{
AZStd::vector<AssetBuilderSDK::ProductDependency> productDependencies;
AssetBuilderSDK::ProductPathDependencySet productPathDependencySet;
ClassWithNoLoadAsset classWithNoLoadAsset;
AZ::Data::AssetId testAssetId("{CAAC5458-0738-43F6-A2BD-4E315C64BFD3}", 71);
classWithNoLoadAsset.m_asset = AZ::Data::Asset<AZ::Data::AssetData>(
testAssetId,
azrtti_typeid<AZ::Data::AssetData>());
classWithNoLoadAsset.m_asset.SetAutoLoadBehavior(AZ::Data::AssetLoadBehavior::NoLoad);
bool gatherResult = AssetBuilderSDK::GatherProductDependencies(*m_serializeContext, &classWithNoLoadAsset, productDependencies, productPathDependencySet);
ASSERT_TRUE(gatherResult);
ASSERT_EQ(productDependencies.size(), 1);
auto behaviorFromFlags = AZ::Data::ProductDependencyInfo::LoadBehaviorFromFlags(productDependencies[0].m_flags);
ASSERT_EQ(behaviorFromFlags, AZ::Data::AssetLoadBehavior::NoLoad);
}
TEST_F(SerializationDependenciesTests, GatherProductDependencies_HasEmptyStringSimpleAsset_NoDependencyEmitted)
{
AZStd::vector<AssetBuilderSDK::ProductDependency> productDependencies;
AssetBuilderSDK::ProductPathDependencySet productPathDependencySet;
ClassWithSimpleAsset classWithSimpleAsset;
classWithSimpleAsset.m_simpleAsset.SetAssetPath("");
bool gatherResult = AssetBuilderSDK::GatherProductDependencies(*m_serializeContext, &classWithSimpleAsset, productDependencies, productPathDependencySet);
ASSERT_TRUE(gatherResult);
ASSERT_EQ(productDependencies.size(), 0);
ASSERT_EQ(productPathDependencySet.size(), 0);
}
}
@@ -0,0 +1,159 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "assetBuilderSDKTest.h"
namespace AssetProcessor
{
#if defined(ENABLE_LEGACY_PLATFORMFLAGS_SUPPORT)
TEST_F(AssetBuilderSDKTest, GetEnabledPlatformsCountUnitTest)
{
AssetBuilderSDK::CreateJobsRequest createJobsRequest;
ASSERT_EQ(createJobsRequest.GetEnabledPlatformsCount(), 0);
createJobsRequest.m_enabledPlatforms = {
{ "pc", {}
}
};
ASSERT_EQ(createJobsRequest.GetEnabledPlatformsCount(), 1);
createJobsRequest.m_enabledPlatforms = {
{ "pc", {}
}, { "es3", {}
}
};
ASSERT_EQ(createJobsRequest.GetEnabledPlatformsCount(), 2);
}
TEST_F(AssetBuilderSDKTest, GetEnabledPlatformAtUnitTest)
{
UnitTestUtils::AssertAbsorber absorb;
AssetBuilderSDK::CreateJobsRequest createJobsRequest;
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(0), AssetBuilderSDK::Platform_NONE);
createJobsRequest.m_enabledPlatforms = {
{ "pc", { }
}
};
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(0), AssetBuilderSDK::Platform_PC);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(1), AssetBuilderSDK::Platform_NONE);
createJobsRequest.m_enabledPlatforms = {
{ "es3", {}
}
};
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(0), AssetBuilderSDK::Platform_ES3);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(1), AssetBuilderSDK::Platform_NONE);
createJobsRequest.m_enabledPlatforms = {
{ "pc", {}
}, { "es3", {}
}
};
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(0), AssetBuilderSDK::Platform_PC);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(1), AssetBuilderSDK::Platform_ES3);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(2), AssetBuilderSDK::Platform_NONE);
createJobsRequest.m_enabledPlatforms = {
{ "ios", {}
}
};
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(0), AssetBuilderSDK::Platform_IOS);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(1), AssetBuilderSDK::Platform_NONE);
createJobsRequest.m_enabledPlatforms = {
{ "pc", {}
}, { "es3", {}
}, { "ios", {}
}, { "osx_gl", {}
}
};
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(0), AssetBuilderSDK::Platform_PC);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(1), AssetBuilderSDK::Platform_ES3);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(2), AssetBuilderSDK::Platform_IOS);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(3), AssetBuilderSDK::Platform_OSX);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(4), AssetBuilderSDK::Platform_NONE);
#if defined(TOOLS_SUPPORT_XENIA)
createJobsRequest.m_enabledPlatforms = {
{ "xenia", {}
}
};
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(0), AssetBuilderSDK::Platform_XENIA);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(1), AssetBuilderSDK::Platform_NONE);
#endif
createJobsRequest.m_enabledPlatforms = {
{ "pc", {}
}, { "es3", {}
}
};
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(0), AssetBuilderSDK::Platform_PC);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(1), AssetBuilderSDK::Platform_ES3);
ASSERT_EQ(createJobsRequest.GetEnabledPlatformAt(2), AssetBuilderSDK::Platform_NONE);
// using a deprecated API should have generated warnings.
// but we can't test for it because these warnings are WarningOnce and some other unit test might have already triggered it
}
TEST_F(AssetBuilderSDKTest, IsPlatformEnabledUnitTest)
{
UnitTestUtils::AssertAbsorber absorb;
AssetBuilderSDK::CreateJobsRequest createJobsRequest;
ASSERT_FALSE(createJobsRequest.IsPlatformEnabled(AssetBuilderSDK::Platform_PC));
createJobsRequest.m_enabledPlatforms = {
{ "pc", {}
}
};
ASSERT_TRUE(createJobsRequest.IsPlatformEnabled(AssetBuilderSDK::Platform_PC));
ASSERT_FALSE(createJobsRequest.IsPlatformEnabled(AssetBuilderSDK::Platform_ES3));
createJobsRequest.m_enabledPlatforms = {
{ "pc", {}
}, { "es3", {}
}
};
ASSERT_TRUE(createJobsRequest.IsPlatformEnabled(AssetBuilderSDK::Platform_PC));
ASSERT_TRUE(createJobsRequest.IsPlatformEnabled(AssetBuilderSDK::Platform_ES3));
createJobsRequest.m_enabledPlatforms = {
{ "pc", {}
}, { "es3", {}
}
};
ASSERT_TRUE(createJobsRequest.IsPlatformEnabled(AssetBuilderSDK::Platform_PC));
ASSERT_TRUE(createJobsRequest.IsPlatformEnabled(AssetBuilderSDK::Platform_ES3));
// using a deprecated API should have generated warnings.
// but we can't test for it because these warnings are WarningOnce and some other unit test might have already triggered it
}
TEST_F(AssetBuilderSDKTest, IsPlatformValidUnitTest)
{
AssetBuilderSDK::CreateJobsRequest createJobsRequest;
UnitTestUtils::AssertAbsorber absorb;
ASSERT_TRUE(createJobsRequest.IsPlatformValid(AssetBuilderSDK::Platform_PC));
ASSERT_TRUE(createJobsRequest.IsPlatformValid(AssetBuilderSDK::Platform_ES3));
ASSERT_TRUE(createJobsRequest.IsPlatformValid(AssetBuilderSDK::Platform_IOS));
ASSERT_TRUE(createJobsRequest.IsPlatformValid(AssetBuilderSDK::Platform_OSX));
ASSERT_TRUE(createJobsRequest.IsPlatformValid(AssetBuilderSDK::Platform_XENIA));
ASSERT_TRUE(createJobsRequest.IsPlatformValid(AssetBuilderSDK::Platform_PROVO));
ASSERT_TRUE(createJobsRequest.IsPlatformValid(AssetBuilderSDK::Platform_SALEM));
ASSERT_TRUE(createJobsRequest.IsPlatformValid(AssetBuilderSDK::Platform_JASPER));
//64 is 0x040 which currently is the next valid platform value which is invalid as of now, if we ever add a new platform entry to the Platform enum
//we will have to update this failure unit test
ASSERT_FALSE(createJobsRequest.IsPlatformValid(static_cast<AssetBuilderSDK::Platform>(256)));
// using a deprecated API should have generated warnings.
// but we can't test for it because these warnings are WarningOnce and some other unit test might have already triggered it
}
#endif // defined(ENABLE_LEGACY_PLATFORMFLAGS_SUPPORT)
};
@@ -0,0 +1,35 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzTest/AzTest.h>
#include <AzCore/Memory/Memory.h>
#include <AzCore/Memory/SystemAllocator.h>
namespace AssetProcessor
{
class AssetBuilderSDKTest
: public ::testing::Test
{
protected:
void SetUp() override
{
AZ::AllocatorInstance<AZ::SystemAllocator>::Create();
}
void TearDown() override
{
AZ::AllocatorInstance<AZ::SystemAllocator>::Destroy();
}
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,265 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzTest/AzTest.h>
#include <AzCore/std/parallel/atomic.h>
#include <qcoreapplication.h>
#include "native/tests/AssetProcessorTest.h"
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include "native/assetprocessor.h"
#include "native/unittests/UnitTestRunner.h"
#include "native/AssetManager/assetProcessorManager.h"
#include "native/utilities/PlatformConfiguration.h"
#include "native/unittests/MockApplicationManager.h"
#include <AssetManager/FileStateCache.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <QTemporaryDir>
#include <QMetaObject>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzToolsFramework/API/AssetDatabaseBus.h>
#include "resourcecompiler/rccontroller.h"
class AssetProcessorManager_Test;
class MockDatabaseLocationListener : public AzToolsFramework::AssetDatabase::AssetDatabaseRequests::Bus::Handler
{
public:
MOCK_METHOD1(GetAssetDatabaseLocation, bool(AZStd::string&));
};
class AssetProcessorManagerTest
: public AssetProcessor::AssetProcessorTest
{
public:
AssetProcessorManagerTest();
virtual ~AssetProcessorManagerTest()
{
}
// utility function. Blocks and runs the QT event pump for up to millisecondsMax and will break out as soon as the APM is idle.
bool BlockUntilIdle(int millisecondsMax);
protected:
void SetUp() override;
void TearDown() override;
QTemporaryDir m_tempDir;
AZStd::unique_ptr<AssetProcessorManager_Test> m_assetProcessorManager;
AZStd::unique_ptr<AssetProcessor::MockApplicationManager> m_mockApplicationManager;
AZStd::unique_ptr<AssetProcessor::PlatformConfiguration> m_config;
UnitTestUtils::AssertAbsorber m_assertAbsorber; // absorb asserts/warnings/errors so that the unit test output is not cluttered
QString m_gameName;
QDir m_normalizedCacheRootDir;
AZStd::atomic_bool m_isIdling;
QMetaObject::Connection m_idleConnection;
struct StaticData
{
AZStd::string m_databaseLocation;
::testing::NiceMock<MockDatabaseLocationListener> m_databaseLocationListener;
};
AZStd::unique_ptr<StaticData> m_data;
private:
int m_argc;
char** m_argv;
AZStd::unique_ptr<UnitTestUtils::ScopedDir> m_scopeDir;
AZStd::unique_ptr<QCoreApplication> m_qApp;
};
struct AbsolutePathProductDependencyTest
: public AssetProcessorManagerTest
{
void SetUp() override;
AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry SetAndReadAbsolutePathProductDependencyFromRelativePath(
const AZStd::string& relativePath);
AZStd::string BuildScanFolderRelativePath(const AZStd::string& relativePath) const;
AzToolsFramework::AssetDatabase::ProductDatabaseEntry m_productToHaveDependency;
const AssetProcessor::ScanFolderInfo* m_scanFolderInfo = nullptr;
AZStd::string m_testPlatform = "SomePlatform";
};
struct PathDependencyTest
: public AssetProcessorManagerTest
{
void SetUp() override;
void TearDown() override;
using OutputAssetSet = AZStd::vector<AZStd::vector<const char*>>;
struct TestAsset
{
TestAsset() = default;
TestAsset(const char* name) : m_name(name) {}
AZStd::string m_name;
AZStd::vector<AZ::Data::AssetId> m_products;
};
void CaptureJobs(AZStd::vector<AssetProcessor::JobDetails>& jobDetails, const char* sourceFilePath);
bool ProcessAsset(TestAsset& asset, const OutputAssetSet& outputAssets, const AssetBuilderSDK::ProductPathDependencySet& dependencies = {}, const AZStd::string& folderPath = "subfolder1/", const AZStd::string& extension = ".txt");
void RunWildcardTest(bool useCorrectDatabaseSeparator, AssetBuilderSDK::ProductPathDependencyType pathDependencyType, bool buildDependenciesFirst);
AssetProcessor::AssetDatabaseConnection* m_sharedConnection{};
};
struct DuplicateProcessTest
: public PathDependencyTest
{
void SetUp() override;
};
struct MultiplatformPathDependencyTest
: public PathDependencyTest
{
void SetUp() override;
};
struct MockBuilderInfoHandler
: public AssetProcessor::AssetBuilderInfoBus::Handler
{
~MockBuilderInfoHandler();
//! AssetProcessor::AssetBuilderInfoBus Interface
void GetMatchingBuildersInfo(const AZStd::string& assetPath, AssetProcessor::BuilderInfoList& builderInfoList) override;
void GetAllBuildersInfo(AssetProcessor::BuilderInfoList& builderInfoList) override;
void CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response);
void ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response);
AssetBuilderSDK::AssetBuilderDesc CreateBuilderDesc(const QString& builderName, const QString& builderId, const AZStd::vector<AssetBuilderSDK::AssetBuilderPattern>& builderPatterns);
AssetBuilderSDK::AssetBuilderDesc m_builderDesc;
QString m_jobFingerprint;
QString m_dependencyFilePath;
QString m_jobDependencyFilePath;
int m_createJobsCount = 0;
};
struct ModtimeScanningTest
: public AssetProcessorManagerTest
{
void SetUp() override;
void TearDown() override;
void ProcessAssetJobs();
void SimulateAssetScanner(QSet<AssetProcessor::AssetFileInfo> filePaths);
QSet<AssetProcessor::AssetFileInfo> BuildFileSet();
void ExpectWork(int createJobs, int processJobs);
void ExpectNoWork();
void SetFileContents(QString filePath, QString contents);
struct StaticData
{
QString m_relativePathFromWatchFolder[3];
AZStd::vector<QString> m_absolutePath;
AZStd::vector<AssetProcessor::JobDetails> m_processResults;
AZStd::vector<QString> m_deletedSources;
AZStd::shared_ptr<AssetProcessor::InternalMockBuilder> m_builderTxtBuilder;
MockBuilderInfoHandler m_mockBuilderInfoHandler;
};
AZStd::unique_ptr<StaticData> m_data;
};
struct FingerprintTest
: public AssetProcessorManagerTest
{
void SetUp() override;
void TearDown() override;
void RunFingerprintTest(QString builderFingerprint, QString jobFingerprint, bool expectedResult);
QString m_absolutePath;
MockBuilderInfoHandler m_mockBuilderInfoHandler;
AZStd::vector<AssetProcessor::JobDetails> m_jobResults;
};
struct JobDependencyTest
: public PathDependencyTest
{
void SetUp() override;
void TearDown() override;
struct StaticData
{
MockBuilderInfoHandler m_mockBuilderInfoHandler;
AZ::Uuid m_builderUuid;
};
AZStd::unique_ptr<StaticData> m_data;
};
struct MockMultiBuilderInfoHandler
: public AssetProcessor::AssetBuilderInfoBus::Handler
{
~MockMultiBuilderInfoHandler();
struct AssetBuilderExtraInfo
{
QString m_jobDependencyFilePath;
};
//! AssetProcessor::AssetBuilderInfoBus Interface
void GetMatchingBuildersInfo(const AZStd::string& assetPath, AssetProcessor::BuilderInfoList& builderInfoList) override;
void GetAllBuildersInfo(AssetProcessor::BuilderInfoList& builderInfoList) override;
void CreateJobs(AssetBuilderExtraInfo extraInfo, const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response);
void ProcessJob(AssetBuilderExtraInfo extraInfo, const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response);
void CreateBuilderDesc(const QString& builderName, const QString& builderId, const AZStd::vector<AssetBuilderSDK::AssetBuilderPattern>& builderPatterns, AssetBuilderExtraInfo extraInfo);
AZStd::vector<AssetBuilderSDK::AssetBuilderDesc> m_builderDesc;
AZStd::vector<AssetUtilities::BuilderFilePatternMatcher> m_matcherBuilderPatterns;
AZStd::unordered_map<AZ::Uuid, AssetBuilderSDK::AssetBuilderDesc> m_builderDescMap;
int m_createJobsCount = 0;
};
struct ChainJobDependencyTest
: public PathDependencyTest
{
void SetUp() override;
void TearDown() override;
struct StaticData
{
MockMultiBuilderInfoHandler m_mockBuilderInfoHandler;
AZStd::unique_ptr<AssetProcessor::RCController> m_rcController;
};
static constexpr int ChainLength = 10;
AZStd::unique_ptr<StaticData> m_data;
};
struct DuplicateProductsTest
: public AssetProcessorManagerTest
{
void SetupDuplicateProductsTest(QString& sourceFile, QDir& tempPath, QString& productFile, AZStd::vector<AssetProcessor::JobDetails>& jobDetails, AssetBuilderSDK::ProcessJobResponse& response, bool multipleOutputs, QString extension);
};
struct DeleteTest
: public ModtimeScanningTest
{
void SetUp() override;
};
@@ -0,0 +1,161 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <native/tests/assetscanner/AssetScannerTests.h>
#include <native/AssetManager/assetScanner.h>
namespace AssetProcessor
{
class AssetScanner_Test
: public AssetScanner
{
public:
AssetScanner_Test(PlatformConfiguration* config, QObject* parent = nullptr)
:AssetScanner(config, parent)
{
}
friend class GTEST_TEST_CLASS_NAME_(AssetScannerTest, AssetScannerExcludeFileTest);
friend class GTEST_TEST_CLASS_NAME_(AssetScannerTest, AssetScannerExcludeFolderTest);
};
AssetScannerTest::AssetScannerTest()
:m_argc(0)
,m_argv(0)
{
m_qApp.reset(new QCoreApplication(m_argc,m_argv));
qRegisterMetaType<QSet<QString> >("QSet<QString>");
qRegisterMetaType<AssetProcessor::AssetScanningStatus>("AssetScanningStatus");
qRegisterMetaType<QSet<AssetFileInfo>>("QSet<AssetFileInfo>");
}
bool AssetScannerTest::BlockUntilScanComplete(int millisecondsMax)
{
QElapsedTimer limit;
limit.start();
while ((!m_scanComplete) && (limit.elapsed() < millisecondsMax))
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
}
// and then once more, so that any queued events as a result of the above finish.
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
return m_scanComplete;
}
void AssetScannerTest::SetUp()
{
using namespace UnitTestUtils;
AssetProcessorTest::SetUp();
QDir tempPath(m_tempDir.path());
QSet<QString> expectedFiles;
expectedFiles << tempPath.absoluteFilePath("rootfile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder1/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/basefile.txt");
for (const QString& expect : expectedFiles)
{
EXPECT_TRUE(CreateDummyFile(expect));
}
m_platformConfig.reset(new AssetProcessor::PlatformConfiguration());
AZStd::vector<AssetBuilderSDK::PlatformInfo> platforms;
m_platformConfig.get()->PopulatePlatformsForScanFolder(platforms);
// PATH DisplayName PortKey outputfolder root recurse platforms
m_platformConfig.get()->AddScanFolder(ScanFolderInfo(tempPath.absolutePath(), "", "ap1", "", true, false, platforms));
m_platformConfig.get()->AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder1"), "", "ap2", "", false, true, platforms));
m_platformConfig.get()->AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder2"), "", "ap3", "", false, true, platforms));
m_assetScanner.reset(new AssetScanner_Test(m_platformConfig.get()));
QObject::connect(m_assetScanner.get(), &AssetScanner::FilesFound, [this](QSet<AssetProcessor::AssetFileInfo> fileList)
{
for (AssetProcessor::AssetFileInfo foundFile : fileList)
{
m_files.insert(foundFile.m_filePath);
}
}
);
QObject::connect(m_assetScanner.get(), &AssetScanner::AssetScanningStatusChanged, [this](AssetProcessor::AssetScanningStatus status)
{
if ((status == AssetProcessor::AssetScanningStatus::Completed) || (status == AssetProcessor::AssetScanningStatus::Stopped))
{
m_scanComplete = true;
}
}
);
QObject::connect(m_assetScanner.get(), &AssetScanner::FoldersFound, [this](QSet<AssetProcessor::AssetFileInfo> folderList)
{
for (AssetProcessor::AssetFileInfo foundFolder : folderList)
{
m_folders.insert(foundFolder.m_filePath);
}
}
);
}
void AssetScannerTest::TearDown()
{
m_assetScanner.reset();
m_platformConfig.reset();
QDir tempDir(m_tempDir.path());
tempDir.removeRecursively();
m_qApp.reset();
AssetProcessor::AssetProcessorTest::TearDown();
}
TEST_F(AssetScannerTest, AssetScannerExcludeFileTest)
{
QDir tempDir(m_tempDir.path());
ExcludeAssetRecognizer excludeRecogniser;
excludeRecogniser.m_name = "backup";
// we are excluding all the files in the folder but not the folder itself
excludeRecogniser.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher(".*\\/subfolder2\\/aaa\\/.*", AssetBuilderSDK::AssetBuilderPattern::Regex);
m_platformConfig.get()->AddExcludeRecognizer(excludeRecogniser);
m_assetScanner.get()->StartScan();
BlockUntilScanComplete(5000);
EXPECT_EQ(m_files.size(), 3);
EXPECT_FALSE(m_files.contains(tempDir.filePath("subfolder2/aaa/basefile.txt")));
EXPECT_EQ(m_folders.size(), 1);
EXPECT_TRUE(m_folders.contains(tempDir.filePath("subfolder2/aaa")));
}
TEST_F(AssetScannerTest, AssetScannerExcludeFolderTest)
{
QDir tempDir(m_tempDir.path());
ExcludeAssetRecognizer excludeRecogniser;
excludeRecogniser.m_name = "backup";
// we are excluding the complete folder here
excludeRecogniser.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher(".*\\/subfolder2\\/aaa", AssetBuilderSDK::AssetBuilderPattern::Regex);
m_platformConfig.get()->AddExcludeRecognizer(excludeRecogniser);
m_assetScanner.get()->StartScan();
BlockUntilScanComplete(5000);
EXPECT_EQ(m_files.size(), 3);
EXPECT_FALSE(m_files.contains(tempDir.filePath("subfolder2/aaa/basefile.txt")));
EXPECT_EQ(m_folders.size(), 0);
}
}
@@ -0,0 +1,44 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <native/tests/AssetProcessorTest.h>
#include <QTemporaryDir>
#include <QCoreApplication>
#include <native/utilities/PlatformConfiguration.h>
#include <QSet>
#include <QString>
namespace AssetProcessor
{
class AssetScanner_Test;
class AssetScannerTest
: public AssetProcessor::AssetProcessorTest
{
public:
AssetScannerTest();
// Blocks and runs the QT event pump for up to millisecondsMax and will break out as soon as the scan completes.
bool BlockUntilScanComplete(int millisecondsMax);
protected:
void SetUp() override;
void TearDown() override;
int m_argc;
char** m_argv;
QTemporaryDir m_tempDir;
AZStd::unique_ptr<PlatformConfiguration> m_platformConfig;
AZStd::unique_ptr<AssetScanner_Test> m_assetScanner;
QSet<QString> m_files;
QSet<QString> m_folders;
bool m_scanComplete = false;
AZStd::unique_ptr<QCoreApplication> m_qApp;
};
}
@@ -0,0 +1,659 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include "native/tests/platformconfiguration/platformconfigurationtests.h"
const char TestAppRoot[] = ":/testdata";
const char EmptyDummyProjectName[] = "EmptyDummyProject";
const char DummyProjectName[] = "DummyProject";
// make the internal calls public for the purposes of the unit test!
class UnitTestPlatformConfiguration : public AssetProcessor::PlatformConfiguration
{
friend class GTEST_TEST_CLASS_NAME_(PlatformConfigurationUnitTests, Test_GemHandling);
friend class GTEST_TEST_CLASS_NAME_(PlatformConfigurationUnitTests, Test_MetaFileTypes);
protected:
};
PlatformConfigurationUnitTests::PlatformConfigurationUnitTests()
: m_argc(0)
, m_argv(0)
{
}
void PlatformConfigurationUnitTests::SetUp()
{
using namespace AssetProcessor;
m_qApp = new QCoreApplication(m_argc, m_argv);
AssetProcessorTest::SetUp();
AssetUtilities::ResetAssetRoot();
}
void PlatformConfigurationUnitTests::TearDown()
{
AssetUtilities::ResetAssetRoot();
delete m_qApp;
AssetProcessor::AssetProcessorTest::TearDown();
}
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_BadPlatform)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
const char* configRoot = ":/testdata/config_broken_badplatform";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_FALSE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, EmptyDummyProjectName, false, false));
ASSERT_GT(m_absorber.m_numErrorsAbsorbed, 0);
}
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_NoPlatform)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
const char* configRoot = ":/testdata/config_broken_noplatform";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_FALSE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, EmptyDummyProjectName, false, false));
ASSERT_GT(m_absorber.m_numErrorsAbsorbed, 0);
}
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_NoScanFolders)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
const char* configRoot = ":/testdata/config_broken_noscans";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_FALSE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, EmptyDummyProjectName, false, false));
ASSERT_GT(m_absorber.m_numErrorsAbsorbed, 0);
}
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_BrokenRecognizers)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
const char* configRoot = ":/testdata/config_broken_recognizers";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_FALSE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, EmptyDummyProjectName, false, false));
ASSERT_GT(m_absorber.m_numErrorsAbsorbed, 0);
}
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_Regular_Platforms)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
const char* configRoot = ":/testdata/config_regular";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_TRUE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, EmptyDummyProjectName, false, false));
ASSERT_EQ(m_absorber.m_numErrorsAbsorbed, 0);
// verify the data.
ASSERT_NE(config.GetPlatformByIdentifier(AzToolsFramework::AssetSystem::GetHostAssetPlatform()), nullptr);
ASSERT_NE(config.GetPlatformByIdentifier("es3"), nullptr);
ASSERT_NE(config.GetPlatformByIdentifier("server"), nullptr);
ASSERT_EQ(config.GetPlatformByIdentifier("xenia"), nullptr);
ASSERT_TRUE(config.GetPlatformByIdentifier("es3")->HasTag("mobile"));
ASSERT_TRUE(config.GetPlatformByIdentifier("es3")->HasTag("renderer"));
ASSERT_TRUE(config.GetPlatformByIdentifier("es3")->HasTag("android"));
ASSERT_TRUE(config.GetPlatformByIdentifier("server")->HasTag("server"));
ASSERT_FALSE(config.GetPlatformByIdentifier("es3")->HasTag("server"));
ASSERT_FALSE(config.GetPlatformByIdentifier("server")->HasTag("renderer"));
}
TEST_F(PlatformConfigurationUnitTests, TestReadScanFolderRoot_FromSettingsRegistry_Succeeds)
{
auto settingsRegistry = AZ::SettingsRegistry::Get();
ASSERT_NE(nullptr, settingsRegistry);
AZ::SettingsRegistryInterface::Specializations apSpecializations;
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_EngineRegistry(*settingsRegistry, AZ_TRAIT_OS_PLATFORM_CODENAME, apSpecializations);
struct ScanFolderVisitor
: AZ::SettingsRegistryInterface::Visitor
{
void Visit([[maybe_unused]] AZStd::string_view path, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, AZ::s64 value)
{
if (valueName == "recursive")
{
m_isRecursive = value != 0;
}
else if (valueName == "order")
{
m_scanOrder = value;
}
}
void Visit([[maybe_unused]] AZStd::string_view path, AZStd::string_view valueName, AZ::SettingsRegistryInterface::Type, AZStd::string_view value)
{
if (valueName == "watch")
{
m_watchPath = value;
}
}
AZ::SettingsRegistryInterface::FixedValueString m_watchPath;
bool m_isRecursive{};
int m_scanOrder{};
};
ScanFolderVisitor scanFolderVisitor;
EXPECT_TRUE(settingsRegistry->Visit(scanFolderVisitor, "/Amazon/AssetProcessor/Settings/ScanFolder Root"));
// These test values come from the <dev_root>/Engine/Registry/AssetProcessorPlatformConfig.setreg file
EXPECT_STREQ("@ROOT@", scanFolderVisitor.m_watchPath.c_str());
EXPECT_FALSE(scanFolderVisitor.m_isRecursive);
EXPECT_EQ(10000, scanFolderVisitor.m_scanOrder);
}
// a reusable fixture that sets up one host as a pc with a temp path and such.
class PlatformConfigurationUnitTests_OnePCHostFixture : public PlatformConfigurationUnitTests
{
public:
void SetUp() override
{
PlatformConfigurationUnitTests::SetUp();
m_tempEngineRoot.reset(new QTemporaryDir());
m_tempPath = QDir(m_tempEngineRoot->path());
m_config.reset(new UnitTestPlatformConfiguration());
m_config->EnablePlatform({ "pc",{ "desktop", "host" } }, true);
m_config->PopulatePlatformsForScanFolder(m_platforms);
}
void TearDown() override
{
m_platforms.set_capacity(0);
m_tempEngineRoot.reset();
m_config.reset();
PlatformConfigurationUnitTests::TearDown();
}
AZStd::vector<AssetBuilderSDK::PlatformInfo> m_platforms;
AZStd::unique_ptr<UnitTestPlatformConfiguration> m_config;
AZStd::unique_ptr<QTemporaryDir> m_tempEngineRoot = nullptr; // this actually creates the folder in its constructor, so hold off until setup..
QDir m_tempPath;
};
// ensures that when a file in the root (non recursive) folder is searched for, the root is found.
TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetScanFolderForFile_RootFolderFile_IsFound)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
m_config->AddScanFolder(ScanFolderInfo(m_tempPath.filePath("scanfolder1"), "ScanFolder1", "sf1", "", true, false, m_platforms), true); // a root folder that has watched subfolders, not recursive
m_config->AddScanFolder(ScanFolderInfo(m_tempPath.filePath("scanfolder1/Editor"), "Editor", "sf2", "", false, true, m_platforms), true); // a child folder that exists within that scan folder.
const ScanFolderInfo* info = m_config->GetScanFolderForFile(m_tempPath.filePath("scanfolder1/something.txt"));
ASSERT_TRUE(info);
EXPECT_STREQ(info->ScanPath().toUtf8().constData(), m_tempPath.filePath("scanfolder1").toUtf8().constData());
EXPECT_STREQ(info->GetDisplayName().toUtf8().constData(), "ScanFolder1");
}
// ensures that when a file in a subfolder (recursive) is searched for, the subfolder is found despite it being inside the root, technically.
TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetScanFolderForFile_SubFolderFile_IsFound)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
m_config->AddScanFolder(ScanFolderInfo(m_tempPath.filePath("scanfolder1"), "ScanFolder1", "sf1", "", true, false, m_platforms), true); // a root folder that has watched subfolders, not recursive
m_config->AddScanFolder(ScanFolderInfo(m_tempPath.filePath("scanfolder1/Editor"), "Editor ScanFolder", "sf2", "", false, true, m_platforms), true); // a child folder that exists within that scan folder.
const ScanFolderInfo* info = m_config->GetScanFolderForFile(m_tempPath.filePath("scanfolder1/Editor/something.txt"));
ASSERT_TRUE(info);
EXPECT_STREQ(info->ScanPath().toUtf8().constData(), m_tempPath.filePath("scanfolder1/Editor").toUtf8().constData());
EXPECT_STREQ(info->GetDisplayName().toUtf8().constData(), "Editor ScanFolder");
}
// note that in the case of GetOverridingFile, this SHOULD return the correct case if an override is found
// because its possible to override a file with another file with different case in a different scan folder
// such a situation is supposed to be very rare, so the cost of correcting the case is mitigated.
TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetOverridingFile_Exists_ReturnsCorrectCase)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
// create two scan folders, since its order dependent, the ScanFolder1 is the "winner" in tie breakers (when they both contain same file relpath)
QString scanfolder1Path = m_tempPath.filePath("scanfolder1");
QString scanfolder2Path = m_tempPath.filePath("scanfolder2");
QString caseSensitiveDummyFileName = m_tempPath.absoluteFilePath("scanfolder1/TestCase.tXt");
QString differentCaseDummyFileName = m_tempPath.absoluteFilePath("scanfolder2/testcase.txt");
UnitTestUtils::CreateDummyFile(caseSensitiveDummyFileName, QString("testcase1\n"));
UnitTestUtils::CreateDummyFile(differentCaseDummyFileName, QString("testcase2\n"));
m_config->AddScanFolder(ScanFolderInfo(scanfolder1Path, "ScanFolder1", "sf1", "", false, true, m_platforms), true);
m_config->AddScanFolder(ScanFolderInfo(scanfolder2Path, "ScanFolder2", "sf2", "", false, true, m_platforms), true);
// Perform the test by asking it whether anyone overrides "testcase" (lowercase) in scanfolder 2.
QString overrider = m_config->GetOverridingFile("testcase.txt", scanfolder2Path);
ASSERT_FALSE(overrider.isEmpty());
// the result should be the real actual case of the file in scanfolder 1:
EXPECT_STREQ(overrider.toUtf8().constData(), caseSensitiveDummyFileName.toUtf8().constData());
}
TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetOverridingFile_ExistsButNotOverridden_ReturnsEmpty)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
// create two scan folders, since its order dependent, the ScanFolder1 is the "winner" in tie breakers.
QString scanfolder1Path = m_tempPath.filePath("scanfolder1");
QString scanfolder2Path = m_tempPath.filePath("scanfolder2");
QString caseSensitiveDummyFileName = m_tempPath.absoluteFilePath("scanfolder1/TestCase.tXt");
QString differentCaseDummyFileName = m_tempPath.absoluteFilePath("scanfolder2/testcase.txt");
UnitTestUtils::CreateDummyFile(caseSensitiveDummyFileName, QString("testcase1\n"));
UnitTestUtils::CreateDummyFile(differentCaseDummyFileName, QString("testcase2\n"));
m_config->AddScanFolder(ScanFolderInfo(scanfolder1Path, "ScanFolder1", "sf1", "", false, true, m_platforms), true);
m_config->AddScanFolder(ScanFolderInfo(scanfolder2Path, "ScanFolder2", "sf2", "", false, true, m_platforms), true);
// Perform the test by asking it whether the existing real winning file is being overridden by anyone.
QString overrider = m_config->GetOverridingFile("TestCase.tXt", scanfolder1Path);
// note that this should return the emptystring, because there is nothing that OVERRIDES it (ie, its already the winner).
EXPECT_TRUE(overrider.isEmpty());
}
TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetOverridingFile_DoesNotExist_ReturnsEmptyString)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
// create two scan folders, since its order dependent, the ScanFolder1 is the "winner" in tie breakers.
QString scanfolder1Path = m_tempPath.filePath("scanfolder1");
QString scanfolder2Path = m_tempPath.filePath("scanfolder2");
m_config->AddScanFolder(ScanFolderInfo(scanfolder1Path, "ScanFolder1", "sf1", "", false, true, m_platforms), true);
m_config->AddScanFolder(ScanFolderInfo(scanfolder2Path, "ScanFolder2", "sf2", "", false, true, m_platforms), true);
// Perform the test by asking it whether anyone overrides "testcase" (lowercase) in scanfolder 2.
QString overrider = m_config->GetOverridingFile("doesntExist.txt", scanfolder2Path);
EXPECT_TRUE(overrider.isEmpty());
}
TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, FindFirstMatchingFile_DoesNotExist_ReturnsEmptyString)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
// create two scan folders, since its order dependent, the ScanFolder1 is the "winner" in tie breakers.
QString scanfolder1Path = m_tempPath.filePath("scanfolder1");
QString scanfolder2Path = m_tempPath.filePath("scanfolder2");
m_config->AddScanFolder(ScanFolderInfo(scanfolder1Path, "ScanFolder1", "sf1", "", false, true, m_platforms), true);
m_config->AddScanFolder(ScanFolderInfo(scanfolder2Path, "ScanFolder2", "sf2", "", false, true, m_platforms), true);
// Perform the test by asking it whether anyone overrides "testcase" (lowercase) in scanfolder 2.
QString foundFile = m_config->FindFirstMatchingFile("doesntExist.txt");
EXPECT_TRUE(foundFile.isEmpty());
}
// note that we do not guarantee that FindFirstMatchingFile always returns the correct case, as it is a super hot path
// function, and the only time case could be incorrect is in the situation where a file with different case overrides
// an underlying file, ie,
// Engine/EngineAssets/Textures/StartScreen.tif
// MyGame/EngineAssets/textures/startscreen.tif <-- would override the above because game has higher / more important priority.
// ensures that exact matches take priority over subfolder matches
TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetScanFolderForFile_SubFolder_ExactMatch_IsFound)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
m_config->AddScanFolder(ScanFolderInfo(m_tempPath.filePath("scanfolder1"), "ScanFolder1", "sf1", "", true, false, m_platforms), true); // a root folder that has watched subfolders, not recursive
m_config->AddScanFolder(ScanFolderInfo(m_tempPath.filePath("scanfolder1/Editor"), "Editor ScanFolder", "sf2", "", false, true, m_platforms), true); // a child folder that exists within that scan folder.
const ScanFolderInfo* info = m_config->GetScanFolderForFile(m_tempPath.filePath("scanfolder1/Editor"));
ASSERT_TRUE(info);
EXPECT_STREQ(info->ScanPath().toUtf8().constData(), m_tempPath.filePath("scanfolder1/Editor").toUtf8().constData());
EXPECT_STREQ(info->GetDisplayName().toUtf8().constData(), "Editor ScanFolder");
}
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_RegularScanfolder)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
const char* configRoot = ":/testdata/config_regular";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_TRUE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, EmptyDummyProjectName, false, false));
ASSERT_EQ(m_absorber.m_numErrorsAbsorbed, 0);
ASSERT_EQ(config.GetScanFolderCount(), 3); // the two, and then the one that has the same data as prior but different identifier.
QString scanName = AssetUtilities::ComputeGameName() + " Scan Folder";
ASSERT_EQ(config.GetScanFolderAt(0).GetDisplayName(), scanName);
ASSERT_EQ(config.GetScanFolderAt(0).GetOutputPrefix(), QString());
ASSERT_EQ(config.GetScanFolderAt(0).RecurseSubFolders(), true);
ASSERT_EQ(config.GetScanFolderAt(0).GetOrder(), 0);
// its important that this does NOT change and this makes sure the old way of doing it (case-sensitive name) persists
ASSERT_EQ(config.GetScanFolderAt(0).GetPortableKey(), QString("from-ini-file-Game"));
ASSERT_EQ(config.GetScanFolderAt(1).GetDisplayName(), QString("FeatureTests"));
ASSERT_EQ(config.GetScanFolderAt(1).GetOutputPrefix(), QString("featuretestsoutputfolder")); // to prove its not related to display name
ASSERT_EQ(config.GetScanFolderAt(1).RecurseSubFolders(), false);
ASSERT_EQ(config.GetScanFolderAt(1).GetOrder(), 5000);
// this proves that the featuretests name is used instead of the output prefix
ASSERT_EQ(config.GetScanFolderAt(1).GetPortableKey(), QString("from-ini-file-FeatureTests"));
ASSERT_EQ(config.GetScanFolderAt(2).GetDisplayName(), QString("FeatureTests2"));
ASSERT_EQ(config.GetScanFolderAt(2).GetOutputPrefix(), QString("featuretestsoutputfolder")); // to prove its not related to display name
ASSERT_EQ(config.GetScanFolderAt(2).RecurseSubFolders(), false);
ASSERT_EQ(config.GetScanFolderAt(2).GetOrder(), 6000);
// this proves that the featuretests name is used instead of the output prefix
ASSERT_EQ(config.GetScanFolderAt(2).GetPortableKey(), QString("from-ini-file-FeatureTests2"));
}
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_RegularScanfolderPlatformSpecific)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
const char* configRoot = ":/testdata/config_regular_platform_scanfolder";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_TRUE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, EmptyDummyProjectName, false, false));
ASSERT_EQ(m_absorber.m_numErrorsAbsorbed, 0);
ASSERT_EQ(config.GetScanFolderCount(), 5);
ASSERT_EQ(config.GetScanFolderAt(0).GetDisplayName(), QString("gameoutput"));
AZStd::vector<AssetBuilderSDK::PlatformInfo> platforms = config.GetScanFolderAt(0).GetPlatforms();
ASSERT_EQ(platforms.size(), 4);
ASSERT_TRUE(AZStd::find(platforms.begin(), platforms.end(), AssetBuilderSDK::PlatformInfo(AzToolsFramework::AssetSystem::GetHostAssetPlatform(), AZStd::unordered_set<AZStd::string>{})) != platforms.end());
ASSERT_TRUE(AZStd::find(platforms.begin(), platforms.end(), AssetBuilderSDK::PlatformInfo("es3", AZStd::unordered_set<AZStd::string>{})) != platforms.end());
ASSERT_TRUE(AZStd::find(platforms.begin(), platforms.end(), AssetBuilderSDK::PlatformInfo("ios", AZStd::unordered_set<AZStd::string>{})) != platforms.end());
ASSERT_TRUE(AZStd::find(platforms.begin(), platforms.end(), AssetBuilderSDK::PlatformInfo("server", AZStd::unordered_set<AZStd::string>{})) != platforms.end());
ASSERT_EQ(config.GetScanFolderAt(1).GetDisplayName(), QString("editoroutput"));
platforms = config.GetScanFolderAt(1).GetPlatforms();
ASSERT_EQ(platforms.size(), 2);
ASSERT_TRUE(AZStd::find(platforms.begin(), platforms.end(), AssetBuilderSDK::PlatformInfo(AzToolsFramework::AssetSystem::GetHostAssetPlatform(), AZStd::unordered_set<AZStd::string>{})) != platforms.end());
ASSERT_TRUE(AZStd::find(platforms.begin(), platforms.end(), AssetBuilderSDK::PlatformInfo("es3", AZStd::unordered_set<AZStd::string>{})) != platforms.end());
ASSERT_EQ(config.GetScanFolderAt(2).GetDisplayName(), QString("folder1output"));
platforms = config.GetScanFolderAt(2).GetPlatforms();
ASSERT_EQ(platforms.size(), 1);
ASSERT_TRUE(AZStd::find(platforms.begin(), platforms.end(), AssetBuilderSDK::PlatformInfo("es3", AZStd::unordered_set<AZStd::string>{})) != platforms.end());
ASSERT_EQ(config.GetScanFolderAt(3).GetDisplayName(), QString("folder2output"));
platforms = config.GetScanFolderAt(3).GetPlatforms();
ASSERT_EQ(platforms.size(), 3);
ASSERT_TRUE(AZStd::find(platforms.begin(), platforms.end(), AssetBuilderSDK::PlatformInfo(AzToolsFramework::AssetSystem::GetHostAssetPlatform(), AZStd::unordered_set<AZStd::string>{})) != platforms.end());
ASSERT_TRUE(AZStd::find(platforms.begin(), platforms.end(), AssetBuilderSDK::PlatformInfo("ios", AZStd::unordered_set<AZStd::string>{})) != platforms.end());
ASSERT_TRUE(AZStd::find(platforms.begin(), platforms.end(), AssetBuilderSDK::PlatformInfo("server", AZStd::unordered_set<AZStd::string>{})) != platforms.end());
ASSERT_EQ(config.GetScanFolderAt(4).GetDisplayName(), QString("folder3output"));
platforms = config.GetScanFolderAt(4).GetPlatforms();
ASSERT_EQ(platforms.size(), 0);
}
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_RegularExcludes)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
const char* configRoot = ":/testdata/config_regular";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_TRUE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, EmptyDummyProjectName, false, false));
ASSERT_EQ(m_absorber.m_numErrorsAbsorbed, 0);
ASSERT_TRUE(config.IsFileExcluded("blahblah/$tmp_01.test"));
ASSERT_FALSE(config.IsFileExcluded("blahblah/tmp_01.test"));
ASSERT_TRUE(config.IsFileExcluded("blahblah/Levels/blahblah_hold/whatever.test"));
ASSERT_FALSE(config.IsFileExcluded("blahblah/Levels/blahblahhold/whatever.test"));
}
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_Recognizers)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
#if defined(AZ_PLATFORM_WINDOWS)
const char* platformWhichIsNotCurrentPlatform = "osx_gl";
#else
const char* platformWhichIsNotCurrentPlatform = "pc";
#endif
const char* configRoot = ":/testdata/config_regular";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_TRUE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, EmptyDummyProjectName, false, false));
ASSERT_EQ(m_absorber.m_numErrorsAbsorbed, 0);
const AssetProcessor::RecognizerContainer& recogs = config.GetAssetRecognizerContainer();
ASSERT_EQ(recogs.size(), 6);
ASSERT_TRUE(recogs.contains("i_caf"));
ASSERT_EQ(recogs["i_caf"].m_patternMatcher.GetBuilderPattern().m_pattern, "*.i_caf");
ASSERT_EQ(recogs["i_caf"].m_patternMatcher.GetBuilderPattern().m_type, AssetBuilderSDK::AssetBuilderPattern::Wildcard);
ASSERT_EQ(recogs["i_caf"].m_platformSpecs.size(), 2);
ASSERT_TRUE(recogs["i_caf"].m_platformSpecs.contains("es3"));
ASSERT_TRUE(recogs["i_caf"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
ASSERT_FALSE(recogs["i_caf"].m_platformSpecs.contains("server")); // server has been set to skip.
ASSERT_EQ(recogs["i_caf"].m_platformSpecs["es3"].m_extraRCParams, "mobile");
ASSERT_EQ(recogs["i_caf"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "defaultparams");
ASSERT_TRUE(recogs.contains("caf"));
ASSERT_TRUE(recogs["caf"].m_platformSpecs.contains("es3"));
ASSERT_TRUE(recogs["caf"].m_platformSpecs.contains("server"));
ASSERT_TRUE(recogs["caf"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
ASSERT_EQ(recogs["caf"].m_platformSpecs.size(), 3);
ASSERT_EQ(recogs["caf"].m_platformSpecs["es3"].m_extraRCParams, "rendererparams");
ASSERT_EQ(recogs["caf"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "rendererparams");
ASSERT_EQ(recogs["caf"].m_platformSpecs["server"].m_extraRCParams, "copy");
ASSERT_TRUE(recogs.contains("mov"));
ASSERT_TRUE(recogs["mov"].m_platformSpecs.contains("es3"));
ASSERT_TRUE(recogs["mov"].m_platformSpecs.contains("server"));
ASSERT_TRUE(recogs["mov"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
ASSERT_EQ(recogs["mov"].m_platformSpecs.size(), 3);
ASSERT_EQ(recogs["mov"].m_platformSpecs["es3"].m_extraRCParams, "platformspecificoverride");
ASSERT_EQ(recogs["mov"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "rendererparams");
ASSERT_EQ(recogs["mov"].m_platformSpecs["server"].m_extraRCParams, "copy");
// the "rend" test makes sure that even if you dont specify 'params' its still there by default for all enabled platforms.
// (but platforms can override it)
ASSERT_TRUE(recogs.contains("rend"));
ASSERT_TRUE(recogs["rend"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
ASSERT_TRUE(recogs["rend"].m_platformSpecs.contains("es3"));
ASSERT_TRUE(recogs["rend"].m_platformSpecs.contains("server"));
ASSERT_FALSE(recogs["rend"].m_platformSpecs.contains(platformWhichIsNotCurrentPlatform)); // this is not an enabled platform and should not be there.
ASSERT_EQ(recogs["rend"].m_platformSpecs.size(), 3);
ASSERT_EQ(recogs["rend"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "rendererparams");
ASSERT_EQ(recogs["rend"].m_platformSpecs["es3"].m_extraRCParams, "rendererparams");
ASSERT_EQ(recogs["rend"].m_platformSpecs["server"].m_extraRCParams, ""); // default if not specified is empty string
ASSERT_TRUE(recogs.contains("alldefault"));
ASSERT_TRUE(recogs["alldefault"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
ASSERT_TRUE(recogs["alldefault"].m_platformSpecs.contains("es3"));
ASSERT_TRUE(recogs["alldefault"].m_platformSpecs.contains("server"));
ASSERT_FALSE(recogs["alldefault"].m_platformSpecs.contains(platformWhichIsNotCurrentPlatform)); // this is not an enabled platform and should not be there.
ASSERT_EQ(recogs["alldefault"].m_platformSpecs.size(), 3);
ASSERT_EQ(recogs["alldefault"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "");
ASSERT_EQ(recogs["alldefault"].m_platformSpecs["es3"].m_extraRCParams, "");
ASSERT_EQ(recogs["alldefault"].m_platformSpecs["server"].m_extraRCParams, "");
ASSERT_TRUE(recogs.contains("skipallbutone"));
ASSERT_FALSE(recogs["skipallbutone"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
ASSERT_FALSE(recogs["skipallbutone"].m_platformSpecs.contains("es3"));
ASSERT_TRUE(recogs["skipallbutone"].m_platformSpecs.contains("server")); // server is only one enabled (set to copy)
ASSERT_EQ(recogs["skipallbutone"].m_platformSpecs.size(), 1);
ASSERT_EQ(recogs["skipallbutone"].m_platformSpecs["server"].m_extraRCParams, "copy");
}
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_Overrides)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
const char* configRoot = ":/testdata/config_regular";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_TRUE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, DummyProjectName, false, false));
ASSERT_EQ(m_absorber.m_numErrorsAbsorbed, 0);
const AssetProcessor::RecognizerContainer& recogs = config.GetAssetRecognizerContainer();
// note that the override config DISABLES the server platform - and this in turn disables the "server only" compile rule called skipallbutone
// verify the data.
ASSERT_NE(config.GetPlatformByIdentifier(AzToolsFramework::AssetSystem::GetHostAssetPlatform()), nullptr);
ASSERT_NE(config.GetPlatformByIdentifier("es3"), nullptr);
ASSERT_NE(config.GetPlatformByIdentifier("provo"), nullptr);
// this override swaps server with provo in that it turns ON provo, turns off server
ASSERT_EQ(config.GetPlatformByIdentifier("xenia"), nullptr);
ASSERT_EQ(config.GetPlatformByIdentifier("server"), nullptr); // this should be off due to overrides
// there is a rule which only output on server, so that rule should be omitted
ASSERT_FALSE(recogs.contains("skipallbutone")); // this is the rule that had only a server.
// this exists in config_regular.ini but is removed by config_overrides.ini
ASSERT_FALSE(recogs.contains("mov"));
ASSERT_EQ(recogs.size(), 4); // so there's 4 instead of 6 because of the above omissions
ASSERT_TRUE(recogs.contains("i_caf"));
ASSERT_EQ(recogs["i_caf"].m_patternMatcher.GetBuilderPattern().m_pattern, "*.i_caf");
ASSERT_EQ(recogs["i_caf"].m_patternMatcher.GetBuilderPattern().m_type, AssetBuilderSDK::AssetBuilderPattern::Wildcard);
ASSERT_EQ(recogs["i_caf"].m_platformSpecs.size(), 3);
ASSERT_TRUE(recogs["i_caf"].m_platformSpecs.contains("es3"));
ASSERT_TRUE(recogs["i_caf"].m_platformSpecs.contains("provo"));
ASSERT_TRUE(recogs["i_caf"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
ASSERT_FALSE(recogs["i_caf"].m_platformSpecs.contains("server")); // server has been set to skip.
ASSERT_EQ(recogs["i_caf"].m_platformSpecs["es3"].m_extraRCParams, "mobile");
ASSERT_EQ(recogs["i_caf"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "defaultparams");
ASSERT_EQ(recogs["i_caf"].m_platformSpecs["provo"].m_extraRCParams, "copy");
}
TEST_F(PlatformConfigurationUnitTests, Test_GemHandling)
{
UnitTestPlatformConfiguration config;
QTemporaryDir tempEngineRoot;
QDir tempPath(tempEngineRoot.path());
AssetUtilities::ResetAssetRoot();
AssetUtilities::ComputeGameName("SamplesProject", true);
QDir computedEngineRoot;
ASSERT_TRUE(AssetUtilities::ComputeAssetRoot(computedEngineRoot, &tempPath));
ASSERT_TRUE(!computedEngineRoot.absolutePath().isEmpty());
ASSERT_TRUE(tempPath.absolutePath() == computedEngineRoot.absolutePath());
// create ONE of the two files - they are optional, but the paths to them should always be checked and generated.
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("Gems/LyShine/AssetProcessorGemConfig.ini"), ";nothing to see here"));
// note that it is expected that the gems system gives us absolute paths.
AZStd::vector<AzToolsFramework::AssetUtils::GemInfo> fakeGems;
fakeGems.push_back({ "LyShine", "Gems/LyShine", tempPath.absoluteFilePath("Gems/LyShine").toUtf8().constData(), "0fefab3f13364722b2eab3b96ce2bf20", true, false });// true = pretend this is a game gem.
fakeGems.push_back({ "LmbrCentral", "Gems/LmbrCentral/v2", tempPath.absoluteFilePath("Gems/LmbrCentral/v2").toUtf8().constData(), "ff06785f7145416b9d46fde39098cb0c", false, false });
// reading gems via the Gems System is already to be tested in the actual Gems API tests.
// to avoid trying to load those DLLs we avoid calling the actual ReadGems function
config.AddGemScanFolders(fakeGems);
QString expectedScanFolder = tempPath.absoluteFilePath("Gems/LyShine/Assets");
AssetUtilities::ResetAssetRoot();
ASSERT_EQ(2, config.GetScanFolderCount());
EXPECT_FALSE(config.GetScanFolderAt(0).IsRoot());
EXPECT_TRUE(config.GetScanFolderAt(0).GetOutputPrefix().isEmpty());
EXPECT_TRUE(config.GetScanFolderAt(0).RecurseSubFolders());
// the first one is a game gem, so its order should be above 1 but below 100.
EXPECT_GE(config.GetScanFolderAt(0).GetOrder(), 1);
EXPECT_LE(config.GetScanFolderAt(0).GetOrder(), 100);
EXPECT_EQ(0, config.GetScanFolderAt(0).ScanPath().compare(expectedScanFolder, Qt::CaseInsensitive));
// for each gem, there are currently 1 scan folder, the gem assets folder, with no output prefix
expectedScanFolder = tempPath.absoluteFilePath("Gems/LmbrCentral/v2/Assets");
EXPECT_FALSE(config.GetScanFolderAt(1).IsRoot() );
EXPECT_TRUE(config.GetScanFolderAt(1).GetOutputPrefix().isEmpty());
EXPECT_TRUE(config.GetScanFolderAt(1).RecurseSubFolders());
EXPECT_GT(config.GetScanFolderAt(1).GetOrder(), config.GetScanFolderAt(0).GetOrder());
EXPECT_EQ(0, config.GetScanFolderAt(1).ScanPath().compare(expectedScanFolder, Qt::CaseInsensitive));
}
TEST_F(PlatformConfigurationUnitTests, Test_MetaFileTypes)
{
UnitTestPlatformConfiguration config;
config.AddMetaDataType("xxxx", "");
config.AddMetaDataType("yyyy", "zzzz");
ASSERT_TRUE(config.MetaDataFileTypesCount() == 2);
ASSERT_TRUE(QString::compare(config.GetMetaDataFileTypeAt(1).first, "yyyy", Qt::CaseInsensitive) == 0);
ASSERT_TRUE(QString::compare(config.GetMetaDataFileTypeAt(1).second, "zzzz", Qt::CaseInsensitive) == 0);
}
TEST_F(PlatformConfigurationUnitTests, ReadCheckSever_FromConfig_Valid)
{
using namespace AzToolsFramework::AssetSystem;
using namespace AssetProcessor;
const char* configRoot = ":/testdata/config_regular";
UnitTestPlatformConfiguration config;
m_absorber.Clear();
ASSERT_TRUE(config.InitializeFromConfigFiles(configRoot, TestAppRoot, EmptyDummyProjectName, false, false));
ASSERT_EQ(m_absorber.m_numErrorsAbsorbed, 0);
const AssetProcessor::RecognizerContainer& recogs = config.GetAssetRecognizerContainer();
// verify that check server flag is set to true for i_caf
ASSERT_TRUE(recogs.contains("i_caf"));
ASSERT_TRUE(recogs["i_caf"].m_checkServer);
}
TEST_F(PlatformConfigurationUnitTests, PlatformConfigFile_IsPresent_Found)
{
UnitTestPlatformConfiguration config;
QTemporaryDir tempEngineRoot;
QDir tempPath(tempEngineRoot.path());
AssetUtilities::ResetAssetRoot();
AssetUtilities::ComputeGameName("SamplesProject", true);
QDir computedEngineRoot;
ASSERT_TRUE(AssetUtilities::ComputeAssetRoot(computedEngineRoot, &tempPath));
ASSERT_TRUE(AssetUtilities::ComputeEngineRoot(computedEngineRoot, &tempPath));
ASSERT_TRUE(!computedEngineRoot.absolutePath().isEmpty());
ASSERT_TRUE(tempPath.absolutePath() == computedEngineRoot.absolutePath());
// create ONE of the two files - they are optional, but the paths to them should always be checked and generated.
QString platformConfigPath{ AssetProcessor::AssetConfigPlatformDir };
platformConfigPath.append("TestPlatform/");
platformConfigPath.append(AssetProcessor::AssetProcessorPlatformConfigFileName);
QStringList platformConfigList;
ASSERT_FALSE(config.AddPlatformConfigFilePaths(platformConfigList));
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath(platformConfigPath), ";nothing to see here"));
ASSERT_TRUE(config.AddPlatformConfigFilePaths(platformConfigList));
ASSERT_EQ(platformConfigList.size(), 1);
}
@@ -0,0 +1,42 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzTest/AzTest.h>
#include <QCoreApplication>
#include "native/tests/AssetProcessorTest.h"
#include "native/unittests/UnitTestRunner.h"
#include "native/utilities/PlatformConfiguration.h"
#include <AssetManager/FileStateCache.h>
class PlatformConfigurationUnitTests
: public AssetProcessor::AssetProcessorTest
{
public:
PlatformConfigurationUnitTests();
virtual ~PlatformConfigurationUnitTests()
{
}
protected:
void SetUp() override;
void TearDown() override;
UnitTestUtils::AssertAbsorber m_absorber;
AssetProcessor::FileStatePassthrough m_fileStateCache;
private:
int m_argc;
char** m_argv;
QCoreApplication* m_qApp;
};
@@ -0,0 +1,888 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "RCBuilderTest.h"
TEST_F(RCBuilderTest, CreateBuilderDesc_CreateBuilder_Valid)
{
AssetBuilderSDK::AssetBuilderPattern pattern;
pattern.m_pattern = "*.foo";
AZStd::vector<AssetBuilderSDK::AssetBuilderPattern> builderPatterns;
builderPatterns.push_back(pattern);
TestInternalRecognizerBasedBuilder test(new MockRCCompiler());
AssetBuilderSDK::AssetBuilderDesc result = test.CreateBuilderDesc(this->GetBuilderID(), builderPatterns);
ASSERT_EQ(this->GetBuilderName(), result.m_name);
ASSERT_EQ(this->GetBuilderUUID(), result.m_busId);
ASSERT_EQ(false, result.IsExternalBuilder());
ASSERT_TRUE(result.m_patterns.size() == 1);
ASSERT_EQ(result.m_patterns[0].m_pattern, pattern.m_pattern);
}
TEST_F(RCBuilderTest, Shutdown_NormalShutdown_Requested)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
test.ShutDown();
ASSERT_EQ(mockRC->m_request_quit, 1);
}
TEST_F(RCBuilderTest, Initialize_StandardInitialization_Fail)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
mockRC->SetResultInitialize(false);
bool initialization_result = test.Initialize(configuration);
ASSERT_FALSE(initialization_result);
}
TEST_F(RCBuilderTest, Initialize_StandardInitializationWithDuplicateAndInvalidRecognizers_Valid)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
// 3 Asset recognizers, 1 duplicate & 1 without platform should result in only 1 InternalAssetRecognizer
// Good spec
AssetRecognizer good;
good.m_name = "Good";
good.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.foo", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
AssetPlatformSpec good_spec;
good_spec.m_extraRCParams = "/i";
good.m_platformSpecs["pc"] = good_spec;
// No Platform spec
AssetRecognizer no_platform;
no_platform.m_name = "No Platform";
no_platform.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.ccc", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
// Duplicate
AssetRecognizer duplicate(good.m_name, good.m_testLockSource, good.m_priority, good.m_isCritical, good.m_supportsCreateJobs, good.m_patternMatcher, good.m_version, good.m_productAssetType, good.m_outputProductDependencies);
duplicate.m_platformSpecs["pc"] = good_spec;
configuration.m_recognizerContainer["good"] = good;
configuration.m_recognizerContainer["no_platform"] = no_platform;
configuration.m_recognizerContainer["duplicate"] = duplicate;
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
ASSERT_EQ(mockRC->m_initialize, 1);
AZStd::vector<AssetBuilderSDK::PlatformInfo> platformInfos;
AZStd::unordered_set<AZStd::string> tags;
tags.insert("tools");
tags.insert("desktop");
platformInfos.emplace_back(AssetBuilderSDK::PlatformInfo("pc", tags));
InternalRecognizerPointerContainer good_recognizers;
bool good_recognizers_found = test.GetMatchingRecognizers(platformInfos, "test.foo", good_recognizers);
ASSERT_TRUE(good_recognizers_found); // Should find at least 1
ASSERT_EQ(good_recognizers.size(), 1); // 1, not 2 since the duplicates should be removed
ASSERT_EQ(good_recognizers.at(0)->m_name, good.m_name); // Match the same recognizer
InternalRecognizerPointerContainer bad_recognizers;
bool no_recognizers_found = !test.GetMatchingRecognizers(platformInfos, "test.ccc", good_recognizers);
ASSERT_TRUE(no_recognizers_found);
ASSERT_EQ(bad_recognizers.size(), 0); // 1, not 2 since the duplicates should be removed
ASSERT_EQ(m_errorAbsorber->m_numWarningsAbsorbed, 1); // this should be the "duplicate builder" warning.
ASSERT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 0);
ASSERT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 0);
}
TEST_F(RCBuilderTest, CreateJobs_CreateSingleJobStandard_Valid)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
AssetRecognizer good;
good.m_name = "Good";
good.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.foo", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
AssetPlatformSpec good_spec;
good_spec.m_extraRCParams = "/i";
good.m_platformSpecs["pc"] = good_spec;
configuration.m_recognizerContainer["good"] = good;
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
AssetBuilderSDK::CreateJobsRequest request;
AssetBuilderSDK::CreateJobsResponse response;
request.m_watchFolder = "c:\temp";
request.m_sourceFile = "test.foo";
request.m_enabledPlatforms = { AssetBuilderSDK::PlatformInfo("pc", { "desktop", "renderer" }) };
AssetProcessor::BUILDER_ID_RC.GetUuid(request.m_builderid);
test.CreateJobs(request, response);
ASSERT_EQ(response.m_result, AssetBuilderSDK::CreateJobsResultCode::Success);
ASSERT_EQ(response.m_createJobOutputs.size(), 1);
AssetBuilderSDK::JobDescriptor descriptor = response.m_createJobOutputs.at(0);
ASSERT_EQ(descriptor.GetPlatformIdentifier(), "pc");
ASSERT_FALSE(descriptor.m_critical);
}
TEST_F(RCBuilderTest, CreateJobs_CreateMultiplesJobStandard_Valid)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
AssetRecognizer standard_AR_RC;
const AZStd::string job_key_rc = "RCjob";
{
standard_AR_RC.m_name = "RCjob";
standard_AR_RC.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.foo", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
AssetPlatformSpec good_rc_spec;
good_rc_spec.m_extraRCParams = "/i";
standard_AR_RC.m_platformSpecs["pc"] = good_rc_spec;
}
configuration.m_recognizerContainer["rc_foo"] = standard_AR_RC;
AssetRecognizer standard_AR_Copy;
const AZStd::string job_key_copy = "Copyjob";
{
standard_AR_Copy.m_name = QString(job_key_copy.c_str());
standard_AR_Copy.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.foo", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
AssetPlatformSpec good_copy_spec;
good_copy_spec.m_extraRCParams = "copy";
standard_AR_Copy.m_platformSpecs["pc"] = good_copy_spec;
}
configuration.m_recognizerContainer["copy_foo"] = standard_AR_Copy;
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
// Request is for the copy builder
{
AssetBuilderSDK::CreateJobsRequest request_copy;
AssetBuilderSDK::CreateJobsResponse response_copy;
request_copy.m_watchFolder = "c:\temp";
request_copy.m_sourceFile = "test.foo";
request_copy.m_enabledPlatforms = { AssetBuilderSDK::PlatformInfo("pc",{ "desktop", "renderer" }) };
AssetProcessor::BUILDER_ID_COPY.GetUuid(request_copy.m_builderid);
test.CreateJobs(request_copy, response_copy);
ASSERT_EQ(response_copy.m_result, AssetBuilderSDK::CreateJobsResultCode::Success);
ASSERT_EQ(response_copy.m_createJobOutputs.size(), 1);
AssetBuilderSDK::JobDescriptor descriptor = response_copy.m_createJobOutputs.at(0);
ASSERT_EQ(descriptor.GetPlatformIdentifier(), "pc");
ASSERT_EQ(descriptor.m_jobKey.compare(job_key_copy), 0);
ASSERT_TRUE(descriptor.m_critical);
}
// Request is for the rc builder
{
AssetBuilderSDK::CreateJobsRequest request_rc;
AssetBuilderSDK::CreateJobsResponse response_rc;
request_rc.m_watchFolder = "c:\temp";
request_rc.m_sourceFile = "test.foo";
request_rc.m_enabledPlatforms = { AssetBuilderSDK::PlatformInfo("pc",{ "desktop", "renderer" }) };
AssetProcessor::BUILDER_ID_RC.GetUuid(request_rc.m_builderid);
test.CreateJobs(request_rc, response_rc);
ASSERT_EQ(response_rc.m_result, AssetBuilderSDK::CreateJobsResultCode::Success);
ASSERT_EQ(response_rc.m_createJobOutputs.size(), 1);
AssetBuilderSDK::JobDescriptor descriptor = response_rc.m_createJobOutputs.at(0);
ASSERT_EQ(descriptor.GetPlatformIdentifier(), "pc");
ASSERT_EQ(descriptor.m_jobKey.compare(job_key_rc), 0);
ASSERT_FALSE(descriptor.m_critical);
}
}
TEST_F(RCBuilderTest, CreateJobs_CreateSingleJobCopy_Valid)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
AssetRecognizer copy;
copy.m_name = "Copy";
copy.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.copy", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
AssetPlatformSpec copy_spec;
copy_spec.m_extraRCParams = "copy";
copy.m_platformSpecs["pc"] = copy_spec;
configuration.m_recognizerContainer["copy"] = copy;
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
AssetBuilderSDK::CreateJobsRequest request;
AssetBuilderSDK::CreateJobsResponse response;
request.m_watchFolder = "c:\temp";
request.m_sourceFile = "test.copy";
request.m_enabledPlatforms = { AssetBuilderSDK::PlatformInfo("pc",{ "desktop", "renderer" }) };
AssetProcessor::BUILDER_ID_COPY.GetUuid(request.m_builderid);
test.CreateJobs(request, response);
ASSERT_EQ(response.m_result, AssetBuilderSDK::CreateJobsResultCode::Success);
ASSERT_EQ(response.m_createJobOutputs.size(), 1);
AssetBuilderSDK::JobDescriptor descriptor = response.m_createJobOutputs.at(0);
ASSERT_EQ(descriptor.GetPlatformIdentifier(), "pc");
ASSERT_TRUE(descriptor.m_critical);
}
TEST_F(RCBuilderTest, CreateJobs_CreateSingleJobStandardSkip_Valid)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
{
AssetRecognizer skip;
skip.m_name = "Skip";
skip.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.skip", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
AssetPlatformSpec skip_spec;
skip_spec.m_extraRCParams = "skip";
skip.m_platformSpecs["pc"] = skip_spec;
configuration.m_recognizerContainer["skip"] = skip;
}
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
AssetBuilderSDK::CreateJobsRequest request;
AssetBuilderSDK::CreateJobsResponse response;
request.m_watchFolder = "c:\temp";
request.m_sourceFile = "test.skip";
request.m_enabledPlatforms = { AssetBuilderSDK::PlatformInfo("pc",{ "desktop", "renderer" }) };
request.m_builderid = this->GetBuilderUUID();
test.CreateJobs(request, response);
ASSERT_EQ(response.m_result, AssetBuilderSDK::CreateJobsResultCode::Success);
ASSERT_EQ(response.m_createJobOutputs.size(), 0);
}
TEST_F(RCBuilderTest, CreateJobs_CreateSingleJobStandard_Failed)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
AssetRecognizer good;
good.m_name = "Good";
good.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.foo", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
AssetPlatformSpec good_spec;
good_spec.m_extraRCParams = "/i";
good.m_platformSpecs["pc"] = good_spec;
configuration.m_recognizerContainer["good"] = good;
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
AssetBuilderSDK::CreateJobsRequest request;
AssetBuilderSDK::CreateJobsResponse response;
request.m_watchFolder = "c:\temp";
request.m_sourceFile = "test.ccc";
request.m_enabledPlatforms = { AssetBuilderSDK::PlatformInfo("pc",{ "desktop", "renderer" }) };
request.m_builderid = this->GetBuilderUUID();
test.CreateJobs(request, response);
ASSERT_EQ(response.m_result, AssetBuilderSDK::CreateJobsResultCode::Failed);
ASSERT_EQ(response.m_createJobOutputs.size(), 0);
}
TEST_F(RCBuilderTest, CreateJobs_CreateSingleJobStandard_ShuttingDown)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
AssetRecognizer good;
good.m_name = "Good";
good.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.foo", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
AssetPlatformSpec good_spec;
good_spec.m_extraRCParams = "/i";
good.m_platformSpecs["pc"] = good_spec;
configuration.m_recognizerContainer["good"] = good;
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
test.ShutDown();
AssetBuilderSDK::CreateJobsRequest request;
AssetBuilderSDK::CreateJobsResponse response;
request.m_watchFolder = "c:\temp";
request.m_sourceFile = "test.ccc";
request.m_enabledPlatforms = { AssetBuilderSDK::PlatformInfo("pc",{ "desktop", "renderer" }) };
request.m_builderid = this->GetBuilderUUID();
test.CreateJobs(request, response);
ASSERT_EQ(response.m_result, AssetBuilderSDK::CreateJobsResultCode::ShuttingDown);
ASSERT_EQ(response.m_createJobOutputs.size(), 0);
}
TEST_F(RCBuilderTest, CreateJobs_CreateSingleJobBadJobRequest1_Failed)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
AssetRecognizer good;
good.m_name = "Good";
good.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.foo", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
AssetPlatformSpec good_spec;
good_spec.m_extraRCParams = "/i";
good.m_platformSpecs["pc"] = good_spec;
configuration.m_recognizerContainer["good"] = good;
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
AssetBuilderSDK::CreateJobsRequest request;
AssetBuilderSDK::CreateJobsResponse response;
request.m_sourceFile = "test.ccc";
request.m_enabledPlatforms = { AssetBuilderSDK::PlatformInfo("pc",{ "desktop", "renderer" }) };
request.m_builderid = this->GetBuilderUUID();
test.CreateJobs(request, response);
ASSERT_EQ(response.m_result, AssetBuilderSDK::CreateJobsResultCode::Failed);
ASSERT_EQ(response.m_createJobOutputs.size(), 0);
}
TEST_F(RCBuilderTest, ProcessLegacyRCJob_ProcessStandardSingleJob_Failed)
{
AZ::Uuid assetTypeUUid = AZ::Uuid::CreateRandom();
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
// Create the test job
AssetBuilderSDK::ProcessJobRequest request = CreateTestJobRequest("file.c", false, "pc", 1);
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
// Case 1: execution failed
mockRC->SetResultResultExecute(NativeLegacyRCCompiler::Result(1, true, ""));
mockRC->SetResultExecute(false);
AssetBuilderSDK::ProcessJobResponse responseCrashed;
AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
test.TestProcessLegacyRCJob(request, "/i", assetTypeUUid, jobCancelListener, responseCrashed);
ASSERT_EQ(responseCrashed.m_resultCode, AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Crashed);
// case 2: result code from execution non-zero
mockRC->SetResultExecute(true);
mockRC->SetResultResultExecute(NativeLegacyRCCompiler::Result(1, false, ""));
AssetBuilderSDK::ProcessJobResponse responseFailed;
test.TestProcessLegacyRCJob(request, "/i", assetTypeUUid, jobCancelListener, responseFailed);
ASSERT_EQ(responseFailed.m_resultCode, AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Failed);
}
TEST_F(RCBuilderTest, ProcessLegacyRCJob_ProcessStandardSingleJob_Valid)
{
AZ::Uuid assetTypeUUid = AZ::Uuid::CreateRandom();
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
// Create the test job
AssetBuilderSDK::ProcessJobRequest request = CreateTestJobRequest("file.c", false, "pc");
test.AddTestFileInfo("c:\\temp\\file.a").AddTestFileInfo("c:\\temp\\file.b");
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
mockRC->SetResultResultExecute(NativeLegacyRCCompiler::Result(0, false, "c:\\temp"));
mockRC->SetResultExecute(true);
AssetBuilderSDK::ProcessJobResponse response;
AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
test.TestProcessLegacyRCJob(request, "/i", assetTypeUUid, jobCancelListener, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Success);
ASSERT_EQ(response.m_outputProducts.size(), 2); // file.c->(file.a, file.b)
}
TEST_F(RCBuilderTest, ProcessLegacyRCJob_ProcessCopySingleJob_Valid)
{
AZStd::string name = "test";
AZ::Uuid builderUuid = AZ::Uuid::CreateRandom();
AZ::Uuid assetTypeUUid = AZ::Uuid::CreateRandom();
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
// Create the test job
AssetBuilderSDK::ProcessJobRequest request = CreateTestJobRequest("file.c", false, "pc");
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
mockRC->SetResultResultExecute(NativeLegacyRCCompiler::Result(0, false, "c:\\temp"));
mockRC->SetResultExecute(true);
AssetBuilderSDK::ProcessJobResponse response;
AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
test.TestProcessCopyJob(request, assetTypeUUid, jobCancelListener, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Success);
ASSERT_EQ(response.m_outputProducts.size(), 1); // file.c->(file.a, file.b)
AssetBuilderSDK::JobProduct resultJobProd = response.m_outputProducts.at(0);
ASSERT_EQ(resultJobProd.m_productAssetType, assetTypeUUid);
ASSERT_EQ(resultJobProd.m_productFileName, request.m_fullPath);
}
TEST_F(RCBuilderTest, MatchTempFileToSkip_SkipRCFiles_true)
{
const char* rc_skip_fileNames[] = {
"rc_createdfiles.txt",
"rc_log.log",
"rc_log_warnings.log",
"rc_log_errors.log"
};
for (const char* filename : rc_skip_fileNames)
{
ASSERT_TRUE(AssetProcessor::InternalRecognizerBasedBuilder::MatchTempFileToSkip(filename));
}
}
TEST_F(RCBuilderTest, MatchTempFileToSkip_SkipRCFiles_false)
{
const char* rc_not_skip_fileNames[] = {
"foo.log",
"bar.txt"
};
for (const char* filename : rc_not_skip_fileNames)
{
ASSERT_FALSE(AssetProcessor::InternalRecognizerBasedBuilder::MatchTempFileToSkip(filename));
}
}
TEST_F(RCBuilderTest, ProcessJob_ProcessStandardRCSingleJob_Valid)
{
AZ::Uuid assetTypeUUid = AZ::Uuid::CreateRandom();
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
// Create a dummy test recognizer
AZ::u32 recID = test.AddTestRecognizer(this->GetBuilderID(),QString("/i"), "pc");
// Create the test job
AssetBuilderSDK::ProcessJobRequest request = CreateTestJobRequest("test.tif", false, "pc");
request.m_jobDescription.m_jobParameters[recID] = "/i";
test.AddTestFileInfo("c:\\temp\\file.a").AddTestFileInfo("c:\\temp\\file.b");
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
mockRC->SetResultResultExecute(NativeLegacyRCCompiler::Result(0, false, "c:\\temp"));
mockRC->SetResultExecute(true);
AssetBuilderSDK::ProcessJobResponse response;
test.TestProcessJob(request, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Success);
ASSERT_EQ(response.m_outputProducts.size(), 2); // file.c->(file.a, file.b)
}
TEST_F(RCBuilderTest, ProcessJob_ProcessStandardRCSingleJob_Failed)
{
AZ::Uuid assetTypeUUid = AZ::Uuid::CreateRandom();
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
// Create a dummy test recognizer
AZ::u32 recID = test.AddTestRecognizer(this->GetBuilderID(), QString("/i"), "pc");
// Create the test job
AssetBuilderSDK::ProcessJobRequest request = CreateTestJobRequest("test.tif", false, "pc");
request.m_jobDescription.m_jobParameters[recID] = "/i";
test.AddTestFileInfo("c:\\temp\\file.a").AddTestFileInfo("c:\\temp\\file.b");
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
mockRC->SetResultResultExecute(NativeLegacyRCCompiler::Result(1, false, "c:\\temp"));
mockRC->SetResultExecute(true);
AssetBuilderSDK::ProcessJobResponse response;
test.TestProcessJob(request, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Failed);
}
TEST_F(RCBuilderTest, ProcessJob_ProcessStandardCopySingleJob_Valid)
{
AZ::Uuid assetTypeUUid = AZ::Uuid::CreateRandom();
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
// Create a dummy test recognizer
AZ::u32 recID = test.AddTestRecognizer(this->GetBuilderID(), QString("copy"), "pc");
// Create the test job
AssetBuilderSDK::ProcessJobRequest request = CreateTestJobRequest("test.tif", true, "pc");
request.m_jobDescription.m_jobParameters[recID] = "copy";
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
mockRC->SetResultExecute(true);
AssetBuilderSDK::ProcessJobResponse response;
test.TestProcessJob(request, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Success);
ASSERT_EQ(response.m_outputProducts.size(), 1); // test.
ASSERT_TRUE(response.m_outputProducts[0].m_productFileName.find("test.tif") != AZStd::string::npos);
}
TEST_F(RCBuilderTest, ProcessJob_ProcessStandardSkippedSingleJob_Invalid)
{
AZ::Uuid assetTypeUUid = AZ::Uuid::CreateRandom();
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
// Create a dummy test recognizer
AZ::u32 recID = test.AddTestRecognizer(this->GetBuilderID(), QString("skip"), "pc");
// Create the test job
AssetBuilderSDK::ProcessJobRequest request = CreateTestJobRequest("test.tif", true, "pc");
request.m_jobDescription.m_jobParameters[recID] = "copy";
bool initialization_result = test.Initialize(configuration);
ASSERT_TRUE(initialization_result);
mockRC->SetResultExecute(true);
AssetBuilderSDK::ProcessJobResponse response;
test.TestProcessJob(request, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Failed);
}
TEST_F(RCBuilderTest, TestProcessRCResultFolder_LegacySystem)
{
TestInternalRecognizerBasedBuilder test(new MockRCCompiler());
test.AddTestFileInfo("file.dds")
.AddTestFileInfo("file.caf")
.AddTestFileInfo("file.png")
.AddTestFileInfo("rc_createdfiles.txt")
.AddTestFileInfo("rc_log.log")
.AddTestFileInfo("rc_log_warnings.log")
.AddTestFileInfo("rc_log_errors.log")
.AddTestFileInfo("ProcessJobRequest.xml")
.AddTestFileInfo("ProcessJobResponse.xml");
AZ::Uuid productGUID = AZ::Uuid("{60554E3C-D8D5-4429-AC77-740F0ED46193}");
AssetBuilderSDK::ProcessJobResponse response;
test.TestProcessRCResultFolder("c:\\temp", productGUID, false, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Success);
// we expect it to have ignored most of the file cruft.
ASSERT_EQ(response.m_outputProducts.size(), 3);
AZStd::string fileJoined;
AzFramework::StringFunc::Path::Join("c:\\temp", "file.dds", fileJoined);
ASSERT_EQ(response.m_outputProducts[0].m_productFileName, fileJoined);
ASSERT_EQ(response.m_outputProducts[0].m_productAssetType, productGUID);
ASSERT_EQ(response.m_outputProducts[0].m_productSubID, 0x00000000);
ASSERT_EQ(response.m_outputProducts[0].m_legacySubIDs.size(), 0);
AzFramework::StringFunc::Path::Join("c:\\temp", "file.caf", fileJoined);
ASSERT_EQ(response.m_outputProducts[1].m_productFileName, fileJoined);
ASSERT_EQ(response.m_outputProducts[1].m_productAssetType, productGUID);
ASSERT_EQ(response.m_outputProducts[1].m_productSubID, (AZ_CRC("file.caf", 0x91277b80) & 0x0000FFFF)); // legacy subids are just the lower 16 bits of the crc of filename.
ASSERT_EQ(response.m_outputProducts[1].m_legacySubIDs.size(), 0);
AzFramework::StringFunc::Path::Join("c:\\temp", "file.png", fileJoined);
ASSERT_EQ(response.m_outputProducts[2].m_productFileName, fileJoined);
ASSERT_EQ(response.m_outputProducts[2].m_productAssetType, productGUID);
ASSERT_EQ(response.m_outputProducts[2].m_productSubID, (AZ_CRC("file.png", 0x7fd84af0) & 0x0000FFFF));
ASSERT_EQ(response.m_outputProducts[2].m_legacySubIDs.size(), 0);
}
TEST_F(RCBuilderTest, TestProcessRCResultFolder_Fail_Fail)
{
TestInternalRecognizerBasedBuilder test(new MockRCCompiler());
AssetBuilderSDK::ProcessJobResponse response;
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
AZ::Uuid productGUID = AZ::Uuid("{60554E3C-D8D5-4429-AC77-740F0ED46193}");
test.TestProcessRCResultFolder("c:\\temp", productGUID, true, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResult_Failed);
}
TEST_F(RCBuilderTest, TestProcessRCResultFolder_Succeed_NothingBuilt)
{
TestInternalRecognizerBasedBuilder test(new MockRCCompiler());
AssetBuilderSDK::ProcessJobResponse response;
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
AZ::Uuid productGUID = AZ::Uuid("{60554E3C-D8D5-4429-AC77-740F0ED46193}");
test.TestProcessRCResultFolder("c:\\temp", productGUID, true, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResult_Success);
}
TEST_F(RCBuilderTest, TestProcessRCResultFolder_Fail_BadName)
{
TestInternalRecognizerBasedBuilder test(new MockRCCompiler());
AssetBuilderSDK::ProcessJobResponse response;
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
AZ::Uuid productGUID = AZ::Uuid("{60554E3C-D8D5-4429-AC77-740F0ED46193}");
// note: empty name on next line
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct("", productGUID, 1234));
test.TestProcessRCResultFolder("c:\\temp", productGUID, true, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResult_Failed);
}
TEST_F(RCBuilderTest, TestProcessRCResultFolder_Fail_DuplicateFile)
{
m_errorAbsorber->m_debugMessages = true;
TestInternalRecognizerBasedBuilder test(new MockRCCompiler());
AssetBuilderSDK::ProcessJobResponse response;
test.AddTestFileInfo("file.dds");
AZ::Uuid productGUID = AZ::Uuid("{60554E3C-D8D5-4429-AC77-740F0ED46193}");
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct("file.dds", productGUID, 1234));
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct("file.dds", productGUID, 5679));
test.TestProcessRCResultFolder("c:\\temp", productGUID, true, response);
m_errorAbsorber->AssertErrors(1);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResult_Failed);
}
TEST_F(RCBuilderTest, TestProcessRCResultFolder_Fail_DuplicateSubID)
{
TestInternalRecognizerBasedBuilder test(new MockRCCompiler());
AssetBuilderSDK::ProcessJobResponse response;
test.AddTestFileInfo("file.dds")
.AddTestFileInfo("file.caf");
AZ::Uuid productGUID = AZ::Uuid("{60554E3C-D8D5-4429-AC77-740F0ED46193}");
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct("file.dds", productGUID, 1234));
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct("file.caf", productGUID, 1234));
test.TestProcessRCResultFolder("c:\\temp", productGUID, true, response);
ASSERT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 1);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResult_Failed);
}
TEST_F(RCBuilderTest, TestProcessRCResultFolder_WithResponseFromRC)
{
TestInternalRecognizerBasedBuilder test(new MockRCCompiler());
test.AddTestFileInfo("file.dds")
.AddTestFileInfo("file.caf")
.AddTestFileInfo("file.png")
.AddTestFileInfo("rc_createdfiles.txt")
.AddTestFileInfo("rc_log.log")
.AddTestFileInfo("rc_log_warnings.log")
.AddTestFileInfo("rc_log_errors.log")
.AddTestFileInfo("ProcessJobRequest.xml")
.AddTestFileInfo("ProcessJobResponse.xml");
AZ::Uuid productGUID = AZ::Uuid::CreateNull(); // this is to make sure that it doesn't matter what we pass in
AZ::Uuid actualGUID = AZ::Uuid("{60554E3C-D8D5-4429-AC77-740F0ED46193}");
AssetBuilderSDK::ProcessJobResponse response;
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct("file.dds", actualGUID, 1234));
response.m_outputProducts.back().m_legacySubIDs.push_back(3333);
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct("file.caf", actualGUID, 3456));
response.m_outputProducts.back().m_legacySubIDs.push_back(2222);
response.m_outputProducts.back().m_legacySubIDs.push_back((AZ_CRC("file.caf", 0x91277b80) & 0x0000FFFF)); // push back the existing one to make sure no dupes.
response.m_resultCode = AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Success;
// in this test we pretend the response was actually populated by the builder and make sure it populates the legacy IDs correctly
// 1. there should actually BE legacy IDs
// 2. there should be no duplicate IDs (legacy IDs should not duplicate ACTUAL ids)
// 3. there should be no duplicate Legacy IDs (legacy IDs should not duplicate each other)
// 4. if we provide legacy Ids, they should be used in addition to the automatic ones.
test.TestProcessRCResultFolder("c:\\temp", productGUID, true, response);
ASSERT_EQ(response.m_resultCode, AssetBuilderSDK::ProcessJobResultCode::ProcessJobResult_Success);
// we expect it to only have accepted the products we specified.
ASSERT_EQ(response.m_outputProducts.size(), 2);
AZStd::string fileJoined;
AzFramework::StringFunc::Path::Join("c:\\temp", "file.dds", fileJoined);
ASSERT_EQ(response.m_outputProducts[0].m_productFileName, fileJoined);
ASSERT_EQ(response.m_outputProducts[0].m_productAssetType, actualGUID);
ASSERT_EQ(response.m_outputProducts[0].m_productSubID, 1234);
ASSERT_EQ(response.m_outputProducts[0].m_legacySubIDs.size(), 2); // it must include our new one AND the zero that it would have generated before.
ASSERT_EQ(response.m_outputProducts[0].m_legacySubIDs[0], 3333);
ASSERT_EQ(response.m_outputProducts[0].m_legacySubIDs[1], 0);
AzFramework::StringFunc::Path::Join("c:\\temp", "file.caf", fileJoined);
ASSERT_EQ(response.m_outputProducts[1].m_productFileName, fileJoined);
ASSERT_EQ(response.m_outputProducts[1].m_productAssetType, actualGUID);
ASSERT_EQ(response.m_outputProducts[1].m_productSubID, 3456); // legacy subids are just the lower 16 bits of the crc of filename.
ASSERT_EQ(response.m_outputProducts[1].m_legacySubIDs.size(), 2); // we only expect the one legacy, no dupes!
ASSERT_EQ(response.m_outputProducts[1].m_legacySubIDs[0], 2222);
ASSERT_EQ(response.m_outputProducts[1].m_legacySubIDs[1], (AZ_CRC("file.caf", 0x91277b80) & 0x0000FFFF));
}
class MockBuilderListener : public AssetBuilderSDK::AssetBuilderBus::Handler
{
public:
void RegisterBuilderInformation(const AssetBuilderSDK::AssetBuilderDesc& builderDesc) override
{
m_wasCalled = true;
m_result = builderDesc;
}
bool m_wasCalled = false;
AssetBuilderSDK::AssetBuilderDesc m_result;
};
class RCBuilderFingerprintTest
: public RCBuilderTest
{
public:
// A utility function which feeds in the version and asset type to the builder, fingerprints it, and returns the fingerprint
AZStd::string BuildFingerprint(int versionNumber, AZ::Uuid builderProductType)
{
MockRCCompiler* mockRC = new MockRCCompiler();
TestInternalRecognizerBasedBuilder test(mockRC);
MockRecognizerConfiguration configuration;
AssetPlatformSpec good_spec;
good_spec.m_extraRCParams = "/i";
AssetRecognizer good;
good.m_name = "Good";
good.m_version = versionNumber;
good.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.foo", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard);
good.m_platformSpecs["pc"] = good_spec;
good.m_productAssetType = builderProductType;
configuration.m_recognizerContainer["good"] = good;
MockBuilderListener listener;
listener.BusConnect();
bool initialization_result = test.Initialize(configuration);
listener.BusDisconnect();
EXPECT_TRUE(listener.m_wasCalled);
EXPECT_TRUE(initialization_result);
EXPECT_STRNE(listener.m_result.m_analysisFingerprint.c_str(), "");
return listener.m_result.m_analysisFingerprint;
}
};
TEST_F(RCBuilderFingerprintTest, DifferentVersion_Has_DifferentAnalysisFingerprint)
{
AZ::Uuid uuid1 = AZ::Uuid::CreateRandom();
AZStd::string analysisFingerprint1 = BuildFingerprint(1, uuid1);
AZStd::string analysisFingerprint2 = BuildFingerprint(2, uuid1);
EXPECT_STRNE(analysisFingerprint1.c_str(), analysisFingerprint2.c_str());
}
TEST_F(RCBuilderFingerprintTest, DifferentAssetType_Has_DifferentAnalysisFingerprint)
{
AZ::Uuid uuid1 = AZ::Uuid::CreateRandom();
AZ::Uuid uuid2 = AZ::Uuid::CreateRandom();
AZStd::string analysisFingerprint1 = BuildFingerprint(1, uuid1);
AZStd::string analysisFingerprint2 = BuildFingerprint(1, uuid2);
EXPECT_STRNE(analysisFingerprint1.c_str(), analysisFingerprint2.c_str());
}
TEST_F(RCBuilderFingerprintTest, DifferentAssetTypeAndVersion_Has_DifferentAnalysisFingerprint)
{
AZ::Uuid uuid1 = AZ::Uuid::CreateRandom();
AZ::Uuid uuid2 = AZ::Uuid::CreateRandom();
AZStd::string analysisFingerprint1 = BuildFingerprint(1, uuid1);
AZStd::string analysisFingerprint2 = BuildFingerprint(2, uuid2);
EXPECT_STRNE(analysisFingerprint1.c_str(), analysisFingerprint2.c_str());
}
TEST_F(RCBuilderFingerprintTest, SameVersionAndSameType_Has_SameAnalysisFingerprint)
{
AZ::Uuid uuid1 = AZ::Uuid::CreateRandom();
AZStd::string analysisFingerprint1 = BuildFingerprint(1, uuid1);
AZStd::string analysisFingerprint2 = BuildFingerprint(1, uuid1);
EXPECT_STREQ(analysisFingerprint1.c_str(), analysisFingerprint2.c_str());
}
@@ -0,0 +1,250 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzTest/AzTest.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include <qcoreapplication.h>
#include "../../utilities/assetUtils.h"
#include "../../resourcecompiler/RCBuilder.h"
#include "native/tests/AssetProcessorTest.h"
using namespace AssetProcessor;
extern const BuilderIdAndName BUILDER_ID_COPY;
extern const BuilderIdAndName BUILDER_ID_RC;
extern const BuilderIdAndName BUILDER_ID_SKIP;
class MockRCCompiler
: public AssetProcessor::RCCompiler
{
public:
MockRCCompiler()
: m_executeResultResult(0, false, "c:\temp")
{
}
bool Initialize([[maybe_unused]] const QString& systemRoot, [[maybe_unused]] const QString& rcExecutableFullPath) override
{
m_initialize++;
return m_initializeResult;
}
bool Execute([[maybe_unused]] const QString& inputFile, [[maybe_unused]] const QString& watchFolder, [[maybe_unused]] const QString& platformIdentifier, [[maybe_unused]] const QString& params, [[maybe_unused]] const QString& dest,
[[maybe_unused]] const AssetBuilderSDK::JobCancelListener* jobCancelListener, Result& result) const override
{
m_execute++;
result = m_executeResultResult;
return m_executeResult;
}
void RequestQuit() override
{
m_request_quit++;
}
void ResetCounters()
{
this->m_initialize = 0;
this->m_execute = 0;
this->m_request_quit = 0;
}
void SetResultInitialize(bool result)
{
m_initializeResult = result;
}
void SetResultExecute(bool result)
{
m_executeResult = result;
}
void SetResultResultExecute(Result result)
{
m_executeResultResult = result;
}
bool m_initializeResult = true;
bool m_executeResult = true;
Result m_executeResultResult;
mutable int m_initialize = 0;
mutable int m_execute = 0;
mutable int m_request_quit = 0;
};
struct MockRecognizerConfiguration
: public RecognizerConfiguration
{
const RecognizerContainer& GetAssetRecognizerContainer() const override
{
return m_recognizerContainer;
}
const ExcludeRecognizerContainer& GetExcludeAssetRecognizerContainer() const override
{
return m_excludeContainer;
}
RecognizerContainer m_recognizerContainer;
ExcludeRecognizerContainer m_excludeContainer;
};
struct TestInternalRecognizerBasedBuilder
: public InternalRecognizerBasedBuilder
{
TestInternalRecognizerBasedBuilder(RCCompiler* rcCompiler = nullptr)
: InternalRecognizerBasedBuilder()
{
if (rcCompiler != nullptr)
{
m_rcCompiler.reset(rcCompiler);
}
}
bool FindRC([[maybe_unused]] QString& rcPathOut) override
{
return true;
}
QFileInfoList GetFilesInDirectory([[maybe_unused]] const QString& directoryPath) override
{
QFileInfoList mockFileInfoList;
mockFileInfoList.append(m_testFileInfo);
return mockFileInfoList;
}
bool SaveProcessJobRequestFile(const char* /*requestFileDir*/, const char* /*requestFileName*/, const AssetBuilderSDK::ProcessJobRequest& /*request*/) override
{
m_savedProcessJob = true;
return true;
}
// returns false only if there is a critical failure.
bool LoadProcessJobResponseFile(const char* /*responseFileDir*/, const char* /*responseFileName*/, AssetBuilderSDK::ProcessJobResponse& /*response*/, bool& /*responseLoaded*/) override
{
m_loadedProcessJob = true;
return true;
}
void TestProcessJob(const AssetBuilderSDK::ProcessJobRequest& request,
AssetBuilderSDK::ProcessJobResponse& response)
{
InternalRecognizerBasedBuilder::ProcessJob(request, response);
}
void TestProcessLegacyRCJob(const AssetBuilderSDK::ProcessJobRequest& request,
QString rcParam,
AZ::Uuid productAssetType,
const AssetBuilderSDK::JobCancelListener& jobCancelListener,
AssetBuilderSDK::ProcessJobResponse& response)
{
InternalRecognizerBasedBuilder::ProcessLegacyRCJob(request, rcParam, productAssetType, jobCancelListener, response);
}
void TestProcessCopyJob(const AssetBuilderSDK::ProcessJobRequest& request,
AZ::Uuid productAssetType,
const AssetBuilderSDK::JobCancelListener& jobCancelListener,
AssetBuilderSDK::ProcessJobResponse& response)
{
const bool outputProductDependency = false;
InternalRecognizerBasedBuilder::ProcessCopyJob(request, productAssetType, outputProductDependency, jobCancelListener, response);
}
TestInternalRecognizerBasedBuilder& AddTestFileInfo(const QString& testFileFullPath)
{
QFileInfo testFileInfo(testFileFullPath);
m_testFileInfo.push_back(testFileInfo);
return *this;
}
AZ::u32 AddTestRecognizer(QString builderID, QString extraRCParam, QString platformString)
{
// Create a dummy test recognizer
AssetBuilderSDK::FilePatternMatcher patternMatcher;
QString versionZero("0");
AZ::Data::AssetType productAssetType = AZ::Uuid::CreateRandom();
AssetRecognizer baseAssetRecognizer(QString("test-").append(extraRCParam), false, 1, false, false, patternMatcher, versionZero, productAssetType, false);
QHash<QString, AssetPlatformSpec> assetPlatformSpecByPlatform;
AssetPlatformSpec assetSpec;
assetSpec.m_extraRCParams = extraRCParam;
assetPlatformSpecByPlatform[platformString] = assetSpec;
InternalAssetRecognizer* pTestInternalRecognizer = new InternalAssetRecognizer(baseAssetRecognizer, builderID, assetPlatformSpecByPlatform);
this->m_assetRecognizerDictionary[pTestInternalRecognizer->m_paramID] = pTestInternalRecognizer;
return pTestInternalRecognizer->m_paramID;
}
void TestProcessRCResultFolder(const QString &dest, const AZ::Uuid& productAssetType, bool responseFromRCCompiler, AssetBuilderSDK::ProcessJobResponse &response)
{
ProcessRCResultFolder(dest, productAssetType, responseFromRCCompiler, response);
}
QList<QFileInfo> m_testFileInfo;
bool m_savedProcessJob = false;
bool m_loadedProcessJob = false;
};
class RCBuilderTest
: public AssetProcessor::AssetProcessorTest
{
int m_argc;
char** m_argv;
QCoreApplication* m_qApp = nullptr;
public:
RCBuilderTest()
: m_argc(0)
, m_argv(0)
{
m_qApp = new QCoreApplication(m_argc, m_argv);
}
virtual ~RCBuilderTest()
{
delete m_qApp;
}
AZ::Uuid GetBuilderUUID() const
{
AZ::Uuid rcUuid;
AssetProcessor::BUILDER_ID_RC.GetUuid(rcUuid);
return rcUuid;
}
AZStd::string GetBuilderName() const
{
return AZStd::string(AssetProcessor::BUILDER_ID_RC.GetName().toUtf8().data());
}
QString GetBuilderID() const
{
return AssetProcessor::BUILDER_ID_RC.GetId();
}
AssetBuilderSDK::ProcessJobRequest CreateTestJobRequest(const AZStd::string& testFileName, bool critical, QString platform, AZ::s64 jobId = 0)
{
AssetBuilderSDK::ProcessJobRequest request;
request.m_builderGuid = this->GetBuilderUUID();
request.m_sourceFile = testFileName;
request.m_fullPath = AZStd::string("c:\\temp\\") + testFileName;
request.m_tempDirPath = "c:\\temp";
request.m_jobDescription.m_critical = critical;
request.m_jobDescription.SetPlatformIdentifier(platform.toUtf8().constData());
request.m_jobId = jobId;
return request;
}
};
@@ -0,0 +1,300 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "RCControllerTest.h"
#include "native/resourcecompiler/rccontroller.h"
#include "AzCore/std/parallel/binary_semaphore.h"
TEST_F(RCcontrollerTest, CompileGroupCreatedWithUnknownStatusForFailedJobs)
{
//Strategy Add a failed job to the job queue list and than ask the rc controller to request compile, it should emit unknown status
using namespace AssetProcessor;
// we have to initialize this to something other than Assetstatus_Unknown here because later on we will be testing the value of assetstatus
AzFramework::AssetSystem::AssetStatus assetStatus = AzFramework::AssetSystem::AssetStatus_Failed;
RCController rcController;
QObject::connect(&rcController, &RCController::CompileGroupCreated,
[&assetStatus]([[maybe_unused]] AssetProcessor::NetworkRequestID groupID, AzFramework::AssetSystem::AssetStatus status)
{
assetStatus = status;
}
);
RCJobListModel* rcJobListModel = rcController.GetQueueModel();
RCJob* job = new RCJob(rcJobListModel);
AssetProcessor::JobDetails jobDetails;
jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = jobDetails.m_jobEntry.m_databaseSourceName = "somepath/failed.dds";
jobDetails.m_jobEntry.m_platformInfo = { "pc", {"desktop", "renderer"} };
jobDetails.m_jobEntry.m_jobKey = "Compile Stuff";
job->SetState(RCJob::failed);
job->Init(jobDetails);
rcJobListModel->addNewJob(job);
// Exact Match
NetworkRequestID requestID(1, 1234);
rcController.OnRequestCompileGroup(requestID, "pc", "somepath/failed.dds", AZ::Data::AssetId());
ASSERT_TRUE(assetStatus == AzFramework::AssetSystem::AssetStatus_Unknown);
assetStatus = AzFramework::AssetSystem::AssetStatus_Failed;
// Broader Match
rcController.OnRequestCompileGroup(requestID, "pc", "somepath", AZ::Data::AssetId() );
ASSERT_TRUE(assetStatus == AzFramework::AssetSystem::AssetStatus_Unknown);
}
class RCcontrollerTest_Cancellation
: public RCcontrollerTest
{
public:
RCcontrollerTest_Cancellation()
{
}
virtual ~RCcontrollerTest_Cancellation()
{
}
void SetUp() override
{
RCcontrollerTest::SetUp();
using namespace AssetProcessor;
m_rcController.reset(new RCController());
m_rcController->SetDispatchPaused(true);
m_rcJobListModel = m_rcController->GetQueueModel();
{
RCJob* job = new RCJob(m_rcJobListModel);
AssetProcessor::JobDetails jobDetails;
jobDetails.m_jobEntry.m_computedFingerprint = 1;
jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = jobDetails.m_jobEntry.m_databaseSourceName = "somepath/failed.dds";
jobDetails.m_jobEntry.m_platformInfo = { "ios",{ "mobile", "renderer" } };
jobDetails.m_jobEntry.m_jobRunKey = 1;
jobDetails.m_jobEntry.m_jobKey = "tiff";
job->SetState(RCJob::JobState::pending);
job->Init(jobDetails);
m_rcJobListModel->addNewJob(job);
}
{
RCJob* job = new RCJob(m_rcJobListModel);
// note that Init() is a move operation. we cannot reuse jobDetails.
AssetProcessor::JobDetails jobDetails;
jobDetails.m_jobEntry.m_computedFingerprint = 1;
jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = jobDetails.m_jobEntry.m_databaseSourceName = "somepath/failed.dds";
jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobRunKey = 2;
jobDetails.m_jobEntry.m_jobKey = "tiff";
job->SetState(RCJob::JobState::pending);
job->Init(jobDetails);
m_rcJobListModel->addNewJob(job);
m_rcJobListModel->markAsStarted(job);
m_rcJobListModel->markAsProcessing(job); // job is now "in flight"
}
}
void TearDown() override
{
m_rcJobListModel = nullptr;
m_rcController.reset();
RCcontrollerTest::TearDown();
}
AZStd::unique_ptr<AssetProcessor::RCController> m_rcController;
AssetProcessor::RCJobListModel* m_rcJobListModel = nullptr; // convenience pointer into m_rcController->GetQueueModel()
};
TEST_F(RCcontrollerTest_Cancellation, JobSubmitted_SameFingerprint_DoesNotCancelTheJob)
{
// submit a new job for the same details as the already running one.
{
AssetProcessor::JobDetails jobDetails;
jobDetails.m_jobEntry.m_computedFingerprint = 1; // same as above in SetUp
jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = jobDetails.m_jobEntry.m_databaseSourceName = "somepath/failed.dds";
jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "tiff";
jobDetails.m_jobEntry.m_jobRunKey = 3;
m_rcController->JobSubmitted(jobDetails);
}
for (int idx = 0; idx < m_rcJobListModel->itemCount(); idx++)
{
// neither job should be cancelled.
AssetProcessor::RCJob* rcJob = m_rcJobListModel->getItem(idx);
ASSERT_TRUE(rcJob->GetState() != AssetProcessor::RCJob::JobState::cancelled);
}
}
TEST_F(RCcontrollerTest_Cancellation, JobSubmitted_DifferentFingerprint_CancelsTheJob_OnlyIfInProgress)
{
// submit a new job for the same details as the already running one.
{
AssetProcessor::JobDetails jobDetails;
jobDetails.m_jobEntry.m_computedFingerprint = 2; // different from setup.
jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = jobDetails.m_jobEntry.m_databaseSourceName = "somepath/failed.dds";
jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "tiff";
jobDetails.m_jobEntry.m_jobRunKey = 3;
m_rcController->JobSubmitted(jobDetails);
}
for (int idx = 0; idx < m_rcJobListModel->itemCount(); idx++)
{
// neither job should be cancelled.
AssetProcessor::RCJob* rcJob = m_rcJobListModel->getItem(idx);
if (rcJob->GetJobEntry().m_jobRunKey == 2)
{
// the one with run key 2 should have been cancelled and replaced with run key 3
ASSERT_TRUE(rcJob->GetState() == AssetProcessor::RCJob::JobState::cancelled);
}
else
{
// the other one should have been left alone since it had not yet begun.
ASSERT_TRUE(rcJob->GetState() != AssetProcessor::RCJob::JobState::cancelled);
}
}
}
class RCcontrollerTest_Simple
: public RCcontrollerTest
{
public:
void SetUp() override
{
RCcontrollerTest::SetUp();
using namespace AssetProcessor;
m_rcController.reset(new RCController(/*minJobs*/1, /*maxJobs*/1));
m_rcController->SetDispatchPaused(false);
m_rcJobListModel = m_rcController->GetQueueModel();
qRegisterMetaType<AssetBuilderSDK::ProcessJobResponse>("ProcessJobResponse");
QObject::connect(m_rcController.get(), &RCController::BecameIdle, [this]()
{
m_wait.release();
});
}
void TearDown() override
{
m_rcJobListModel = nullptr;
m_rcController.reset();
RCcontrollerTest::TearDown();
}
void SubmitJob();
AZStd::binary_semaphore m_wait;
AZStd::unique_ptr<AssetProcessor::RCController> m_rcController;
AssetProcessor::RCJobListModel* m_rcJobListModel = nullptr; // convenience pointer into m_rcController->GetQueueModel()
};
void RCcontrollerTest_Simple::SubmitJob()
{
using namespace AssetBuilderSDK;
{
AssetProcessor::JobDetails jobDetails;
jobDetails.m_jobEntry.m_computedFingerprint = 123;
jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = jobDetails.m_jobEntry.m_databaseSourceName = "somepath/a.dds";
jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "tiff";
jobDetails.m_jobEntry.m_jobRunKey = 3;
jobDetails.m_assetBuilderDesc.m_processJobFunction = []([[maybe_unused]] const ProcessJobRequest& request, ProcessJobResponse& response)
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
};
m_rcController->JobSubmitted(jobDetails);
}
// Numbers are a bit arbitrary but this should result in a max wait time of 5s
int retryCount = 100;
do
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
} while (m_wait.try_acquire_for(AZStd::chrono::milliseconds(5)) == false && --retryCount > 0);
ASSERT_GT(retryCount, 0);
}
// This is a regresssion test to ensure the rccontroller can handle multiple jobs for the same file being completed before
// the APM has a chance to send OnFinishedProcesssingJob events
TEST_F(RCcontrollerTest_Simple, SameJobIsCompletedMultipleTimes_CompletesWithoutError)
{
using namespace AssetProcessor;
AZStd::vector<JobEntry> jobEntries;
QObject::connect(m_rcController.get(), &RCController::FileCompiled, [&jobEntries](JobEntry entry, AssetBuilderSDK::ProcessJobResponse response)
{
jobEntries.push_back(entry);
});
SubmitJob();
SubmitJob();
ASSERT_EQ(jobEntries.size(), 2);
for (const JobEntry& entry : jobEntries)
{
m_rcController->OnAddedToCatalog(entry);
}
ASSERT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 4); // Expected that there are 4 errors related to the files not existing on disk. Error message: GenerateFingerprint was called but no input files were requested for fingerprinting.
ASSERT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 0);
}
// makes sure to expose parts of RCJob to the unit test
class TestRCJob : public AssetProcessor::RCJob
{
friend class GTEST_TEST_CLASS_NAME_(RCcontrollerTest, BuilderSDK_API_ProcessJob_HasValidParameters_WithOutputFolder);
public:
explicit TestRCJob(QObject* parent = 0) : AssetProcessor::RCJob(parent) {};
};
TEST_F(RCcontrollerTest, BuilderSDK_API_ProcessJob_HasValidParameters_WithOutputFolder)
{
// this test makes sure that the BuilderSDK API is not exposed to any database internals.
AZ::Uuid sourceUUID = AZ::Uuid::CreateRandom();
AZ::Uuid builderGuid = AZ::Uuid::CreateRandom();
AssetBuilderSDK::ProcessJobRequest req;
{
// note that this scope is intentional. job.Init(jobDetails) below is actually a by-ref operation that destroys JobDetails in the process.
AssetProcessor::JobDetails jobDetails;
// the crux of this test: the database source name differs from the relative path to watch folder:
jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = "SomeThing.tif"; // case sensitive
// Note that while this is an OS-SPECIFIC path, this unit test does not actually invoke the file system at all
// and only operates on in-memory structures, so it should work on every platform.
jobDetails.m_jobEntry.m_watchFolderPath = "c:/test/a/B/c"; // just to make sure case is preserved
jobDetails.m_jobEntry.m_databaseSourceName = "somepath/SomeThing.tif"; // case sensitive but outputprefixes are generally lowcase
jobDetails.m_jobEntry.m_sourceFileUUID = sourceUUID;
jobDetails.m_jobEntry.m_platformInfo = { "ios",{ "mobile", "renderer" } };
jobDetails.m_jobEntry.m_jobRunKey = 1;
jobDetails.m_jobEntry.m_jobKey = "tiff";
jobDetails.m_jobEntry.m_builderGuid = builderGuid;
TestRCJob job;
job.Init(jobDetails);
job.PopulateProcessJobRequest(req);
}
EXPECT_STREQ(req.m_sourceFile.c_str(), "SomeThing.tif");
EXPECT_STREQ(req.m_watchFolder.c_str(), "c:/test/a/B/c");
// the crux of the test, tested: make sure that it does not contain 'somepath' in there just becuase its part of the Database Source Name.
EXPECT_STREQ(req.m_fullPath.c_str(), "c:/test/a/B/c/SomeThing.tif");
EXPECT_EQ(req.m_builderGuid, builderGuid);
EXPECT_TRUE(req.m_platformInfo.HasTag("renderer"));
EXPECT_TRUE(req.m_platformInfo.HasTag("mobile"));
EXPECT_STREQ(req.m_platformInfo.m_identifier.c_str(), "ios");
EXPECT_EQ(req.m_sourceFileUUID, sourceUUID);
}
@@ -0,0 +1,42 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include "native/tests/AssetProcessorTest.h"
#include <QCoreApplication>
#include "native/assetprocessor.h"
#include <AzFramework/Asset/AssetSystemTypes.h>
class RCcontrollerTest
: public AssetProcessor::AssetProcessorTest
{
public:
RCcontrollerTest()
: m_argc(0)
, m_argv(0)
{
m_qApp = new QCoreApplication(m_argc, m_argv);
qRegisterMetaType<AzFramework::AssetSystem::AssetStatus>("AzFramework::AssetSystem::AssetStatus");
qRegisterMetaType<AssetProcessor::NetworkRequestID>("NetworkRequestID");
}
virtual ~RCcontrollerTest()
{
delete m_qApp;
}
private:
int m_argc;
char** m_argv;
QCoreApplication* m_qApp;
};
@@ -0,0 +1,290 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "RCJobTest.h"
#include <native/tests/AssetProcessorTest.h>
#include <native/resourcecompiler/rcjob.h>
namespace UnitTests
{
using namespace testing;
using ::testing::NiceMock;
using namespace AssetProcessor;
using namespace AssetBuilderSDK;
class MockDiskSpaceResponder : public DiskSpaceInfoBus::Handler
{
public:
MOCK_METHOD3(CheckSufficientDiskSpace, bool(const QString&, qint64, bool));
};
class IgnoreNotifyTracker : public ProcessingJobInfoBus::Handler
{
public:
// Will notify other systems which old product is just about to get removed from the cache
// before we copy the new product instead along.
void BeginCacheFileUpdate(const char* productPath) override
{
m_capturedStartPaths.push_back(productPath);
}
// Will notify other systems which product we are trying to copy in the cache
// along with status of whether that copy succeeded or failed.
void EndCacheFileUpdate(const char* productPath, bool /*queueAgainForProcessing*/) override
{
m_capturedStopPaths.push_back(productPath);
}
AZStd::vector<AZStd::string> m_capturedStartPaths;
AZStd::vector<AZStd::string> m_capturedStopPaths;
};
class RCJobTest : public AssetProcessorTest
{
public:
void SetUp() override
{
AssetProcessorTest::SetUp();
m_data.reset(new StaticData());
m_data->tempDirPath = QDir(m_data->m_tempDir.path());
m_data->m_absolutePathToTempInputFolder = m_data->tempDirPath.absoluteFilePath("InputFolder").toUtf8().constData();
// note that the case of OutputFolder is intentionally upper/lower case becuase
// while files inside the output folder should be lowercased, the path to there should not be lowercased by RCJob.
m_data->m_absolutePathToTempOutputFolder = m_data->tempDirPath.absoluteFilePath("OutputFolder").toUtf8().constData();
m_data->tempDirPath.mkpath(QString::fromUtf8(m_data->m_absolutePathToTempInputFolder.c_str()));
m_data->m_diskSpaceResponder.BusConnect();
m_data->m_notifyTracker.BusConnect();
// this can be overridden in each test but if you don't override it, then this fixture will do it.
ON_CALL(m_data->m_diskSpaceResponder, CheckSufficientDiskSpace(_, _, _))
.WillByDefault(Return(true));
}
void TearDown() override
{
m_data->m_diskSpaceResponder.BusDisconnect();
m_data->m_notifyTracker.BusDisconnect();
m_data.reset();
AssetProcessorTest::TearDown();
}
protected:
struct StaticData
{
QTemporaryDir m_tempDir;
QDir tempDirPath;
AZStd::string m_absolutePathToTempInputFolder;
AZStd::string m_absolutePathToTempOutputFolder;
NiceMock<MockDiskSpaceResponder> m_diskSpaceResponder;
IgnoreNotifyTracker m_notifyTracker;
};
AZStd::unique_ptr<StaticData> m_data;
};
TEST_F(RCJobTest, CopyCompiledAssets_NoWorkToDo_Succeeds)
{
BuilderParams builderParams;
ProcessJobResponse response;
EXPECT_TRUE(RCJob::CopyCompiledAssets(builderParams, response));
EXPECT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 0);
EXPECT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 0);
EXPECT_EQ(m_errorAbsorber->m_numWarningsAbsorbed, 0);
}
TEST_F(RCJobTest, CopyCompiledAssets_InvalidOutputPath_FailsAndAsserts)
{
BuilderParams builderParams;
ProcessJobResponse response;
response.m_resultCode = ProcessJobResult_Success;
response.m_outputProducts.push_back({ "file1.txt" }); // make sure that there is at least one product so that it doesn't early out.
// set only the input path, not the output path:
builderParams.m_processJobRequest.m_tempDirPath = m_data->m_absolutePathToTempInputFolder.c_str(); // input working scratch space folder
EXPECT_FALSE(RCJob::CopyCompiledAssets(builderParams, response));
EXPECT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 1);
}
TEST_F(RCJobTest, CopyCompiledAssets_InvalidInputPath_FailsAndAsserts)
{
BuilderParams builderParams;
ProcessJobResponse response;
response.m_resultCode = ProcessJobResult_Success;
response.m_outputProducts.push_back({ "file1.txt" }); // make sure that there is at least one product so that it doesn't early out.
// set the input dir to be a broken invalid dir:
builderParams.m_processJobRequest.m_tempDirPath = AZ::Uuid::CreateRandom().ToString<AZStd::string>();
builderParams.m_finalOutputDir = QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str()); // output folder in the 'cache'
EXPECT_FALSE(RCJob::CopyCompiledAssets(builderParams, response));
EXPECT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 1);
}
TEST_F(RCJobTest, CopyCompiledAssets_TooLongPath_FailsButDoesNotAssert)
{
BuilderParams builderParams;
ProcessJobResponse response;
response.m_resultCode = ProcessJobResult_Success;
// set only the output path, but not the input path:
builderParams.m_processJobRequest.m_tempDirPath = m_data->m_absolutePathToTempInputFolder.c_str(); // input working scratch space folder
builderParams.m_finalOutputDir = QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str()); // output folder in the 'cache'
// give it an overly long file name:
AZStd::string reallyLongFileName;
reallyLongFileName.resize(4096, 'x');
response.m_outputProducts.push_back({ reallyLongFileName.c_str() });
EXPECT_FALSE(RCJob::CopyCompiledAssets(builderParams, response));
EXPECT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 0);
EXPECT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 1);
}
TEST_F(RCJobTest, CopyCompiledAssets_OutOfDiskSpace_FailsButDoesNotAssert)
{
BuilderParams builderParams;
ProcessJobResponse response;
response.m_resultCode = ProcessJobResult_Success;
// set only the output path, but not the input path:
builderParams.m_processJobRequest.m_tempDirPath = m_data->m_absolutePathToTempInputFolder.c_str(); // input working scratch space folder
builderParams.m_finalOutputDir = QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str()); // output folder in the 'cache'
response.m_resultCode = ProcessJobResult_Success;
response.m_outputProducts.push_back({ "file1.txt" }); // make sure that there is at least one product so that it doesn't early out.
UnitTestUtils::CreateDummyFile(QDir(m_data->m_absolutePathToTempInputFolder.c_str()).absoluteFilePath("file1.txt"), "output of file 1");
response.m_outputProducts.push_back({ "file2.txt" }); // make sure that there is at least one product so that it doesn't early out.
UnitTestUtils::CreateDummyFile(QDir(m_data->m_absolutePathToTempInputFolder.c_str()).absoluteFilePath("file2.txt"), "output of file 2");
// we exepct exactly one call to check for disk space, (not once for each file), and in this case, we'll return false.
EXPECT_CALL(m_data->m_diskSpaceResponder, CheckSufficientDiskSpace(_,_,_))
.Times(1)
.WillRepeatedly(Return(false));
EXPECT_FALSE(RCJob::CopyCompiledAssets(builderParams, response));
EXPECT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 0);
EXPECT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 1);
// no notifies should be hit since the operation should not have been attempted at all (disk space should be checked up front)
ASSERT_EQ(m_data->m_notifyTracker.m_capturedStartPaths.size(), 0);
ASSERT_EQ(m_data->m_notifyTracker.m_capturedStopPaths.size(), 0);
// no cached files should have been copied at all.
QString expectedFinalOutputPath = QDir(QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str())).absoluteFilePath("file1.txt");
EXPECT_FALSE(QFile::exists(expectedFinalOutputPath));
expectedFinalOutputPath = QDir(QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str())).absoluteFilePath("file2.txt");
EXPECT_FALSE(QFile::exists(expectedFinalOutputPath));
}
// The RC Copy Compiled Assets routine is supposed to check up front for problem situations such as out of disk space
// or missing source files, before it tries to perform any operation. This test gives it one file which does work
// but one missing file also, and expects it to fail (without asserting) but without even trying to copy the files at all.
TEST_F(RCJobTest, CopyCompiledAssets_MissingInputFile_Fails_DoesNotAssert_DoesNotAlterCache)
{
BuilderParams builderParams;
ProcessJobResponse response;
response.m_resultCode = ProcessJobResult_Success;
// set only the output path, but not the input path:
builderParams.m_processJobRequest.m_tempDirPath = m_data->m_absolutePathToTempInputFolder.c_str(); // input working scratch space folder
builderParams.m_finalOutputDir = QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str()); // output folder in the 'cache'
response.m_resultCode = ProcessJobResult_Success;
response.m_outputProducts.push_back({ "FiLe1.TxT" }); // make sure that there is at least one product so that it doesn't early out.
UnitTestUtils::CreateDummyFile(QDir(m_data->m_absolutePathToTempInputFolder.c_str()).absoluteFilePath("FiLe1.TxT"), "output of file 1");
response.m_outputProducts.push_back({ "FiLe2.txt" });
// note well that we create the first file but we don't acutally create the second one, so it is missing.
EXPECT_FALSE(RCJob::CopyCompiledAssets(builderParams, response));
EXPECT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 0);
EXPECT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 1);
// no notifies should be hit since the operation should not have been attempted at all.
ASSERT_EQ(m_data->m_notifyTracker.m_capturedStartPaths.size(), 0);
ASSERT_EQ(m_data->m_notifyTracker.m_capturedStopPaths.size(), 0);
QString expectedFinalOutputPath = QDir(QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str())).absoluteFilePath("file1.txt");
EXPECT_FALSE(QFile::exists(expectedFinalOutputPath));
}
TEST_F(RCJobTest, CopyCompiledAssets_AbsolutePath_SucceedsAndNotifiesAboutCacheDelete)
{
BuilderParams builderParams;
ProcessJobResponse response;
response.m_resultCode = ProcessJobResult_Success;
// set only the output path, but not the input path:
builderParams.m_processJobRequest.m_tempDirPath = m_data->m_absolutePathToTempInputFolder.c_str(); // input working scratch space folder
builderParams.m_finalOutputDir = QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str()); // output folder in the 'cache'
response.m_resultCode = ProcessJobResult_Success;
// make up a completely different random path to put an absolute file in:
QTemporaryDir extraDir;
QDir randomDir(extraDir.path());
randomDir.mkpath(extraDir.path());
QString absolutePathToCreate = randomDir.absoluteFilePath("someabsolutefile.txt");
UnitTestUtils::CreateDummyFile(absolutePathToCreate, "output of the file");
response.m_outputProducts.push_back({ absolutePathToCreate.toUtf8().constData() }); // absolute path to file not actually in the product scratch space folder.
// this should copy that file into the target path.
EXPECT_TRUE(RCJob::CopyCompiledAssets(builderParams, response));
EXPECT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 0);
EXPECT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 0);
ASSERT_EQ(m_data->m_notifyTracker.m_capturedStartPaths.size(), 1);
ASSERT_EQ(m_data->m_notifyTracker.m_capturedStopPaths.size(), 1);
// note that output files are automatically lowercased within the cache but the path to the cache folder itself is not lowered, just the output file.
// this is to make sure that game code never has to worry about the casing of output file paths, CRYPAK can just always lower the relpath and always know
// that even on case-sensitive platforms it won't cause trouble or a difference of behavior from non-case-sensitive ones.
QString expectedFinalOutputPath = QDir(QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str())).absoluteFilePath("someabsolutefile.txt");
ASSERT_STREQ(m_data->m_notifyTracker.m_capturedStartPaths[0].c_str(), m_data->m_notifyTracker.m_capturedStopPaths[0].c_str());
ASSERT_STREQ(m_data->m_notifyTracker.m_capturedStartPaths[0].c_str(), expectedFinalOutputPath.toUtf8().constData());
EXPECT_TRUE(QFile::exists(expectedFinalOutputPath));
}
TEST_F(RCJobTest, CopyCompiledAssets_RelativePath_SucceedsAndNotifiesAboutCacheDelete)
{
BuilderParams builderParams;
ProcessJobResponse response;
response.m_resultCode = ProcessJobResult_Success;
// set only the output path, but not the input path:
builderParams.m_processJobRequest.m_tempDirPath = m_data->m_absolutePathToTempInputFolder.c_str(); // input working scratch space folder
builderParams.m_finalOutputDir = QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str()); // output folder in the 'cache'
response.m_resultCode = ProcessJobResult_Success;
response.m_outputProducts.push_back({ "FiLe1.TxT" }); // make sure that there is at least one product so that it doesn't early out.
UnitTestUtils::CreateDummyFile(QDir(m_data->m_absolutePathToTempInputFolder.c_str()).absoluteFilePath("FiLe1.TxT"), "output of file 1");
EXPECT_TRUE(RCJob::CopyCompiledAssets(builderParams, response));
EXPECT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 0);
EXPECT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 0);
ASSERT_EQ(m_data->m_notifyTracker.m_capturedStartPaths.size(), 1);
ASSERT_EQ(m_data->m_notifyTracker.m_capturedStopPaths.size(), 1);
// note that output files are automatically lowercased within the cache but the path to the cache folder itself is not lowered, just the output file.
// this is to make sure that game code never has to worry about the casing of output file paths, CRYPAK can just always lower the relpath and always know
// that even on case-sensitive platforms it won't cause trouble or a difference of behavior from non-case-sensitive ones.
QString expectedFinalOutputPath = QDir(QString::fromUtf8(m_data->m_absolutePathToTempOutputFolder.c_str())).absoluteFilePath("file1.txt");
ASSERT_STREQ(m_data->m_notifyTracker.m_capturedStartPaths[0].c_str(), m_data->m_notifyTracker.m_capturedStopPaths[0].c_str());
ASSERT_STREQ(m_data->m_notifyTracker.m_capturedStartPaths[0].c_str(), expectedFinalOutputPath.toUtf8().constData());
EXPECT_TRUE(QFile::exists(expectedFinalOutputPath));
// Start and end paths should, however, be normalized even if the input is not.
QString normalizedStartPath = QString::fromUtf8(m_data->m_notifyTracker.m_capturedStartPaths[0].c_str());
normalizedStartPath = AssetUtilities::NormalizeFilePath(normalizedStartPath);
EXPECT_STREQ(normalizedStartPath.toUtf8().constData(), m_data->m_notifyTracker.m_capturedStartPaths[0].c_str());
QString normalizedStopPath = QString::fromUtf8(m_data->m_notifyTracker.m_capturedStopPaths[0].c_str());
normalizedStopPath = AssetUtilities::NormalizeFilePath(normalizedStopPath);
EXPECT_STREQ(normalizedStopPath.toUtf8().constData(), m_data->m_notifyTracker.m_capturedStopPaths[0].c_str());
}
} // end namespace UnitTests
@@ -0,0 +1,17 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzTest/AzTest.h>
@@ -0,0 +1,81 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "utilities/BatchApplicationManager.h"
#include <AzTest/AzTest.h>
#include <AzTest/Utils.h>
#include <native/tests/BaseAssetProcessorTest.h>
DECLARE_AZ_UNIT_TEST_MAIN()
int RunUnitTests(int argc, char* argv[], bool& ranUnitTests)
{
ranUnitTests = true;
INVOKE_AZ_UNIT_TEST_MAIN(nullptr); // nullptr turns off default test environment used to catch stray asserts
// This looks a bit weird, but the macro returns conditionally, so *if* we get here, it means the unit tests didn't run
ranUnitTests = false;
return 0;
}
int main(int argc, char* argv[])
{
qputenv("QT_MAC_DISABLE_FOREGROUND_APPLICATION_TRANSFORM", "1");
// If "--unittest" is present on the command line, run unit testing
// and return immediately. Otherwise, continue as normal.
AZ::Test::addTestEnvironment(new BaseAssetProcessorTestEnvironment());
bool pauseOnComplete = false;
if (AZ::Test::ContainsParameter(argc, argv, "--pause-on-completion"))
{
pauseOnComplete = true;
}
bool ranUnitTests;
int result = RunUnitTests(argc, argv, ranUnitTests);
if (ranUnitTests)
{
if (pauseOnComplete)
{
system("pause");
}
return result;
}
BatchApplicationManager applicationManager(&argc, &argv);
setvbuf(stdout, NULL, _IONBF, 0); // Disabling output buffering to fix test failures due to incomplete logs
ApplicationManager::BeforeRunStatus status = applicationManager.BeforeRun();
if (status != ApplicationManager::BeforeRunStatus::Status_Success)
{
if (status == ApplicationManager::BeforeRunStatus::Status_Restarting)
{
//AssetProcessor will restart
return 0;
}
else
{
//Initialization failed
return 1;
}
}
return applicationManager.Run() ? 0 : 1;
}

Some files were not shown because too many files have changed in this diff Show More