rcjob.cpp 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "rcjob.h"
  9. #include <AzToolsFramework/UI/Logging/LogLine.h>
  10. #include <AzToolsFramework/Metadata/UuidUtils.h>
  11. #include <native/utilities/BuilderManager.h>
  12. #include <native/utilities/ThreadHelper.h>
  13. #include <QtConcurrent/QtConcurrentRun>
  14. #include <QElapsedTimer>
  15. #include "native/utilities/JobDiagnosticTracker.h"
  16. #include <qstorageinfo.h>
  17. #include <native/utilities/ProductOutputUtil.h>
  18. namespace
  19. {
  20. bool s_typesRegistered = false;
  21. // You have up to 60 minutes to finish processing an asset.
  22. // This was increased from 10 to account for PVRTC compression
  23. // taking up to an hour for large normal map textures, and should
  24. // be reduced again once we move to the ASTC compression format, or
  25. // find another solution to reduce processing times to be reasonable.
  26. const unsigned int g_jobMaximumWaitTime = 1000 * 60 * 60;
  27. const unsigned int g_sleepDurationForLockingAndFingerprintChecking = 100;
  28. const unsigned int g_timeoutInSecsForRetryingCopy = 30;
  29. const char* const s_tempString = "%TEMP%";
  30. const char* const s_jobLogFileName = "jobLog.xml";
  31. bool MoveCopyFile(QString sourceFile, QString productFile, bool isCopyJob = false)
  32. {
  33. if (!isCopyJob && (AssetUtilities::MoveFileWithTimeout(sourceFile, productFile, g_timeoutInSecsForRetryingCopy)))
  34. {
  35. //We do not want to rename the file if it is a copy job
  36. return true;
  37. }
  38. else if (AssetUtilities::CopyFileWithTimeout(sourceFile, productFile, g_timeoutInSecsForRetryingCopy))
  39. {
  40. // try to copy instead
  41. return true;
  42. }
  43. AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Failed to move OR copy file from Source directory: %s to Destination Directory: %s", sourceFile.toUtf8().data(), productFile.toUtf8().data());
  44. return false;
  45. }
  46. }
  47. using namespace AssetProcessor;
  48. bool Params::IsValidParams() const
  49. {
  50. return !m_cacheOutputDir.empty() && !m_intermediateOutputDir.empty() && !m_relativePath.empty();
  51. }
  52. bool RCParams::IsValidParams() const
  53. {
  54. return (
  55. (!m_rcExe.isEmpty()) &&
  56. (!m_rootDir.isEmpty()) &&
  57. (!m_inputFile.isEmpty()) &&
  58. Params::IsValidParams()
  59. );
  60. }
  61. namespace AssetProcessor
  62. {
  63. RCJob::RCJob(QObject* parent)
  64. : QObject(parent)
  65. , m_timeCreated(QDateTime::currentDateTime())
  66. , m_scanFolderID(0)
  67. {
  68. m_jobState = RCJob::pending;
  69. if (!s_typesRegistered)
  70. {
  71. qRegisterMetaType<RCParams>("RCParams");
  72. qRegisterMetaType<BuilderParams>("BuilderParams");
  73. qRegisterMetaType<JobOutputInfo>("JobOutputInfo");
  74. s_typesRegistered = true;
  75. }
  76. }
  77. RCJob::~RCJob()
  78. {
  79. }
  80. void RCJob::Init(JobDetails& details)
  81. {
  82. // jobs for the "Common" platform exist to emit additional source files, which themselves could be critical
  83. // so they are automatically critical as well.
  84. if (GetPlatformInfo().m_identifier == AssetBuilderSDK::CommonPlatformName)
  85. {
  86. details.m_critical = true;
  87. }
  88. m_jobDetails = AZStd::move(details);
  89. m_queueElementID = QueueElementID(GetJobEntry().m_sourceAssetReference, GetPlatformInfo().m_identifier.c_str(), GetJobKey());
  90. }
  91. const JobEntry& RCJob::GetJobEntry() const
  92. {
  93. return m_jobDetails.m_jobEntry;
  94. }
  95. bool RCJob::HasMissingSourceDependency() const
  96. {
  97. return m_jobDetails.HasMissingSourceDependency();
  98. }
  99. QDateTime RCJob::GetTimeCreated() const
  100. {
  101. return m_timeCreated;
  102. }
  103. void RCJob::SetTimeCreated(const QDateTime& timeCreated)
  104. {
  105. m_timeCreated = timeCreated;
  106. }
  107. QDateTime RCJob::GetTimeLaunched() const
  108. {
  109. return m_timeLaunched;
  110. }
  111. void RCJob::SetTimeLaunched(const QDateTime& timeLaunched)
  112. {
  113. m_timeLaunched = timeLaunched;
  114. }
  115. QDateTime RCJob::GetTimeCompleted() const
  116. {
  117. return m_timeCompleted;
  118. }
  119. void RCJob::SetTimeCompleted(const QDateTime& timeCompleted)
  120. {
  121. m_timeCompleted = timeCompleted;
  122. }
  123. AZ::u32 RCJob::GetOriginalFingerprint() const
  124. {
  125. return m_jobDetails.m_jobEntry.m_computedFingerprint;
  126. }
  127. void RCJob::SetOriginalFingerprint(unsigned int fingerprint)
  128. {
  129. m_jobDetails.m_jobEntry.m_computedFingerprint = fingerprint;
  130. }
  131. RCJob::JobState RCJob::GetState() const
  132. {
  133. return m_jobState;
  134. }
  135. void RCJob::SetState(const JobState& state)
  136. {
  137. bool wasPending = (m_jobState == pending);
  138. m_jobState = state;
  139. if ((wasPending)&&(m_jobState == cancelled))
  140. {
  141. // if we were pending (had not started yet) and we are now canceled, we still have to emit the finished signal
  142. // so that all the various systems waiting for us can do their housekeeping.
  143. Q_EMIT Finished();
  144. }
  145. }
  146. void RCJob::SetJobEscalation(int jobEscalation)
  147. {
  148. m_JobEscalation = jobEscalation;
  149. }
  150. void RCJob::SetCheckExclusiveLock(bool value)
  151. {
  152. m_jobDetails.m_jobEntry.m_checkExclusiveLock = value;
  153. }
  154. QString RCJob::GetStateDescription(const RCJob::JobState& state)
  155. {
  156. switch (state)
  157. {
  158. case RCJob::pending:
  159. return tr("Pending");
  160. case RCJob::processing:
  161. return tr("Processing");
  162. case RCJob::completed:
  163. return tr("Completed");
  164. case RCJob::crashed:
  165. return tr("Crashed");
  166. case RCJob::terminated:
  167. return tr("Terminated");
  168. case RCJob::failed:
  169. return tr("Failed");
  170. case RCJob::cancelled:
  171. return tr("Cancelled");
  172. }
  173. return QString();
  174. }
  175. const AZ::Uuid& RCJob::GetInputFileUuid() const
  176. {
  177. return m_jobDetails.m_jobEntry.m_sourceFileUUID;
  178. }
  179. AZ::IO::Path RCJob::GetCacheOutputPath() const
  180. {
  181. return m_jobDetails.m_cachePath;
  182. }
  183. AZ::IO::Path RCJob::GetIntermediateOutputPath() const
  184. {
  185. return m_jobDetails.m_intermediatePath;
  186. }
  187. AZ::IO::Path RCJob::GetRelativePath() const
  188. {
  189. return m_jobDetails.m_relativePath;
  190. }
  191. const AssetBuilderSDK::PlatformInfo& RCJob::GetPlatformInfo() const
  192. {
  193. return m_jobDetails.m_jobEntry.m_platformInfo;
  194. }
  195. AssetBuilderSDK::ProcessJobResponse& RCJob::GetProcessJobResponse()
  196. {
  197. return m_processJobResponse;
  198. }
  199. void RCJob::PopulateProcessJobRequest(AssetBuilderSDK::ProcessJobRequest& processJobRequest)
  200. {
  201. processJobRequest.m_jobDescription.m_critical = IsCritical();
  202. processJobRequest.m_jobDescription.m_additionalFingerprintInfo = m_jobDetails.m_extraInformationForFingerprinting;
  203. processJobRequest.m_jobDescription.m_jobKey = GetJobKey().toUtf8().data();
  204. processJobRequest.m_jobDescription.m_jobParameters = AZStd::move(m_jobDetails.m_jobParam);
  205. processJobRequest.m_jobDescription.SetPlatformIdentifier(GetPlatformInfo().m_identifier.c_str());
  206. processJobRequest.m_jobDescription.m_priority = GetPriority();
  207. processJobRequest.m_platformInfo = GetPlatformInfo();
  208. processJobRequest.m_builderGuid = GetBuilderGuid();
  209. processJobRequest.m_sourceFile = GetJobEntry().m_sourceAssetReference.RelativePath().c_str();
  210. processJobRequest.m_sourceFileUUID = GetInputFileUuid();
  211. processJobRequest.m_watchFolder = GetJobEntry().m_sourceAssetReference.ScanFolderPath().c_str();
  212. processJobRequest.m_fullPath = GetJobEntry().GetAbsoluteSourcePath().toUtf8().data();
  213. processJobRequest.m_jobId = GetJobEntry().m_jobRunKey;
  214. }
  215. QString RCJob::GetJobKey() const
  216. {
  217. return m_jobDetails.m_jobEntry.m_jobKey;
  218. }
  219. AZ::Uuid RCJob::GetBuilderGuid() const
  220. {
  221. return m_jobDetails.m_jobEntry.m_builderGuid;
  222. }
  223. bool RCJob::IsCritical() const
  224. {
  225. return m_jobDetails.m_critical;
  226. }
  227. bool RCJob::IsAutoFail() const
  228. {
  229. return m_jobDetails.m_autoFail;
  230. }
  231. int RCJob::GetPriority() const
  232. {
  233. return m_jobDetails.m_priority;
  234. }
  235. void RCJob::SetPriority(int newPriority)
  236. {
  237. m_jobDetails.m_priority = newPriority;
  238. }
  239. const AZStd::vector<AssetProcessor::JobDependencyInternal>& RCJob::GetJobDependencies()
  240. {
  241. return m_jobDetails.m_jobDependencyList;
  242. }
  243. bool RCJob::UpdateMissingDependencies(const SourceAssetReference& sourceRef)
  244. {
  245. bool anyChanged = false;
  246. bool stillHasMissingDep = false;
  247. for (JobDependencyInternal& jobDependencyInternal : m_jobDetails.m_jobDependencyList)
  248. {
  249. if (jobDependencyInternal.m_isMissingSource)
  250. {
  251. SourceAssetReference thisDep(jobDependencyInternal.m_jobDependency.m_sourceFile.m_sourceFileDependencyPath.c_str());
  252. if (thisDep == sourceRef)
  253. {
  254. // the missing dep is no longer missing.
  255. jobDependencyInternal.m_isMissingSource = false;
  256. anyChanged = true;
  257. }
  258. else
  259. {
  260. stillHasMissingDep = true;
  261. }
  262. }
  263. }
  264. return !stillHasMissingDep && anyChanged;
  265. }
  266. void RCJob::Start()
  267. {
  268. // the following trace can be uncommented if there is a need to deeply inspect job running.
  269. //AZ_TracePrintf(AssetProcessor::DebugChannel, "JobTrace Start(%i %s,%s,%s)\n", this, GetInputFileAbsolutePath().toUtf8().data(), GetPlatform().toUtf8().data(), GetJobKey().toUtf8().data());
  270. AssetUtilities::QuitListener listener;
  271. listener.BusConnect();
  272. RCParams rc(this);
  273. BuilderParams builderParams(this);
  274. //Create the process job request
  275. AssetBuilderSDK::ProcessJobRequest processJobRequest;
  276. PopulateProcessJobRequest(processJobRequest);
  277. builderParams.m_processJobRequest = processJobRequest;
  278. builderParams.m_cacheOutputDir = GetCacheOutputPath();
  279. builderParams.m_intermediateOutputDir = GetIntermediateOutputPath();
  280. builderParams.m_relativePath = GetRelativePath();
  281. builderParams.m_assetBuilderDesc = m_jobDetails.m_assetBuilderDesc;
  282. builderParams.m_sourceUuid = m_jobDetails.m_sourceUuid;
  283. // when the job finishes, record the results and emit Finished()
  284. connect(this, &RCJob::JobFinished, this, [this](AssetBuilderSDK::ProcessJobResponse result)
  285. {
  286. m_processJobResponse = AZStd::move(result);
  287. switch (m_processJobResponse.m_resultCode)
  288. {
  289. case AssetBuilderSDK::ProcessJobResult_Crashed:
  290. {
  291. SetState(crashed);
  292. }
  293. break;
  294. case AssetBuilderSDK::ProcessJobResult_Success:
  295. {
  296. SetState(completed);
  297. }
  298. break;
  299. case AssetBuilderSDK::ProcessJobResult_Cancelled:
  300. {
  301. SetState(cancelled);
  302. }
  303. break;
  304. default:
  305. {
  306. SetState(failed);
  307. }
  308. break;
  309. }
  310. Q_EMIT Finished();
  311. });
  312. if (!listener.WasQuitRequested())
  313. {
  314. QtConcurrent::run(&RCJob::ExecuteBuilderCommand, builderParams);
  315. }
  316. else
  317. {
  318. AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Job canceled due to quit being requested.");
  319. SetState(terminated);
  320. Q_EMIT Finished();
  321. }
  322. listener.BusDisconnect();
  323. }
  324. void RCJob::ExecuteBuilderCommand(BuilderParams builderParams)
  325. {
  326. // Note: this occurs inside a worker thread.
  327. // Signal start and end of the job
  328. ScopedJobSignaler signaler;
  329. // listen for the user quitting (CTRL-C or otherwise)
  330. AssetUtilities::QuitListener listener;
  331. listener.BusConnect();
  332. QElapsedTimer ticker;
  333. ticker.start();
  334. AssetBuilderSDK::ProcessJobResponse result;
  335. AssetBuilderSDK::JobCancelListener cancelListener(builderParams.m_processJobRequest.m_jobId);
  336. if (builderParams.m_rcJob->m_jobDetails.m_autoFail)
  337. {
  338. // if this is an auto-fail job, we should avoid doing any additional work besides the work required to fail the job and
  339. // write the details into its log. This is because Auto-fail jobs have 'incomplete' job descriptors, and only exist to
  340. // force a job to fail with a reasonable log file stating the reason for failure. An example of where it is useful to
  341. // use auto-fail jobs is when, after compilation was successful, something goes wrong integrating the result into the
  342. // cache. (For example, files collide, or the product file name would be too long). The job will have at that point
  343. // already completed, the thread long gone, so we can 'append' to the log in this manner post-build by creating a new
  344. // job that will automatically fail and ingest the old (success) log along with additional fail reasons and then fail.
  345. AutoFailJob(builderParams);
  346. result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
  347. Q_EMIT builderParams.m_rcJob->JobFinished(result);
  348. return;
  349. }
  350. // If requested, make sure we can open the file with exclusive permissions
  351. QString inputFile = builderParams.m_rcJob->GetJobEntry().GetAbsoluteSourcePath();
  352. if (builderParams.m_rcJob->GetJobEntry().m_checkExclusiveLock && QFile::exists(inputFile))
  353. {
  354. // We will only continue once we get exclusive lock on the source file
  355. while (!AssetUtilities::CheckCanLock(inputFile))
  356. {
  357. // Wait for a while before checking again, we need to let some time pass for the other process to finish whatever work it is doing
  358. QThread::msleep(g_sleepDurationForLockingAndFingerprintChecking);
  359. // If AP shutdown is requested, the job is canceled or we exceeded the max wait time, abort the loop and mark the job as canceled
  360. if (listener.WasQuitRequested() || cancelListener.IsCancelled() || (ticker.elapsed() > g_jobMaximumWaitTime))
  361. {
  362. result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
  363. Q_EMIT builderParams.m_rcJob->JobFinished(result);
  364. return;
  365. }
  366. }
  367. }
  368. Q_EMIT builderParams.m_rcJob->BeginWork();
  369. // 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
  370. builderParams.m_rcJob->DoWork(result, builderParams, listener);
  371. Q_EMIT builderParams.m_rcJob->JobFinished(result);
  372. }
  373. void RCJob::AutoFailJob(BuilderParams& builderParams)
  374. {
  375. // force the fail data to be captured to the log file.
  376. // because this is being executed in a thread worker, this won't stomp the main thread's job id.
  377. AssetProcessor::SetThreadLocalJobId(builderParams.m_rcJob->GetJobEntry().m_jobRunKey);
  378. AssetUtilities::JobLogTraceListener jobLogTraceListener(builderParams.m_rcJob->m_jobDetails.m_jobEntry);
  379. #if defined(AZ_ENABLE_TRACING)
  380. QString sourceFullPath(builderParams.m_processJobRequest.m_fullPath.c_str());
  381. auto failReason = builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.find(AZ_CRC_CE(AssetProcessor::AutoFailReasonKey));
  382. if (failReason != builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.end())
  383. {
  384. // you are allowed to have many lines in your fail reason.
  385. AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Failed processing %s", sourceFullPath.toUtf8().data());
  386. AZStd::vector<AZStd::string> delimited;
  387. AzFramework::StringFunc::Tokenize(failReason->second.c_str(), delimited, "\n");
  388. for (const AZStd::string& token : delimited)
  389. {
  390. AZ_Error(AssetBuilderSDK::ErrorWindow, false, "%s", token.c_str());
  391. }
  392. }
  393. else
  394. {
  395. // since we didn't have a custom auto-fail reason, add a token to the log file that will help with
  396. // forensic debugging to differentiate auto-fails from regular fails (although it should also be
  397. // obvious from the output in other ways)
  398. AZ_TracePrintf("Debug", "(auto-failed)\n");
  399. }
  400. auto failLogFile = builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.find(AZ_CRC_CE(AssetProcessor::AutoFailLogFile));
  401. if (failLogFile != builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.end())
  402. {
  403. AzToolsFramework::Logging::LogLine::ParseLog(failLogFile->second.c_str(), failLogFile->second.size(),
  404. [](AzToolsFramework::Logging::LogLine& target)
  405. {
  406. switch (target.GetLogType())
  407. {
  408. case AzToolsFramework::Logging::LogLine::TYPE_DEBUG:
  409. AZ_TracePrintf(target.GetLogWindow().c_str(), "%s", target.GetLogMessage().c_str());
  410. break;
  411. case AzToolsFramework::Logging::LogLine::TYPE_MESSAGE:
  412. AZ_TracePrintf(target.GetLogWindow().c_str(), "%s", target.GetLogMessage().c_str());
  413. break;
  414. case AzToolsFramework::Logging::LogLine::TYPE_WARNING:
  415. AZ_Warning(target.GetLogWindow().c_str(), false, "%s", target.GetLogMessage().c_str());
  416. break;
  417. case AzToolsFramework::Logging::LogLine::TYPE_ERROR:
  418. AZ_Error(target.GetLogWindow().c_str(), false, "%s", target.GetLogMessage().c_str());
  419. break;
  420. case AzToolsFramework::Logging::LogLine::TYPE_CONTEXT:
  421. AZ_TracePrintf(target.GetLogWindow().c_str(), " %s", target.GetLogMessage().c_str());
  422. break;
  423. }
  424. });
  425. }
  426. #endif
  427. // note that this line below is printed out to be consistent with the output from a job that normally failed, so
  428. // applications reading log file will find it.
  429. AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Builder indicated that the job has failed.\n");
  430. if (builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.find(AZ_CRC_CE(AssetProcessor::AutoFailOmitFromDatabaseKey)) != builderParams.m_processJobRequest.m_jobDescription.m_jobParameters.end())
  431. {
  432. // we don't add Auto-fail jobs to the database if they have asked to be emitted.
  433. builderParams.m_rcJob->m_jobDetails.m_jobEntry.m_addToDatabase = false;
  434. }
  435. AssetProcessor::SetThreadLocalJobId(0);
  436. }
  437. void RCJob::DoWork(AssetBuilderSDK::ProcessJobResponse& result, BuilderParams& builderParams, AssetUtilities::QuitListener& listener)
  438. {
  439. // Setting job id for logging purposes
  440. AssetProcessor::SetThreadLocalJobId(builderParams.m_rcJob->GetJobEntry().m_jobRunKey);
  441. AssetUtilities::JobLogTraceListener jobLogTraceListener(builderParams.m_rcJob->m_jobDetails.m_jobEntry);
  442. {
  443. AssetBuilderSDK::JobCancelListener JobCancelListener(builderParams.m_rcJob->m_jobDetails.m_jobEntry.m_jobRunKey);
  444. result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed; // failed by default
  445. #if defined(AZ_ENABLE_TRACING)
  446. for (const auto& warningMessage : builderParams.m_rcJob->m_jobDetails.m_warnings)
  447. {
  448. // you are allowed to have many lines in your warning message.
  449. AZStd::vector<AZStd::string> delimited;
  450. AzFramework::StringFunc::Tokenize(warningMessage.c_str(), delimited, "\n");
  451. for (const AZStd::string& token : delimited)
  452. {
  453. AZ_Warning(AssetBuilderSDK::WarningWindow, false, "%s", token.c_str());
  454. }
  455. jobLogTraceListener.AddWarning();
  456. }
  457. #endif
  458. // create a temporary directory for Builder to work in.
  459. // lets make it as a subdir of a known temp dir
  460. QString workFolder;
  461. if (!AssetUtilities::CreateTempWorkspace(workFolder))
  462. {
  463. AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Could not create temporary directory for Builder!\n");
  464. result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
  465. Q_EMIT builderParams.m_rcJob->JobFinished(result);
  466. return;
  467. }
  468. builderParams.m_processJobRequest.m_tempDirPath = AZStd::string(workFolder.toUtf8().data());
  469. QString sourceFullPath(builderParams.m_processJobRequest.m_fullPath.c_str());
  470. if (sourceFullPath.length() >= ASSETPROCESSOR_WARN_PATH_LEN && sourceFullPath.length() < ASSETPROCESSOR_TRAIT_MAX_PATH_LEN)
  471. {
  472. AZ_Warning(
  473. AssetBuilderSDK::WarningWindow,
  474. false,
  475. "Source Asset: %s filepath length %d exceeds the suggested max path length (%d). This may not work on all platforms.\n",
  476. sourceFullPath.toUtf8().data(),
  477. sourceFullPath.length(),
  478. ASSETPROCESSOR_WARN_PATH_LEN);
  479. }
  480. if (sourceFullPath.length() >= ASSETPROCESSOR_TRAIT_MAX_PATH_LEN)
  481. {
  482. AZ_Warning(
  483. AssetBuilderSDK::WarningWindow,
  484. false,
  485. "Source Asset: %s filepath length %d exceeds the maximum path length (%d) allowed.\n",
  486. sourceFullPath.toUtf8().data(),
  487. sourceFullPath.length(),
  488. ASSETPROCESSOR_TRAIT_MAX_PATH_LEN);
  489. result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
  490. }
  491. else
  492. {
  493. if (!JobCancelListener.IsCancelled())
  494. {
  495. bool runProcessJob = true;
  496. if (m_jobDetails.m_checkServer)
  497. {
  498. AssetServerMode assetServerMode = AssetServerMode::Inactive;
  499. AssetServerBus::BroadcastResult(assetServerMode, &AssetServerBus::Events::GetRemoteCachingMode);
  500. QFileInfo fileInfo(builderParams.m_processJobRequest.m_sourceFile.c_str());
  501. builderParams.m_serverKey = QString("%1_%2_%3_%4")
  502. .arg(fileInfo.completeBaseName(),
  503. builderParams.m_processJobRequest.m_jobDescription.m_jobKey.c_str(),
  504. builderParams.m_processJobRequest.m_platformInfo.m_identifier.c_str())
  505. .arg(builderParams.m_rcJob->GetOriginalFingerprint());
  506. bool operationResult = false;
  507. if (assetServerMode == AssetServerMode::Server)
  508. {
  509. // sending process job command to the builder
  510. builderParams.m_assetBuilderDesc.m_processJobFunction(builderParams.m_processJobRequest, result);
  511. runProcessJob = false;
  512. if (result.m_resultCode == AssetBuilderSDK::ProcessJobResult_Success)
  513. {
  514. auto beforeStoreResult = BeforeStoringJobResult(builderParams, result);
  515. if (beforeStoreResult.IsSuccess())
  516. {
  517. AssetProcessor::AssetServerBus::BroadcastResult(operationResult, &AssetProcessor::AssetServerBusTraits::StoreJobResult, builderParams, beforeStoreResult.GetValue());
  518. }
  519. else
  520. {
  521. AZ_Warning(AssetBuilderSDK::WarningWindow, false, "Failed preparing store result for %s", builderParams.m_processJobRequest.m_sourceFile.c_str());
  522. }
  523. if (!operationResult)
  524. {
  525. AZ_TracePrintf(AssetProcessor::DebugChannel, "Unable to save job (%s, %s, %s) with fingerprint (%u) to the server.\n",
  526. builderParams.m_rcJob->GetJobEntry().m_sourceAssetReference.AbsolutePath().c_str(), builderParams.m_rcJob->GetJobKey().toUtf8().data(),
  527. builderParams.m_rcJob->GetPlatformInfo().m_identifier.c_str(), builderParams.m_rcJob->GetOriginalFingerprint());
  528. }
  529. else
  530. {
  531. for (auto& product : result.m_outputProducts)
  532. {
  533. product.m_outputFlags |= AssetBuilderSDK::ProductOutputFlags::CachedAsset;
  534. }
  535. }
  536. }
  537. }
  538. else if (assetServerMode == AssetServerMode::Client)
  539. {
  540. // running as client, check with the server whether it has already
  541. // processed this asset, if not or if the operation fails then process locally
  542. AssetProcessor::AssetServerBus::BroadcastResult(operationResult, &AssetProcessor::AssetServerBusTraits::RetrieveJobResult, builderParams);
  543. if (operationResult)
  544. {
  545. operationResult = AfterRetrievingJobResult(builderParams, jobLogTraceListener, result);
  546. }
  547. else
  548. {
  549. AZ_TracePrintf(AssetProcessor::DebugChannel, "Unable to get job (%s, %s, %s) with fingerprint (%u) from the server. Processing locally.\n",
  550. builderParams.m_rcJob->GetJobEntry().m_sourceAssetReference.AbsolutePath().c_str(), builderParams.m_rcJob->GetJobKey().toUtf8().data(),
  551. builderParams.m_rcJob->GetPlatformInfo().m_identifier.c_str(), builderParams.m_rcJob->GetOriginalFingerprint());
  552. }
  553. if (operationResult)
  554. {
  555. for (auto& product : result.m_outputProducts)
  556. {
  557. product.m_outputFlags |= AssetBuilderSDK::ProductOutputFlags::CachedAsset;
  558. }
  559. }
  560. runProcessJob = !operationResult;
  561. }
  562. }
  563. if(runProcessJob)
  564. {
  565. result.m_outputProducts.clear();
  566. // sending process job command to the builder
  567. builderParams.m_assetBuilderDesc.m_processJobFunction(builderParams.m_processJobRequest, result);
  568. }
  569. }
  570. }
  571. if (JobCancelListener.IsCancelled())
  572. {
  573. result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
  574. }
  575. }
  576. bool shouldRemoveTempFolder = true;
  577. if (result.m_resultCode == AssetBuilderSDK::ProcessJobResult_Success)
  578. {
  579. // do a final check of this job to make sure its not making colliding subIds.
  580. AZStd::unordered_map<AZ::u32, AZStd::string> subIdsFound;
  581. for (const AssetBuilderSDK::JobProduct& product : result.m_outputProducts)
  582. {
  583. if (!subIdsFound.insert({ product.m_productSubID, product.m_productFileName }).second)
  584. {
  585. // if this happens the element was already in the set.
  586. AZ_Error(AssetBuilderSDK::ErrorWindow, false,
  587. "The builder created more than one asset with the same subID (%u) when emitting product %.*s, colliding with %.*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.",
  588. product.m_productSubID,
  589. AZ_STRING_ARG(product.m_productFileName),
  590. AZ_STRING_ARG(subIdsFound[product.m_productSubID]));
  591. result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
  592. break;
  593. }
  594. }
  595. }
  596. if(result.m_resultCode == AssetBuilderSDK::ProcessJobResult_Success)
  597. {
  598. bool handledDependencies = true; // True in case there are no outputs
  599. for (const AssetBuilderSDK::JobProduct& jobProduct : result.m_outputProducts)
  600. {
  601. handledDependencies = false; // False by default since there are outputs
  602. if(jobProduct.m_dependenciesHandled)
  603. {
  604. handledDependencies = true;
  605. break;
  606. }
  607. }
  608. if(!handledDependencies)
  609. {
  610. 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());
  611. 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.");
  612. 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.");
  613. jobLogTraceListener.AddWarning();
  614. }
  615. WarningLevel warningLevel = WarningLevel::Default;
  616. JobDiagnosticRequestBus::BroadcastResult(warningLevel, &JobDiagnosticRequestBus::Events::GetWarningLevel);
  617. const bool hasErrors = jobLogTraceListener.GetErrorCount() > 0;
  618. const bool hasWarnings = jobLogTraceListener.GetWarningCount() > 0;
  619. if(warningLevel == WarningLevel::FatalErrors && hasErrors)
  620. {
  621. AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Failing job, fatal errors setting is enabled");
  622. result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
  623. }
  624. else if(warningLevel == WarningLevel::FatalErrorsAndWarnings && (hasErrors || hasWarnings))
  625. {
  626. AZ_Error(AssetBuilderSDK::ErrorWindow, false, "Failing job, fatal errors and warnings setting is enabled");
  627. result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
  628. }
  629. }
  630. switch (result.m_resultCode)
  631. {
  632. case AssetBuilderSDK::ProcessJobResult_Success:
  633. // make sure there's no subid collision inside a job.
  634. {
  635. if (!CopyCompiledAssets(builderParams, result))
  636. {
  637. result.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
  638. shouldRemoveTempFolder = false;
  639. }
  640. shouldRemoveTempFolder = shouldRemoveTempFolder && !result.m_keepTempFolder && !s_createRequestFileForSuccessfulJob;
  641. }
  642. break;
  643. case AssetBuilderSDK::ProcessJobResult_Crashed:
  644. AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Builder indicated that its process crashed!");
  645. break;
  646. case AssetBuilderSDK::ProcessJobResult_Cancelled:
  647. AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Builder indicates that the job was cancelled.");
  648. break;
  649. case AssetBuilderSDK::ProcessJobResult_Failed:
  650. AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Builder indicated that the job has failed.");
  651. shouldRemoveTempFolder = false;
  652. break;
  653. }
  654. if ((shouldRemoveTempFolder) || (listener.WasQuitRequested()))
  655. {
  656. QDir workingDir(QString(builderParams.m_processJobRequest.m_tempDirPath.c_str()));
  657. workingDir.removeRecursively();
  658. }
  659. // Setting the job id back to zero for error detection
  660. AssetProcessor::SetThreadLocalJobId(0);
  661. listener.BusDisconnect();
  662. JobDiagnosticRequestBus::Broadcast(&JobDiagnosticRequestBus::Events::RecordDiagnosticInfo, builderParams.m_rcJob->GetJobEntry().m_jobRunKey, JobDiagnosticInfo(aznumeric_cast<AZ::u32>(jobLogTraceListener.GetWarningCount()), aznumeric_cast<AZ::u32>(jobLogTraceListener.GetErrorCount())));
  663. }
  664. bool RCJob::CopyCompiledAssets(BuilderParams& params, AssetBuilderSDK::ProcessJobResponse& response)
  665. {
  666. if (response.m_outputProducts.empty())
  667. {
  668. // early out here for performance - no need to do anything at all here so don't waste time with IsDir or Exists or anything.
  669. return true;
  670. }
  671. AZ::IO::Path cacheDirectory = params.m_cacheOutputDir;
  672. AZ::IO::Path intermediateDirectory = params.m_intermediateOutputDir;
  673. AZ::IO::Path relativeFilePath = params.m_relativePath;
  674. QString tempFolder = params.m_processJobRequest.m_tempDirPath.c_str();
  675. QDir tempDir(tempFolder);
  676. if (params.m_cacheOutputDir.empty() || params.m_intermediateOutputDir.empty())
  677. {
  678. AZ_Assert(false, "CopyCompiledAssets: params.m_finalOutputDir or m_intermediateOutputDir 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");
  679. return false;
  680. }
  681. if (!tempDir.exists())
  682. {
  683. AZ_Assert(false, "CopyCompiledAssets: 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");
  684. return false;
  685. }
  686. // copy the built products into the appropriate location in the real cache and update the job status accordingly.
  687. // note that we go to the trouble of first doing all the checking for disk space and existence of the source files
  688. // before we notify the AP or start moving any of the files so that failures cause the least amount of damage possible.
  689. // this vector is a set of pairs where the first of each pair is the source file (absolute) we intend to copy
  690. // and the second is the product destination we intend to copy it to.
  691. QList< QPair<QString, QString> > outputsToCopy;
  692. outputsToCopy.reserve(static_cast<int>(response.m_outputProducts.size()));
  693. QList<QPair<QString, AZ::Uuid>> intermediateOutputPaths;
  694. qint64 fileSizeRequired = 0;
  695. bool needCacheDirectory = false;
  696. bool needIntermediateDirectory = false;
  697. for (AssetBuilderSDK::JobProduct& product : response.m_outputProducts)
  698. {
  699. // each Output Product communicated by the builder will either be
  700. // * a relative path, which means we assume its relative to the temp folder, and we attempt to move the file
  701. // * an absolute path in the temp folder, and we attempt to move also
  702. // * an absolute path outside the temp folder, in which we assume you'd like to just copy a file somewhere.
  703. QString outputProduct = QString::fromUtf8(product.m_productFileName.c_str()); // could be a relative path.
  704. QFileInfo fileInfo(outputProduct);
  705. if (fileInfo.isRelative())
  706. {
  707. // we assume that its relative to the TEMP folder.
  708. fileInfo = QFileInfo(tempDir.absoluteFilePath(outputProduct));
  709. }
  710. QString absolutePathOfSource = fileInfo.absoluteFilePath();
  711. QString outputFilename = fileInfo.fileName();
  712. bool outputToCache = (product.m_outputFlags & AssetBuilderSDK::ProductOutputFlags::ProductAsset) == AssetBuilderSDK::ProductOutputFlags::ProductAsset;
  713. bool outputToIntermediate = (product.m_outputFlags & AssetBuilderSDK::ProductOutputFlags::IntermediateAsset) ==
  714. AssetBuilderSDK::ProductOutputFlags::IntermediateAsset;
  715. if (outputToCache && outputToIntermediate)
  716. {
  717. // We currently do not support both since intermediate outputs require the Common platform, which is not supported for cache outputs yet
  718. AZ_Error(AssetProcessor::ConsoleChannel, false, "Outputting an asset as both a product and intermediate is not supported. To output both, please split the job into two separate ones.");
  719. return false;
  720. }
  721. if (!outputToCache && !outputToIntermediate)
  722. {
  723. AZ_Error(AssetProcessor::ConsoleChannel, false, "An output asset must be flagged as either a product or an intermediate asset. "
  724. "Please update the output job to include either AssetBuilderSDK::ProductOutputFlags::ProductAsset "
  725. "or AssetBuilderSDK::ProductOutputFlags::IntermediateAsset");
  726. return false;
  727. }
  728. // Intermediates are required to output for the common platform only
  729. if (outputToIntermediate && params.m_processJobRequest.m_platformInfo.m_identifier != AssetBuilderSDK::CommonPlatformName)
  730. {
  731. AZ_Error(AssetProcessor::ConsoleChannel, false, "Intermediate outputs are only supported for the %s platform. "
  732. "Either change the Job platform to %s or change the output flag to AssetBuilderSDK::ProductOutputFlags::ProductAsset",
  733. AssetBuilderSDK::CommonPlatformName,
  734. AssetBuilderSDK::CommonPlatformName);
  735. return false;
  736. }
  737. // Common platform is not currently supported for product assets
  738. if (outputToCache && params.m_processJobRequest.m_platformInfo.m_identifier == AssetBuilderSDK::CommonPlatformName)
  739. {
  740. AZ_Error(
  741. AssetProcessor::ConsoleChannel, false,
  742. "Product asset outputs are not currently supported for the %s platform. "
  743. "Either change the Job platform to a normal platform or change the output flag to AssetBuilderSDK::ProductOutputFlags::IntermediateAsset",
  744. AssetBuilderSDK::CommonPlatformName);
  745. return false;
  746. }
  747. const bool isSourceMetadataEnabled = !params.m_sourceUuid.IsNull();
  748. if (isSourceMetadataEnabled)
  749. {
  750. // For metadata enabled files, the output file needs to be prefixed to handle multiple files with the same relative path.
  751. // This phase will just use a temporary prefix which is longer and less likely to result in accidental conflicts.
  752. // During AssetProcessed_Impl in APM, the prefixing will be resolved to figure out which file is highest priority and gets renamed
  753. // back to the non-prefixed, backwards compatible format and every other file with the same rel path will be re-prefixed to a finalized form.
  754. ProductOutputUtil::GetInterimProductPath(outputFilename, params.m_rcJob->GetJobEntry().m_sourceAssetReference.ScanFolderId());
  755. }
  756. if(outputToCache)
  757. {
  758. needCacheDirectory = true;
  759. if(!product.m_outputPathOverride.empty())
  760. {
  761. AZ_Error(AssetProcessor::ConsoleChannel, false, "%s specified m_outputPathOverride on a ProductAsset. This is not supported."
  762. " Please update the builder accordingly.", params.m_processJobRequest.m_sourceFile.c_str());
  763. return false;
  764. }
  765. if (!VerifyOutputProduct(
  766. QDir(cacheDirectory.c_str()), outputFilename, absolutePathOfSource, fileSizeRequired,
  767. outputsToCopy))
  768. {
  769. return false;
  770. }
  771. }
  772. if(outputToIntermediate)
  773. {
  774. needIntermediateDirectory = true;
  775. if(!product.m_outputPathOverride.empty())
  776. {
  777. relativeFilePath = product.m_outputPathOverride;
  778. }
  779. if (VerifyOutputProduct(
  780. QDir(intermediateDirectory.c_str()), outputFilename, absolutePathOfSource, fileSizeRequired, outputsToCopy))
  781. {
  782. // A null uuid indicates the source is not using metadata files.
  783. // The assumption for the UUID generated below is that the source UUID will not change. A type which has no metadata
  784. // file currently may be updated later to have a metadata file, which would break that assumption. In that case, stick
  785. // with the default path-based UUID.
  786. if (isSourceMetadataEnabled)
  787. {
  788. // Generate a UUID for the intermediate as:
  789. // SourceUuid:BuilderUuid:SubId
  790. auto uuid = AZ::Uuid::CreateName(AZStd::string::format(
  791. "%s:%s:%d",
  792. params.m_sourceUuid.ToFixedString().c_str(),
  793. params.m_assetBuilderDesc.m_busId.ToFixedString().c_str(),
  794. product.m_productSubID));
  795. // Add the product absolute path to the list of intermediates
  796. intermediateOutputPaths.append(QPair(outputsToCopy.back().second, uuid));
  797. }
  798. }
  799. else
  800. {
  801. return false;
  802. }
  803. }
  804. // update the productFileName to be the scanfolder relative path (without the platform)
  805. product.m_productFileName = (relativeFilePath / outputFilename.toUtf8().constData()).c_str();
  806. }
  807. // now we can check if there's enough space for ALL the files before we copy any.
  808. bool hasSpace = false;
  809. auto* diskSpaceInfoInterface = AZ::Interface<AssetProcessor::IDiskSpaceInfo>::Get();
  810. if (diskSpaceInfoInterface)
  811. {
  812. hasSpace = diskSpaceInfoInterface->CheckSufficientDiskSpace(fileSizeRequired, false);
  813. }
  814. if (!hasSpace)
  815. {
  816. AZ_Error(
  817. AssetProcessor::ConsoleChannel, false,
  818. "Cannot save file(s) to cache, not enough disk space to save all the products of %s. Total needed: %lli bytes",
  819. params.m_processJobRequest.m_sourceFile.c_str(), fileSizeRequired);
  820. return false;
  821. }
  822. // 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.
  823. // if outputDirectory does not exist then create it
  824. unsigned int waitTimeInSecs = 3;
  825. if (needCacheDirectory && !AssetUtilities::CreateDirectoryWithTimeout(QDir(cacheDirectory.AsPosix().c_str()), waitTimeInSecs))
  826. {
  827. AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Failed to create output directory: %s\n", cacheDirectory.c_str());
  828. return false;
  829. }
  830. if (needIntermediateDirectory && !AssetUtilities::CreateDirectoryWithTimeout(QDir(intermediateDirectory.AsPosix().c_str()), waitTimeInSecs))
  831. {
  832. AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Failed to create intermediate directory: %s\n", intermediateDirectory.c_str());
  833. return false;
  834. }
  835. auto* uuidInterface = AZ::Interface<AzToolsFramework::IUuidUtil>::Get();
  836. if (!uuidInterface)
  837. {
  838. AZ_Assert(false, "Programmer Error - IUuidUtil interface is not available");
  839. return false;
  840. }
  841. // Go through all the intermediate products and output the assigned UUID
  842. for (auto [intermediateProduct, uuid] : intermediateOutputPaths)
  843. {
  844. if(!uuidInterface->CreateSourceUuid(intermediateProduct.toUtf8().constData(), uuid))
  845. {
  846. AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Failed to create metadata file for intermediate product " AZ_STRING_FORMAT, AZ_STRING_ARG(intermediateProduct));
  847. }
  848. }
  849. bool anyFileFailed = false;
  850. for (const QPair<QString, QString>& filePair : outputsToCopy)
  851. {
  852. const QString& sourceAbsolutePath = filePair.first;
  853. const QString& productAbsolutePath = filePair.second;
  854. bool isCopyJob = !(sourceAbsolutePath.startsWith(tempFolder, Qt::CaseInsensitive));
  855. isCopyJob |= response.m_keepTempFolder; // Copy instead of Move if the builder wants to keep the Temp Folder.
  856. if (!MoveCopyFile(sourceAbsolutePath, productAbsolutePath, isCopyJob)) // this has its own traceprintf for failure
  857. {
  858. // MoveCopyFile will have output to the log. No need to double output here.
  859. anyFileFailed = true;
  860. continue;
  861. }
  862. //we now ensure that the file is writable - this is just a warning if it fails, not a complete failure.
  863. if (!AssetUtilities::MakeFileWritable(productAbsolutePath))
  864. {
  865. AZ_TracePrintf(AssetBuilderSDK::WarningWindow, "Unable to change permission for the file: %s.\n", productAbsolutePath.toUtf8().data());
  866. }
  867. }
  868. return !anyFileFailed;
  869. }
  870. bool RCJob::VerifyOutputProduct(
  871. QDir outputDirectory,
  872. QString outputFilename,
  873. QString absolutePathOfSource,
  874. qint64& totalFileSizeRequired,
  875. QList<QPair<QString, QString>>& outputsToCopy)
  876. {
  877. QString productFile = AssetUtilities::NormalizeFilePath(outputDirectory.filePath(outputFilename.toLower()));
  878. // Don't make productFile all lowercase for case-insensitive as this
  879. // breaks macOS. The case is already setup properly when the job
  880. // was created.
  881. if (productFile.length() >= ASSETPROCESSOR_WARN_PATH_LEN && productFile.length() < ASSETPROCESSOR_TRAIT_MAX_PATH_LEN)
  882. {
  883. AZ_Warning(
  884. AssetBuilderSDK::WarningWindow,
  885. false,
  886. "Product '%s' path length (%d) exceeds the suggested max path length (%d). This may not work on all platforms.\n",
  887. productFile.toUtf8().data(),
  888. productFile.length(),
  889. ASSETPROCESSOR_WARN_PATH_LEN);
  890. }
  891. if (productFile.length() >= ASSETPROCESSOR_TRAIT_MAX_PATH_LEN)
  892. {
  893. AZ_Error(
  894. AssetBuilderSDK::ErrorWindow,
  895. false,
  896. "Cannot copy file: Product '%s' path length (%d) exceeds the max path length (%d) allowed on disk\n",
  897. productFile.toUtf8().data(),
  898. productFile.length(),
  899. ASSETPROCESSOR_TRAIT_MAX_PATH_LEN);
  900. return false;
  901. }
  902. QFileInfo inFile(absolutePathOfSource);
  903. if (!inFile.exists())
  904. {
  905. AZ_Error(
  906. AssetBuilderSDK::ErrorWindow, false,
  907. "Cannot copy file - product file with absolute path '%s' attempting to save into cache could not be found",
  908. absolutePathOfSource.toUtf8().constData());
  909. return false;
  910. }
  911. totalFileSizeRequired += inFile.size();
  912. outputsToCopy.push_back(qMakePair(absolutePathOfSource, productFile));
  913. return true;
  914. }
  915. AZ::Outcome<AZStd::vector<AZStd::string>> RCJob::BeforeStoringJobResult(const BuilderParams& builderParams, AssetBuilderSDK::ProcessJobResponse jobResponse)
  916. {
  917. AZStd::string normalizedTempFolderPath = builderParams.m_processJobRequest.m_tempDirPath;
  918. AzFramework::StringFunc::Path::Normalize(normalizedTempFolderPath);
  919. AZStd::vector<AZStd::string> sourceFiles;
  920. for (AssetBuilderSDK::JobProduct& product : jobResponse.m_outputProducts)
  921. {
  922. // Try to handle Absolute paths within the temp folder
  923. AzFramework::StringFunc::Path::Normalize(product.m_productFileName);
  924. if (!AzFramework::StringFunc::Replace(product.m_productFileName, normalizedTempFolderPath.c_str(), s_tempString))
  925. {
  926. // From CopyCompiledAssets:
  927. // each Output Product communicated by the builder will either be
  928. // * a relative path, which means we assume its relative to the temp folder, and we attempt to move the file
  929. // * an absolute path in the temp folder, and we attempt to move also
  930. // * an absolute path outside the temp folder, in which we assume you'd like to just copy a file somewhere.
  931. // We need to handle case 3 here (Case 2 was above, case 1 is treated as relative within temp)
  932. // 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)
  933. // meaning a copy job which needs to be added to our archive.
  934. if (!AzFramework::StringFunc::Path::IsRelative(product.m_productFileName.c_str()))
  935. {
  936. AZStd::string sourceFile{ builderParams.m_rcJob->GetJobEntry().GetAbsoluteSourcePath().toUtf8().data() };
  937. AzFramework::StringFunc::Path::Normalize(sourceFile);
  938. AzFramework::StringFunc::Path::StripFullName(sourceFile);
  939. size_t sourcePathPos = product.m_productFileName.find(sourceFile.c_str());
  940. if(sourcePathPos != AZStd::string::npos)
  941. {
  942. sourceFiles.push_back(product.m_productFileName.substr(sourceFile.size()).c_str());
  943. AzFramework::StringFunc::Path::Join(s_tempString, product.m_productFileName.substr(sourceFile.size()).c_str(), product.m_productFileName);
  944. }
  945. else
  946. {
  947. AZ_Warning(AssetBuilderSDK::WarningWindow, false,
  948. "Failed to find source path %s or temp path %s in non relative path in %s",
  949. sourceFile.c_str(), normalizedTempFolderPath.c_str(), product.m_productFileName.c_str());
  950. }
  951. }
  952. }
  953. }
  954. AZStd::string responseFilePath;
  955. AzFramework::StringFunc::Path::ConstructFull(builderParams.m_processJobRequest.m_tempDirPath.c_str(), AssetBuilderSDK::s_processJobResponseFileName, responseFilePath, true);
  956. //Save ProcessJobResponse to disk
  957. if (!AZ::Utils::SaveObjectToFile(responseFilePath, AZ::DataStream::StreamType::ST_XML, &jobResponse))
  958. {
  959. return AZ::Failure();
  960. }
  961. AzToolsFramework::AssetSystem::JobInfo jobInfo;
  962. AzToolsFramework::AssetSystem::AssetJobLogResponse jobLogResponse;
  963. jobInfo.m_sourceFile = builderParams.m_rcJob->GetJobEntry().m_sourceAssetReference.RelativePath().c_str();
  964. jobInfo.m_platform = builderParams.m_rcJob->GetPlatformInfo().m_identifier.c_str();
  965. jobInfo.m_jobKey = builderParams.m_rcJob->GetJobKey().toUtf8().data();
  966. jobInfo.m_builderGuid = builderParams.m_rcJob->GetBuilderGuid();
  967. jobInfo.m_jobRunKey = builderParams.m_rcJob->GetJobEntry().m_jobRunKey;
  968. jobInfo.m_watchFolder = builderParams.m_processJobRequest.m_watchFolder;
  969. AssetUtilities::ReadJobLog(jobInfo, jobLogResponse);
  970. //Save joblog to disk
  971. AZStd::string jobLogFilePath;
  972. AzFramework::StringFunc::Path::ConstructFull(builderParams.m_processJobRequest.m_tempDirPath.c_str(), s_jobLogFileName, jobLogFilePath, true);
  973. if (!AZ::Utils::SaveObjectToFile(jobLogFilePath, AZ::DataStream::StreamType::ST_XML, &jobLogResponse))
  974. {
  975. return AZ::Failure();
  976. }
  977. return AZ::Success(sourceFiles);
  978. }
  979. bool RCJob::AfterRetrievingJobResult(const BuilderParams& builderParams, AssetUtilities::JobLogTraceListener& jobLogTraceListener, AssetBuilderSDK::ProcessJobResponse& jobResponse)
  980. {
  981. AZStd::string responseFilePath;
  982. AzFramework::StringFunc::Path::ConstructFull(builderParams.m_processJobRequest.m_tempDirPath.c_str(), AssetBuilderSDK::s_processJobResponseFileName, responseFilePath, true);
  983. if (!AZ::Utils::LoadObjectFromFileInPlace(responseFilePath.c_str(), jobResponse))
  984. {
  985. return false;
  986. }
  987. //Ensure that ProcessJobResponse have the correct absolute paths
  988. for (AssetBuilderSDK::JobProduct& product : jobResponse.m_outputProducts)
  989. {
  990. AzFramework::StringFunc::Replace(product.m_productFileName, s_tempString, builderParams.m_processJobRequest.m_tempDirPath.c_str(), s_tempString);
  991. }
  992. AZStd::string jobLogFilePath;
  993. AzFramework::StringFunc::Path::ConstructFull(builderParams.m_processJobRequest.m_tempDirPath.c_str(), s_jobLogFileName, jobLogFilePath, true);
  994. AzToolsFramework::AssetSystem::AssetJobLogResponse jobLogResponse;
  995. if (!AZ::Utils::LoadObjectFromFileInPlace(jobLogFilePath.c_str(), jobLogResponse))
  996. {
  997. return false;
  998. }
  999. if (!jobLogResponse.m_isSuccess)
  1000. {
  1001. AZ_TracePrintf(AssetProcessor::DebugChannel, "Job log request was unsuccessful for job (%s, %s, %s) from the server.\n",
  1002. builderParams.m_rcJob->GetJobEntry().m_sourceAssetReference.AbsolutePath().c_str(), builderParams.m_rcJob->GetJobKey().toUtf8().data(),
  1003. builderParams.m_rcJob->GetPlatformInfo().m_identifier.c_str());
  1004. if(jobLogResponse.m_jobLog.find("No log file found") != AZStd::string::npos)
  1005. {
  1006. 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, "
  1007. "please check the assetprocessorplatformconfig.ini file and ensure that server cache is disabled for the job.\n");
  1008. }
  1009. return false;
  1010. }
  1011. // writing server logs
  1012. AZ_TracePrintf(AssetProcessor::DebugChannel, "------------SERVER BEGIN----------\n");
  1013. AzToolsFramework::Logging::LogLine::ParseLog(jobLogResponse.m_jobLog.c_str(), jobLogResponse.m_jobLog.size(),
  1014. [&jobLogTraceListener](AzToolsFramework::Logging::LogLine& line)
  1015. {
  1016. jobLogTraceListener.AppendLog(line);
  1017. });
  1018. AZ_TracePrintf(AssetProcessor::DebugChannel, "------------SERVER END----------\n");
  1019. return true;
  1020. }
  1021. AZStd::string BuilderParams::GetTempJobDirectory() const
  1022. {
  1023. return m_processJobRequest.m_tempDirPath;
  1024. }
  1025. QString BuilderParams::GetServerKey() const
  1026. {
  1027. return m_serverKey;
  1028. }
  1029. } // namespace AssetProcessor
  1030. //////////////////////////////////////////////////////////////////////////