assetProcessorManager.cpp 297 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213
  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 <QStringList>
  9. #include <QCoreApplication>
  10. #include <QElapsedTimer>
  11. #include <AzCore/Casting/lossy_cast.h>
  12. #include <AssetBuilderSDK/AssetBuilderBusses.h>
  13. #include <AssetBuilderSDK/AssetBuilderSDK.h>
  14. #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
  15. #include <AzFramework/FileFunc/FileFunc.h>
  16. #include <AzToolsFramework/Debug/TraceContext.h>
  17. #include "native/AssetManager/assetProcessorManager.h"
  18. #include <AzCore/std/sort.h>
  19. #include <AzToolsFramework/API/AssetDatabaseBus.h>
  20. #include <native/AssetManager/PathDependencyManager.h>
  21. #include <native/AssetManager/Validators/LfsPointerFileValidator.h>
  22. #include <native/utilities/BuilderConfigurationBus.h>
  23. #include <native/utilities/StatsCapture.h>
  24. #include "AssetRequestHandler.h"
  25. #include <AssetManager/ProductAsset.h>
  26. #include <native/AssetManager/SourceAssetReference.h>
  27. #include <AzToolsFramework/Metadata/MetadataManager.h>
  28. #include <native/utilities/UuidManager.h>
  29. #include <native/utilities/ProductOutputUtil.h>
  30. #include <native/AssetManager/FileStateCache.h>
  31. namespace AssetProcessor
  32. {
  33. const AZ::u32 FAILED_FINGERPRINT = 1;
  34. const int MILLISECONDS_BETWEEN_CREATE_JOBS_STATUS_UPDATE = 1000;
  35. const int MILLISECONDS_BETWEEN_PROCESS_JOBS_STATUS_UPDATE = 100;
  36. constexpr AZStd::size_t s_lengthOfUuid = 38;
  37. using namespace AzToolsFramework::AssetSystem;
  38. using namespace AzFramework::AssetSystem;
  39. AssetProcessorManager::AssetProcessorManager(AssetProcessor::PlatformConfiguration* config, QObject* parent)
  40. : QObject(parent)
  41. , m_platformConfig(config)
  42. {
  43. m_stateData = AZStd::shared_ptr<AssetDatabaseConnection>(aznew AssetDatabaseConnection());
  44. // note that this is not the first time we're opening the database - the main thread also opens it before this happens,
  45. // which allows it to upgrade it and check it for errors. If we get here, it means the database is already good to go.
  46. m_stateData->OpenDatabase();
  47. MigrateScanFolders();
  48. m_highestJobRunKeySoFar = m_stateData->GetHighestJobRunKey() + 1;
  49. // cache this up front. Note that it can fail here, and will retry later.
  50. InitializeCacheRoot();
  51. QDir assetRoot;
  52. AssetUtilities::ComputeAssetRoot(assetRoot);
  53. using namespace AZStd::placeholders;
  54. m_pathDependencyManager = AZStd::make_unique<PathDependencyManager>(m_stateData, m_platformConfig);
  55. m_pathDependencyManager->SetDependencyResolvedCallback(AZStd::bind(&AssetProcessorManager::EmitResolvedDependency, this, _1, _2));
  56. m_sourceFileRelocator = AZStd::make_unique<SourceFileRelocator>(m_stateData, m_platformConfig);
  57. m_excludedFolderCache = AZStd::make_unique<ExcludedFolderCache>(m_platformConfig);
  58. PopulateJobStateCache();
  59. AssetProcessor::ProcessingJobInfoBus::Handler::BusConnect();
  60. AZ::Interface<AssetProcessor::RecognizerConfiguration>::Register(m_platformConfig);
  61. }
  62. AssetProcessorManager::~AssetProcessorManager()
  63. {
  64. AZ::Interface<AssetProcessor::RecognizerConfiguration>::Unregister(m_platformConfig);
  65. AssetProcessor::ProcessingJobInfoBus::Handler::BusDisconnect();
  66. }
  67. void AssetProcessorManager::PopulateJobStateCache()
  68. {
  69. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs;
  70. AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sources;
  71. m_stateData->GetJobs(jobs);
  72. for (const auto& jobEntry : jobs)
  73. {
  74. AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceEntry;
  75. if (m_stateData->GetSourceByJobID(jobEntry.m_jobID, sourceEntry))
  76. {
  77. SourceAssetReference sourceAsset(sourceEntry.m_scanFolderPK, sourceEntry.m_sourceName.c_str());
  78. if (sourceAsset)
  79. {
  80. JobDesc jobDesc(sourceAsset, jobEntry.m_jobKey, jobEntry.m_platform);
  81. JobIndentifier jobIdentifier(jobDesc, jobEntry.m_builderGuid);
  82. m_jobDescToBuilderUuidMap[jobDesc].insert(jobEntry.m_builderGuid);
  83. m_jobFingerprintMap[jobIdentifier] = jobEntry.m_fingerprint;
  84. }
  85. }
  86. }
  87. }
  88. template <class R>
  89. inline bool AssetProcessorManager::Recv(unsigned int connId, QByteArray payload, R& request)
  90. {
  91. bool readFromStream = AZ::Utils::LoadObjectFromBufferInPlace(payload.data(), payload.size(), request);
  92. AZ_Assert(readFromStream, "AssetProcessorManager::Recv: Could not deserialize from stream (type=%u)", request.GetMessageType());
  93. return readFromStream;
  94. }
  95. bool AssetProcessorManager::InitializeCacheRoot()
  96. {
  97. if (AssetUtilities::ComputeProjectCacheRoot(m_cacheRootDir))
  98. {
  99. m_normalizedCacheRootPath = AssetUtilities::NormalizeDirectoryPath(m_cacheRootDir.absolutePath());
  100. return !m_normalizedCacheRootPath.isEmpty();
  101. }
  102. return false;
  103. }
  104. void AssetProcessorManager::OnAssetScannerStatusChange(AssetProcessor::AssetScanningStatus status)
  105. {
  106. if (status == AssetProcessor::AssetScanningStatus::Started)
  107. {
  108. // capture scanning stats:
  109. AssetProcessor::StatsCapture::BeginCaptureStat("AssetScanning");
  110. // Ensure that the source file list is populated before a scan begins
  111. m_sourceFilesInDatabase.clear();
  112. m_fileModTimes.clear();
  113. m_fileHashes.clear();
  114. auto sourcesFunction = [this](AzToolsFramework::AssetDatabase::SourceAndScanFolderDatabaseEntry& entry)
  115. {
  116. SourceAssetReference sourceAsset(entry.m_scanFolderID, entry.m_scanFolder.c_str(), entry.m_sourceName.c_str());
  117. if (sourceAsset)
  118. {
  119. m_sourceFilesInDatabase[sourceAsset.AbsolutePath().c_str()] = { sourceAsset, entry.m_analysisFingerprint.c_str() };
  120. }
  121. return true;
  122. };
  123. m_stateData->QuerySourceAndScanfolder(sourcesFunction);
  124. m_stateData->QueryFilesTable([this](AzToolsFramework::AssetDatabase::FileDatabaseEntry& entry)
  125. {
  126. if (entry.m_isFolder)
  127. {
  128. // Ignore folders
  129. return true;
  130. }
  131. QString scanFolderPath;
  132. QString relativeToScanFolderPath = QString::fromUtf8(entry.m_fileName.c_str());
  133. for (int i = 0; i < m_platformConfig->GetScanFolderCount(); ++i)
  134. {
  135. const auto& scanFolderInfo = m_platformConfig->GetScanFolderAt(i);
  136. if (scanFolderInfo.ScanFolderID() == entry.m_scanFolderPK)
  137. {
  138. scanFolderPath = scanFolderInfo.ScanPath();
  139. break;
  140. }
  141. }
  142. QString finalAbsolute = (QString("%1/%2").arg(scanFolderPath).arg(relativeToScanFolderPath));
  143. m_fileModTimes.emplace(finalAbsolute.toUtf8().data(), entry.m_modTime);
  144. m_fileHashes.emplace(finalAbsolute.toUtf8().constData(), entry.m_hash);
  145. return true;
  146. });
  147. m_isCurrentlyScanning = true;
  148. }
  149. else if ((status == AssetProcessor::AssetScanningStatus::Completed) ||
  150. (status == AssetProcessor::AssetScanningStatus::Stopped))
  151. {
  152. AssetProcessor::StatsCapture::EndCaptureStat("AssetScanning");
  153. }
  154. }
  155. void AssetProcessorManager::FinishAssetScan()
  156. {
  157. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Initial Scan complete, checking for missing files...\n");
  158. m_isCurrentlyScanning = false;
  159. CheckMissingFiles();
  160. }
  161. ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  162. // JOB STATUS REQUEST HANDLING
  163. void AssetProcessorManager::OnJobStatusChanged(JobEntry jobEntry, JobStatus status)
  164. {
  165. //this function just adds an removes to a maps to speed up job status, we don't actually write
  166. //to the database until it either succeeds or fails
  167. auto sourceUUID = AssetUtilities::GetSourceUuid(jobEntry.m_sourceAssetReference);
  168. auto legacySourceUUIDs = AssetUtilities::GetLegacySourceUuids(jobEntry.m_sourceAssetReference);
  169. if (!sourceUUID || !legacySourceUUIDs)
  170. {
  171. AZ_Error(AssetProcessor::ConsoleChannel, false, sourceUUID.GetError().c_str());
  172. return;
  173. }
  174. if (status == JobStatus::Queued)
  175. {
  176. // freshly queued files start out queued.
  177. JobInfo& jobInfo = m_jobRunKeyToJobInfoMap.insert_key(jobEntry.m_jobRunKey).first->second;
  178. jobInfo.m_platform = jobEntry.m_platformInfo.m_identifier;
  179. jobInfo.m_builderGuid = jobEntry.m_builderGuid;
  180. jobInfo.m_sourceFile = jobEntry.m_sourceAssetReference.RelativePath().Native();
  181. jobInfo.m_watchFolder = jobEntry.m_sourceAssetReference.ScanFolderPath().Native();
  182. jobInfo.m_jobKey = jobEntry.m_jobKey.toUtf8().constData();
  183. jobInfo.m_jobRunKey = jobEntry.m_jobRunKey;
  184. jobInfo.m_status = status;
  185. m_jobKeyToJobRunKeyMap.insert(AZStd::make_pair(jobEntry.m_jobKey.toUtf8().data(), jobEntry.m_jobRunKey));
  186. Q_EMIT SourceQueued(sourceUUID.GetValue(), legacySourceUUIDs.GetValue(), jobEntry.m_sourceAssetReference);
  187. }
  188. else
  189. {
  190. QString statKey = QString("ProcessJob,%1,%2,%3,%4,%5")
  191. .arg(jobEntry.m_sourceAssetReference.ScanFolderPath().c_str())
  192. .arg(jobEntry.m_sourceAssetReference.RelativePath().c_str())
  193. .arg(jobEntry.m_jobKey)
  194. .arg(jobEntry.m_platformInfo.m_identifier.c_str())
  195. .arg(jobEntry.m_builderGuid.ToString<AZStd::string>().c_str());
  196. if (status == JobStatus::InProgress)
  197. {
  198. //update to in progress status
  199. m_jobRunKeyToJobInfoMap[jobEntry.m_jobRunKey].m_status = JobStatus::InProgress;
  200. // stats tracking. Start accumulating time.
  201. AssetProcessor::StatsCapture::BeginCaptureStat(statKey.toUtf8().constData());
  202. }
  203. else //if failed or succeeded remove from the map
  204. {
  205. // note that sometimes this gets called twice, once by the RCJobs thread and once by the AP itself,
  206. // because sometimes jobs take a short cut from "started" -> "failed" or "started" -> "complete
  207. // without going thru the RC.
  208. // as such, all the code in this block should be crafted to work regardless of whether its double called.
  209. AZStd::optional<AZStd::sys_time_t> operationDuration =
  210. AssetProcessor::StatsCapture::EndCaptureStat(statKey.toUtf8().constData(), true);
  211. if (operationDuration)
  212. {
  213. Q_EMIT JobProcessDurationChanged(jobEntry, aznumeric_cast<int>(operationDuration.value()));
  214. }
  215. m_jobRunKeyToJobInfoMap.erase(jobEntry.m_jobRunKey);
  216. Q_EMIT SourceFinished(sourceUUID.GetValue(), legacySourceUUIDs.GetValue());
  217. Q_EMIT JobComplete(jobEntry, status);
  218. auto found = m_jobKeyToJobRunKeyMap.equal_range(jobEntry.m_jobKey.toUtf8().data());
  219. for (auto iter = found.first; iter != found.second; ++iter)
  220. {
  221. if (iter->second == jobEntry.m_jobRunKey)
  222. {
  223. m_jobKeyToJobRunKeyMap.erase(iter);
  224. break;
  225. }
  226. }
  227. }
  228. }
  229. }
  230. //! A network request came in, Given a Job Run Key (from the above Job Request), asking for the actual log for that job.
  231. AssetJobLogResponse AssetProcessorManager::ProcessGetAssetJobLogRequest(MessageData<AssetJobLogRequest> messageData)
  232. {
  233. AssetJobLogResponse response;
  234. ProcessGetAssetJobLogRequest(*messageData.m_message, response);
  235. return response;
  236. }
  237. void AssetProcessorManager::ProcessGetAssetJobLogRequest(const AssetJobLogRequest& request, AssetJobLogResponse& response)
  238. {
  239. JobInfo jobInfo;
  240. bool hasSpace = false;
  241. auto* diskSpaceInfoBus = AZ::Interface<IDiskSpaceInfo>::Get();
  242. if(diskSpaceInfoBus)
  243. {
  244. hasSpace = diskSpaceInfoBus->CheckSufficientDiskSpace(0, false);
  245. }
  246. if (!hasSpace)
  247. {
  248. AZ_TracePrintf("AssetProcessorManager", "Warn: AssetProcessorManager: Low disk space detected\n");
  249. response.m_jobLog = "Warn: Low disk space detected. Log file may be missing or truncated. Asset processing is likely to fail.\n";
  250. }
  251. //look for the job in flight first
  252. auto foundElement = m_jobRunKeyToJobInfoMap.find(request.m_jobRunKey);
  253. if (foundElement != m_jobRunKeyToJobInfoMap.end())
  254. {
  255. jobInfo = foundElement->second;
  256. }
  257. else
  258. {
  259. // get the job infos by that job run key.
  260. JobInfoContainer jobInfos;
  261. if (!m_stateData->GetJobInfoByJobRunKey(request.m_jobRunKey, jobInfos))
  262. {
  263. AZ_TracePrintf("AssetProcessorManager", "Error: AssetProcessorManager: Failed to find the job for a request.\n");
  264. response.m_jobLog.append("Error: AssetProcessorManager: Failed to find the job for a request.");
  265. response.m_isSuccess = false;
  266. return;
  267. }
  268. AZ_Assert(jobInfos.size() == 1, "Should only have found one jobInfo!!!");
  269. jobInfo = AZStd::move(jobInfos[0]);
  270. }
  271. if (jobInfo.m_status == JobStatus::Failed_InvalidSourceNameExceedsMaxLimit)
  272. {
  273. response.m_jobLog.append(
  274. AZStd::string::format("Warn: Source file name exceeds the maximum length allowed (%d).", ASSETPROCESSOR_TRAIT_MAX_PATH_LEN)
  275. .c_str());
  276. response.m_isSuccess = true;
  277. return;
  278. }
  279. AssetUtilities::ReadJobLog(jobInfo, response);
  280. }
  281. //! A network request came in, Given a Job Run Key (from the above Job Request), asking for the actual log for that job.
  282. GetAbsoluteAssetDatabaseLocationResponse AssetProcessorManager::ProcessGetAbsoluteAssetDatabaseLocationRequest(MessageData<GetAbsoluteAssetDatabaseLocationRequest> messageData)
  283. {
  284. GetAbsoluteAssetDatabaseLocationResponse response;
  285. AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Broadcast(&AzToolsFramework::AssetDatabase::AssetDatabaseRequests::GetAssetDatabaseLocation, response.m_absoluteAssetDatabaseLocation);
  286. if (response.m_absoluteAssetDatabaseLocation.size() > 0)
  287. {
  288. response.m_isSuccess = true;
  289. }
  290. return response;
  291. }
  292. AssetFingerprintClearResponse AssetProcessorManager::ProcessFingerprintClearRequest(MessageData<AssetFingerprintClearRequest> messageData)
  293. {
  294. AssetFingerprintClearResponse response;
  295. ProcessFingerprintClearRequest(*messageData.m_message, response);
  296. return response;
  297. }
  298. void AssetProcessorManager::ProcessFingerprintClearRequest(
  299. AssetFingerprintClearRequest& request, AssetFingerprintClearResponse& response)
  300. {
  301. SourceAssetReference sourceAsset;
  302. if (QFileInfo(request.m_searchTerm.c_str()).isAbsolute())
  303. {
  304. sourceAsset = SourceAssetReference(request.m_searchTerm.c_str());
  305. }
  306. else
  307. {
  308. QString absolutePath = m_platformConfig->FindFirstMatchingFile(request.m_searchTerm.c_str());
  309. if (absolutePath.isEmpty())
  310. {
  311. response.m_isSuccess = false;
  312. return;
  313. }
  314. sourceAsset = SourceAssetReference(absolutePath.toUtf8().constData());
  315. }
  316. response.m_isSuccess = m_stateData->UpdateFileHashByFileNameAndScanFolderId(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderId(), 0);
  317. // if setting the file hash failed, still try to clear the job fingerprints.
  318. AzToolsFramework::AssetDatabase::SourceDatabaseEntry source;
  319. if (!m_stateData->GetSourceBySourceNameScanFolderId(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderId(), source))
  320. {
  321. response.m_isSuccess = false;
  322. return;
  323. }
  324. response.m_isSuccess = response.m_isSuccess && m_stateData->SetJobFingerprintsBySourceID(source.m_sourceID, 0);
  325. }
  326. //! A network request came in asking, for a given input asset, what the status is of any jobs related to that request
  327. AssetJobsInfoResponse AssetProcessorManager::ProcessGetAssetJobsInfoRequest(MessageData<AssetJobsInfoRequest> messageData)
  328. {
  329. AssetJobsInfoResponse response;
  330. ProcessGetAssetJobsInfoRequest(*messageData.m_message, response);
  331. return response;
  332. }
  333. void AssetProcessorManager::ProcessGetAssetJobsInfoRequest(AssetJobsInfoRequest& request, AssetJobsInfoResponse& response)
  334. {
  335. SourceAssetReference sourceAsset;
  336. if (request.m_assetId.IsValid())
  337. {
  338. //If the assetId is valid then search both the database and the pending queue and update the searchTerm with the source name
  339. if (!SearchSourceInfoBySourceUUID(request.m_assetId.m_guid, sourceAsset))
  340. {
  341. // If still not found it means that this source asset is neither in the database nor in the queue for processing
  342. AZ_TracePrintf(AssetProcessor::DebugChannel, "ProcessGetAssetJobsInfoRequest: AssetProcessor unable to find the requested source asset having uuid (%s).\n",
  343. request.m_assetId.m_guid.ToString<AZStd::string>().c_str());
  344. response = AssetJobsInfoResponse(AzToolsFramework::AssetSystem::JobInfoContainer(), false);
  345. return;
  346. }
  347. }
  348. AzToolsFramework::AssetSystem::JobInfoContainer jobList;
  349. AssetProcessor::JobIdEscalationList jobIdEscalationList;
  350. if (!request.m_isSearchTermJobKey)
  351. {
  352. if(sourceAsset.AbsolutePath().empty())
  353. {
  354. if (QFileInfo(request.m_searchTerm.c_str()).isAbsolute())
  355. {
  356. sourceAsset = SourceAssetReference(request.m_searchTerm.c_str());
  357. }
  358. else
  359. {
  360. QString absolutePath = m_platformConfig->FindFirstMatchingFile(request.m_searchTerm.c_str());
  361. if(absolutePath.isEmpty())
  362. {
  363. response = AssetJobsInfoResponse(AzToolsFramework::AssetSystem::JobInfoContainer(), false);
  364. return;
  365. }
  366. sourceAsset = SourceAssetReference(absolutePath.toUtf8().constData());
  367. }
  368. }
  369. //any queued or in progress jobs will be in the map:
  370. for (const auto& entry : m_jobRunKeyToJobInfoMap)
  371. {
  372. if ((AZ::IO::Path(entry.second.m_watchFolder) / entry.second.m_sourceFile) == sourceAsset.AbsolutePath())
  373. {
  374. jobList.push_back(entry.second);
  375. if (request.m_escalateJobs)
  376. {
  377. jobIdEscalationList.append(qMakePair(entry.second.m_jobRunKey, AssetProcessor::JobEscalation::AssetJobRequestEscalation));
  378. }
  379. }
  380. }
  381. }
  382. else
  383. {
  384. auto found = m_jobKeyToJobRunKeyMap.equal_range(request.m_searchTerm.c_str());
  385. for (auto iter = found.first; iter != found.second; ++iter)
  386. {
  387. auto foundJobRunKey = m_jobRunKeyToJobInfoMap.find(iter->second);
  388. if (foundJobRunKey != m_jobRunKeyToJobInfoMap.end())
  389. {
  390. AzToolsFramework::AssetSystem::JobInfo& jobInfo = foundJobRunKey->second;
  391. jobList.push_back(jobInfo);
  392. if (request.m_escalateJobs)
  393. {
  394. jobIdEscalationList.append(qMakePair(iter->second, AssetProcessor::JobEscalation::AssetJobRequestEscalation));
  395. }
  396. }
  397. }
  398. }
  399. if (!jobIdEscalationList.empty())
  400. {
  401. Q_EMIT EscalateJobs(jobIdEscalationList);
  402. }
  403. AzToolsFramework::AssetSystem::JobInfoContainer jobListDataBase;
  404. if (!request.m_isSearchTermJobKey)
  405. {
  406. //any succeeded or failed jobs will be in the table
  407. m_stateData->GetJobInfoBySourceNameScanFolderId(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderId(), jobListDataBase);
  408. }
  409. else
  410. {
  411. //check the database for all jobs with that job key
  412. m_stateData->GetJobInfoByJobKey(request.m_searchTerm, jobListDataBase);
  413. }
  414. for (const AzToolsFramework::AssetSystem::JobInfo& job : jobListDataBase)
  415. {
  416. auto result = AZStd::find_if(jobList.begin(), jobList.end(),
  417. [&job](AzToolsFramework::AssetSystem::JobInfo& entry) -> bool
  418. {
  419. return
  420. AzFramework::StringFunc::Equal(entry.m_platform.c_str(), job.m_platform.c_str()) &&
  421. AzFramework::StringFunc::Equal(entry.m_jobKey.c_str(), job.m_jobKey.c_str()) &&
  422. AzFramework::StringFunc::Equal(entry.m_sourceFile.c_str(), job.m_sourceFile.c_str());
  423. });
  424. if (result == jobList.end())
  425. {
  426. // A job for this asset has already completed and was registered with the database so report that one as well.
  427. jobList.push_back(job);
  428. }
  429. }
  430. // resolve any paths here before sending the job info back, in case the AP's %log% is different than whatever is reading
  431. // the AssetJobsInfoResponse
  432. for (AzToolsFramework::AssetSystem::JobInfo& job : jobList)
  433. {
  434. char resolvedBuffer[AZ_MAX_PATH_LEN] = { 0 };
  435. AZ::IO::FileIOBase::GetInstance()->ResolvePath(job.m_firstFailLogFile.c_str(), resolvedBuffer, AZ_MAX_PATH_LEN);
  436. job.m_firstFailLogFile = resolvedBuffer;
  437. AZ::IO::FileIOBase::GetInstance()->ResolvePath(job.m_lastFailLogFile.c_str(), resolvedBuffer, AZ_MAX_PATH_LEN);
  438. job.m_lastFailLogFile = resolvedBuffer;
  439. }
  440. response = AssetJobsInfoResponse(jobList, true);
  441. }
  442. void AssetProcessorManager::CheckMissingFiles()
  443. {
  444. if (!m_activeFiles.isEmpty())
  445. {
  446. // not ready yet, we have not drained the queue.
  447. QTimer::singleShot(10, this, SLOT(CheckMissingFiles()));
  448. return;
  449. }
  450. if (m_isCurrentlyScanning)
  451. {
  452. return;
  453. }
  454. // note that m_SourceFilesInDatabase is a map from (full absolute path) --> (database name for file)
  455. for (auto iter = m_sourceFilesInDatabase.begin(); iter != m_sourceFilesInDatabase.end(); iter++)
  456. {
  457. if (iter.value().m_sourceAssetReference)
  458. {
  459. // CheckDeletedSourceFile actually expects the database name as the second value
  460. // iter.key is the full path normalized. iter.value is the database path.
  461. // we need the relative path too:
  462. CheckDeletedSourceFile(iter.value().m_sourceAssetReference, AZStd::chrono::steady_clock::now());
  463. }
  464. }
  465. // we want to remove any left over scan folders from the database only after
  466. // we remove all the products from source files we are no longer interested in,
  467. // we do it last instead of when we update scan folders because the scan folders table CASCADE DELETE on the sources, jobs,
  468. // products table and we want to do this last after cleanup of disk.
  469. for (auto entry : m_scanFoldersInDatabase)
  470. {
  471. if (!m_stateData->RemoveScanFolder(entry.second.m_scanFolderID))
  472. {
  473. AZ_TracePrintf(AssetProcessor::DebugChannel, "CheckMissingFiles: Unable to remove Scan Folder having id %d from the database.", entry.second.m_scanFolderID);
  474. return;
  475. }
  476. }
  477. m_scanFoldersInDatabase.clear();
  478. m_sourceFilesInDatabase.clear();
  479. QueueIdleCheck();
  480. }
  481. void AssetProcessorManager::QueueIdleCheck()
  482. {
  483. // avoid putting many check for idle requests in the queue if its already there.
  484. if (!m_alreadyQueuedCheckForIdle)
  485. {
  486. m_alreadyQueuedCheckForIdle = true;
  487. QMetaObject::invokeMethod(this, "CheckForIdle", Qt::QueuedConnection);
  488. }
  489. }
  490. void AssetProcessorManager::QuitRequested()
  491. {
  492. m_quitRequested = true;
  493. m_filesToExamine.clear();
  494. Q_EMIT ReadyToQuit(this);
  495. }
  496. //! 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.
  497. void AssetProcessorManager::OnRequestAssetExists(NetworkRequestID groupID, QString platform, QString searchTerm, AZ::Data::AssetId assetId)
  498. {
  499. // if an assetId is available there is no guessing to do.
  500. if (assetId.IsValid())
  501. {
  502. bool foundOne = false;
  503. m_stateData->QueryCombinedBySourceGuidProductSubId(
  504. assetId.m_guid,
  505. assetId.m_subId,
  506. [&foundOne](AzToolsFramework::AssetDatabase::CombinedDatabaseEntry& /*combinedDatabaseEntry*/)
  507. {
  508. foundOne = true;
  509. return true;
  510. },
  511. AZ::Uuid::CreateNull(),
  512. nullptr,
  513. platform.toUtf8().constData(),
  514. AzToolsFramework::AssetSystem::JobStatus::Any);
  515. // respond with whether or not the entry was found in the DB.
  516. Q_EMIT SendAssetExistsResponse(groupID, foundOne);
  517. return;
  518. }
  519. // otherwise, we have to guess
  520. QString productName = GuessProductOrSourceAssetName(searchTerm, platform, false);
  521. Q_EMIT SendAssetExistsResponse(groupID, !productName.isEmpty());
  522. }
  523. QString AssetProcessorManager::GuessProductOrSourceAssetName(QString searchTerm, QString platform, bool useLikeSearch)
  524. {
  525. // Search the product table
  526. QString productName = AssetUtilities::GuessProductNameInDatabase(searchTerm, platform, m_stateData.get());
  527. if (!productName.isEmpty())
  528. {
  529. return productName;
  530. }
  531. // Search the source table
  532. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  533. if (!useLikeSearch && m_stateData->GetProductsBySourceName(searchTerm, products))
  534. {
  535. return searchTerm;
  536. }
  537. else if (useLikeSearch && m_stateData->GetProductsLikeSourceName(searchTerm, AzToolsFramework::AssetDatabase::AssetDatabaseConnection::LikeType::StartsWith, products))
  538. {
  539. return searchTerm;
  540. }
  541. return QString();
  542. }
  543. void AssetProcessorManager::AssetCancelled(AssetProcessor::JobEntry jobEntry)
  544. {
  545. if (m_quitRequested)
  546. {
  547. return;
  548. }
  549. // Remove the log file for the cancelled job
  550. AZStd::string logFile = AssetUtilities::ComputeJobLogFolder() + "/" + AssetUtilities::ComputeJobLogFileName(jobEntry);
  551. EraseLogFile(logFile.c_str());
  552. // cancelled jobs are replaced by new jobs to process the same asset, so keep track of that for the analysis tracker too
  553. // note that this isn't a failure - the job just isn't there anymore.
  554. UpdateAnalysisTrackerForFile(jobEntry, AnalysisTrackerUpdateType::JobFinished);
  555. OnJobStatusChanged(jobEntry, JobStatus::Failed);
  556. // we know that things have changed at this point; ensure that we check for idle
  557. QueueIdleCheck();
  558. }
  559. void AssetProcessorManager::AssetFailed(AssetProcessor::JobEntry jobEntry)
  560. {
  561. if (m_quitRequested)
  562. {
  563. return;
  564. }
  565. m_AssetProcessorIsBusy = true;
  566. Q_EMIT AssetProcessorManagerIdleState(false);
  567. // when an asset fails, we must make sure we notify the Analysis Tracker that it has failed, so that it doesn't write into the database
  568. // that it can be skipped next time:
  569. UpdateAnalysisTrackerForFile(jobEntry, AnalysisTrackerUpdateType::JobFailed);
  570. QString absolutePathToFile = jobEntry.GetAbsoluteSourcePath();
  571. // Set the thread local job ID so that JobLogTraceListener can capture the error and write it to the corresponding job log.
  572. // The error message will be available in the Event Log Details table when users click on the failed job in the Asset Proessor GUI.
  573. AssetProcessor::SetThreadLocalJobId(jobEntry.m_jobRunKey);
  574. AssetUtilities::JobLogTraceListener jobLogTraceListener(jobEntry);
  575. if (IsLfsPointerFile(absolutePathToFile.toUtf8().constData()))
  576. {
  577. AZ_Error(AssetProcessor::ConsoleChannel, false,
  578. "%s is a git large file storage (LFS) file. "
  579. "This is a placeholder file used by the git source control system to manage content. "
  580. "This issue usually happens if you've downloaded all of O3DE as a zip file. "
  581. "Please sync all of the files from the LFS endpoint following https://www.o3de.org/docs/welcome-guide/setup/setup-from-github/#fork-and-clone.",
  582. jobEntry.GetAbsoluteSourcePath().toUtf8().constData());
  583. }
  584. AssetProcessor::SetThreadLocalJobId(0);
  585. // if its a fake "autofail job" or other reason for it not to exist in the DB, don't do anything here.
  586. if (!jobEntry.m_addToDatabase)
  587. {
  588. return;
  589. }
  590. // wipe the times so that it will try again next time.
  591. // note: Leave the prior successful products where they are, though.
  592. // We have to include a fingerprint in the database for this job, otherwise when assets change that
  593. // affect this failed job, the failed assets won't get rescanned and won't be in the database and
  594. // therefore won't get reprocessed. Set it to FAILED_FINGERPRINT.
  595. //create/update the source record for this job
  596. AzToolsFramework::AssetDatabase::SourceDatabaseEntry source;
  597. if (!m_stateData->GetSourceBySourceNameScanFolderId(jobEntry.m_sourceAssetReference.RelativePath().c_str(), jobEntry.m_sourceAssetReference.ScanFolderId(), source))
  598. {
  599. //if we didn't find a source, we make a new source
  600. const ScanFolderInfo* scanFolder = m_platformConfig->GetScanFolderByPath(jobEntry.m_sourceAssetReference.ScanFolderPath().c_str());
  601. if (!scanFolder)
  602. {
  603. //can't find the scan folder this source came from!?
  604. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to find the scan folder for this source!!!");
  605. }
  606. //add the new source
  607. if (!QFile::exists(absolutePathToFile))
  608. {
  609. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Source file %s no longer exists, it will not be added to database.\n",
  610. absolutePathToFile.toUtf8().constData());
  611. //notify the GUI to remove the failed job that is currently onscreen:
  612. AzToolsFramework::AssetSystem::JobInfo jobInfo;
  613. jobInfo.m_watchFolder = jobEntry.m_sourceAssetReference.ScanFolderPath().Native();
  614. jobInfo.m_sourceFile = jobEntry.m_sourceAssetReference.RelativePath().Native();
  615. jobInfo.m_platform = jobEntry.m_platformInfo.m_identifier;
  616. jobInfo.m_jobKey = jobEntry.m_jobKey.toUtf8().constData();
  617. Q_EMIT JobRemoved(jobInfo);
  618. return;
  619. }
  620. else
  621. {
  622. AddSourceToDatabase(source, jobEntry.m_sourceAssetReference);
  623. }
  624. }
  625. //create/update the job
  626. AzToolsFramework::AssetDatabase::JobDatabaseEntry job;
  627. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs;
  628. if (m_stateData->GetJobsBySourceID(source.m_sourceID, jobs, jobEntry.m_builderGuid, jobEntry.m_jobKey, jobEntry.m_platformInfo.m_identifier.c_str()))
  629. {
  630. AZ_Assert(jobs.size() == 1, "Should have only found one job!!!");
  631. job = AZStd::move(jobs[0]);
  632. //we only want to keep the first fail and the last fail log
  633. //if it has failed before, both first and last will be set, only delete last fail file if its not the first fail
  634. if (job.m_firstFailLogTime && job.m_firstFailLogTime != job.m_lastFailLogTime)
  635. {
  636. EraseLogFile(job.m_lastFailLogFile.c_str());
  637. }
  638. //we failed so the last fail is the same as the current
  639. job.m_lastFailLogTime = job.m_lastLogTime = QDateTime::currentMSecsSinceEpoch();
  640. job.m_lastFailLogFile = job.m_lastLogFile = AssetUtilities::ComputeJobLogFolder() + "/" + AssetUtilities::ComputeJobLogFileName(jobEntry);
  641. //if we have never failed before also set the first fail to be the last fail
  642. if (!job.m_firstFailLogTime)
  643. {
  644. job.m_firstFailLogTime = job.m_lastFailLogTime;
  645. job.m_firstFailLogFile = job.m_lastFailLogFile;
  646. }
  647. }
  648. else
  649. {
  650. //if we didn't find a job, we make a new one
  651. job.m_sourcePK = source.m_sourceID;
  652. job.m_jobKey = jobEntry.m_jobKey.toUtf8().constData();
  653. job.m_platform = jobEntry.m_platformInfo.m_identifier;
  654. job.m_builderGuid = jobEntry.m_builderGuid;
  655. //if this is a new job that failed then first filed ,last failed and current are the same
  656. job.m_firstFailLogTime = job.m_lastFailLogTime = job.m_lastLogTime = QDateTime::currentMSecsSinceEpoch();
  657. job.m_firstFailLogFile = job.m_lastFailLogFile = job.m_lastLogFile = AssetUtilities::ComputeJobLogFolder() + "/" + AssetUtilities::ComputeJobLogFileName(jobEntry);
  658. }
  659. // invalidate the fingerprint
  660. job.m_fingerprint = FAILED_FINGERPRINT;
  661. //set the random key
  662. job.m_jobRunKey = jobEntry.m_jobRunKey;
  663. job.m_failureCauseSourcePK = jobEntry.m_failureCauseSourceId;
  664. job.m_failureCauseFingerprint = jobEntry.m_failureCauseFingerprint;
  665. QString fullPath = jobEntry.GetAbsoluteSourcePath();
  666. //set the new status
  667. job.m_status =
  668. fullPath.length() < ASSETPROCESSOR_TRAIT_MAX_PATH_LEN ? JobStatus::Failed : JobStatus::Failed_InvalidSourceNameExceedsMaxLimit;
  669. JobDiagnosticInfo info{};
  670. JobDiagnosticRequestBus::BroadcastResult(info, &JobDiagnosticRequestBus::Events::GetDiagnosticInfo, job.m_jobRunKey);
  671. job.m_warningCount = info.m_warningCount;
  672. job.m_errorCount = info.m_errorCount;
  673. // check to see if builder request deletion of LKG asset on failure, and delete them if so
  674. {
  675. AssetBuilderSDK::AssetBuilderDesc description;
  676. bool findResult = false;
  677. AssetBuilderSDK::AssetBuilderBus::BroadcastResult(findResult, &AssetBuilderSDK::AssetBuilderBus::Events::FindBuilderInformation, jobEntry.m_builderGuid, description);
  678. if (findResult && description.HasFlag(AssetBuilderSDK::AssetBuilderDesc::BF_DeleteLastKnownGoodProductOnFailure, jobEntry.m_jobKey.toUtf8().constData()))
  679. {
  680. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  681. m_stateData->GetProductsByJobID(job.m_jobID, products);
  682. auto keepProductsIter = description.m_productsToKeepOnFailure.find(jobEntry.m_jobKey.toUtf8().constData());
  683. if (keepProductsIter != description.m_productsToKeepOnFailure.end())
  684. {
  685. // keep some products
  686. AZStd::erase_if(products, [&](const auto& entry) { return !keepProductsIter->second.contains(entry.m_subID); });
  687. }
  688. DeleteProducts(products);
  689. }
  690. }
  691. //create/update job
  692. if (!m_stateData->SetJob(job))
  693. {
  694. //somethings wrong...
  695. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to update the job in the database!!!");
  696. }
  697. if (MessageInfoBus::HasHandlers())
  698. {
  699. // send a network message when not in batch mode.
  700. const ScanFolderInfo* scanFolder = m_platformConfig->GetScanFolderByPath(jobEntry.m_sourceAssetReference.ScanFolderPath().c_str());
  701. AzToolsFramework::AssetSystem::SourceFileNotificationMessage message(AZ::OSString(source.m_sourceName.c_str()), AZ::OSString(scanFolder->ScanPath().toUtf8().constData()), AzToolsFramework::AssetSystem::SourceFileNotificationMessage::FileFailed, source.m_sourceGuid);
  702. AssetProcessor::ConnectionBus::Broadcast(&AssetProcessor::ConnectionBus::Events::Send, 0, message);
  703. MessageInfoBus::Broadcast(&MessageInfoBusTraits::OnAssetFailed, source.m_sourceName);
  704. }
  705. OnJobStatusChanged(jobEntry, JobStatus::Failed);
  706. // note that we always print out the failed job status here in both batch and GUI mode.
  707. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Failed %s, (%s)... \n",
  708. jobEntry.m_sourceAssetReference.AbsolutePath().c_str(),
  709. jobEntry.m_platformInfo.m_identifier.c_str());
  710. AZ_TracePrintf(AssetProcessor::DebugChannel, "AssetProcessed [fail] Jobkey \"%s\", Builder UUID \"%s\", Fingerprint %u ) \n",
  711. jobEntry.m_jobKey.toUtf8().constData(),
  712. jobEntry.m_builderGuid.ToString<AZStd::string>().c_str(),
  713. jobEntry.m_computedFingerprint);
  714. // we know that things have changed at this point; ensure that we check for idle after we've finished processing all of our assets
  715. // and don't rely on the file watcher to check again.
  716. // If we rely on the file watcher only, it might fire before the AssetMessage signal has been responded to and the
  717. // Asset Catalog may not realize that things are dirty by that point.
  718. QueueIdleCheck();
  719. }
  720. bool AssetProcessorManager::IsLfsPointerFile(const AZStd::string& filePath)
  721. {
  722. if (!m_lfsPointerFileValidator)
  723. {
  724. m_lfsPointerFileValidator = AZStd::make_unique<LfsPointerFileValidator>(GetPotentialRepositoryRoots());
  725. }
  726. return m_lfsPointerFileValidator->IsLfsPointerFile(filePath);
  727. }
  728. AZStd::vector<AZStd::string> AssetProcessorManager::GetPotentialRepositoryRoots()
  729. {
  730. AZStd::vector<AZStd::string> scanDirectories;
  731. if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
  732. {
  733. scanDirectories.emplace_back(AZ::Utils::GetEnginePath(settingsRegistry).c_str());
  734. scanDirectories.emplace_back(AZ::Utils::GetProjectPath(settingsRegistry).c_str());
  735. auto RetrieveActiveGemRootDirectories = [&scanDirectories](AZStd::string_view, AZStd::string_view gemPath)
  736. {
  737. scanDirectories.emplace_back(gemPath.data());
  738. };
  739. AZ::SettingsRegistryMergeUtils::VisitActiveGems(*settingsRegistry, RetrieveActiveGemRootDirectories);
  740. }
  741. else
  742. {
  743. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to retrieve the registered setting registry.");
  744. }
  745. return AZStd::move(scanDirectories);
  746. }
  747. AssetProcessorManager::ConflictResult AssetProcessorManager::CheckIntermediateProductConflict(const char* searchSourcePath)
  748. {
  749. AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sources;
  750. if (m_stateData->GetSourcesBySourceName(searchSourcePath, sources))
  751. {
  752. for (const auto& source : sources)
  753. {
  754. AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanfolder;
  755. if (!m_stateData->GetScanFolderByScanFolderID(source.m_scanFolderPK, scanfolder))
  756. {
  757. AZ_Error(AssetProcessor::ConsoleChannel, false, "CheckIntermediateProductConflict: Failed to get scanfolder %d for source %s",
  758. source.m_scanFolderPK,
  759. source.m_sourceName.c_str());
  760. }
  761. auto intermediateScanfolderId = m_platformConfig->GetIntermediateAssetsScanFolderId();
  762. if (!intermediateScanfolderId)
  763. {
  764. AZ_Error(
  765. AssetProcessor::ConsoleChannel, false,
  766. "GetIntermediateAssetsScanFolderId is invalid. Make sure CacheIntermediateAssetsScanFolderId has been called");
  767. return ConflictResult{ ConflictResult::ConflictType::None };
  768. }
  769. bool scanfolderIsIntermediateAssetsFolder = intermediateScanfolderId.value() == scanfolder.m_scanFolderID;
  770. // Check if this newly created intermediate will conflict with an existing source
  771. if (!scanfolderIsIntermediateAssetsFolder)
  772. {
  773. return ConflictResult{ ConflictResult::ConflictType::Intermediate, SourceAssetReference(scanfolder.m_scanFolder.c_str(), source.m_sourceName.c_str()) };
  774. }
  775. }
  776. }
  777. // Its possible we haven't recorded the source in the database yet, so check the filesystem to confirm there's no normal source we're overriding
  778. if (QString overriddenFile = m_platformConfig->FindFirstMatchingFile(searchSourcePath, true); !overriddenFile.isEmpty())
  779. {
  780. return ConflictResult{ ConflictResult::ConflictType::Intermediate, SourceAssetReference(overriddenFile) };
  781. }
  782. return ConflictResult{ ConflictResult::ConflictType::None };
  783. }
  784. bool AssetProcessorManager::CheckForIntermediateAssetLoop(
  785. const SourceAssetReference& sourceAsset, const SourceAssetReference& productAsset)
  786. {
  787. auto intermediateSources =
  788. AssetUtilities::GetAllIntermediateSources(sourceAsset, m_stateData);
  789. // Locate the sourceAsset in the chain
  790. auto sourceItr = AZStd::find_if(
  791. intermediateSources.begin(), intermediateSources.end(),
  792. [&sourceAsset](auto intermediateAsset)
  793. {
  794. return intermediateAsset == sourceAsset;
  795. });
  796. // Locate the productAsset in the chain
  797. auto productItr = AZStd::find_if(
  798. intermediateSources.begin(), intermediateSources.end(),
  799. [&productAsset](auto intermediateAsset)
  800. {
  801. return intermediateAsset == productAsset;
  802. });
  803. // If both are found, check if the product exists BEFORE the source in the chain
  804. // If so, this indicates a product which already exists as the output of a previous source
  805. if (sourceItr != intermediateSources.end() && productItr != intermediateSources.end())
  806. {
  807. if (AZStd::distance(sourceItr, productItr) <= 0)
  808. {
  809. return true;
  810. }
  811. }
  812. return false;
  813. }
  814. void AssetProcessorManager::AssetProcessed_Impl()
  815. {
  816. using AssetBuilderSDK::ProductOutputFlags;
  817. m_processedQueued = false;
  818. if (m_quitRequested || m_assetProcessedList.empty())
  819. {
  820. return;
  821. }
  822. // Note: if we get here, the scanning / createjobs phase has finished
  823. // because we no longer start any jobs until it has finished. So there is no reason
  824. // to delay notification or processing.
  825. // before we accept this outcome, do one final check to make sure its not about to double-address things by stomping on the same subID across many products.
  826. // let's also make sure that the same product was not emitted by some other job. we detect this by finding other jobs
  827. // with the same product, but with different sources.
  828. for (auto itProcessedAsset = m_assetProcessedList.begin(); itProcessedAsset != m_assetProcessedList.end(); )
  829. {
  830. AZStd::unordered_set<AZ::u32> existingSubIDs;
  831. bool remove = false;
  832. for (const AssetBuilderSDK::JobProduct& product : itProcessedAsset->m_response.m_outputProducts)
  833. {
  834. AssetUtilities::ProductPath productPath(product.m_productFileName, itProcessedAsset->m_entry.m_platformInfo.m_identifier);
  835. ProductAssetWrapper productWrapper(product, productPath);
  836. if (!existingSubIDs.insert(product.m_productSubID).second)
  837. {
  838. // insert pair<iter,bool> will return false if the item was already there, indicating a collision.
  839. QString sourceName = itProcessedAsset->m_entry.GetAbsoluteSourcePath();
  840. auto autofailReason = AZStd::string::format(
  841. "More than one product was emitted for this source file with the same SubID.\n"
  842. "Source file:\n"
  843. "%s\n"
  844. "Product SubID %u from product:\n"
  845. "%s\n"
  846. "Please check the builder code, specifically where it decides what subIds to assign to its output products and make sure it assigns a unique one to each.",
  847. sourceName.toUtf8().constData(),
  848. product.m_productSubID,
  849. product.m_productFileName.c_str());
  850. AutoFailJob("", autofailReason, itProcessedAsset);
  851. remove = true;
  852. break;
  853. }
  854. AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sources;
  855. if (productWrapper.HasIntermediateProduct() && CheckForIntermediateAssetLoop(
  856. itProcessedAsset->m_entry.m_sourceAssetReference, SourceAssetReference(productPath.GetIntermediatePath().c_str())))
  857. {
  858. // Loop detected
  859. auto errorMessage = AZStd::string::format(
  860. "An output loop has been detected. File %s has already been output as an intermediate in the processing chain. "
  861. "This is most likely an issue that must be fixed in the builder (%s)",
  862. productPath.GetRelativePath().c_str(), itProcessedAsset->m_entry.m_builderGuid.ToString<AZStd::string>().c_str());
  863. AutoFailJob(errorMessage, errorMessage, itProcessedAsset);
  864. productWrapper.DeleteFiles(false);
  865. FailTopLevelSourceForIntermediate(itProcessedAsset->m_entry.m_sourceAssetReference, errorMessage);
  866. remove = true;
  867. break;
  868. }
  869. // Check if there is an intermediate product that conflicts with a normal source asset
  870. // Its possible for the intermediate product to process first, so we need to do this check
  871. // for both the intermediate product and normal products
  872. if (productWrapper.HasIntermediateProduct())
  873. {
  874. if (auto result = CheckIntermediateProductConflict(productPath.GetRelativePath().c_str());
  875. result.m_type != ConflictResult::ConflictType::None)
  876. {
  877. if (result.m_type == ConflictResult::ConflictType::Intermediate)
  878. {
  879. auto errorMessage = AZStd::string::format(
  880. "Asset (%s) has produced an intermediate asset file which conflicts with an existing source asset "
  881. "with the same relative path: %s. Please move/rename one of the files to fix the conflict.",
  882. itProcessedAsset->m_entry.m_sourceAssetReference.AbsolutePath().c_str(),
  883. result.m_conflictingFile.AbsolutePath().c_str());
  884. // Fail this job and delete its files, since it might actually be the top level source, and since we haven't
  885. // recorded it yet, FailTopLevelSourceForIntermediate will do nothing in that case
  886. AutoFailJob(errorMessage, errorMessage, itProcessedAsset);
  887. productWrapper.DeleteFiles(false);
  888. FailTopLevelSourceForIntermediate(itProcessedAsset->m_entry.m_sourceAssetReference, errorMessage);
  889. remove = true;
  890. break;
  891. }
  892. else
  893. {
  894. auto errorMessage = AZStd::string::format(
  895. "Asset (%s) has produced an intermediate asset file which conflicts with an existing source asset "
  896. "with the same relative path: %s. Please move/rename one of the files to fix the conflict.",
  897. result.m_conflictingFile.AbsolutePath().c_str(),
  898. itProcessedAsset->m_entry.m_sourceAssetReference.AbsolutePath().c_str());
  899. // We need to fail the other, intermediate asset job
  900. FailTopLevelSourceForIntermediate(result.m_conflictingFile, errorMessage);
  901. }
  902. }
  903. }
  904. if(!remove && !productWrapper.IsValid())
  905. {
  906. auto errorMessage = AZStd::string::format("Output product %s for file %s is not valid. The file may have been deleted unexpectedly or have an invalid path.",
  907. product.m_productFileName.c_str(),
  908. itProcessedAsset->m_entry.GetAbsoluteSourcePath().toUtf8().constData()
  909. );
  910. AutoFailJob(errorMessage, errorMessage, itProcessedAsset);
  911. continue;
  912. }
  913. if(!remove && !productWrapper.ExistOnDisk(true))
  914. {
  915. remove = true;
  916. }
  917. if(!remove)
  918. {
  919. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobEntries;
  920. if (m_stateData->GetJobsByProductName(productPath.GetDatabasePath().c_str(), jobEntries, AZ::Uuid::CreateNull(), QString(), QString(), AzToolsFramework::AssetSystem::JobStatus::Completed))
  921. {
  922. for (auto& job : jobEntries)
  923. {
  924. AzToolsFramework::AssetDatabase::SourceDatabaseEntry source;
  925. if (m_stateData->GetSourceBySourceID(job.m_sourcePK, source))
  926. {
  927. if (AzFramework::StringFunc::Equal(source.m_sourceName.c_str(), itProcessedAsset->m_entry.m_sourceAssetReference.RelativePath().c_str()))
  928. {
  929. if (!AzFramework::StringFunc::Equal(job.m_jobKey.c_str(), itProcessedAsset->m_entry.m_jobKey.toUtf8().data())
  930. && AzFramework::StringFunc::Equal(job.m_platform.c_str(), itProcessedAsset->m_entry.m_platformInfo.m_identifier.c_str()))
  931. {
  932. // If we are here it implies that for the same source file we have another job that outputs the same product.
  933. // This is usually the case when two builders process the same source file and outputs the same product file.
  934. remove = true;
  935. AZStd::string consoleMsg = AZStd::string::format(
  936. "Failing Job (source : %s , jobkey %s) because another job (source : %s , jobkey : %s ) "
  937. "outputted the same product %s.\n",
  938. itProcessedAsset->m_entry.m_sourceAssetReference.AbsolutePath().c_str(),
  939. itProcessedAsset->m_entry.m_jobKey.toUtf8().data(),
  940. source.m_sourceName.c_str(),
  941. job.m_jobKey.c_str(),
  942. productPath.GetRelativePath().c_str());
  943. AZStd::string autoFailReason = AZStd::string::format(
  944. "Source file ( %s ) and builder (%s) are also outputting the product (%s)."
  945. "Please ensure that the same product file is not output to the cache multiple times by the same or different builders.\n",
  946. source.m_sourceName.c_str(),
  947. job.m_builderGuid.ToString<AZStd::string>().c_str(),
  948. productPath.GetCachePath().c_str());
  949. AutoFailJob(consoleMsg, autoFailReason, itProcessedAsset);
  950. }
  951. }
  952. else
  953. {
  954. remove = true;
  955. //this means we have a dupe product name for a different source
  956. //usually this is caused by /blah/x.tif and an /blah/x.dds in the source folder
  957. //they both become /blah/x.dds in the cache
  958. //Not much of an option here, if we find a dupe we already lost access to the
  959. //first one in the db because it was overwritten. So do not commit this new one and
  960. //set the first for reprocessing. That way we will get the original back.
  961. //delete the original sources products
  962. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  963. m_stateData->GetProductsBySourceID(source.m_sourceID, products);
  964. DeleteProducts(products);
  965. auto jobFingerprint = job.m_fingerprint;
  966. //set the fingerprint to failed
  967. job.m_fingerprint = FAILED_FINGERPRINT;
  968. m_stateData->SetJob(job);
  969. //delete product files for this new source
  970. for (const auto& outputProduct : itProcessedAsset->m_response.m_outputProducts)
  971. {
  972. // The product file path is always lower cased, we can't check that for existance.
  973. // Let rebuild a fs sensitive file path by replacing the cache path.
  974. // We assume any file paths normalized, ie no .. nor (back) slashes.
  975. AssetUtilities::ProductPath outputProductPath(outputProduct.m_productFileName, itProcessedAsset->m_entry.m_platformInfo.m_identifier);
  976. ProductAssetWrapper wrapper(outputProduct, outputProductPath);
  977. // This will handle outputting debug messages on its own
  978. wrapper.DeleteFiles(false);
  979. }
  980. //let people know what happened
  981. AZStd::string consoleMsg = AZStd::string::format("%s has failed because another source %s has already produced the same product %s. Rebuild the original Source.\n",
  982. itProcessedAsset->m_entry.m_sourceAssetReference.AbsolutePath().c_str(), source.m_sourceName.c_str(), productPath.GetRelativePath().c_str());
  983. AZStd::string fullSourcePath = source.m_sourceName;
  984. AZStd::string autoFailReason = AZStd::string::format(
  985. "A different source file\n%s\nis already outputting the product\n%s\n"
  986. "Please check other files in the same folder as source file and make sure no two sources output the product file.\n"
  987. "For example, you can't have a DDS file and a TIF file in the same folder, as they would cause overwriting.\n",
  988. fullSourcePath.c_str(),
  989. productPath.GetCachePath().c_str());
  990. AutoFailJob(consoleMsg, autoFailReason, itProcessedAsset, source.m_sourceID, jobFingerprint);
  991. //recycle the original source
  992. AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanfolder;
  993. if (m_stateData->GetScanFolderByScanFolderID(source.m_scanFolderPK, scanfolder))
  994. {
  995. fullSourcePath = AZStd::string::format("%s/%s", scanfolder.m_scanFolder.c_str(), source.m_sourceName.c_str());
  996. AssessFileInternal(fullSourcePath.c_str(), false);
  997. }
  998. }
  999. }
  1000. }
  1001. }
  1002. }
  1003. }
  1004. if (remove)
  1005. {
  1006. //we found a dupe remove this entry from the processed list so it does not get into the db
  1007. itProcessedAsset = m_assetProcessedList.erase(itProcessedAsset);
  1008. }
  1009. else
  1010. {
  1011. ++itProcessedAsset;
  1012. }
  1013. }
  1014. //process the asset list
  1015. for (AssetProcessedEntry& processedAsset : m_assetProcessedList)
  1016. {
  1017. // update products / delete no longer relevant products
  1018. // note that the cache stores products WITH the name of the platform in it so you don't have to do anything
  1019. // to those strings to process them.
  1020. //create/update the source record for this job
  1021. AzToolsFramework::AssetDatabase::SourceDatabaseEntry source;
  1022. auto sourceUuidOutcome = AssetUtilities::GetSourceUuid(processedAsset.m_entry.m_sourceAssetReference);
  1023. if (!m_stateData->GetSourceBySourceNameScanFolderId(
  1024. processedAsset.m_entry.m_sourceAssetReference.RelativePath().c_str(),
  1025. processedAsset.m_entry.m_sourceAssetReference.ScanFolderId(),
  1026. source))
  1027. {
  1028. //if we didn't find a source, we make a new source
  1029. //add the new source
  1030. AddSourceToDatabase(source, processedAsset.m_entry.m_sourceAssetReference);
  1031. }
  1032. else if (sourceUuidOutcome && sourceUuidOutcome.GetValue() != source.m_sourceGuid)
  1033. {
  1034. // UUID has changed, update catalog and database
  1035. HandleSourceUuidChange(source, sourceUuidOutcome.GetValue());
  1036. }
  1037. //create/update the job
  1038. AzToolsFramework::AssetDatabase::JobDatabaseEntry job;
  1039. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs;
  1040. if (m_stateData->GetJobsBySourceID(source.m_sourceID, jobs, processedAsset.m_entry.m_builderGuid, processedAsset.m_entry.m_jobKey, processedAsset.m_entry.m_platformInfo.m_identifier.c_str()))
  1041. {
  1042. AZ_Assert(jobs.size() == 1, "Should have only found one job!!!");
  1043. job = AZStd::move(jobs[0]);
  1044. }
  1045. else
  1046. {
  1047. //if we didn't find a job, we make a new one
  1048. job.m_sourcePK = source.m_sourceID;
  1049. }
  1050. job.m_fingerprint = processedAsset.m_entry.m_computedFingerprint;
  1051. job.m_jobKey = processedAsset.m_entry.m_jobKey.toUtf8().constData();
  1052. job.m_platform = processedAsset.m_entry.m_platformInfo.m_identifier;
  1053. job.m_builderGuid = processedAsset.m_entry.m_builderGuid;
  1054. job.m_jobRunKey = processedAsset.m_entry.m_jobRunKey;
  1055. if (!AZ::IO::FileIOBase::GetInstance()->Exists(job.m_lastLogFile.c_str()))
  1056. {
  1057. // its okay for the log to not exist, if there was no log for it (for example simple jobs that just copy assets and did not encounter any problems will generate no logs)
  1058. job.m_lastLogFile.clear();
  1059. }
  1060. // delete any previous failed job logs:
  1061. bool deletedFirstFailedLog = EraseLogFile(job.m_firstFailLogFile.c_str());
  1062. bool deletedLastFailedLog = EraseLogFile(job.m_lastFailLogFile.c_str());
  1063. // also delete the existing log file since we're about to replace it:
  1064. EraseLogFile(job.m_lastLogFile.c_str());
  1065. // if we deleted them, then make sure the DB no longer tracks them either.
  1066. if (deletedLastFailedLog)
  1067. {
  1068. job.m_lastFailLogTime = 0;
  1069. job.m_lastFailLogFile.clear();
  1070. }
  1071. if (deletedFirstFailedLog)
  1072. {
  1073. job.m_firstFailLogTime = 0;
  1074. job.m_firstFailLogFile.clear();
  1075. }
  1076. //set the new status and update log
  1077. job.m_status = JobStatus::Completed;
  1078. job.m_lastLogTime = QDateTime::currentMSecsSinceEpoch();
  1079. job.m_lastLogFile = AssetUtilities::ComputeJobLogFolder() + "/" + AssetUtilities::ComputeJobLogFileName(processedAsset.m_entry);
  1080. JobDiagnosticInfo info{};
  1081. JobDiagnosticRequestBus::BroadcastResult(info, &JobDiagnosticRequestBus::Events::GetDiagnosticInfo, job.m_jobRunKey);
  1082. job.m_warningCount = info.m_warningCount;
  1083. job.m_errorCount = info.m_errorCount;
  1084. // create/update job:
  1085. if (!m_stateData->SetJob(job))
  1086. {
  1087. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to update the job in the database!");
  1088. }
  1089. //query prior products for this job id
  1090. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer priorProducts;
  1091. m_stateData->GetProductsByJobID(job.m_jobID, priorProducts);
  1092. ProductOutputUtil::FinalizeProduct(
  1093. m_stateData,
  1094. m_platformConfig,
  1095. processedAsset.m_entry.m_sourceAssetReference,
  1096. processedAsset.m_response.m_outputProducts,
  1097. processedAsset.m_entry.m_platformInfo.m_identifier);
  1098. //make new product entries from the job response output products
  1099. ProductInfoList newProducts;
  1100. AZStd::vector<AZStd::vector<AZ::u32> > newLegacySubIDs; // each product has a vector of legacy subids;
  1101. for (const AssetBuilderSDK::JobProduct& product : processedAsset.m_response.m_outputProducts)
  1102. {
  1103. AssetUtilities::ProductPath productPath(product.m_productFileName, processedAsset.m_entry.m_platformInfo.m_identifier);
  1104. ProductAssetWrapper wrapper{ product, productPath };
  1105. //make a new product entry for this file
  1106. AzToolsFramework::AssetDatabase::ProductDatabaseEntry newProduct;
  1107. newProduct.m_jobPK = job.m_jobID;
  1108. newProduct.m_productName = productPath.GetDatabasePath();
  1109. newProduct.m_assetType = product.m_productAssetType;
  1110. newProduct.m_subID = product.m_productSubID;
  1111. newProduct.m_hash = wrapper.ComputeHash();
  1112. newProduct.m_flags = static_cast<AZ::s64>(product.m_outputFlags);
  1113. // This is the legacy product guid, its only use is for backward compatibility as before the asset id's guid was created off of the relative product name.
  1114. // Right now when we query for an asset guid we first match on the source guid which is correct and secondarily match on the product guid. Eventually this will go away.
  1115. // Strip the <asset_platform> from the front of a relative product path
  1116. newProduct.m_legacyGuid = AZ::Uuid::CreateName(productPath.GetRelativePath().c_str());
  1117. //push back the new product into the new products list
  1118. newProducts.emplace_back(newProduct, &product);
  1119. newLegacySubIDs.push_back(product.m_legacySubIDs);
  1120. }
  1121. // To find the set of products that were either new, or updated, this code starts with the new products, and erases
  1122. // the prior products that are exactly the same from that list.
  1123. // Note that because it uses operator==, it includes comparing the hash. This means that if the file data has changed
  1124. // it won't count as being the same, and will not remove it from the list. This results in 'updatedProducts' containing
  1125. // the list of new products that were EITHER literally new, or, had data that was new/changed.
  1126. auto updatedProducts = newProducts;
  1127. if(!updatedProducts.empty())
  1128. {
  1129. for (const auto& priorProductEntry : priorProducts)
  1130. {
  1131. updatedProducts.erase(AZStd::remove_if(updatedProducts.begin(), updatedProducts.end(), [&priorProductEntry](const auto& pair){ return pair.first == priorProductEntry; }), updatedProducts.end());
  1132. }
  1133. }
  1134. // Remove any lingering product files from the previous build that no longer exist.
  1135. // Get that set by starting with the set of products from last time, and erasing any that were emitted this time
  1136. // which leaves just the products that were emitted last time, and not this time.
  1137. // Note that in this case, only care whether the product represents the same logical asset, not whether its hash
  1138. // changed or not. Doing it this way will result in priorProducts containing only those products that were emitted last time
  1139. // and not this time regardless of what the hash or flags may have changed to (only comparing identifier fields).
  1140. if (!priorProducts.empty())
  1141. {
  1142. for (const auto& pair : newProducts)
  1143. {
  1144. const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& newProductEntry = pair.first;
  1145. auto logicalCompare = [&newProductEntry](const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& other)
  1146. {
  1147. return other.IsSameLogicalProductAs(newProductEntry);
  1148. };
  1149. priorProducts.erase(AZStd::remove_if(priorProducts.begin(), priorProducts.end(), logicalCompare), priorProducts.end());
  1150. }
  1151. }
  1152. // we need to delete these product files from the disk as they no longer exist and inform everyone we did so
  1153. for (const auto& priorProduct : priorProducts)
  1154. {
  1155. auto productPath = AssetUtilities::ProductPath::FromDatabasePath(priorProduct.m_productName);
  1156. bool shouldDeleteFile = true;
  1157. for (const auto& pair : newProducts)
  1158. {
  1159. const auto& currentProduct = pair.first;
  1160. if (AzFramework::StringFunc::Equal(currentProduct.m_productName.c_str(), priorProduct.m_productName.c_str()))
  1161. {
  1162. // This is a special case - The subID and other fields differ but it outputs the same actual product file on disk
  1163. // so let's not delete that product file since by the time we get here, it has already replaced it in the cache folder
  1164. // with the new product.
  1165. shouldDeleteFile = false;
  1166. break;
  1167. }
  1168. }
  1169. //delete the full file path
  1170. if (shouldDeleteFile)
  1171. {
  1172. DeleteProducts({ priorProduct });
  1173. }
  1174. else
  1175. {
  1176. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "File %s was replaced with a new, but different file.\n", productPath.GetCachePath().c_str());
  1177. // Don't report that the file has been removed as it's still there, but as a different kind of file (different sub id, type, etc.).
  1178. }
  1179. AZ_TracePrintf(AssetProcessor::DebugChannel, "Removed lingering prior product %s\n", priorProduct.ToString().c_str());
  1180. }
  1181. //trace that we are about to update the products in the database
  1182. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Processed \"%s\" (\"%s\")... \n",
  1183. processedAsset.m_entry.m_sourceAssetReference.AbsolutePath().c_str(),
  1184. processedAsset.m_entry.m_platformInfo.m_identifier.c_str());
  1185. AZ_TracePrintf(AssetProcessor::DebugChannel, "JobKey \"%s\", Builder UUID \"%s\", Fingerprint %u ) \n",
  1186. processedAsset.m_entry.m_jobKey.toUtf8().constData(),
  1187. processedAsset.m_entry.m_builderGuid.ToString<AZStd::string>().c_str(),
  1188. processedAsset.m_entry.m_computedFingerprint);
  1189. for (const AZStd::string& affectedSourceFile : processedAsset.m_response.m_sourcesToReprocess)
  1190. {
  1191. // the data coming in m_sourcesToReprocess comes directly from a builder, we have to sanitize, check, etc.
  1192. QString sanitizedFilePath = AssetUtilities::NormalizeFilePath(QString::fromUtf8(affectedSourceFile.c_str())).toUtf8().constData();
  1193. if (IsInCacheFolder(sanitizedFilePath.toUtf8().constData()))
  1194. {
  1195. // this is a silent notification, that is, it makes it into the log but not
  1196. // into any ui display or console.
  1197. AZ_TracePrintf(AssetProcessor::DebugChannel,
  1198. "File \"%s\" \n"
  1199. "Builder UUID \"%s\" \n"
  1200. "requested reprocess \"%s\" \n"
  1201. "but that file lives in the Cache folder. \nIgnored.\n",
  1202. processedAsset.m_entry.m_sourceAssetReference.AbsolutePath().c_str(),
  1203. processedAsset.m_entry.m_builderGuid.ToFixedString().c_str(),
  1204. affectedSourceFile.c_str());
  1205. continue;
  1206. }
  1207. // is it even in a scan folder?
  1208. QString watchedFolder;
  1209. QString relPath;
  1210. if (!m_platformConfig->ConvertToRelativePath(sanitizedFilePath, watchedFolder, relPath))
  1211. {
  1212. // this is probably a mistake by either the builder author, or asset author, so
  1213. // it gets surfaced to the UI.
  1214. AZ_Warning(
  1215. AssetProcessor::DebugChannel,
  1216. false,
  1217. "File \"%s\" \n"
  1218. "Builder UUID \"%s\" \n"
  1219. "requested reprocess \"%s\" \n"
  1220. "That file does not live in any folder monitored by AP. \nIgnored.\n",
  1221. processedAsset.m_entry.m_sourceAssetReference.AbsolutePath().c_str(),
  1222. processedAsset.m_entry.m_builderGuid.ToFixedString().c_str(),
  1223. affectedSourceFile.c_str());
  1224. continue;
  1225. }
  1226. if (m_platformConfig->IsFileExcluded(sanitizedFilePath))
  1227. {
  1228. // This is a silent notification, that is, it makes it into the log but not
  1229. // into any ui display or console.
  1230. AZ_TracePrintf(
  1231. AssetProcessor::DebugChannel,
  1232. "File \"%s\" \n"
  1233. "Builder UUID \"%s\" \n"
  1234. "requested reprocess \"%s\" \n"
  1235. "That file is excluded by an exclude rule. \nIgnored.\n",
  1236. processedAsset.m_entry.m_sourceAssetReference.AbsolutePath().c_str(),
  1237. processedAsset.m_entry.m_builderGuid.ToFixedString().c_str(),
  1238. affectedSourceFile.c_str());
  1239. continue;
  1240. }
  1241. AssessFileInternal(sanitizedFilePath.toUtf8().constData(), false);
  1242. }
  1243. // If there are any new or updated products, trigger any source dependencies which depend on a specific product
  1244. if(!updatedProducts.empty())
  1245. {
  1246. QStringList dependencies = GetSourceFilesWhichDependOnSourceFile(processedAsset.m_entry.GetAbsoluteSourcePath(), updatedProducts);
  1247. for(const auto& dependency : dependencies)
  1248. {
  1249. AssessFileInternal(dependency, false);
  1250. }
  1251. }
  1252. // Check for any jobs that previously failed due to a conflict.
  1253. // This allows users to fix the 'successful' job in a conflict and have the failed job reprocess automatically.
  1254. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer priorConflictedJobs;
  1255. if(m_stateData->GetJobsByFailureCauseSourceId(source.m_sourceID, priorConflictedJobs))
  1256. {
  1257. for(const auto& conflictedJob : priorConflictedJobs)
  1258. {
  1259. // If the fingerprint has changed, try re-running the job.
  1260. // The fingerprint check prevents an infinite loop because the job being queued will re-run this job if it fails again.
  1261. if (conflictedJob.m_failureCauseFingerprint != job.m_fingerprint)
  1262. {
  1263. AzToolsFramework::AssetDatabase::SourceDatabaseEntry conflictedSource;
  1264. if (m_stateData->GetSourceBySourceID(conflictedJob.m_sourcePK, conflictedSource))
  1265. {
  1266. SourceAssetReference conflictedSourceRef(
  1267. conflictedSource.m_scanFolderPK, conflictedSource.m_sourceName.c_str());
  1268. AZ_Info(
  1269. AssetProcessor::ConsoleChannel,
  1270. "Re-queuing previously conflicted source " AZ_STRING_FORMAT " - source " AZ_STRING_FORMAT
  1271. " has changed and may no longer conflict\n",
  1272. AZ_STRING_ARG(conflictedSource.m_sourceName),
  1273. AZ_STRING_ARG(source.m_sourceName));
  1274. AssessFileInternal(conflictedSourceRef.AbsolutePath().c_str(), false);
  1275. }
  1276. }
  1277. }
  1278. }
  1279. auto* uuidInterface = AZ::Interface<AssetProcessor::IUuidRequests>::Get();
  1280. AZ_Assert(uuidInterface, "Programmer Error - IUuidRequests interface is not available.");
  1281. //set the new products
  1282. for (size_t productIdx = 0; productIdx < newProducts.size(); ++productIdx)
  1283. {
  1284. AZStd::unordered_set<AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry> dependencySet;
  1285. auto& pair = newProducts[productIdx];
  1286. auto pathDependencies = AZStd::move(pair.second->m_pathDependencies);
  1287. AZStd::vector<AssetBuilderSDK::ProductDependency> resolvedDependencies;
  1288. m_pathDependencyManager->ResolveDependencies(pathDependencies, resolvedDependencies, job.m_platform, pair.first.m_productName);
  1289. AzToolsFramework::AssetDatabase::ProductDatabaseEntry productEntry;
  1290. if (pair.first.m_productID == AzToolsFramework::AssetDatabase::InvalidEntryId)
  1291. {
  1292. m_stateData->GetProductByJobIDSubId(pair.first.m_jobPK, pair.second->m_productSubID, productEntry);
  1293. }
  1294. WriteProductTableInfo(pair, newLegacySubIDs[productIdx], dependencySet, job.m_platform);
  1295. // Add the resolved path dependencies to the dependency set
  1296. for (const auto& resolvedPathDep : resolvedDependencies)
  1297. {
  1298. dependencySet.emplace(pair.first.m_productID, resolvedPathDep.m_dependencyId.m_guid, resolvedPathDep.m_dependencyId.m_subId, resolvedPathDep.m_flags, job.m_platform, false);
  1299. }
  1300. // Ensure this product does not list itself as a product dependency
  1301. auto conflictItr = find_if(dependencySet.begin(), dependencySet.end(),
  1302. [&](AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry& dependencyEntry)
  1303. {
  1304. return dependencyEntry.m_dependencySubID == pair.first.m_subID
  1305. && dependencyEntry.m_dependencySourceGuid == source.m_sourceGuid;
  1306. });
  1307. if (conflictItr != dependencySet.end())
  1308. {
  1309. dependencySet.erase(conflictItr);
  1310. AZ_Warning(AssetProcessor::ConsoleChannel, false,
  1311. "Invalid dependency: Product Asset ( %s ) has listed itself as one of its own Product Dependencies.",
  1312. pair.first.m_productName.c_str());
  1313. }
  1314. // Add all dependencies to the dependency container
  1315. AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer dependencyContainer;
  1316. dependencyContainer.reserve(dependencySet.size());
  1317. for(auto& entry : dependencySet)
  1318. {
  1319. // Attempt to update legacy UUID references to canonical UUIDs if possible
  1320. if (auto canonicalUuid = uuidInterface->GetCanonicalUuid(entry.m_dependencySourceGuid); canonicalUuid)
  1321. {
  1322. entry.m_dependencySourceGuid = canonicalUuid.value();
  1323. }
  1324. dependencyContainer.push_back(entry);
  1325. }
  1326. // Set the new dependencies
  1327. if (!m_stateData->SetProductDependencies(dependencyContainer))
  1328. {
  1329. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to set product dependencies");
  1330. }
  1331. // Save any unresolved dependencies
  1332. m_pathDependencyManager->SaveUnresolvedDependenciesToDatabase(pathDependencies, pair.first, job.m_platform);
  1333. // now we need notify everyone about the new products
  1334. AzToolsFramework::AssetDatabase::ProductDatabaseEntry& newProduct = pair.first;
  1335. // product name will be in the form "platform/relativeProductPath"
  1336. QString productName = QString::fromUtf8(newProduct.m_productName.c_str());
  1337. // the full file path is gotten by adding the product name to the cache root
  1338. QString fullProductPath = m_cacheRootDir.absoluteFilePath(productName);
  1339. // relative file path is gotten by removing the platform and game from the product name
  1340. // Strip the <asset_platform> from the front of a relative product path
  1341. AZStd::string relativeProductPath = AssetUtilities::StripAssetPlatform(productName.toUtf8().constData()).toUtf8().constData();
  1342. AssetNotificationMessage message(relativeProductPath, AssetNotificationMessage::AssetChanged, newProduct.m_assetType, processedAsset.m_entry.m_platformInfo.m_identifier.c_str());
  1343. AZ::Data::AssetId assetId(source.m_sourceGuid, newProduct.m_subID);
  1344. message.m_data = relativeProductPath;
  1345. message.m_sizeBytes = QFileInfo(fullProductPath).size();
  1346. message.m_assetId = assetId;
  1347. message.m_dependencies.reserve(dependencySet.size());
  1348. for (const auto& entry : dependencySet)
  1349. {
  1350. message.m_dependencies.emplace_back(AZ::Data::AssetId(entry.m_dependencySourceGuid, entry.m_dependencySubID), entry.m_dependencyFlags);
  1351. }
  1352. Q_EMIT AssetMessage(message);
  1353. AddKnownFoldersRecursivelyForFile(fullProductPath, m_cacheRootDir.absolutePath());
  1354. auto productPath = AssetUtilities::ProductPath::FromDatabasePath(pair.first.m_productName);
  1355. ProductAssetWrapper wrapper{*pair.second, productPath};
  1356. if (wrapper.HasIntermediateProduct())
  1357. {
  1358. // Now that we've verified that the output doesn't conflict with an existing source
  1359. // And we've updated the database, trigger processing the output
  1360. Q_EMIT IntermediateAssetCreated(QString::fromUtf8(productPath.GetIntermediatePath().c_str()));
  1361. AssessFileInternal(QString::fromUtf8(productPath.GetIntermediatePath().c_str()), false);
  1362. }
  1363. }
  1364. QString fullSourcePath = processedAsset.m_entry.GetAbsoluteSourcePath();
  1365. // notify the system about inputs:
  1366. Q_EMIT InputAssetProcessed(fullSourcePath, QString(processedAsset.m_entry.m_platformInfo.m_identifier.c_str()));
  1367. Q_EMIT AddedToCatalog(processedAsset.m_entry);
  1368. OnJobStatusChanged(processedAsset.m_entry, JobStatus::Completed);
  1369. // notify the analysis tracking system of our success (each processed entry is one job)
  1370. // do this after the various checks above and database updates, so that the finalization step can take it all into account if it needs to.
  1371. UpdateAnalysisTrackerForFile(processedAsset.m_entry, AnalysisTrackerUpdateType::JobFinished);
  1372. if (!QFile::exists(fullSourcePath))
  1373. {
  1374. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Source file %s deleted during processing - re-checking...\n",
  1375. fullSourcePath.toUtf8().constData());
  1376. AssessFileInternal(fullSourcePath, true);
  1377. }
  1378. }
  1379. m_assetProcessedList.clear();
  1380. // we know that things have changed at this point; ensure that we check for idle after we've finished processing all of our assets
  1381. // and don't rely on the file watcher to check again.
  1382. // If we rely on the file watcher only, it might fire before the AssetMessage signal has been responded to and the
  1383. // Asset Catalog may not realize that things are dirty by that point.
  1384. QueueIdleCheck();
  1385. }
  1386. void AssetProcessorManager::HandleSourceUuidChange(AzToolsFramework::AssetDatabase::SourceDatabaseEntry& source, AZ::Uuid newUuid)
  1387. {
  1388. const AZ::Uuid oldUuid = source.m_sourceGuid;
  1389. // Send a Removed message for each existing product, otherwise they'll just get stuck in the catalog
  1390. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer oldProducts;
  1391. m_stateData->GetProductsBySourceID(source.m_sourceID, oldProducts);
  1392. for (const auto& product : oldProducts)
  1393. {
  1394. AzToolsFramework::AssetDatabase::JobDatabaseEntry job;
  1395. m_stateData->GetJobByJobID(product.m_jobPK, job);
  1396. AZStd::string relativeProductPath = AssetUtilities::StripAssetPlatform(product.m_productName).toUtf8().constData();
  1397. AssetNotificationMessage oldAssetRemovedMessage(
  1398. relativeProductPath,
  1399. AzFramework::AssetSystem::AssetNotificationMessage::NotificationType::AssetRemoved,
  1400. product.m_assetType,
  1401. job.m_platform);
  1402. oldAssetRemovedMessage.m_assetId = AZ::Data::AssetId(oldUuid, product.m_subID);
  1403. Q_EMIT AssetMessage(oldAssetRemovedMessage);
  1404. }
  1405. // Update the database
  1406. source.m_sourceGuid = newUuid;
  1407. m_stateData->SetSource(source);
  1408. }
  1409. void AssetProcessorManager::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)
  1410. {
  1411. AzToolsFramework::AssetDatabase::ProductDatabaseEntry& newProduct = pair.first;
  1412. const AssetBuilderSDK::JobProduct* jobProduct = pair.second;
  1413. if (!m_stateData->SetProduct(newProduct))
  1414. {
  1415. //somethings wrong...
  1416. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to set new product in the the database!!! %s", newProduct.ToString().c_str());
  1417. }
  1418. else
  1419. {
  1420. m_stateData->RemoveLegacySubIDsByProductID(newProduct.m_productID);
  1421. for (AZ::u32 subId : subIds)
  1422. {
  1423. AzToolsFramework::AssetDatabase::LegacySubIDsEntry entryToCreate(newProduct.m_productID, subId);
  1424. m_stateData->CreateOrUpdateLegacySubID(entryToCreate);
  1425. }
  1426. // Remove all previous dependencies
  1427. if (!m_stateData->RemoveProductDependencyByProductId(newProduct.m_productID))
  1428. {
  1429. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to remove old product dependencies for product %d", newProduct.m_productID);
  1430. }
  1431. // Build up the list of new dependencies
  1432. for (auto& productDependency : jobProduct->m_dependencies)
  1433. {
  1434. dependencyContainer.emplace(newProduct.m_productID, productDependency.m_dependencyId.m_guid, productDependency.m_dependencyId.m_subId, productDependency.m_flags, platform, true);
  1435. }
  1436. }
  1437. }
  1438. void AssetProcessorManager::AssetProcessed(JobEntry jobEntry, AssetBuilderSDK::ProcessJobResponse response)
  1439. {
  1440. if (m_quitRequested)
  1441. {
  1442. return;
  1443. }
  1444. m_AssetProcessorIsBusy = true;
  1445. Q_EMIT AssetProcessorManagerIdleState(false);
  1446. // if its a fake "autosuccess job" or other reason for it not to exist in the DB, don't do anything here.
  1447. if (!jobEntry.m_addToDatabase)
  1448. {
  1449. return;
  1450. }
  1451. m_assetProcessedList.push_back(AssetProcessedEntry(jobEntry, response));
  1452. if (!m_processedQueued)
  1453. {
  1454. m_processedQueued = true;
  1455. AssetProcessed_Impl();
  1456. }
  1457. }
  1458. void AssetProcessorManager::CheckSource(const FileEntry& source)
  1459. {
  1460. // when this function is triggered, it means that a file appeared because it was modified or added or deleted,
  1461. // and the grace period has elapsed.
  1462. // this is the first point at which we MIGHT be interested in a file.
  1463. // to avoid flooding threads we queue these up for later checking.
  1464. AZ_TracePrintf(AssetProcessor::DebugChannel, "CheckSource: %s %s\n", source.m_fileName.toUtf8().constData(), source.m_isDelete ? "true" : "false");
  1465. QString normalizedFilePath = AssetUtilities::NormalizeFilePath(source.m_fileName);
  1466. if (!source.m_isFromScanner) // the scanner already checks for exclusions.
  1467. {
  1468. if (m_platformConfig->IsFileExcluded(normalizedFilePath))
  1469. {
  1470. return;
  1471. }
  1472. }
  1473. // Indicates if this CheckSource event was originally started due to a file change from a metadata file
  1474. bool triggeredByMetadata = false;
  1475. // if metadata file change, pretend the actual file changed
  1476. // the fingerprint will be different anyway since metadata file is folded in
  1477. for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
  1478. {
  1479. QPair<QString, QString> metaInfo = m_platformConfig->GetMetaDataFileTypeAt(idx);
  1480. QString originalName = normalizedFilePath;
  1481. if (normalizedFilePath.endsWith("." + metaInfo.first, Qt::CaseInsensitive))
  1482. {
  1483. //its a meta file. What was the original?
  1484. triggeredByMetadata = true;
  1485. normalizedFilePath = normalizedFilePath.left(normalizedFilePath.length() - (metaInfo.first.length() + 1));
  1486. if (!metaInfo.second.isEmpty())
  1487. {
  1488. // its not empty - replace the meta file with the original extension
  1489. normalizedFilePath += ".";
  1490. normalizedFilePath += metaInfo.second;
  1491. }
  1492. // we need the actual casing of the source file
  1493. // but the metafile might have different casing... Qt will fail to get the -actual- casing of the source file, which we need. It uses string ops internally.
  1494. // so we have to work around this by using the Dir that the file is in:
  1495. QFileInfo newInfo(normalizedFilePath);
  1496. QStringList searchPattern;
  1497. searchPattern << newInfo.fileName();
  1498. QStringList actualCasing = newInfo.absoluteDir().entryList(searchPattern, QDir::Files);
  1499. if (actualCasing.empty())
  1500. {
  1501. QString warning = QCoreApplication::translate("Warning", "Warning: Metadata file (%1) missing source file (%2)\n").arg(originalName).arg(normalizedFilePath);
  1502. AZ_TracePrintf(AssetProcessor::ConsoleChannel, warning.toUtf8().constData());
  1503. return;
  1504. }
  1505. // Record the modtime for the metadata file so we don't re-analyze this change again next time AP starts up
  1506. QFileInfo metadataFileInfo(originalName);
  1507. auto* scanFolder = m_platformConfig->GetScanFolderForFile(originalName);
  1508. if (scanFolder)
  1509. {
  1510. QString databaseName;
  1511. m_platformConfig->ConvertToRelativePath(originalName, scanFolder, databaseName);
  1512. m_stateData->UpdateFileModTimeAndHashByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(),
  1513. AssetUtilities::AdjustTimestamp(metadataFileInfo.lastModified()),
  1514. AssetUtilities::GetFileHash(metadataFileInfo.absoluteFilePath().toUtf8().constData()));
  1515. }
  1516. else
  1517. {
  1518. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Failed to find scanfolder for metadata file %s\n", originalName.toUtf8().constData());
  1519. }
  1520. // the casing might be different, too, so retrieve the actual case of the actual source file here:
  1521. normalizedFilePath = newInfo.absoluteDir().absoluteFilePath(actualCasing[0]);
  1522. break;
  1523. }
  1524. }
  1525. // Check if this event is part of an in-process file move.
  1526. // If so, ignore the event.
  1527. if (ShouldIgnorePendingMove(normalizedFilePath.toUtf8().constData(), triggeredByMetadata, source.m_isDelete))
  1528. {
  1529. AZ_Trace(
  1530. AssetProcessor::DebugChannel,
  1531. "Ignoring processing of " AZ_STRING_FORMAT " - file is marked as pending move\n",
  1532. AZ_STRING_ARG(normalizedFilePath));
  1533. return;
  1534. }
  1535. // Check if this event should be delayed for a while to allow time for moving/renaming to be completed
  1536. if (ShouldDelayProcessingFile(source, normalizedFilePath, triggeredByMetadata))
  1537. {
  1538. return;
  1539. }
  1540. // even if the entry already exists,
  1541. // overwrite the entry here, so if you modify, then delete it, its the latest action thats always on the list.
  1542. m_filesToExamine[normalizedFilePath] = FileEntry(normalizedFilePath, source.m_isDelete, source.m_isFromScanner, source.m_initialProcessTime);
  1543. // this block of code adds anything which DEPENDS ON the file that was changed, back into the queue so that files
  1544. // that depend on it also re-analyze in case they need rebuilding. However, files that are deleted will be added
  1545. // in CheckDeletedSourceFile instead, so there's no reason in that case to do that here.
  1546. if (!source.m_isDelete && (!source.m_isFromScanner || m_allowModtimeSkippingFeature))
  1547. {
  1548. // since the scanner walks over EVERY file, there's no reason to process dependencies during scan but it is necessary to process deletes.
  1549. // if modtime skipping is enabled, only changed files are processed, so we actually DO need to do this work when enabled
  1550. QStringList absoluteSourcePathList = GetSourceFilesWhichDependOnSourceFile(normalizedFilePath, {});
  1551. for (const QString& absolutePath : absoluteSourcePathList)
  1552. {
  1553. // we need to check if its already in the "active files" (things that we are looking over)
  1554. // or if its in the "currently being examined" list. The latter is likely to be the smaller list,
  1555. // so we check it first. Both of those are absolute paths, so we convert to absolute path before
  1556. // searching those lists:
  1557. if (m_filesToExamine.find(absolutePath) != m_filesToExamine.end())
  1558. {
  1559. // its already in the file to examine queue.
  1560. continue;
  1561. }
  1562. if (m_alreadyActiveFiles.find(absolutePath) != m_alreadyActiveFiles.end())
  1563. {
  1564. // its already been picked up by a file monitoring / scanning step.
  1565. continue;
  1566. }
  1567. if (source.m_fromDependencyChain.contains(absolutePath))
  1568. {
  1569. AZ_Trace(AssetProcessor::DebugChannel, "Ignoring dependant file: " AZ_STRING_FORMAT " - cyclic dependency detected\n", AZ_STRING_ARG(absolutePath));
  1570. continue;
  1571. }
  1572. AssessFileInternal(absolutePath, false, false, normalizedFilePath + QString(";") + source.m_fromDependencyChain);
  1573. }
  1574. }
  1575. m_AssetProcessorIsBusy = true;
  1576. if (!m_queuedExamination)
  1577. {
  1578. m_queuedExamination = true;
  1579. QTimer::singleShot(0, this, SLOT(ProcessFilesToExamineQueue()));
  1580. Q_EMIT NumRemainingJobsChanged(m_activeFiles.size() + m_filesToExamine.size() + m_numOfJobsToAnalyze, AnalysisExtraInfo());
  1581. }
  1582. }
  1583. void AssetProcessorManager::CheckDeletedProductFile(QString fullProductFile)
  1584. {
  1585. // this might be interesting, but only if its a known product!
  1586. // the dictionary in statedata stores only the relative path, not the platform.
  1587. // which means right now we have, for example
  1588. // d:/AutomatedTesting/Cache/ios/textures/favorite.tga
  1589. // ^^^^^^^^^ projectroot
  1590. // ^^^^^^^^^^^^^^^^^^^^^ cache root
  1591. // ^^^^^^^^^^^^^^^^^^^^^^^^^ platform root
  1592. {
  1593. QMutexLocker locker(&m_processingJobMutex);
  1594. auto found = m_processingProductInfoList.find(fullProductFile.toUtf8().constData());
  1595. if (found != m_processingProductInfoList.end())
  1596. {
  1597. // if we get here because we just deleted a product file before we copy/move the new product file
  1598. // than its totally safe to ignore this deletion.
  1599. return;
  1600. }
  1601. }
  1602. if (QFile::exists(fullProductFile))
  1603. {
  1604. // this is actually okay - it may have been temporarily deleted because it was in the process of being compiled.
  1605. return;
  1606. }
  1607. AZStd::string platform;
  1608. auto productPath = AssetUtilities::ProductPath::FromAbsoluteProductPath(fullProductFile.toUtf8().constData(), platform);
  1609. //remove the cache root from the cached product path
  1610. AZStd::string productDatabasePath = productPath.GetDatabasePath();
  1611. //we are going to force the processor to re process the source file associated with this product
  1612. //we do that by setting the fingerprint to some other value than which will be recomputed
  1613. //we only want to notify any listeners that the product file was removed for this particular product
  1614. AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sources;
  1615. if (!m_stateData->GetSourcesByProductName(productDatabasePath.c_str(), sources))
  1616. {
  1617. return;
  1618. }
  1619. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs;
  1620. if (!m_stateData->GetJobsByProductName(productDatabasePath.c_str(), jobs, AZ::Uuid::CreateNull(), QString(), platform.c_str()))
  1621. {
  1622. return;
  1623. }
  1624. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  1625. if (!m_stateData->GetProductsByProductName(productDatabasePath.c_str(), products, AZ::Uuid::CreateNull(), QString(), platform.c_str()))
  1626. {
  1627. return;
  1628. }
  1629. // pretend that its source changed. Add it to the things to keep watching so that in case MORE
  1630. // products change. We don't start processing until all have been deleted
  1631. for (auto& source : sources)
  1632. {
  1633. //we should only have one source
  1634. AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanfolder;
  1635. if (m_stateData->GetScanFolderByScanFolderID(source.m_scanFolderPK, scanfolder))
  1636. {
  1637. AZStd::string fullSourcePath = AZStd::string::format("%s/%s", scanfolder.m_scanFolder.c_str(), source.m_sourceName.c_str());
  1638. AssessFileInternal(fullSourcePath.c_str(), false);
  1639. }
  1640. }
  1641. //set the fingerprint on the job that made this product
  1642. for (auto& job : jobs)
  1643. {
  1644. for (auto& product : products)
  1645. {
  1646. if (job.m_jobID == product.m_jobPK)
  1647. {
  1648. //set failed fingerprint
  1649. job.m_fingerprint = FAILED_FINGERPRINT;
  1650. // clear it and then queue reprocess on its parent:
  1651. m_stateData->SetJob(job);
  1652. // note that over here, we do not notify connected clients that their product has vanished
  1653. // this is because we have a record of its source file, and it is in the queue for processing.
  1654. // Even if the source has disappeared too, that will simply result in the rest of the code
  1655. // dealing with this issue later when it figures that out.
  1656. // If the source file is reprocessed and no longer outputs this product, the "AssetProcessed_impl" function will handle notifying
  1657. // of actually removed products.
  1658. // If the source file is gone, that will notify for the products right there and then.
  1659. }
  1660. }
  1661. }
  1662. }
  1663. bool AssetProcessorManager::DeleteProducts(const AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& products)
  1664. {
  1665. bool successfullyRemoved = true;
  1666. // delete the products.
  1667. // products have names like "pc/textures/blah.dds" and do include platform roots!
  1668. // this means the actual full path is something like
  1669. // [cache root] / [platform]
  1670. for (const auto& product : products)
  1671. {
  1672. //get the source for this product
  1673. AzToolsFramework::AssetDatabase::SourceDatabaseEntry source;
  1674. if (!m_stateData->GetSourceByProductID(product.m_productID, source))
  1675. {
  1676. AZ_Error(AssetProcessor::ConsoleChannel, false, "Source for Product %s not found!!!", product.m_productName.c_str());
  1677. }
  1678. AZStd::string_view platform;
  1679. auto productPath = AssetUtilities::ProductPath::FromDatabasePath(product.m_productName, &platform);
  1680. AssetProcessor::ProductAssetWrapper wrapper{ product, productPath };
  1681. AZ_TracePrintf(AssetProcessor::ConsoleChannel,
  1682. "Deleting file %s because either its source file %s was removed or the builder did not emit this job.\n",
  1683. productPath.GetRelativePath().c_str(), source.m_sourceName.c_str());
  1684. successfullyRemoved = wrapper.DeleteFiles(true);
  1685. if(!successfullyRemoved)
  1686. {
  1687. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Failed to delete product files for %s\n", product.m_productName.c_str());
  1688. }
  1689. else
  1690. {
  1691. if (!m_stateData->RemoveProduct(product.m_productID))
  1692. {
  1693. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to remove Product %s", product.m_productName.c_str());
  1694. continue;
  1695. }
  1696. if(wrapper.HasCacheProduct())
  1697. {
  1698. AZ::Data::AssetId assetId(source.m_sourceGuid, product.m_subID);
  1699. AZ::Data::AssetId legacyAssetId(product.m_legacyGuid, 0);
  1700. AssetNotificationMessage message(productPath.GetRelativePath(), AssetNotificationMessage::AssetRemoved, product.m_assetType, AZ::OSString(platform.data(), platform.size()));
  1701. message.m_assetId = assetId;
  1702. // Note: legacy asset ids are not needed in the message, they'll be looked up based on the actual id
  1703. Q_EMIT AssetMessage(message);
  1704. }
  1705. if (wrapper.HasIntermediateProduct())
  1706. {
  1707. CheckDeletedSourceFile(SourceAssetReference(productPath.GetIntermediatePath().c_str()),
  1708. AZStd::chrono::steady_clock::now());
  1709. }
  1710. m_checkFoldersToRemove.insert(productPath.GetCachePath().c_str());
  1711. m_checkFoldersToRemove.insert(productPath.GetIntermediatePath().c_str());
  1712. }
  1713. }
  1714. return successfullyRemoved;
  1715. }
  1716. void AssetProcessorManager::CheckDeletedSourceFile(const SourceAssetReference& sourceAsset,
  1717. AZStd::chrono::steady_clock::time_point initialProcessTime)
  1718. {
  1719. // getting here means an input asset has been deleted
  1720. // and no overrides exist for it.
  1721. // we must delete its products.
  1722. using namespace AzToolsFramework::AssetDatabase;
  1723. // If we fail to delete a product, the deletion event gets requeued
  1724. // To avoid retrying forever, we keep track of the time of the first deletion failure and only retry
  1725. // if less than this amount of time has passed.
  1726. constexpr int MaxRetryPeriodMS = 500;
  1727. AZStd::chrono::duration<double, AZStd::milli> duration = AZStd::chrono::steady_clock::now() - initialProcessTime;
  1728. if (initialProcessTime > AZStd::chrono::steady_clock::time_point{}
  1729. && duration >= AZStd::chrono::milliseconds(MaxRetryPeriodMS))
  1730. {
  1731. AZ_Warning(AssetProcessor::ConsoleChannel, false, "Failed to delete product(s) from source file `%s` after retrying for %fms. Giving up.",
  1732. sourceAsset.AbsolutePath().c_str(), duration.count());
  1733. return;
  1734. }
  1735. bool deleteFailure = false;
  1736. AzToolsFramework::AssetDatabase::SourceDatabaseEntry source;
  1737. if (m_stateData->GetSourceBySourceNameScanFolderId(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderId(), source))
  1738. {
  1739. if (IsInIntermediateAssetsFolder(sourceAsset))
  1740. {
  1741. auto topLevelSource = AssetUtilities::GetTopLevelSourcePathForIntermediateAsset(sourceAsset, m_stateData);
  1742. if (topLevelSource)
  1743. {
  1744. if (AZ::IO::SystemFile::Exists(topLevelSource.value().c_str()))
  1745. {
  1746. // The top level file for this intermediate exists, treat this as a product deletion in that case which should
  1747. // regenerate the product
  1748. CheckDeletedProductFile(sourceAsset.AbsolutePath().c_str());
  1749. return;
  1750. }
  1751. else
  1752. {
  1753. // The top level file is gone, so we need to continue on to delete the child products
  1754. }
  1755. }
  1756. }
  1757. AzToolsFramework::AssetSystem::JobInfo jobInfo;
  1758. jobInfo.m_watchFolder = sourceAsset.ScanFolderPath().Native();
  1759. jobInfo.m_sourceFile = sourceAsset.RelativePath().Native();
  1760. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs;
  1761. if (m_stateData->GetJobsBySourceID(source.m_sourceID, jobs))
  1762. {
  1763. for (auto& job : jobs)
  1764. {
  1765. // ToDo: Add BuilderUuid here once we do the JobKey feature.
  1766. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  1767. if (m_stateData->GetProductsByJobID(job.m_jobID, products))
  1768. {
  1769. if (!DeleteProducts(products))
  1770. {
  1771. // DeleteProducts will make an attempt to retry deleting each product
  1772. // We can't just re-queue the whole file with CheckSource because we're deleting bits from the database as we go
  1773. deleteFailure = true;
  1774. CheckSource(FileEntry(
  1775. sourceAsset.AbsolutePath().c_str(), true, false,
  1776. initialProcessTime > AZStd::chrono::steady_clock::time_point{} ? initialProcessTime
  1777. : AZStd::chrono::steady_clock::now()));
  1778. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Delete failed on %s. Will retry!\n", sourceAsset.AbsolutePath().c_str());
  1779. continue;
  1780. }
  1781. }
  1782. else
  1783. {
  1784. // even with no products, still need to clear the fingerprint:
  1785. job.m_fingerprint = FAILED_FINGERPRINT;
  1786. m_stateData->SetJob(job);
  1787. }
  1788. // notify the GUI to remove any failed jobs that are currently onscreen:
  1789. jobInfo.m_platform = job.m_platform;
  1790. jobInfo.m_jobKey = job.m_jobKey;
  1791. Q_EMIT JobRemoved(jobInfo);
  1792. }
  1793. }
  1794. if (!deleteFailure)
  1795. {
  1796. // delete the source from the database too since otherwise it believes we have no products.
  1797. m_stateData->RemoveSource(source.m_sourceID);
  1798. }
  1799. }
  1800. if(deleteFailure)
  1801. {
  1802. return;
  1803. }
  1804. // Check if this file causes any file types to be re-evaluated
  1805. CheckMetaDataRealFiles(sourceAsset.AbsolutePath().c_str());
  1806. // when a source is deleted, we also have to queue anything that depended on it, for re-processing:
  1807. QStringList dependents = GetSourceFilesWhichDependOnSourceFile(sourceAsset.AbsolutePath().c_str(), {});
  1808. for (QString dependent : dependents)
  1809. {
  1810. AssessFileInternal(dependent, false);
  1811. }
  1812. // now that the right hand column (in terms of [thing] -> [depends on thing]) has been updated, eliminate anywhere its on the left
  1813. // hand side:
  1814. if (!source.m_sourceGuid.IsNull())
  1815. {
  1816. SourceFileDependencyEntryContainer results;
  1817. m_stateData->GetDependsOnSourceBySource(source.m_sourceGuid, SourceFileDependencyEntry::DEP_Any, results);
  1818. m_stateData->RemoveSourceFileDependencies(results);
  1819. }
  1820. Q_EMIT SourceDeleted(sourceAsset); // note that this removes it from the RC Queue Model, also
  1821. }
  1822. void AssetProcessorManager::AddKnownFoldersRecursivelyForFile(QString fullFile, QString root)
  1823. {
  1824. QString normalizedRoot = AssetUtilities::NormalizeFilePath(root);
  1825. // also track parent folders up to the specified root.
  1826. QFileInfo fullFileInfo(fullFile);
  1827. QString parentFolderName = fullFileInfo.isDir() ? fullFileInfo.absoluteFilePath() : fullFileInfo.absolutePath();
  1828. QString normalizedParentFolder = AssetUtilities::NormalizeFilePath(parentFolderName);
  1829. if (!normalizedParentFolder.startsWith(normalizedRoot, Qt::CaseInsensitive))
  1830. {
  1831. return; // not interested in folders not in the root.
  1832. }
  1833. // Record the root while we're at it
  1834. // Scanfolders are folders too and in the rare case a user deletes one, we need to know it was a folder
  1835. m_knownFolders.insert(root);
  1836. while (normalizedParentFolder.compare(normalizedRoot, Qt::CaseInsensitive) != 0)
  1837. {
  1838. // QSet does not actually have a function that tells us if the set already contained as well as inserts it
  1839. // (unlike std::set and others) but an easy way to tell in o(1) is to just check if the size changed
  1840. int priorSize = m_knownFolders.size();
  1841. m_knownFolders.insert(normalizedParentFolder);
  1842. if (m_knownFolders.size() == priorSize)
  1843. {
  1844. // this folder was already there, and thus there's no point in further recursion because
  1845. // it would have already recursed the first time around.
  1846. break;
  1847. }
  1848. int pos = normalizedParentFolder.lastIndexOf(QChar('/'));
  1849. if (pos >= 0)
  1850. {
  1851. normalizedParentFolder = normalizedParentFolder.left(pos);
  1852. }
  1853. else
  1854. {
  1855. break; // no more slashes
  1856. }
  1857. }
  1858. }
  1859. void AssetProcessorManager::CheckMissingJobs(const SourceAssetReference& sourceAsset, const ScanFolderInfo* scanFolderInfo, const AZStd::vector<JobDetails>& jobsThisTime)
  1860. {
  1861. // Check to see if jobs were emitted last time by this builder, but are no longer being emitted this time - in which case we must eliminate old products.
  1862. // whats going to be in the database is fingerprints for each job last time
  1863. // this function is called once per source file, so in the array of jobsThisTime,
  1864. // the relative path will always be the same.
  1865. if (!sourceAsset && jobsThisTime.empty())
  1866. {
  1867. return;
  1868. }
  1869. // find all jobs from the last time of the platforms that are currently enabled
  1870. JobInfoContainer jobsFromLastTime;
  1871. for (const AssetBuilderSDK::PlatformInfo& platformInfo : scanFolderInfo->GetPlatforms())
  1872. {
  1873. QString platform = QString::fromUtf8(platformInfo.m_identifier.c_str());
  1874. m_stateData->GetJobInfoBySourceNameScanFolderId(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderId(), jobsFromLastTime, AZ::Uuid::CreateNull(), QString(), platform);
  1875. }
  1876. // Check for missing common jobs, too. The common platform won't be in the scan folder platform list.
  1877. m_stateData->GetJobInfoBySourceNameScanFolderId(sourceAsset.RelativePath().c_str(),
  1878. sourceAsset.ScanFolderId(),
  1879. jobsFromLastTime,
  1880. AZ::Uuid::CreateNull(),
  1881. QString(),
  1882. QString(AssetBuilderSDK::CommonPlatformName));
  1883. // so now we have jobsFromLastTime and jobsThisTime. Whats in last time that is no longer being emitted now?
  1884. if (jobsFromLastTime.empty())
  1885. {
  1886. return;
  1887. }
  1888. for (int oldJobIdx = azlossy_cast<int>(jobsFromLastTime.size()) - 1; oldJobIdx >= 0; --oldJobIdx)
  1889. {
  1890. const JobInfo& oldJobInfo = jobsFromLastTime[oldJobIdx];
  1891. // did we find it this time?
  1892. bool foundIt = false;
  1893. for (const JobDetails& newJobInfo : jobsThisTime)
  1894. {
  1895. // the relative path is insensitive because some legacy data didn't have the correct case.
  1896. if ((newJobInfo.m_jobEntry.m_builderGuid == oldJobInfo.m_builderGuid) &&
  1897. (QString::compare(newJobInfo.m_jobEntry.m_platformInfo.m_identifier.c_str(), oldJobInfo.m_platform.c_str()) == 0) &&
  1898. (QString::compare(newJobInfo.m_jobEntry.m_jobKey, oldJobInfo.m_jobKey.c_str()) == 0) &&
  1899. (QString::compare(newJobInfo.m_jobEntry.m_sourceAssetReference.RelativePath().c_str(), oldJobInfo.m_sourceFile.c_str(), Qt::CaseInsensitive) == 0)
  1900. )
  1901. {
  1902. foundIt = true;
  1903. break;
  1904. }
  1905. }
  1906. if (foundIt)
  1907. {
  1908. jobsFromLastTime.erase(jobsFromLastTime.begin() + oldJobIdx);
  1909. }
  1910. }
  1911. // at this point, we contain only the jobs that are left over from last time and not found this time.
  1912. //we want to remove all products for these jobs and the jobs
  1913. for (const JobInfo& oldJobInfo : jobsFromLastTime)
  1914. {
  1915. // ToDo: Add BuilderUuid here once we do the JobKey feature.
  1916. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  1917. if (m_stateData->GetProductsBySourceNameScanFolderID(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderId(), products, oldJobInfo.m_builderGuid, oldJobInfo.m_jobKey.c_str(), oldJobInfo.m_platform.c_str()))
  1918. {
  1919. char tempBuffer[128];
  1920. oldJobInfo.m_builderGuid.ToString(tempBuffer, AZ_ARRAY_SIZE(tempBuffer));
  1921. AZ_TracePrintf(DebugChannel, "Removing products for job (%s, %s, %s, %s, %s) since it is no longer being emitted by its builder.\n",
  1922. oldJobInfo.m_sourceFile.c_str(),
  1923. oldJobInfo.m_platform.c_str(),
  1924. oldJobInfo.m_jobKey.c_str(),
  1925. oldJobInfo.m_builderGuid.ToString<AZStd::string>().c_str(),
  1926. tempBuffer);
  1927. //delete products, which should remove them from the disk and database and send the notifications
  1928. DeleteProducts(products);
  1929. }
  1930. //remove the jobs associated with these products
  1931. m_stateData->RemoveJob(oldJobInfo.m_jobID);
  1932. Q_EMIT JobRemoved(oldJobInfo);
  1933. }
  1934. }
  1935. // clean all folders that are empty until you get to the root, or until you get to one that isn't empty.
  1936. void AssetProcessorManager::CleanEmptyFolder(QString folder, QString root)
  1937. {
  1938. QString normalizedRoot = AssetUtilities::NormalizeFilePath(root);
  1939. // also track parent folders up to the specified root.
  1940. QString normalizedParentFolder = AssetUtilities::NormalizeFilePath(folder);
  1941. QDir parentDir(folder);
  1942. // keep walking up the tree until we either run out of folders or hit the root.
  1943. while ((normalizedParentFolder.compare(normalizedRoot, Qt::CaseInsensitive) != 0) && (parentDir.exists()))
  1944. {
  1945. if (parentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot).empty())
  1946. {
  1947. if (!parentDir.rmdir(normalizedParentFolder))
  1948. {
  1949. break; // if we fail to remove for any reason we don't push our luck.
  1950. }
  1951. }
  1952. if (!parentDir.cdUp())
  1953. {
  1954. break;
  1955. }
  1956. normalizedParentFolder = AssetUtilities::NormalizeFilePath(parentDir.absolutePath());
  1957. }
  1958. }
  1959. void AssetProcessorManager::CheckModifiedSourceFile(const SourceAssetReference& sourceAsset, const ScanFolderInfo* scanFolderInfo)
  1960. {
  1961. // a potential input file was modified or added. We always pass these through our filters and potentially build it.
  1962. // before we know what to do, we need to figure out if it matches some filter we care about.
  1963. // note that if we get here during runtime, we've already eliminated overrides
  1964. // so this is the actual file of importance.
  1965. // check regexes.
  1966. // get list of recognizers which match
  1967. // for each platform in the recognizer:
  1968. // check the fingerprint and queue if appropriate!
  1969. // also queue if products missing.
  1970. // Check if this file causes any file types to be re-evaluated
  1971. CheckMetaDataRealFiles(sourceAsset.AbsolutePath().c_str());
  1972. // keep track of its parent folders so that if a folder disappears or is renamed, and we get the notification that this has occurred
  1973. // we will know that it *was* a folder before now (otherwise we'd have no idea)
  1974. AddKnownFoldersRecursivelyForFile(sourceAsset.AbsolutePath().c_str(), sourceAsset.ScanFolderPath().c_str());
  1975. ++m_numTotalSourcesFound;
  1976. AssetProcessor::BuilderInfoList builderInfoList;
  1977. AssetProcessor::AssetBuilderInfoBus::Broadcast(
  1978. &AssetProcessor::AssetBuilderInfoBus::Events::GetMatchingBuildersInfo, sourceAsset.AbsolutePath().c_str(), builderInfoList);
  1979. if (builderInfoList.size())
  1980. {
  1981. ++m_numSourcesNeedingFullAnalysis;
  1982. ProcessBuilders(sourceAsset, scanFolderInfo, builderInfoList);
  1983. }
  1984. else
  1985. {
  1986. CheckMissingJobs(sourceAsset, scanFolderInfo, {});
  1987. AZ_TracePrintf(AssetProcessor::DebugChannel, "Non-processed file: %s\n", sourceAsset.RelativePath().c_str());
  1988. ++m_numSourcesNotHandledByAnyBuilder;
  1989. // Record the modtime for the file so we know we've already processed it
  1990. QFileInfo fileInfo(sourceAsset.AbsolutePath().c_str());
  1991. QDateTime lastModifiedTime = fileInfo.lastModified();
  1992. m_stateData->UpdateFileModTimeAndHashByFileNameAndScanFolderId(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderId(),
  1993. AssetUtilities::AdjustTimestamp(lastModifiedTime),
  1994. AssetUtilities::GetFileHash(fileInfo.absoluteFilePath().toUtf8().constData()));
  1995. }
  1996. }
  1997. bool AssetProcessorManager::AnalyzeJob(JobDetails& jobDetails)
  1998. {
  1999. // This function checks to see whether we need to process an asset or not, it returns true if we need to process it and false otherwise
  2000. // It processes an asset if either there is a fingerprint mismatch between the computed and the last known fingerprint or if products are missing
  2001. bool shouldProcessAsset = false;
  2002. // First thing it checks is the computed fingerprint with its last known fingerprint in the database, if there is a mismatch than we need to process it
  2003. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs; //should only find one when we specify builder, job key, platform
  2004. bool foundInDatabase = m_stateData->GetJobsBySourceName(jobDetails.m_jobEntry.m_sourceAssetReference, jobs, jobDetails.m_jobEntry.m_builderGuid, jobDetails.m_jobEntry.m_jobKey, jobDetails.m_jobEntry.m_platformInfo.m_identifier.c_str());
  2005. if (foundInDatabase && jobs[0].m_fingerprint == jobDetails.m_jobEntry.m_computedFingerprint)
  2006. {
  2007. // If the fingerprint hasn't changed, we won't process it.. unless...is it missing a product.
  2008. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  2009. if (m_stateData->GetProductsBySourceNameScanFolderID(
  2010. jobDetails.m_jobEntry.m_sourceAssetReference.RelativePath().c_str(),
  2011. jobDetails.m_jobEntry.m_sourceAssetReference.ScanFolderId(),
  2012. products,
  2013. jobDetails.m_jobEntry.m_builderGuid,
  2014. jobDetails.m_jobEntry.m_jobKey,
  2015. jobDetails.m_jobEntry.m_platformInfo.m_identifier.c_str()))
  2016. {
  2017. for (const auto& product : products)
  2018. {
  2019. auto productPath = AssetUtilities::ProductPath::FromDatabasePath(product.m_productName);
  2020. ProductAssetWrapper wrapper{ product, productPath };
  2021. if(!wrapper.ExistOnDisk(true))
  2022. {
  2023. shouldProcessAsset = true;
  2024. }
  2025. else
  2026. {
  2027. QString absoluteCacheRoot = m_cacheRootDir.absolutePath();
  2028. if(wrapper.HasCacheProduct())
  2029. {
  2030. AddKnownFoldersRecursivelyForFile(productPath.GetCachePath().c_str(), absoluteCacheRoot);
  2031. }
  2032. if(wrapper.HasIntermediateProduct())
  2033. {
  2034. AddKnownFoldersRecursivelyForFile(productPath.GetIntermediatePath().c_str(),
  2035. AssetUtilities::GetIntermediateAssetsFolder(absoluteCacheRoot.toUtf8().constData()).AsPosix().c_str());
  2036. }
  2037. }
  2038. }
  2039. }
  2040. if (jobDetails.m_autoProcessJob)
  2041. {
  2042. AZ_TracePrintf(
  2043. AssetProcessor::DebugChannel,
  2044. "AnalyzeJob: auto process job for source '%s' job key '%s' platform '%s') \n",
  2045. jobDetails.m_jobEntry.m_sourceAssetReference.AbsolutePath().c_str(),
  2046. jobDetails.m_jobEntry.m_jobKey.toUtf8().constData(),
  2047. jobDetails.m_jobEntry.m_platformInfo.m_identifier.c_str());
  2048. shouldProcessAsset = true;
  2049. }
  2050. }
  2051. else
  2052. {
  2053. // The fingerprint for this job does not match last time the job was processed.
  2054. // Thus, we need to queue a job to process it
  2055. // If we are in this block of code, it means one of two things: either we didn't find it at all, or it doesn't match.
  2056. // For debugging, it is useful to be able to tell those two code paths apart, so make output a message which cna differentiate.
  2057. AZ_TracePrintf(AssetProcessor::DebugChannel, "AnalyzeJob: %s for source '%s' builder '%s' platform '%s' extra info '%s' job key '%s'\n",
  2058. foundInDatabase ? "fingerprint mismatch" : "new job",
  2059. jobDetails.m_jobEntry.m_sourceAssetReference.RelativePath().c_str(),
  2060. jobDetails.m_assetBuilderDesc.m_name.c_str(),
  2061. jobDetails.m_jobEntry.m_platformInfo.m_identifier.c_str(),
  2062. jobDetails.m_extraInformationForFingerprinting.c_str(),
  2063. jobDetails.m_jobEntry.m_jobKey.toUtf8().constData());
  2064. // Check whether another job emitted this job as a job dependency and if true, queue the dependent job source file also
  2065. JobDesc jobDesc(
  2066. jobDetails.m_jobEntry.m_sourceAssetReference,
  2067. jobDetails.m_jobEntry.m_jobKey.toUtf8().data(), jobDetails.m_jobEntry.m_platformInfo.m_identifier);
  2068. shouldProcessAsset = true;
  2069. QFileInfo file(jobDetails.m_jobEntry.GetAbsoluteSourcePath());
  2070. QDateTime dateTime(file.lastModified());
  2071. qint64 mSecsSinceEpoch = dateTime.toMSecsSinceEpoch();
  2072. auto foundSource = m_sourceFileModTimeMap.find(jobDetails.m_jobEntry.m_sourceFileUUID);
  2073. if (foundSource == m_sourceFileModTimeMap.end() || foundSource->second != mSecsSinceEpoch)
  2074. {
  2075. // send a sourceFile notification message only if its last modified time changed or
  2076. // we have not seen this source file before
  2077. m_sourceFileModTimeMap[jobDetails.m_jobEntry.m_sourceFileUUID] = mSecsSinceEpoch;
  2078. QString sourceFile(jobDetails.m_jobEntry.m_sourceAssetReference.RelativePath().c_str());
  2079. auto sourceUUIDOutcome = AssetUtilities::GetSourceUuid(jobDetails.m_jobEntry.m_sourceAssetReference);
  2080. if (sourceUUIDOutcome)
  2081. {
  2082. AzToolsFramework::AssetSystem::SourceFileNotificationMessage message(
  2083. AZ::OSString(sourceFile.toUtf8().constData()),
  2084. AZ::OSString(jobDetails.m_scanFolder->ScanPath().toUtf8().constData()),
  2085. AzToolsFramework::AssetSystem::SourceFileNotificationMessage::FileChanged,
  2086. sourceUUIDOutcome.GetValue());
  2087. AssetProcessor::ConnectionBus::Broadcast(&AssetProcessor::ConnectionBus::Events::Send, 0, message);
  2088. }
  2089. }
  2090. }
  2091. if (!shouldProcessAsset)
  2092. {
  2093. //AZ_TracePrintf(AssetProcessor::DebugChannel, "AnalyzeJob: UpToDate: not processing on %s.\n", jobDetails.m_jobEntry.m_platform.toUtf8().constData());
  2094. return false;
  2095. }
  2096. else
  2097. {
  2098. UpdateForCacheServer(jobDetails);
  2099. // macOS requires that the cacheRootDir to not be all lowercase, otherwise file copies will not work correctly.
  2100. // So use the lowerCasePath string to capture the parts that need to be lower case while keeping the cache root
  2101. // mixed case.
  2102. QString platformId = jobDetails.m_jobEntry.m_platformInfo.m_identifier.c_str();
  2103. // this may seem odd, but m_databaseSourceName includes the output prefix up front, and we're trying to find where to put it in the cache
  2104. // so we use the databaseSourceName instead of relpath.
  2105. QString pathRel = QFileInfo(jobDetails.m_jobEntry.m_sourceAssetReference.RelativePath().c_str()).path();
  2106. if (pathRel == ".")
  2107. {
  2108. // if its in the current folder, avoid using ./ or /.
  2109. pathRel = QString();
  2110. }
  2111. AssetUtilities::ProductPath productPath{ pathRel.toUtf8().constData(), platformId.toUtf8().constData() };
  2112. jobDetails.m_cachePath = productPath.GetCachePath();
  2113. jobDetails.m_intermediatePath = productPath.GetIntermediatePath();
  2114. jobDetails.m_relativePath = productPath.GetRelativePath();
  2115. }
  2116. return true;
  2117. }
  2118. void AssetProcessorManager::UpdateForCacheServer(JobDetails& jobDetails)
  2119. {
  2120. AssetServerMode assetServerMode = AssetServerMode::Inactive;
  2121. AssetServerBus::BroadcastResult(assetServerMode, &AssetServerBus::Events::GetRemoteCachingMode);
  2122. if (assetServerMode == AssetServerMode::Inactive)
  2123. {
  2124. // Asset Cache Server mode feature is turned off
  2125. return;
  2126. }
  2127. else if (!m_platformConfig)
  2128. {
  2129. AZ_Error(AssetProcessor::ConsoleChannel, m_platformConfig, "Platform not configured. Called too soon?");
  2130. return;
  2131. }
  2132. auto& cacheRecognizerContainer = m_platformConfig->GetAssetCacheRecognizerContainer();
  2133. for(const auto& cacheRecognizerPair : cacheRecognizerContainer)
  2134. {
  2135. auto& cacheRecognizer = cacheRecognizerPair.second;
  2136. bool matchFound =
  2137. cacheRecognizer.m_patternMatcher.MatchesPath(jobDetails.m_jobEntry.m_sourceAssetReference.RelativePath().c_str());
  2138. bool builderNameMatches =
  2139. cacheRecognizer.m_name.compare(jobDetails.m_assetBuilderDesc.m_name.c_str()) == 0;
  2140. if (matchFound || builderNameMatches)
  2141. {
  2142. jobDetails.m_checkServer = cacheRecognizer.m_checkServer;
  2143. return;
  2144. }
  2145. }
  2146. }
  2147. void AssetProcessorManager::CheckDeletedCacheFolder(QString normalizedPath)
  2148. {
  2149. QDir checkDir(normalizedPath);
  2150. if (checkDir.exists())
  2151. {
  2152. // this is possible because it could have been moved back by the time we get here, in which case, we take no action.
  2153. return;
  2154. }
  2155. // going to need to iterate on all files there, recursively, in order to emit them as having been deleted.
  2156. // note that we don't scan here. We use the asset database.
  2157. QString cacheRootRemoved = m_cacheRootDir.relativeFilePath(normalizedPath);
  2158. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  2159. m_stateData->GetProductsLikeProductName(cacheRootRemoved, AzToolsFramework::AssetDatabase::AssetDatabaseConnection::StartsWith, products);
  2160. for (const auto& product : products)
  2161. {
  2162. auto productPath = AssetUtilities::ProductPath::FromDatabasePath(product.m_productName);
  2163. ProductAssetWrapper productWrapper{ product, productPath };
  2164. if (!productWrapper.ExistOnDisk(false))
  2165. {
  2166. AssessDeletedFile(productPath.GetCachePath().c_str());
  2167. }
  2168. }
  2169. m_knownFolders.remove(normalizedPath);
  2170. }
  2171. void AssetProcessorManager::CheckDeletedSourceFolder(const SourceAssetReference& sourceAsset)
  2172. {
  2173. AZ_TracePrintf(AssetProcessor::DebugChannel, "CheckDeletedSourceFolder...\n");
  2174. // we deleted a folder that is somewhere that is a watched input folder.
  2175. if (AZ::IO::SystemFile::Exists(sourceAsset.AbsolutePath().c_str()))
  2176. {
  2177. // this is possible because it could have been moved back by the time we get here, in which case, we take no action.
  2178. return;
  2179. }
  2180. AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sources;
  2181. m_stateData->GetSourcesLikeSourceNameScanFolderId(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderId(), AzToolsFramework::AssetDatabase::AssetDatabaseConnection::StartsWith, sources);
  2182. AZ_TracePrintf(AssetProcessor::DebugChannel, "CheckDeletedSourceFolder: %i matching files.\n", sources.size());
  2183. QDir scanFolder(sourceAsset.ScanFolderPath().c_str());
  2184. for (const auto& source : sources)
  2185. {
  2186. // reconstruct full path:
  2187. QString actualRelativePath = source.m_sourceName.c_str();
  2188. QString finalPath = scanFolder.absoluteFilePath(actualRelativePath);
  2189. if (!QFile::exists(finalPath))
  2190. {
  2191. AssessDeletedFile(finalPath);
  2192. }
  2193. }
  2194. m_knownFolders.remove(sourceAsset.AbsolutePath().c_str());
  2195. SourceFolderDeleted(sourceAsset.AbsolutePath().c_str());
  2196. }
  2197. namespace
  2198. {
  2199. void ScanFolderInternal(QString inputFolderPath, QStringList& outputs)
  2200. {
  2201. QDir inputFolder(inputFolderPath);
  2202. QFileInfoList entries = inputFolder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Files);
  2203. for (const QFileInfo& entry : entries)
  2204. {
  2205. if (entry.isDir())
  2206. {
  2207. //Entry is a directory
  2208. ScanFolderInternal(entry.absoluteFilePath(), outputs);
  2209. }
  2210. else
  2211. {
  2212. //Entry is a file
  2213. outputs.push_back(entry.absoluteFilePath());
  2214. }
  2215. }
  2216. }
  2217. }
  2218. void AssetProcessorManager::CheckMetaDataRealFiles(QString relativeSourceFile)
  2219. {
  2220. if (!m_platformConfig->IsMetaDataTypeRealFile(relativeSourceFile))
  2221. {
  2222. return;
  2223. }
  2224. QStringList extensions;
  2225. for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
  2226. {
  2227. const auto& metaExt = m_platformConfig->GetMetaDataFileTypeAt(idx);
  2228. if (!metaExt.second.isEmpty() && QString::compare(metaExt.first, relativeSourceFile, Qt::CaseInsensitive) == 0)
  2229. {
  2230. extensions.push_back(metaExt.second);
  2231. }
  2232. }
  2233. AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sources;
  2234. for (const auto& ext : extensions)
  2235. {
  2236. m_stateData->GetSourcesLikeSourceName(ext, AzToolsFramework::AssetDatabase::AssetDatabaseConnection::EndsWith, sources);
  2237. }
  2238. QString fullMatchingSourceFile;
  2239. for (const auto& source : sources)
  2240. {
  2241. fullMatchingSourceFile = m_platformConfig->FindFirstMatchingFile(source.m_sourceName.c_str());
  2242. if (!fullMatchingSourceFile.isEmpty())
  2243. {
  2244. AssessFileInternal(fullMatchingSourceFile, false);
  2245. }
  2246. }
  2247. }
  2248. void AssetProcessorManager::CheckCreatedSourceFolder(QString fullSourceFile)
  2249. {
  2250. AZ_TracePrintf(AssetProcessor::DebugChannel, "CheckCreatedSourceFolder...\n");
  2251. // this could have happened because its a directory rename
  2252. QDir checkDir(fullSourceFile);
  2253. if (!checkDir.exists())
  2254. {
  2255. // this is possible because it could have been moved back by the time we get here.
  2256. // find all assets that are products that have this as their normalized path and then indicate that they are all deleted.
  2257. AZ_TracePrintf(AssetProcessor::DebugChannel, "Directory (%s) does not exist.\n", fullSourceFile.toUtf8().data());
  2258. return;
  2259. }
  2260. // we actually need to scan this folder, without invoking the whole asset scanner:
  2261. const AssetProcessor::ScanFolderInfo* info = m_platformConfig->GetScanFolderForFile(fullSourceFile);
  2262. if (!info)
  2263. {
  2264. AZ_TracePrintf(AssetProcessor::DebugChannel, "No scan folder found for the directory: (%s).\n", fullSourceFile.toUtf8().data());
  2265. return; // early out, its nothing we care about.
  2266. }
  2267. QStringList files;
  2268. ScanFolderInternal(fullSourceFile, files);
  2269. for (const QString& fileEntry : files)
  2270. {
  2271. AssessModifiedFile(fileEntry);
  2272. }
  2273. }
  2274. void AssetProcessorManager::FailTopLevelSourceForIntermediate(
  2275. const SourceAssetReference& intermediateAsset, AZStd::string_view errorMessage)
  2276. {
  2277. auto topLevelSourceForIntermediateConflict =
  2278. AssetUtilities::GetTopLevelSourceForIntermediateAsset(intermediateAsset, m_stateData);
  2279. if (!topLevelSourceForIntermediateConflict)
  2280. {
  2281. AZ_TracePrintf(
  2282. AssetProcessor::DebugChannel,
  2283. "FailTopLevelSourceForIntermediate: No top level source found for %s\n",
  2284. intermediateAsset.AbsolutePath().c_str());
  2285. return;
  2286. }
  2287. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs;
  2288. m_stateData->GetJobsBySourceID(topLevelSourceForIntermediateConflict->m_sourceID, jobs);
  2289. AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry topLevelSourceScanFolder;
  2290. if (!m_stateData->GetScanFolderByScanFolderID(topLevelSourceForIntermediateConflict->m_scanFolderPK, topLevelSourceScanFolder))
  2291. {
  2292. AZ_Error(
  2293. AssetProcessor::ConsoleChannel, false, "FailTopLevelSourceForIntermediate: Failed to get scanfolder %d for file %s",
  2294. topLevelSourceForIntermediateConflict->m_scanFolderPK,
  2295. topLevelSourceForIntermediateConflict->m_sourceName.c_str());
  2296. return;
  2297. }
  2298. for (auto& job : jobs)
  2299. {
  2300. JobEntry jobEntry{ SourceAssetReference(topLevelSourceScanFolder.m_scanFolder.c_str(), topLevelSourceForIntermediateConflict->m_sourceName.c_str()),
  2301. job.m_builderGuid,
  2302. *m_platformConfig->GetPlatformByIdentifier(job.m_platform.c_str()),
  2303. job.m_jobKey.c_str(),
  2304. job.m_fingerprint,
  2305. job.m_jobRunKey,
  2306. topLevelSourceForIntermediateConflict->m_sourceGuid };
  2307. AutoFailJob(errorMessage, errorMessage, jobEntry);
  2308. }
  2309. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  2310. m_stateData->GetProductsBySourceID(topLevelSourceForIntermediateConflict->m_sourceID, products);
  2311. DeleteProducts(products);
  2312. m_stateData->RemoveSource(topLevelSourceForIntermediateConflict->m_sourceID);
  2313. }
  2314. void AssetProcessorManager::ProcessFilesToExamineQueue()
  2315. {
  2316. // it is assumed that files entering this function are already normalized
  2317. // that is, the path is normalized
  2318. // and only has forward slashes.
  2319. if (!m_platformConfig)
  2320. {
  2321. // this cannot be recovered from
  2322. qFatal("Platform config is missing, we cannot continue.");
  2323. return;
  2324. }
  2325. if ((m_normalizedCacheRootPath.isEmpty()) && (!InitializeCacheRoot()))
  2326. {
  2327. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Cannot examine the queue yet - cache root is not ready!\n ");
  2328. m_queuedExamination = true;
  2329. QTimer::singleShot(250, this, SLOT(ProcessFilesToExamineQueue()));
  2330. return;
  2331. }
  2332. if (m_isCurrentlyScanning)
  2333. {
  2334. // if we're currently scanning, then don't start processing yet, its not worth the IO thrashing.
  2335. m_queuedExamination = true;
  2336. QTimer::singleShot(250, this, SLOT(ProcessFilesToExamineQueue()));
  2337. return;
  2338. }
  2339. auto* fileStateInterface = AZ::Interface<AssetProcessor::IFileStateRequests>::Get();
  2340. AZ_Assert(fileStateInterface, "Programmer Error - IFileStateRequests interface is not available.");
  2341. auto* uuidManagerInterface = AZ::Interface<AssetProcessor::IUuidRequests>::Get();
  2342. AZ_Assert(uuidManagerInterface, "Programmer Error - IUuidRequests interface is not available.");
  2343. // During unit tests, it can be the case that cache folders are actually in a temp folder structure
  2344. // on OSX this is /var/... , but that is a symlink for real path /private/var. In some circumstances file monitor
  2345. // for deletions may report the canonical path (/private/var/...) when the 'cache root' or watched folder
  2346. // is actually /var/...
  2347. // to account for this, we check if the canonical path to the cache root is not the same as the path we are running at
  2348. // if they are different, incoming files may need a fixup that involves replacing their canonical paths (de-symlinked)
  2349. // with the paths we expect (symlinked).
  2350. QString canonicalRootDir = AssetUtilities::NormalizeFilePath(m_cacheRootDir.canonicalPath());
  2351. // we only need to do a fixup if the root dir is not the same as the canonical root dir.
  2352. bool needCanonicalFixup = canonicalRootDir != m_cacheRootDir.canonicalPath();
  2353. FileExamineContainer swapped;
  2354. m_filesToExamine.swap(swapped); // makes it okay to call CheckSource(...)
  2355. QElapsedTimer elapsedTimer;
  2356. elapsedTimer.start();
  2357. int i = -1; // Starting at -1 so we can increment at the start of the loop instead of the end due to all the control flow that occurs inside the loop
  2358. m_queuedExamination = false;
  2359. for (const FileEntry& examineFile : swapped)
  2360. {
  2361. ++i;
  2362. if (m_quitRequested)
  2363. {
  2364. return;
  2365. }
  2366. // CreateJobs can sometimes take a very long time, update the remaining count occasionally
  2367. if (elapsedTimer.elapsed() >= MILLISECONDS_BETWEEN_CREATE_JOBS_STATUS_UPDATE)
  2368. {
  2369. int remainingInSwapped = swapped.size() - i;
  2370. Q_EMIT NumRemainingJobsChanged(m_activeFiles.size() + remainingInSwapped + m_numOfJobsToAnalyze, AnalysisExtraInfo());
  2371. elapsedTimer.restart();
  2372. }
  2373. // examination occurs here.
  2374. // first, is it a source or is it a product in the cache folder?
  2375. QString normalizedPath = examineFile.m_fileName;
  2376. // debug-only check to make sure our assumption about normalization is correct.
  2377. Q_ASSERT(normalizedPath == AssetUtilities::NormalizeFilePath(normalizedPath));
  2378. if (needCanonicalFixup)
  2379. {
  2380. if (normalizedPath.startsWith(canonicalRootDir))
  2381. {
  2382. // found in canonical location, update its normalized path
  2383. QString withoutCachePath = normalizedPath.mid(canonicalRootDir.length() + 1);
  2384. // the extra +1 is to consume the slash that is after the root dir.
  2385. normalizedPath = AssetUtilities::NormalizeFilePath(m_cacheRootDir.absoluteFilePath(withoutCachePath));
  2386. }
  2387. }
  2388. AZ_TracePrintf(AssetProcessor::DebugChannel, "ProcessFilesToExamineQueue: %s delete: %s.\n", examineFile.m_fileName.toUtf8().constData(), examineFile.m_isDelete ? "true" : "false");
  2389. // if its in the cache root then its a product file:
  2390. bool isProductFile = IsInCacheFolder(normalizedPath.toUtf8().constData());
  2391. // strip the engine off it so that its a "normalized asset path" with appropriate slashes and such:
  2392. if (isProductFile)
  2393. {
  2394. // its a product file.
  2395. if (normalizedPath.length() >= ASSETPROCESSOR_TRAIT_MAX_PATH_LEN)
  2396. {
  2397. // if we are here it means that we have found a cache file whose filepath is greater than the maximum path length allowed
  2398. continue;
  2399. }
  2400. // we only care about deleted product files.
  2401. if (examineFile.m_isDelete)
  2402. {
  2403. if (normalizedPath.endsWith(QString(FENCE_FILE_EXTENSION), Qt::CaseInsensitive))
  2404. {
  2405. // its a fence file, now computing fenceId from it:
  2406. int startPos = normalizedPath.lastIndexOf("~");
  2407. int endPos = normalizedPath.lastIndexOf(".");
  2408. QString fenceIdString = normalizedPath.mid(startPos + 1, endPos - startPos - 1);
  2409. bool isNumber = false;
  2410. int fenceId = fenceIdString.toInt(&isNumber);
  2411. if (isNumber)
  2412. {
  2413. Q_EMIT FenceFileDetected(fenceId);
  2414. }
  2415. else
  2416. {
  2417. AZ_TracePrintf(AssetProcessor::DebugChannel, "AssetProcessor: Unable to compute fenceId from fenceFile name %s.\n", normalizedPath.toUtf8().data());
  2418. }
  2419. continue;
  2420. }
  2421. if (m_knownFolders.contains(normalizedPath))
  2422. {
  2423. CheckDeletedCacheFolder(normalizedPath);
  2424. }
  2425. else
  2426. {
  2427. CheckDeletedProductFile(normalizedPath);
  2428. }
  2429. }
  2430. else
  2431. {
  2432. // a file was added or modified to the cache.
  2433. // we only care about the renames of folders, so cache folders here:
  2434. QFileInfo fileInfo(normalizedPath);
  2435. if (!fileInfo.isDir())
  2436. {
  2437. // keep track of its containing folder.
  2438. AddKnownFoldersRecursivelyForFile(normalizedPath, m_cacheRootDir.absolutePath());
  2439. }
  2440. }
  2441. }
  2442. else
  2443. {
  2444. const ScanFolderInfo* scanFolderInfo = m_platformConfig->GetScanFolderForFile(normalizedPath);
  2445. if(!scanFolderInfo)
  2446. {
  2447. AZ_TracePrintf(
  2448. AssetProcessor::DebugChannel,
  2449. "ProcessFilesToExamineQueue: Unable to find scanfolder for %s. File path is likely not within a valid scanfolder.\n",
  2450. normalizedPath.toUtf8().constData());
  2451. continue;
  2452. }
  2453. SourceAssetReference sourceAssetReference(examineFile.m_fileName.toUtf8().constData());
  2454. if (sourceAssetReference.AbsolutePath() == sourceAssetReference.ScanFolderPath())
  2455. {
  2456. // We found a scanfolder, record it
  2457. m_knownFolders.insert(sourceAssetReference.ScanFolderPath().c_str());
  2458. }
  2459. if (normalizedPath.length() >= ASSETPROCESSOR_TRAIT_MAX_PATH_LEN)
  2460. {
  2461. // if we are here it means that we have found a source file whose filepath is greater than the maximum path length allowed
  2462. AZ_TracePrintf(
  2463. AssetProcessor::ConsoleChannel,
  2464. "ProcessFilesToExamineQueue: %s filepath length %d exceeds the maximum path length (%d) allowed.\n",
  2465. normalizedPath.toUtf8().constData(),
  2466. normalizedPath.length(),
  2467. ASSETPROCESSOR_TRAIT_MAX_PATH_LEN);
  2468. JobInfoContainer jobInfos;
  2469. m_stateData->GetJobInfoBySourceNameScanFolderId(sourceAssetReference.RelativePath().c_str(), scanFolderInfo->ScanFolderID(), jobInfos);
  2470. JobDetails job;
  2471. for (const auto& jobInfo : jobInfos)
  2472. {
  2473. const AssetBuilderSDK::PlatformInfo* const platformFromInfo = m_platformConfig->GetPlatformByIdentifier(jobInfo.m_platform.c_str());
  2474. AZ_Assert(platformFromInfo, "Error - somehow a job was created which was for a platform not in config.");
  2475. if (platformFromInfo)
  2476. {
  2477. AutoFailJob(
  2478. "",
  2479. AZStd::string::format("Product file name would be too long: %s\n", normalizedPath.toUtf8().data()),
  2480. JobEntry(
  2481. sourceAssetReference,
  2482. jobInfo.m_builderGuid,
  2483. *platformFromInfo,
  2484. jobInfo.m_jobKey.c_str(),
  2485. 0,
  2486. GenerateNewJobRunKey(),
  2487. AZ::Uuid::CreateNull())
  2488. );
  2489. }
  2490. }
  2491. continue;
  2492. }
  2493. if (examineFile.m_isDelete)
  2494. {
  2495. // if its a delete for a known folder, we handle it differently.
  2496. if (m_knownFolders.contains(normalizedPath))
  2497. {
  2498. CheckDeletedSourceFolder(sourceAssetReference);
  2499. continue;
  2500. }
  2501. }
  2502. else
  2503. {
  2504. // if we get here, we're either in a modify or add situation
  2505. QFileInfo fileInfo(normalizedPath);
  2506. if (!fileInfo.isDir())
  2507. {
  2508. if (!fileInfo.exists())
  2509. {
  2510. // it got deleted before we got to analyze it, we can ignore this.
  2511. continue;
  2512. }
  2513. // keep track of its parent folder so that if it is deleted later we know it is a folder
  2514. // delete and not a file delete.
  2515. AddKnownFoldersRecursivelyForFile(normalizedPath, sourceAssetReference.ScanFolderPath().c_str());
  2516. if (normalizedPath.toUtf8().length() > normalizedPath.length())
  2517. {
  2518. // if we are here it implies that the source file path contains non ascii characters
  2519. AutoFailJob(
  2520. AZStd::string::format(
  2521. "ProcessFilesToExamineQueue: source file path ( %s ) contains non ascii characters.\n",
  2522. normalizedPath.toUtf8().constData()),
  2523. AZStd::string::format(
  2524. "Source file ( %s ) contains non ASCII characters.\n"
  2525. "O3DE currently only supports file paths having ASCII characters and therefore asset processor will not be able to process this file.\n"
  2526. "Please rename the source file to fix this error.\n",
  2527. normalizedPath.toUtf8().constData()),
  2528. JobEntry(
  2529. sourceAssetReference,
  2530. AZ::Uuid::CreateNull(),
  2531. { "all", {} },
  2532. QString("PreCreateJobs"),
  2533. 0,
  2534. GenerateNewJobRunKey(),
  2535. AZ::Uuid::CreateNull())
  2536. );
  2537. continue;
  2538. }
  2539. }
  2540. else
  2541. {
  2542. // if its a folder that was added or modified, we need to keep track of that too.
  2543. AddKnownFoldersRecursivelyForFile(normalizedPath, sourceAssetReference.ScanFolderPath().c_str());
  2544. // we actually need to scan this folder now...
  2545. CheckCreatedSourceFolder(normalizedPath);
  2546. continue;
  2547. }
  2548. }
  2549. const bool isMetadataEnabledType = uuidManagerInterface->IsGenerationEnabledForFile(normalizedPath.toUtf8().constData());
  2550. // is it being overridden by a higher priority file?
  2551. QString overrider;
  2552. if (examineFile.m_isDelete)
  2553. {
  2554. // Only look for an override if this is not an intermediate asset
  2555. // Intermediate assets don't participate in the override system, so just process as-is without looking for an override
  2556. if (!IsInIntermediateAssetsFolder(normalizedPath))
  2557. {
  2558. // if we delete it, check if its revealed by an underlying file:
  2559. overrider = m_platformConfig->FindFirstMatchingFile(sourceAssetReference.RelativePath().c_str());
  2560. if (!overrider.isEmpty()) // override found!
  2561. {
  2562. if (overrider.compare(normalizedPath, Qt::CaseInsensitive) == 0)
  2563. {
  2564. // if the overrider is the same file, it means that a file was deleted, then reappeared.
  2565. // if that happened there will be a message in the notification queue for that file reappearing, there
  2566. // is no need to add a double here.
  2567. overrider.clear();
  2568. }
  2569. else
  2570. {
  2571. // on the other hand, if we found a file it means that a deleted file revealed a file that
  2572. // was previously overridden by it.
  2573. // Because the deleted file may have "revealed" a file with different case,
  2574. // we have to actually correct its case here. This is rare, so it should be reasonable
  2575. // to call the expensive function to discover correct case.
  2576. QString pathRelativeToScanFolder;
  2577. QString scanFolderPath;
  2578. m_platformConfig->ConvertToRelativePath(overrider, pathRelativeToScanFolder, scanFolderPath);
  2579. AssetUtilities::UpdateToCorrectCase(scanFolderPath, pathRelativeToScanFolder);
  2580. overrider = QDir(scanFolderPath).absoluteFilePath(pathRelativeToScanFolder);
  2581. }
  2582. }
  2583. }
  2584. }
  2585. else if(!isMetadataEnabledType)
  2586. {
  2587. overrider = m_platformConfig->GetOverridingFile(sourceAssetReference.RelativePath().c_str(), sourceAssetReference.ScanFolderPath().c_str());
  2588. }
  2589. if (!overrider.isEmpty())
  2590. {
  2591. if (!IsInIntermediateAssetsFolder(overrider))
  2592. {
  2593. FileStateInfo foundFileInfo;
  2594. bool found = AZ::Interface<IFileStateRequests>::Get()->GetFileInfo(overrider, &foundFileInfo);
  2595. if(!found)
  2596. {
  2597. AZ_Error(AssetProcessor::ConsoleChannel, false, "ProcessFilesToExamineQueue: Found overrider %s for file %s, but FileStateCache has no information about this file. File will not be processed.",
  2598. overrider.toUtf8().constData(), normalizedPath.toUtf8().constData());
  2599. continue;
  2600. }
  2601. if(foundFileInfo.m_isDirectory)
  2602. {
  2603. // It makes no sense for directories to override directories. This happens usually because a directory was deleted, but we have no way of knowing it was a directory (since it's already deleted)
  2604. // Since we know the overrider is a directory, ignore this overrider and continue on processing the actual directory.
  2605. }
  2606. else
  2607. {
  2608. // this file is being overridden by an earlier file.
  2609. // ignore us, and pretend the other file changed:
  2610. AZ_TracePrintf(AssetProcessor::DebugChannel, "File overridden by %s.\n", overrider.toUtf8().constData());
  2611. CheckSource(FileEntry(overrider, false, examineFile.m_isFromScanner));
  2612. continue;
  2613. }
  2614. }
  2615. else
  2616. {
  2617. auto errorMessage = AZStd::string::format(
  2618. "Intermediate asset (%s) conflicts with an existing source asset "
  2619. "with the same relative path: %s. Please move/rename one of the files to fix the conflict.",
  2620. overrider.toUtf8().constData(),
  2621. normalizedPath.toUtf8().constData());
  2622. FailTopLevelSourceForIntermediate(sourceAssetReference, errorMessage);
  2623. }
  2624. }
  2625. // its an input file or a file we don't care about...
  2626. // note that if the file now exists, we have to treat it as an input asset even if it came in as a delete.
  2627. if (examineFile.m_isDelete && !QFile::exists(normalizedPath))
  2628. {
  2629. AZ_TracePrintf(AssetProcessor::DebugChannel, "Input was deleted and no overrider was found.\n");
  2630. AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceDatabaseEntry;
  2631. if (!m_stateData->GetSourceBySourceNameScanFolderId(
  2632. sourceAssetReference.RelativePath().c_str(), sourceAssetReference.ScanFolderId(), sourceDatabaseEntry))
  2633. {
  2634. AZ_TracePrintf(
  2635. AssetProcessor::DebugChannel, "ProcessFilesToExamineQueue - deleted source is not in asset database - cannot send SourceFileNotificationMessage::FileRemoved message - %s\n",
  2636. sourceAssetReference.AbsolutePath().c_str());
  2637. }
  2638. else
  2639. {
  2640. AzToolsFramework::AssetSystem::SourceFileNotificationMessage message(
  2641. AZ::OSString(sourceAssetReference.RelativePath().c_str()),
  2642. AZ::OSString(scanFolderInfo->ScanPath().toUtf8().constData()),
  2643. AzToolsFramework::AssetSystem::SourceFileNotificationMessage::FileRemoved,
  2644. sourceDatabaseEntry.m_sourceGuid);
  2645. AssetProcessor::ConnectionBus::Broadcast(&AssetProcessor::ConnectionBus::Events::Send, 0, message);
  2646. }
  2647. CheckDeletedSourceFile(sourceAssetReference, examineFile.m_initialProcessTime);
  2648. }
  2649. else
  2650. {
  2651. AssetProcessor::FileStateInfo fileStateInfo;
  2652. if (fileStateInterface->GetFileInfo(sourceAssetReference.AbsolutePath().c_str(), &fileStateInfo) &&
  2653. sourceAssetReference.AbsolutePath().Compare(fileStateInfo.m_absolutePath.toUtf8().constData()) != 0)
  2654. {
  2655. // File on disk has different case compared to the file being processed
  2656. // This usually means a file was renamed and a "change" event was fired for both the old and new name
  2657. // Ignore this event as its for the old file name
  2658. continue;
  2659. }
  2660. // log-spam-reduction - the lack of the prior tag (input was deleted) which is rare can infer that the above branch was taken
  2661. //AZ_TracePrintf(AssetProcessor::DebugChannel, "Input is modified or is overriding something.\n");
  2662. CheckModifiedSourceFile(sourceAssetReference, scanFolderInfo);
  2663. }
  2664. }
  2665. }
  2666. // instead of checking here, we place a message at the end of the queue.
  2667. // this is because there may be additional scan or other results waiting in the queue
  2668. // an example would be where the scanner found additional "copy" jobs waiting in the queue for finalization
  2669. QueueIdleCheck();
  2670. }
  2671. void AssetProcessorManager::CheckForIdle()
  2672. {
  2673. m_alreadyQueuedCheckForIdle = false;
  2674. if (IsIdle())
  2675. {
  2676. if (!m_hasProcessedCriticalAssets)
  2677. {
  2678. // only once, when we finish startup
  2679. m_stateData->VacuumAndAnalyze();
  2680. m_hasProcessedCriticalAssets = true;
  2681. }
  2682. if (!m_quitRequested && m_AssetProcessorIsBusy)
  2683. {
  2684. m_AssetProcessorIsBusy = false;
  2685. Q_EMIT NumRemainingJobsChanged(m_activeFiles.size() + m_filesToExamine.size() + m_numOfJobsToAnalyze, AnalysisExtraInfo());
  2686. Q_EMIT AssetProcessorManagerIdleState(true);
  2687. }
  2688. if (!m_reportedAnalysisMetrics)
  2689. {
  2690. // report these metrics only once per session.
  2691. m_reportedAnalysisMetrics = true;
  2692. AZ_TracePrintf(
  2693. ConsoleChannel,
  2694. "Builder optimization: %i / %i files required full analysis, %i sources found but not processed by anyone. %i override "
  2695. "collisions.\n",
  2696. m_numSourcesNeedingFullAnalysis,
  2697. m_numTotalSourcesFound,
  2698. m_numSourcesNotHandledByAnyBuilder,
  2699. m_numOverrides);
  2700. }
  2701. m_dependencyCache = {};
  2702. m_dependencyCacheEnabled = false;
  2703. m_pathDependencyManager->ProcessQueuedDependencyResolves();
  2704. QTimer::singleShot(20, this, SLOT(RemoveEmptyFolders()));
  2705. }
  2706. else
  2707. {
  2708. m_AssetProcessorIsBusy = true;
  2709. Q_EMIT AssetProcessorManagerIdleState(false);
  2710. // amount of jobs to evaluate right now (no deferred jobs)
  2711. int numWorkRemainingNow = m_activeFiles.size() + m_filesToExamine.size();
  2712. // total (GUI Shown) of work remaining (including jobs to do later)
  2713. int numTotalWorkRemaining = numWorkRemainingNow + m_numOfJobsToAnalyze;
  2714. Q_EMIT NumRemainingJobsChanged(numTotalWorkRemaining, AnalysisExtraInfo());
  2715. // wake up if there's work to do and we haven't scheduled to do it.
  2716. if ((!m_alreadyScheduledUpdate) && (numWorkRemainingNow > 0))
  2717. {
  2718. // schedule additional updates
  2719. m_alreadyScheduledUpdate = true;
  2720. QTimer::singleShot(0, this, SLOT(ScheduleNextUpdate()));
  2721. }
  2722. else if (numWorkRemainingNow == 0) // if there are only jobs to process later remaining
  2723. {
  2724. // Process job entries and add jobs to process
  2725. for (JobToProcessEntry& entry : m_jobEntries)
  2726. {
  2727. if (entry.m_jobsToAnalyze.empty())
  2728. {
  2729. // no jobs were emitted this time around.
  2730. // we can assume that all jobs are done for this source file (because none were emitted)
  2731. QMetaObject::invokeMethod(this, "FinishAnalysis", Qt::QueuedConnection, Q_ARG(SourceAssetReference, entry.m_sourceFileInfo.m_sourceAssetReference));
  2732. }
  2733. else
  2734. {
  2735. // All the jobs of the sourcefile needs to be bundled together to check for missing jobs.
  2736. CheckMissingJobs(entry.m_sourceFileInfo.m_sourceAssetReference, entry.m_sourceFileInfo.m_scanFolder, entry.m_jobsToAnalyze);
  2737. // Update source and job dependency list before forwarding the job to RCController
  2738. AnalyzeJobDetail(entry);
  2739. }
  2740. }
  2741. m_jobEntries.clear();
  2742. ProcessJobs();
  2743. }
  2744. }
  2745. }
  2746. // ----------------------------------------------------
  2747. // ------------- File change Queue --------------------
  2748. // ----------------------------------------------------
  2749. // note that this function is on the "hot path" of file analysis
  2750. // during startup, and should avoid logging, sleeping, or doing
  2751. // any more work than is necessary (Log only in error or uncommon
  2752. // circumstances).
  2753. void AssetProcessorManager::AssessFileInternal(QString fullFile, bool isDelete, bool fromScanner, QString fromDependencyChain)
  2754. {
  2755. if (m_quitRequested)
  2756. {
  2757. return;
  2758. }
  2759. QString normalizedFullFile = AssetUtilities::NormalizeFilePath(fullFile);
  2760. if (!fromScanner) // the scanner already does exclusion and doesn't need to deal with metafiles.
  2761. {
  2762. if (m_platformConfig->IsFileExcluded(normalizedFullFile))
  2763. {
  2764. return;
  2765. }
  2766. // over here we also want to invalidate the metafiles on disk map if it COULD Be a metafile
  2767. // note that there is no reason to do an expensive exacting computation here, it will be
  2768. // done later and cached when m_cachedMetaFilesExistMap is set to false, we just need to
  2769. // know if its POSSIBLE that its a metafile, cheaply.
  2770. // if its a metafile match, then invalidate the metafile table.
  2771. for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
  2772. {
  2773. QPair<QString, QString> metaDataFileType = m_platformConfig->GetMetaDataFileTypeAt(idx);
  2774. if (fullFile.endsWith(metaDataFileType.first, Qt::CaseInsensitive))
  2775. {
  2776. m_cachedMetaFilesExistMap = false;
  2777. m_metaFilesWhichActuallyExistOnDisk.clear(); // invalidate the map, force a recompuation later.
  2778. }
  2779. }
  2780. }
  2781. if (!isDelete && IsInIntermediateAssetsFolder(normalizedFullFile) && !m_knownFolders.contains(normalizedFullFile))
  2782. {
  2783. // Make a special case check for MetadataManager files
  2784. // An update to one of these types of files needs to cause a re-process of the intermediate file.
  2785. // Converting a metadata file to the real file normally happens later in the processing but needs to be done
  2786. // up front for intermediates to avoid bypassing this whole block - which stops processing intermediates before they're recorded in the database.
  2787. AZ::IO::FixedMaxPath absolutePath(normalizedFullFile.toUtf8().constData());
  2788. if (absolutePath.Extension() == AzToolsFramework::MetadataManager::MetadataFileExtension)
  2789. {
  2790. // Use the real file for the check below
  2791. absolutePath = absolutePath.ReplaceExtension("");
  2792. }
  2793. QString relativePath, scanfolderPath;
  2794. m_platformConfig->ConvertToRelativePath(absolutePath.c_str(), relativePath, scanfolderPath);
  2795. auto productName = AssetUtilities::GetIntermediateAssetDatabaseName(relativePath.toUtf8().constData());
  2796. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  2797. if(!m_stateData->GetProductsByProductName(productName.c_str(), products))
  2798. {
  2799. // This file is an intermediate asset product but it doesn't exist in the database yet. This means the job which produced this asset has not completed yet.
  2800. // Do not process this file yet. When the job is done it will retrigger processing for this file.
  2801. return;
  2802. }
  2803. auto topLevelSource = AssetUtilities::GetTopLevelSourcePathForIntermediateAsset(SourceAssetReference(fullFile),
  2804. m_stateData);
  2805. if (topLevelSource)
  2806. {
  2807. if (!AZ::Interface<IFileStateRequests>::Get()->Exists(topLevelSource.value().c_str()))
  2808. {
  2809. AZ_TracePrintf(
  2810. AssetProcessor::ConsoleChannel,
  2811. "Top level source '%s' for intermediate asset '%.*s' no longer exists, "
  2812. "so this intermediate asset will not be reprocessed.\n",
  2813. topLevelSource.value().c_str(),
  2814. AZ_STRING_ARG(productName));
  2815. return;
  2816. }
  2817. }
  2818. else
  2819. {
  2820. // Failed to find top level source for this intermediate asset - don't process this asset, there's a gap or issue in the asset database.
  2821. // No reason to print anything out here, no warnings or errors because the asset processor does cleanup on intermediate
  2822. // assets that no longer have a source asset, or the source asset no longer emits them, later in asset processing.
  2823. return;
  2824. }
  2825. }
  2826. m_AssetProcessorIsBusy = true;
  2827. Q_EMIT AssetProcessorManagerIdleState(false);
  2828. // this function is the raw function that gets called from the file monitor
  2829. // whenever an asset has been modified or added (not deleted)
  2830. // it should place the asset on a grace period list and not considered until changes stop happening to it.
  2831. // note that file Paths come in raw, full absolute paths.
  2832. if (!m_sourceFilesInDatabase.empty())
  2833. {
  2834. if (!isDelete)
  2835. {
  2836. m_sourceFilesInDatabase.remove(normalizedFullFile);
  2837. }
  2838. }
  2839. FileEntry newEntry(normalizedFullFile, isDelete, fromScanner);
  2840. newEntry.m_fromDependencyChain = fromDependencyChain;
  2841. if (m_alreadyActiveFiles.find(normalizedFullFile) != m_alreadyActiveFiles.end())
  2842. {
  2843. auto entryIt = std::find_if(m_activeFiles.begin(), m_activeFiles.end(),
  2844. [&normalizedFullFile](const FileEntry& entry)
  2845. {
  2846. return entry.m_fileName == normalizedFullFile;
  2847. });
  2848. if (entryIt != m_activeFiles.end())
  2849. {
  2850. m_activeFiles.erase(entryIt);
  2851. }
  2852. }
  2853. m_AssetProcessorIsBusy = true;
  2854. m_activeFiles.push_back(newEntry);
  2855. m_alreadyActiveFiles.insert(normalizedFullFile);
  2856. Q_EMIT NumRemainingJobsChanged(m_activeFiles.size() + m_filesToExamine.size() + m_numOfJobsToAnalyze, AnalysisExtraInfo());
  2857. if (!m_alreadyScheduledUpdate)
  2858. {
  2859. m_alreadyScheduledUpdate = true;
  2860. QTimer::singleShot(0, this, SLOT(ScheduleNextUpdate()));
  2861. }
  2862. }
  2863. void AssetProcessorManager::AssessAddedFile(QString filePath)
  2864. {
  2865. if (IsInCacheFolder(filePath.toUtf8().constData()))
  2866. {
  2867. // modifies/adds to the cache are irrelevant. Deletions are all we care about
  2868. return;
  2869. }
  2870. AssessFileInternal(filePath, false);
  2871. }
  2872. void AssetProcessorManager::AssessModifiedFile(QString filePath)
  2873. {
  2874. // we don't care about modified folders at this time.
  2875. // you'll get a "folder modified" whenever a file in a folder is removed or added or modified
  2876. // but you'll also get the actual file modify itself.
  2877. if (!QFileInfo(filePath).isDir())
  2878. {
  2879. // we also don't care if you modify files in the cache, only deletions matter.
  2880. if(!IsInCacheFolder(filePath.toUtf8().constData()))
  2881. {
  2882. AssessFileInternal(filePath, false);
  2883. }
  2884. }
  2885. }
  2886. void AssetProcessorManager::CheckReadyToAssessScanFiles()
  2887. {
  2888. if (m_catalogReady && m_buildersReady && m_scannerFiles.size() > 0)
  2889. {
  2890. AssessFilesFromScanner(m_scannerFiles);
  2891. m_scannerFiles = {};
  2892. }
  2893. }
  2894. // The file cache is used before actually hitting physical media to determine the
  2895. // existence of files and to retrieve the file's hash.
  2896. // It assumes that the presence of a file in the cache means the file exists.
  2897. // Because of this, it also monitors for file notifications from the operating system
  2898. // (such as changed, deleted, etc) and invalidates its cache, removing hashes or file entries
  2899. // as appropriate.
  2900. // This means we can 'warm up the cache' from the prior known file list in the database, BUT
  2901. // can only populate the entries discovered by the file scanner (so they are known to exist)
  2902. // and we can only populate the hashes in the cache for files which are known to exist AND
  2903. // whos modtime has not changed.
  2904. void AssetProcessorManager::WarmUpFileCache(QSet<AssetFileInfo> filePaths)
  2905. {
  2906. // if the 'skipping feature' is disabled, do not pre-populate the cache
  2907. // This will cause it to rehash everything every time.
  2908. if (!m_allowModtimeSkippingFeature)
  2909. {
  2910. return;
  2911. }
  2912. IFileStateRequests* fileStateCache = AZ::Interface<IFileStateRequests>::Get();
  2913. if (!fileStateCache)
  2914. {
  2915. return;
  2916. }
  2917. // the strategy here is to only warm up the file cache if absolutely everything
  2918. // is okay - the mod time must match last time, the file must exist, the hash must be present
  2919. // and non zero from last time. If anything at all is not correct, we will not warm the
  2920. // cache up and this will cause it to refetch on demand.
  2921. for (const AssetFileInfo& fileInfo : filePaths)
  2922. {
  2923. // fileInfo represents an file found in the bulk scanning (so it actually exists)
  2924. // m_fileModTimes is a list of last-known modtimes from the database from last run.
  2925. auto fileItr = m_fileModTimes.find(fileInfo.m_filePath.toUtf8().data());
  2926. if (fileItr != m_fileModTimes.end())
  2927. {
  2928. AZ::u64 databaseModTime = fileItr->second;
  2929. if(databaseModTime != 0)
  2930. {
  2931. auto thisModTime = aznumeric_cast<decltype(databaseModTime)>(AssetUtilities::AdjustTimestamp(fileInfo.m_modTime));
  2932. if (thisModTime == databaseModTime)
  2933. {
  2934. // the actual modtime of the file has not changed since last and the file still exists.
  2935. // does the database know what its hash was last time?
  2936. auto hashItr = m_fileHashes.find(fileInfo.m_filePath.toUtf8().constData());
  2937. if(hashItr != m_fileHashes.end())
  2938. {
  2939. AZ::u64 databaseHashValue = hashItr->second;
  2940. if (databaseHashValue != 0)
  2941. {
  2942. // we have a valid database hash value and mod time has not changed.
  2943. // cache it so that future calls to GetFileHash and the like
  2944. // use this cached value.
  2945. if (fileStateCache)
  2946. {
  2947. fileStateCache->WarmUpCache(fileInfo, databaseHashValue);
  2948. continue;
  2949. }
  2950. }
  2951. }
  2952. }
  2953. }
  2954. }
  2955. // Note that the 'continue' statement above, which happens if all conditions are met
  2956. // causes it to skip over the following line. If the execution ends up here, it means
  2957. // that the database's modtime was probably stale or this is a new file or some other
  2958. // disqualifying condition. However, the fileInfo is still a real file on disk that
  2959. // came from the bulk scan, so we can still warm up the file cache with this info.
  2960. fileStateCache->WarmUpCache(fileInfo);
  2961. }
  2962. }
  2963. // this means a file is definitely coming from the file scanner, and not the file monitor.
  2964. // the file scanner does not scan the cache.
  2965. // the scanner should be omitting directory changes.
  2966. void AssetProcessorManager::AssessFilesFromScanner(QSet<AssetFileInfo> filePaths)
  2967. {
  2968. if (m_initialScanSkippingFeature)
  2969. {
  2970. for (const AssetFileInfo& fileInfo : filePaths)
  2971. {
  2972. AddKnownFoldersRecursivelyForFile(fileInfo.m_filePath, fileInfo.m_scanFolder->ScanPath());
  2973. }
  2974. m_sourceFilesInDatabase.clear();
  2975. m_fileModTimes.clear();
  2976. m_fileHashes.clear();
  2977. // place a message in the queue that will cause us to transition
  2978. // into a "no longer scanning" state and then continue with the next phase
  2979. QMetaObject::invokeMethod(this, "FinishAssetScan", Qt::QueuedConnection);
  2980. m_initialScanSkippingFeature = false;
  2981. return;
  2982. }
  2983. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Received %i files from the scanner. Assessing...\n", static_cast<int>(filePaths.size()));
  2984. [[maybe_unused]] int processedFileCount = 0;
  2985. AssetProcessor::StatsCapture::BeginCaptureStat("InitialFileAssessment");
  2986. m_totalScannerFilesToAssess = filePaths.size();
  2987. m_scannerFilesAssessed = 0;
  2988. for (const AssetFileInfo& fileInfo : filePaths)
  2989. {
  2990. if (m_allowModtimeSkippingFeature)
  2991. {
  2992. AZ::u64 fileHash = 0;
  2993. if (CanSkipProcessingFile(fileInfo, fileHash))
  2994. {
  2995. AddKnownFoldersRecursivelyForFile(fileInfo.m_filePath, fileInfo.m_scanFolder->ScanPath());
  2996. if (fileHash != 0)
  2997. {
  2998. QString databaseName;
  2999. m_platformConfig->ConvertToRelativePath(fileInfo.m_filePath, fileInfo.m_scanFolder, databaseName);
  3000. // Update the modtime in the db since its possible that the hash is the same, but the modtime is out of date. Recording the current modtime will allow us to skip hashing the file in the future if no changes are made
  3001. bool updated = m_stateData->UpdateFileModTimeAndHashByFileNameAndScanFolderId(databaseName, fileInfo.m_scanFolder->ScanFolderID(), AssetUtilities::AdjustTimestamp(fileInfo.m_modTime), fileHash);
  3002. if(!updated)
  3003. {
  3004. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to update modtime for file %s during file scan", fileInfo.m_filePath.toUtf8().constData());
  3005. }
  3006. }
  3007. ++m_scannerFilesAssessed;
  3008. continue;
  3009. }
  3010. }
  3011. AssessFileInternal(fileInfo.m_filePath, false, true);
  3012. ++m_scannerFilesAssessed;
  3013. ++processedFileCount;
  3014. }
  3015. if (m_allowModtimeSkippingFeature)
  3016. {
  3017. AZ_TracePrintf(AssetProcessor::DebugChannel, "%d files reported from scanner. %d unchanged files skipped, %d files processed\n", filePaths.size(), filePaths.size() - processedFileCount, processedFileCount);
  3018. }
  3019. AZ_Printf(
  3020. "AssetProcessor",
  3021. "Assessing files from scanner complete. Total files: %i. Assets with new builders: %i. Unregistered/new files: %i. Old timestamps: %i. "
  3022. "Modified dependencies: %i.\n",
  3023. m_scannerFilesAssessed,
  3024. m_assetsNeedingProcessing_BuildersChanged,
  3025. m_assetsNeedingProcessing_NewFile,
  3026. m_assetsNeedingProcessing_TimeStampChanged,
  3027. m_assetsNeedingProcessing_DependenciesChanged);
  3028. AssetProcessor::StatsCapture::EndCaptureStat("InitialFileAssessment");
  3029. // place a message in the queue that will cause us to transition
  3030. // into a "no longer scanning" state and then continue with the next phase
  3031. // we place this at the end of the queue rather than calling it immediately, because
  3032. // other messages may still be in the queue such as the incoming file list.
  3033. QMetaObject::invokeMethod(this, "FinishAssetScan", Qt::QueuedConnection);
  3034. }
  3035. void AssetProcessorManager::RecordFilesFromScanner(QSet<AssetFileInfo> filePaths)
  3036. {
  3037. m_scannerFiles = filePaths;
  3038. AssetProcessor::StatsCapture::BeginCaptureStat("WarmingFileCache");
  3039. WarmUpFileCache(filePaths);
  3040. AssetProcessor::StatsCapture::EndCaptureStat("WarmingFileCache");
  3041. Q_EMIT FileCacheIsReady();
  3042. CheckReadyToAssessScanFiles();
  3043. }
  3044. void AssetProcessorManager::RecordFoldersFromScanner(QSet<AssetFileInfo> folderPaths)
  3045. {
  3046. // Record all the folders so we can differentiate between a folder delete and a file delete later on
  3047. // Sometimes a folder is empty, which is why its not sufficient to only record folders from the AssessFilesFromScanner event
  3048. for(const auto& folder : folderPaths)
  3049. {
  3050. AddKnownFoldersRecursivelyForFile(folder.m_filePath, folder.m_scanFolder->ScanPath());
  3051. }
  3052. }
  3053. // warm up the excluded folder cache with the data from the scanner so that it is not necessary to rescan.
  3054. void AssetProcessorManager::RecordExcludesFromScanner(QSet<AssetFileInfo> excludePaths)
  3055. {
  3056. AZStd::unordered_set<AZStd::string> excludedFolders;
  3057. for (const auto& folder : excludePaths)
  3058. {
  3059. if (folder.m_isDirectory)
  3060. {
  3061. excludedFolders.insert(folder.m_filePath.toUtf8().constData());
  3062. }
  3063. }
  3064. m_excludedFolderCache->InitializeFromKnownSet(AZStd::move(excludedFolders));
  3065. }
  3066. bool AssetProcessorManager::CanSkipProcessingFile(const AssetFileInfo &fileInfo, AZ::u64& fileHashOut)
  3067. {
  3068. // Check to see if the file has changed since the last time we saw it
  3069. // If not, don't even bother processing the file
  3070. // We can only do this if the builders haven't changed however, as they can register to watch files that were previously not processed
  3071. if (m_buildersAddedOrRemoved)
  3072. {
  3073. ++m_assetsNeedingProcessing_BuildersChanged;
  3074. return false;
  3075. }
  3076. auto fileItr = m_fileModTimes.find(fileInfo.m_filePath.toUtf8().data());
  3077. if (fileItr == m_fileModTimes.end())
  3078. {
  3079. // File has not been processed before
  3080. ++m_assetsNeedingProcessing_NewFile;
  3081. return false;
  3082. }
  3083. AZ::u64 databaseModTime = fileItr->second;
  3084. // Remove the file from the list, it's not needed anymore
  3085. m_fileModTimes.erase(fileItr);
  3086. if(databaseModTime == 0)
  3087. {
  3088. // Don't bother with any further checks (particularly hashing), this file hasn't been seen before
  3089. // There should never be a case where we have recorded a hash but not a modtime
  3090. ++m_assetsNeedingProcessing_NewFile;
  3091. return false;
  3092. }
  3093. auto thisModTime = aznumeric_cast<decltype(databaseModTime)>(AssetUtilities::AdjustTimestamp(fileInfo.m_modTime));
  3094. if (databaseModTime != thisModTime)
  3095. {
  3096. ++m_assetsNeedingProcessing_TimeStampChanged;
  3097. // File timestamp has changed since last time
  3098. // Check if the contents have changed or if it's just a timestamp mismatch
  3099. auto hashItr = m_fileHashes.find(fileInfo.m_filePath.toUtf8().constData());
  3100. if(hashItr == m_fileHashes.end())
  3101. {
  3102. // No hash found
  3103. return false;
  3104. }
  3105. AZ::u64 databaseHashValue = hashItr->second;
  3106. m_fileHashes.erase(hashItr);
  3107. if(databaseHashValue == 0)
  3108. {
  3109. // 0 is not a valid hash, don't bother trying to hash the file
  3110. return false;
  3111. }
  3112. AZ::u64 fileHash = AssetUtilities::GetFileHash(fileInfo.m_filePath.toUtf8().constData());
  3113. if(fileHash != databaseHashValue)
  3114. {
  3115. // File contents have changed
  3116. return false;
  3117. }
  3118. fileHashOut = fileHash;
  3119. }
  3120. auto sourceFileItr = m_sourceFilesInDatabase.find(fileInfo.m_filePath.toUtf8().data());
  3121. if (sourceFileItr != m_sourceFilesInDatabase.end())
  3122. {
  3123. // File is a source file that has been processed before
  3124. AZStd::string fingerprintFromDatabase = sourceFileItr->m_analysisFingerprint.toUtf8().data();
  3125. if (fingerprintFromDatabase.empty())
  3126. {
  3127. // No recorded fingerprint
  3128. return false;
  3129. }
  3130. AZStd::string_view builderEntries(fingerprintFromDatabase.begin() + s_lengthOfUuid + 1, fingerprintFromDatabase.end());
  3131. AZStd::string_view dependencyFingerprint(fingerprintFromDatabase.begin(), fingerprintFromDatabase.begin() + s_lengthOfUuid);
  3132. int numBuildersEmittingSourceDependencies = 0;
  3133. // Check for updated builders
  3134. if (!AreBuildersUnchanged(builderEntries, numBuildersEmittingSourceDependencies))
  3135. {
  3136. ++m_assetsNeedingProcessing_BuildersChanged;
  3137. return false;
  3138. }
  3139. // Check for updated fingerprint
  3140. AZStd::string currentFingerprint = ComputeRecursiveDependenciesFingerprint(sourceFileItr->m_sourceAssetReference);
  3141. if (dependencyFingerprint != currentFingerprint)
  3142. {
  3143. // Dependencies have changed
  3144. ++m_assetsNeedingProcessing_DependenciesChanged;
  3145. return false;
  3146. }
  3147. // Success - we can skip this file, nothing has changed!
  3148. // Remove it from the list of to-be-processed files, otherwise the AP will assume the file was deleted
  3149. // Note that this means any files that *were* deleted are already handled by CheckMissingFiles
  3150. m_sourceFilesInDatabase.erase(sourceFileItr);
  3151. return true;
  3152. }
  3153. // File is a non-tracked file, aka a file that no builder cares about.
  3154. // The fact that it has a matching modtime means we've already seen this file and attempted to process it
  3155. // If it were a new, unprocessed source file, there would be no modtime stored
  3156. return true;
  3157. }
  3158. void AssetProcessorManager::AssessDeletedFile(QString filePath)
  3159. {
  3160. {
  3161. filePath = AssetUtilities::NormalizeFilePath(filePath);
  3162. QMutexLocker locker(&m_processingJobMutex);
  3163. // early-out on files that are in the deletion list to save some processing time and spam and prevent rebuild errors where you get stuck rebuilding things in a loop
  3164. auto found = m_processingProductInfoList.find(filePath.toUtf8().constData());
  3165. if (found != m_processingProductInfoList.end())
  3166. {
  3167. m_AssetProcessorIsBusy = true; // re-emit the idle state at least, for listeners waiting for it.
  3168. QueueIdleCheck();
  3169. return;
  3170. }
  3171. }
  3172. AssessFileInternal(filePath, true);
  3173. }
  3174. void AssetProcessorManager::ScheduleNextUpdate()
  3175. {
  3176. m_alreadyScheduledUpdate = false;
  3177. if (m_activeFiles.size() > 0)
  3178. {
  3179. DispatchFileChange();
  3180. }
  3181. else
  3182. {
  3183. QueueIdleCheck();
  3184. }
  3185. }
  3186. void AssetProcessorManager::RemoveEmptyFolders()
  3187. {
  3188. if (!m_AssetProcessorIsBusy)
  3189. {
  3190. if (m_checkFoldersToRemove.size())
  3191. {
  3192. QString dir = *m_checkFoldersToRemove.begin();
  3193. CleanEmptyFolder(dir, m_normalizedCacheRootPath);
  3194. m_checkFoldersToRemove.remove(dir);
  3195. QTimer::singleShot(20, this, SLOT(RemoveEmptyFolders()));
  3196. }
  3197. }
  3198. }
  3199. void AssetProcessorManager::DispatchFileChange()
  3200. {
  3201. Q_ASSERT(m_activeFiles.size() > 0);
  3202. if (m_quitRequested)
  3203. {
  3204. return;
  3205. }
  3206. // This was added because we found out that the consumer was not able to keep up, which led to the app taking forever to shut down
  3207. // we want to make sure that our queue has at least this many to eat in a single gulp, so it remains busy, but we cannot let this number grow too large
  3208. // or else it never returns to the main message pump and thus takes a while to realize that quit has been signalled.
  3209. // if the processing thread ever runs dry, then this needs to be increased.
  3210. int maxPerIteration = 50;
  3211. // Burn through all pending files
  3212. const FileEntry* firstEntry = &m_activeFiles.front();
  3213. while (m_filesToExamine.size() < maxPerIteration)
  3214. {
  3215. m_alreadyActiveFiles.remove(firstEntry->m_fileName);
  3216. CheckSource(*firstEntry);
  3217. m_activeFiles.pop_front();
  3218. if (m_activeFiles.size() == 0)
  3219. {
  3220. break;
  3221. }
  3222. firstEntry = &m_activeFiles.front();
  3223. }
  3224. if (!m_alreadyScheduledUpdate)
  3225. {
  3226. // schedule additional updates
  3227. m_alreadyScheduledUpdate = true;
  3228. QTimer::singleShot(0, this, SLOT(ScheduleNextUpdate()));
  3229. }
  3230. }
  3231. bool AssetProcessorManager::IsIdle()
  3232. {
  3233. if ((!m_queuedExamination) && (m_filesToExamine.isEmpty()) && (m_activeFiles.isEmpty()) &&
  3234. !m_processedQueued && m_assetProcessedList.empty() && !m_numOfJobsToAnalyze)
  3235. {
  3236. return true;
  3237. }
  3238. return false;
  3239. }
  3240. bool AssetProcessorManager::HasProcessedCriticalAssets() const
  3241. {
  3242. return m_hasProcessedCriticalAssets;
  3243. }
  3244. void AssetProcessorManager::ProcessJobs()
  3245. {
  3246. // 1) Loop over all the jobs and analyze each job one by one.
  3247. // 2) Analyzing should return true only when all the dependent jobs fingerprint's are known to APM, if true process that job.
  3248. // 3) If anytime we were unable to analyze even one job even after looping over all the remaining jobs then
  3249. // we will process the first job and loop over the remaining jobs once again since that job might have unblocked other jobs.
  3250. bool anyJobAnalyzed = false;
  3251. QElapsedTimer elapsedTimer;
  3252. elapsedTimer.start();
  3253. for (auto jobIter = m_jobsToProcess.begin(); jobIter != m_jobsToProcess.end();)
  3254. {
  3255. JobDetails& job = *jobIter;
  3256. if (CanAnalyzeJob(job))
  3257. {
  3258. anyJobAnalyzed = true;
  3259. ProcessJob(job);
  3260. jobIter = m_jobsToProcess.erase(jobIter);
  3261. m_numOfJobsToAnalyze--;
  3262. // Update the remaining job status occasionally
  3263. if (elapsedTimer.elapsed() >= MILLISECONDS_BETWEEN_PROCESS_JOBS_STATUS_UPDATE)
  3264. {
  3265. Q_EMIT NumRemainingJobsChanged(m_activeFiles.size() + m_filesToExamine.size() + m_numOfJobsToAnalyze, AnalysisExtraInfo());
  3266. elapsedTimer.restart();
  3267. }
  3268. }
  3269. else
  3270. {
  3271. ++jobIter;
  3272. }
  3273. }
  3274. if (m_jobsToProcess.size())
  3275. {
  3276. if (!anyJobAnalyzed)
  3277. {
  3278. // Process the first job if no jobs were analyzed.
  3279. auto jobIter = m_jobsToProcess.begin();
  3280. JobDetails& job = *jobIter;
  3281. AZ_Warning(
  3282. AssetProcessor::DebugChannel, false, " Cyclic job dependency detected. Processing job (%s, %s, %s, %s) to unblock.",
  3283. job.m_jobEntry.m_sourceAssetReference.AbsolutePath().c_str(), job.m_jobEntry.m_jobKey.toUtf8().data(),
  3284. job.m_jobEntry.m_platformInfo.m_identifier.c_str(), job.m_jobEntry.m_builderGuid.ToString<AZStd::string>().c_str());
  3285. ProcessJob(job);
  3286. m_jobsToProcess.erase(jobIter);
  3287. m_numOfJobsToAnalyze--;
  3288. }
  3289. QMetaObject::invokeMethod(this, "ProcessJobs", Qt::QueuedConnection);
  3290. }
  3291. else
  3292. {
  3293. QueueIdleCheck();
  3294. }
  3295. Q_EMIT NumRemainingJobsChanged(m_activeFiles.size() + m_filesToExamine.size() + m_numOfJobsToAnalyze, AnalysisExtraInfo());
  3296. }
  3297. void AssetProcessorManager::ProcessJob(JobDetails& job)
  3298. {
  3299. // Populate all the files needed for fingerprinting of this job. Note that m_fingerprintFilesList is a sorted set
  3300. // and thus will automatically eliminate duplicates and be sorted. It is expected to contain the absolute paths to all
  3301. // files that contribute to the fingerprint of the job.
  3302. // this automatically adds the input file to the list, too.
  3303. // note that for jobs, we only query source dependencies, here, not Source and Job dependencies.
  3304. // this is because we want to take the fingerprint of SOURCE FILES for source dependencies
  3305. // but for jobs we want the fingerprint of the job itself, not that job's source files.
  3306. QueryAbsolutePathDependenciesRecursive(job.m_jobEntry.m_sourceFileUUID, job.m_fingerprintFiles, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource);
  3307. // Add metadata files for all the fingerprint files
  3308. auto fingerprintFilesCopy = job.m_fingerprintFiles;
  3309. for (const auto& kvp : fingerprintFilesCopy)
  3310. {
  3311. AddMetadataFilesForFingerprinting(kvp.first.c_str(), job.m_fingerprintFiles);
  3312. }
  3313. // Check the current builder jobs with the previous ones in the database:
  3314. job.m_jobEntry.m_computedFingerprint = AssetUtilities::GenerateFingerprint(job);
  3315. JobIndentifier jobIndentifier(JobDesc(job.m_jobEntry.m_sourceAssetReference, job.m_jobEntry.m_jobKey.toUtf8().data(), job.m_jobEntry.m_platformInfo.m_identifier), job.m_jobEntry.m_builderGuid);
  3316. {
  3317. AZStd::lock_guard<AssetProcessor::ProcessingJobInfoBus::MutexType> lock(AssetProcessor::ProcessingJobInfoBus::GetOrCreateContext().m_contextMutex);
  3318. m_jobFingerprintMap[jobIndentifier] = job.m_jobEntry.m_computedFingerprint;
  3319. }
  3320. job.m_jobEntry.m_computedFingerprintTimeStamp = QDateTime::currentMSecsSinceEpoch();
  3321. if (job.m_jobEntry.m_computedFingerprint == 0)
  3322. {
  3323. // unable to fingerprint this file.
  3324. AZ_TracePrintf(AssetProcessor::DebugChannel, "ProcessBuilders: Unable to fingerprint for platform: %s.\n", job.m_jobEntry.m_platformInfo.m_identifier.c_str());
  3325. }
  3326. // Check to see whether we need to process this asset
  3327. if (AnalyzeJob(job))
  3328. {
  3329. Q_EMIT AssetToProcess(job);
  3330. }
  3331. else
  3332. {
  3333. // we're about to drop the job because its already up to date, so that's one job that is "Finished"
  3334. UpdateAnalysisTrackerForFile(job.m_jobEntry, AnalysisTrackerUpdateType::JobFinished);
  3335. }
  3336. }
  3337. bool AssetProcessorManager::IsInCacheFolder(AZ::IO::PathView path) const
  3338. {
  3339. return AssetUtilities::IsInCacheFolder(path, m_normalizedCacheRootPath.toUtf8().constData());
  3340. }
  3341. bool AssetProcessorManager::IsInIntermediateAssetsFolder(const SourceAssetReference& sourceAsset) const
  3342. {
  3343. return sourceAsset.ScanFolderId() == m_platformConfig->GetIntermediateAssetsScanFolderId();
  3344. }
  3345. bool AssetProcessorManager::IsInIntermediateAssetsFolder(AZ::IO::PathView path) const
  3346. {
  3347. return AssetUtilities::IsInIntermediateAssetsFolder(path, m_normalizedCacheRootPath.toUtf8().constData());
  3348. }
  3349. bool AssetProcessorManager::IsInIntermediateAssetsFolder(QString path) const
  3350. {
  3351. return AssetProcessorManager::IsInIntermediateAssetsFolder(AZ::IO::PathView(path.toUtf8().constData()));
  3352. }
  3353. void AssetProcessorManager::UpdateJobDependency(JobDetails& job)
  3354. {
  3355. for(size_t jobDependencySlot = 0; jobDependencySlot < job.m_jobDependencyList.size();)
  3356. {
  3357. AssetProcessor::JobDependencyInternal* jobDependencyInternal = &job.m_jobDependencyList[jobDependencySlot];
  3358. AssetBuilderSDK::SourceFileDependency& sourceFileDependency = jobDependencyInternal->m_jobDependency.m_sourceFile;
  3359. if (sourceFileDependency.m_sourceFileDependencyUUID.IsNull() && sourceFileDependency.m_sourceFileDependencyPath.empty())
  3360. {
  3361. AZ_Warning(
  3362. AssetProcessor::DebugChannel,
  3363. false,
  3364. "Invalid job dependency for job %s - dependency is empty",
  3365. job.ToString().c_str());
  3366. job.m_jobDependencyList.erase(jobDependencyInternal);
  3367. continue;
  3368. }
  3369. QString databaseSourceName;
  3370. QStringList resolvedList;
  3371. if (!ResolveSourceFileDependencyPath(sourceFileDependency, databaseSourceName, resolvedList))
  3372. {
  3373. AZ_Warning(
  3374. AssetProcessor::DebugChannel,
  3375. false,
  3376. "Unable to resolve job dependency for job %s on %s",
  3377. "With this unresolved job dependency, this file may not reprocess in situations where you would expect, "
  3378. "because of this gap in the job dependency graph. This could be caused by a disabled builder, or missing source asset.",
  3379. job.ToString().c_str(),
  3380. sourceFileDependency.ToString().c_str());
  3381. job.m_jobDependencyList.erase(jobDependencyInternal);
  3382. continue;
  3383. }
  3384. if(!sourceFileDependency.m_sourceFileDependencyUUID.IsNull())
  3385. {
  3386. SourceAssetReference sourceAsset;
  3387. if(SearchSourceInfoBySourceUUID(sourceFileDependency.m_sourceFileDependencyUUID, sourceAsset))
  3388. {
  3389. databaseSourceName = sourceAsset.AbsolutePath().c_str();
  3390. }
  3391. else
  3392. {
  3393. AZ_Warning(
  3394. AssetProcessor::DebugChannel,
  3395. false,
  3396. "Unable to resolve job dependency for job %s on %s\n"
  3397. "With this unresolved job dependency, this file may not reprocess in situations where you would expect, "
  3398. "because of this gap in the job dependency graph. This could be caused by a disabled builder, or missing source asset.",
  3399. job.ToString().c_str(),
  3400. sourceFileDependency.ToString().c_str());
  3401. job.m_jobDependencyList.erase(jobDependencyInternal);
  3402. continue;
  3403. }
  3404. }
  3405. else if(!AZ::IO::PathView(databaseSourceName.toUtf8().constData()).IsAbsolute())
  3406. {
  3407. QString absolutePath = m_platformConfig->FindFirstMatchingFile(databaseSourceName);
  3408. if (!absolutePath.isEmpty())
  3409. {
  3410. databaseSourceName = absolutePath;
  3411. }
  3412. else
  3413. {
  3414. // If we can't resolve the dependency, it usually means it doesn't exist
  3415. job.m_jobDependencyList.erase(jobDependencyInternal);
  3416. continue;
  3417. }
  3418. }
  3419. sourceFileDependency.m_sourceFileDependencyPath = AssetUtilities::NormalizeFilePath(databaseSourceName).toUtf8().data();
  3420. if (jobDependencyInternal->m_jobDependency.m_type == AssetBuilderSDK::JobDependencyType::OrderOnce)
  3421. {
  3422. // If the database knows about the job than it implies that AP has processed it sucessfully at least once
  3423. // and therefore the dependent job should not cause the job which depends on it to be processed again.
  3424. // If however we find a dependent job which is not known to AP then we know this job needs to be processed
  3425. // after all the dependent jobs have completed at least once.
  3426. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs;
  3427. if (m_stateData->GetJobsBySourceName(SourceAssetReference(sourceFileDependency.m_sourceFileDependencyPath.c_str()), jobs, AZ::Uuid::CreateNull(), jobDependencyInternal->m_jobDependency.m_jobKey.c_str(), jobDependencyInternal->m_jobDependency.m_platformIdentifier.c_str(), AzToolsFramework::AssetSystem::JobStatus::Completed))
  3428. {
  3429. job.m_jobDependencyList.erase(jobDependencyInternal);
  3430. continue;
  3431. }
  3432. // Since dependent job fingerprint do not affect the fingerprint of this job, we need to always process
  3433. // this job if either it is a new dependency or if the dependent job failed last time, which we check by querying the database.
  3434. job.m_autoProcessJob = true;
  3435. }
  3436. {
  3437. // Listing all the builderUuids that have the same (sourcefile,platform,jobKey) for this job dependency
  3438. JobDesc jobDesc(
  3439. SourceAssetReference(sourceFileDependency.m_sourceFileDependencyPath.c_str()),
  3440. jobDependencyInternal->m_jobDependency.m_jobKey, jobDependencyInternal->m_jobDependency.m_platformIdentifier);
  3441. auto buildersFound = m_jobDescToBuilderUuidMap.find(jobDesc);
  3442. if (buildersFound != m_jobDescToBuilderUuidMap.end())
  3443. {
  3444. for (const AZ::Uuid& builderUuid : buildersFound->second)
  3445. {
  3446. jobDependencyInternal->m_builderUuidList.insert(builderUuid);
  3447. }
  3448. }
  3449. else if(sourceFileDependency.m_sourceDependencyType != AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards)
  3450. {
  3451. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "UpdateJobDependency: Failed to find builder dependency for %s job (%s, %s, %s)\n",
  3452. job.m_jobEntry.GetAbsoluteSourcePath().toUtf8().constData(),
  3453. jobDependencyInternal->m_jobDependency.m_sourceFile.m_sourceFileDependencyPath.c_str(),
  3454. jobDependencyInternal->m_jobDependency.m_jobKey.c_str(),
  3455. jobDependencyInternal->m_jobDependency.m_platformIdentifier.c_str());
  3456. job.m_hasMissingSourceDependency = true;
  3457. }
  3458. }
  3459. if (sourceFileDependency.m_sourceDependencyType == AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards)
  3460. {
  3461. UpdateWildcardDependencies(job, jobDependencySlot, resolvedList);
  3462. }
  3463. ++jobDependencySlot;
  3464. }
  3465. // sorting job dependencies as they can effect the fingerprint of the job
  3466. AZStd::sort(job.m_jobDependencyList.begin(), job.m_jobDependencyList.end(),
  3467. [](const AssetProcessor::JobDependencyInternal& lhs, const AssetProcessor::JobDependencyInternal& rhs)
  3468. {
  3469. return (lhs.ToString() < rhs.ToString());
  3470. }
  3471. );
  3472. }
  3473. void AssetProcessorManager::UpdateWildcardDependencies(JobDetails& job, size_t jobDependencySlot, QStringList& resolvedDependencyList)
  3474. {
  3475. for (int dependencySlot = 0; dependencySlot < resolvedDependencyList.size(); ++dependencySlot)
  3476. {
  3477. job.m_jobDependencyList.push_back(JobDependencyInternal(job.m_jobDependencyList[jobDependencySlot].m_jobDependency));
  3478. job.m_jobDependencyList.back().m_jobDependency.m_sourceFile.m_sourceFileDependencyPath = AssetUtilities::NormalizeFilePath(resolvedDependencyList[dependencySlot]).toUtf8().data();
  3479. job.m_jobDependencyList.back().m_jobDependency.m_sourceFile.m_sourceDependencyType = AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Absolute;
  3480. job.m_jobDependencyList.back().m_jobDependency.m_sourceFile.m_sourceFileDependencyUUID = AZ::Uuid::CreateNull();
  3481. }
  3482. }
  3483. bool AssetProcessorManager::CanAnalyzeJob(const JobDetails& job)
  3484. {
  3485. for (const JobDependencyInternal& jobDependencyInternal : job.m_jobDependencyList)
  3486. {
  3487. // Loop over all the builderUuid and check whether the corresponding entry exists in the jobsFingerprint map.
  3488. // If an entry exists, it implies than we have already send the job over to the RCController
  3489. for (auto builderIter = jobDependencyInternal.m_builderUuidList.begin(); builderIter != jobDependencyInternal.m_builderUuidList.end(); ++builderIter)
  3490. {
  3491. JobIndentifier jobIdentifier(JobDesc(SourceAssetReference(jobDependencyInternal.m_jobDependency.m_sourceFile.m_sourceFileDependencyPath.c_str()),
  3492. jobDependencyInternal.m_jobDependency.m_jobKey, jobDependencyInternal.m_jobDependency.m_platformIdentifier),
  3493. *builderIter);
  3494. auto jobFound = m_jobFingerprintMap.find(jobIdentifier);
  3495. if (jobFound == m_jobFingerprintMap.end())
  3496. {
  3497. // Job cannot be processed, since one of its dependent job hasn't been fingerprinted
  3498. return false;
  3499. }
  3500. }
  3501. }
  3502. // Either this job does not have any dependent jobs or all of its dependent jobs have been fingerprinted
  3503. return true;
  3504. }
  3505. void AssetProcessorManager::ProcessBuilders(const SourceAssetReference& sourceAsset, const ScanFolderInfo* scanFolder, const AssetProcessor::BuilderInfoList& builderInfoList)
  3506. {
  3507. // this function gets called once for every source file.
  3508. // it is expected to send the file to each builder registered to process that type of file
  3509. // and call the CreateJobs function on the builder.
  3510. // it bundles the results up in a JobToProcessEntry struct, while it is doing this:
  3511. JobToProcessEntry entry;
  3512. auto sourceUUIDOutcome = AssetUtilities::GetSourceUuid(sourceAsset);
  3513. if (!sourceUUIDOutcome)
  3514. {
  3515. auto failureMessage = AZStd::string::format(
  3516. "CreateJobs failed - %s", sourceUUIDOutcome.GetError().c_str());
  3517. AZ::Uuid builderUuid = builderInfoList.size() > 0 ? builderInfoList[0].m_busId : AZ::Uuid();
  3518. AutoFailJob(
  3519. failureMessage,
  3520. failureMessage,
  3521. JobEntry(
  3522. sourceAsset,
  3523. builderUuid,
  3524. { "all", {} },
  3525. QString("CreateJobs_%1").arg(builderUuid.ToFixedString().c_str()),
  3526. 0,
  3527. GenerateNewJobRunKey(),
  3528. AZ::Uuid(),
  3529. false));
  3530. return;
  3531. }
  3532. const AZ::Uuid sourceUUID = sourceUUIDOutcome.GetValue();
  3533. {
  3534. // this scope exists only to narrow the range of m_sourceUUIDToSourceNameMapMutex
  3535. AZStd::lock_guard<AZStd::mutex> lock(m_sourceUUIDToSourceInfoMapMutex);
  3536. m_sourceUUIDToSourceInfoMap[sourceUUID] = sourceAsset; // Don't use insert, there may be an outdated entry from a previously overriden file
  3537. }
  3538. // insert the new entry into the analysis tracker:
  3539. auto resultInsert = m_remainingJobsForEachSourceFile.insert_key(sourceAsset.AbsolutePath().c_str());
  3540. AnalysisTracker& analysisTracker = resultInsert.first->second;
  3541. analysisTracker.m_databaseSourceName = sourceAsset.RelativePath().c_str();
  3542. analysisTracker.m_databaseScanFolderId = scanFolder->ScanFolderID();
  3543. analysisTracker.m_buildersInvolved.clear();
  3544. for (const AssetBuilderSDK::AssetBuilderDesc& builderInfo : builderInfoList)
  3545. {
  3546. analysisTracker.m_buildersInvolved.insert(builderInfo.m_busId);
  3547. }
  3548. // collect all the jobs and responses
  3549. for (const AssetBuilderSDK::AssetBuilderDesc& builderInfo : builderInfoList)
  3550. {
  3551. // If the builder's bus ID is null, then avoid processing (this should not happen)
  3552. if (builderInfo.m_busId.IsNull())
  3553. {
  3554. AZ_TracePrintf(AssetProcessor::DebugChannel, "Skipping builder %s, no builder bus id defined.\n", builderInfo.m_name.data());
  3555. continue;
  3556. }
  3557. AZStd::vector<AssetBuilderSDK::PlatformInfo> platforms = scanFolder->GetPlatforms();
  3558. const AssetBuilderSDK::CreateJobsRequest createJobsRequest(builderInfo.m_busId, sourceAsset.RelativePath().c_str(), scanFolder->ScanPath().toUtf8().constData(), platforms, sourceUUID);
  3559. AssetBuilderSDK::CreateJobsResponse createJobsResponse;
  3560. // Wrap with a log listener to redirect logging to a job specific log file and then send job request to the builder
  3561. AZ::s64 runKey = GenerateNewJobRunKey();
  3562. AssetProcessor::SetThreadLocalJobId(runKey);
  3563. AZStd::string logFileName = AssetUtilities::ComputeJobLogFileName(createJobsRequest);
  3564. {
  3565. AssetUtilities::JobLogTraceListener jobLogTraceListener(logFileName, runKey, true);
  3566. // track the time it takes to createJobs. We can perform analysis later to present it by extension and other stats.
  3567. QString statKey = QString("CreateJobs,%1,%2").arg(sourceAsset.RelativePath().c_str()).arg(builderInfo.m_name.c_str());
  3568. AssetProcessor::StatsCapture::BeginCaptureStat(statKey.toUtf8().constData());
  3569. builderInfo.m_createJobFunction(createJobsRequest, createJobsResponse);
  3570. AssetProcessor::StatsCapture::EndCaptureStat(statKey.toUtf8().constData(), true);
  3571. }
  3572. AssetProcessor::SetThreadLocalJobId(0);
  3573. bool isBuilderMissingFingerprint = (createJobsResponse.m_result == AssetBuilderSDK::CreateJobsResultCode::Success
  3574. && !createJobsResponse.m_createJobOutputs.empty()
  3575. && !createJobsResponse.m_createJobOutputs[0].m_additionalFingerprintInfo.empty()
  3576. && builderInfo.m_analysisFingerprint.empty());
  3577. if (createJobsResponse.m_result == AssetBuilderSDK::CreateJobsResultCode::Failed || isBuilderMissingFingerprint)
  3578. {
  3579. AZStd::string fullPathToLogFile = AssetUtilities::ComputeJobLogFolder();
  3580. fullPathToLogFile += "/";
  3581. fullPathToLogFile += logFileName.c_str();
  3582. char resolvedBuffer[AZ_MAX_PATH_LEN] = { 0 };
  3583. AZ::IO::FileIOBase::GetInstance()->ResolvePath(fullPathToLogFile.c_str(), resolvedBuffer, AZ_MAX_PATH_LEN);
  3584. // try reading the log yourself.
  3585. AssetJobLogResponse response;
  3586. AZStd::string failureMessage;
  3587. if (isBuilderMissingFingerprint)
  3588. {
  3589. failureMessage = AZStd::string::format(
  3590. "CreateJobs of %s has failed.\n"
  3591. "The builder (%s, %s) job response contained non-empty m_additionalFingerprintInfo but the builder itself does not contain a fingerprint.\n"
  3592. "Builders must provide a fingerprint so the Asset Processor can detect changes that may require assets to be reprocessed.\n"
  3593. "This is a coding error. Please update the builder to include an m_analysisFingerprint in its registration.\n",
  3594. sourceAsset.AbsolutePath().c_str(),
  3595. builderInfo.m_name.c_str(),
  3596. builderInfo.m_busId.ToString<AZStd::string>().c_str());
  3597. }
  3598. else
  3599. {
  3600. failureMessage = AZStd::string::format(
  3601. "CreateJobs of %s has failed.\n"
  3602. "This is often because the asset is corrupt.\n"
  3603. "Please load it in the editor to see what might be wrong.\n",
  3604. sourceAsset.AbsolutePath().c_str());
  3605. AssetUtilities::ReadJobLog(resolvedBuffer, response);
  3606. }
  3607. AutoFailJob(AZStd::string::format("Createjobs Failed: %s.\n", sourceAsset.AbsolutePath().c_str()),
  3608. failureMessage,
  3609. JobEntry(
  3610. sourceAsset,
  3611. builderInfo.m_busId,
  3612. { "all", {} },
  3613. QString("CreateJobs_%1").arg(builderInfo.m_busId.ToString<AZStd::string>().c_str()),
  3614. 0,
  3615. runKey,
  3616. sourceUUID,
  3617. false), response.m_jobLog
  3618. );
  3619. continue;
  3620. }
  3621. else if (createJobsResponse.m_result == AssetBuilderSDK::CreateJobsResultCode::ShuttingDown)
  3622. {
  3623. return;
  3624. }
  3625. else
  3626. {
  3627. // if we get here, we succeeded.
  3628. {
  3629. // if we succeeded, we can erase any jobs that had failed createjobs last time for this builder:
  3630. AzToolsFramework::AssetSystem::JobInfo jobInfo;
  3631. jobInfo.m_sourceFile = sourceAsset.RelativePath().Native();
  3632. jobInfo.m_watchFolder = sourceAsset.ScanFolderPath().Native();
  3633. jobInfo.m_platform = "all";
  3634. jobInfo.m_jobKey = AZStd::string::format("CreateJobs_%s", builderInfo.m_busId.ToString<AZStd::string>().c_str());
  3635. Q_EMIT JobRemoved(jobInfo);
  3636. }
  3637. int numJobDependencies = 0;
  3638. for (AssetBuilderSDK::JobDescriptor& jobDescriptor : createJobsResponse.m_createJobOutputs)
  3639. {
  3640. // Allow for overrides defined in a BuilderConfig.ini file to update our code defined default values
  3641. AssetProcessor::BuilderConfigurationRequestBus::Broadcast(&AssetProcessor::BuilderConfigurationRequests::UpdateJobDescriptor, jobDescriptor.m_jobKey, jobDescriptor);
  3642. const AssetBuilderSDK::PlatformInfo* const infoForPlatform = m_platformConfig->GetPlatformByIdentifier(jobDescriptor.GetPlatformIdentifier().c_str());
  3643. if (!infoForPlatform)
  3644. {
  3645. AZ_Warning(AssetProcessor::ConsoleChannel, infoForPlatform,
  3646. "CODE BUG: Builder %s emitted jobs for a platform that isn't enabled (%s). This job will be "
  3647. "discarded. Builders should check the input list of platforms and only emit jobs for platforms "
  3648. "in that list", builderInfo.m_name.c_str(), jobDescriptor.GetPlatformIdentifier().c_str());
  3649. continue;
  3650. }
  3651. {
  3652. JobDetails newJob;
  3653. newJob.m_assetBuilderDesc = builderInfo;
  3654. newJob.m_critical = jobDescriptor.m_critical;
  3655. newJob.m_extraInformationForFingerprinting = AZStd::string::format("%i%s", builderInfo.m_version, jobDescriptor.m_additionalFingerprintInfo.c_str());
  3656. newJob.m_jobEntry = JobEntry(
  3657. sourceAsset,
  3658. builderInfo.m_busId,
  3659. *infoForPlatform,
  3660. jobDescriptor.m_jobKey.c_str(), 0, GenerateNewJobRunKey(),
  3661. sourceUUID);
  3662. newJob.m_jobEntry.m_checkExclusiveLock = jobDescriptor.m_checkExclusiveLock;
  3663. newJob.m_jobParam = AZStd::move(jobDescriptor.m_jobParameters);
  3664. newJob.m_priority = jobDescriptor.m_priority;
  3665. newJob.m_scanFolder = scanFolder;
  3666. newJob.m_checkServer = jobDescriptor.m_checkServer;
  3667. auto* uuidInterface = AZ::Interface<AssetProcessor::IUuidRequests>::Get();
  3668. if (!uuidInterface)
  3669. {
  3670. AZ_Assert(uuidInterface, "Programmer Error - IUuidRequests interface is not available.");
  3671. return;
  3672. }
  3673. const bool isEnabledType = uuidInterface->IsGenerationEnabledForFile(sourceAsset.AbsolutePath());
  3674. newJob.m_sourceUuid = isEnabledType ? sourceUUID : AZ::Uuid{};
  3675. if (m_builderDebugFlag)
  3676. {
  3677. newJob.m_jobParam[AZ_CRC_CE("DebugFlag")] = "true";
  3678. }
  3679. // Keep track of the job dependencies as we loop to help detect duplicates
  3680. AZStd::unordered_set<AssetBuilderSDK::JobDependency> jobDependenciesDuplicateCheck;
  3681. for (const AssetBuilderSDK::JobDependency& jobDependency : jobDescriptor.m_jobDependencyList)
  3682. {
  3683. if (auto result = jobDependenciesDuplicateCheck.insert(jobDependency); !result.second)
  3684. {
  3685. // It is not an error or warning to supply the same job dependency
  3686. // repeatedly as a duplicate. It is common for builders to be parsing
  3687. // source files which may mention the same dependency repeatedly.
  3688. // Rather than require all of them do filtering on their end, it is
  3689. // cleaner to do the de-duplication here and drop the duplicates.
  3690. continue;
  3691. }
  3692. newJob.m_jobDependencyList.push_back(JobDependencyInternal(jobDependency));
  3693. ++numJobDependencies;
  3694. }
  3695. // note that until analysis completes, the jobId is not set and neither is the destination path
  3696. JobDesc jobDesc(newJob.m_jobEntry.m_sourceAssetReference, newJob.m_jobEntry.m_jobKey.toUtf8().data(), newJob.m_jobEntry.m_platformInfo.m_identifier);
  3697. m_jobDescToBuilderUuidMap[jobDesc].insert(builderInfo.m_busId);
  3698. // until this job is analyzed, assume its fingerprint is not computed.
  3699. JobIndentifier jobIdentifier(jobDesc, builderInfo.m_busId);
  3700. {
  3701. AZStd::lock_guard<AssetProcessor::ProcessingJobInfoBus::MutexType> lock(AssetProcessor::ProcessingJobInfoBus::GetOrCreateContext().m_contextMutex);
  3702. m_jobFingerprintMap.erase(jobIdentifier);
  3703. }
  3704. entry.m_jobsToAnalyze.push_back(AZStd::move(newJob));
  3705. // because we added / created a job for the queue, we increment the number of outstanding jobs for this item now.
  3706. // when it either later gets analyzed and done, or dropped (because its already up to date), we will decrement it.
  3707. UpdateAnalysisTrackerForFile(sourceAsset, AnalysisTrackerUpdateType::JobStarted);
  3708. m_numOfJobsToAnalyze++;
  3709. }
  3710. }
  3711. // detect if the configuration of the builder is correct:
  3712. if ((!createJobsResponse.m_sourceFileDependencyList.empty()) || (numJobDependencies > 0))
  3713. {
  3714. if ((builderInfo.m_flags & AssetBuilderSDK::AssetBuilderDesc::BF_EmitsNoDependencies) != 0)
  3715. {
  3716. AZ_WarningOnce(ConsoleChannel, false, "Asset builder '%s' registered itself using BF_EmitsNoDependencies flag, but actually emitted dependencies. This will cause rebuilds to be inconsistent.\n", builderInfo.m_name.c_str());
  3717. }
  3718. // remember which builder emitted each dependency:
  3719. for (const AssetBuilderSDK::SourceFileDependency& sourceDependency : createJobsResponse.m_sourceFileDependencyList)
  3720. {
  3721. entry.m_sourceFileDependencies.push_back(AZStd::make_pair(builderInfo.m_busId, sourceDependency));
  3722. }
  3723. }
  3724. }
  3725. }
  3726. // Put the whole set into the 'process later' queue, so it runs after its dependencies
  3727. entry.m_sourceFileInfo.m_sourceAssetReference = sourceAsset;
  3728. entry.m_sourceFileInfo.m_scanFolder = scanFolder;
  3729. entry.m_sourceFileInfo.m_uuid = sourceUUID;
  3730. // entry now contains, for one given source file, all jobs, dependencies, etc, created by ALL builders.
  3731. // now we can update the database with this new information:
  3732. UpdateSourceFileDependenciesDatabase(entry);
  3733. m_jobEntries.push_back(entry);
  3734. // Signals SourceAssetTreeModel so it can update the CreateJobs duration change
  3735. Q_EMIT CreateJobsDurationChanged(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderId());
  3736. }
  3737. bool AssetProcessorManager::ResolveSourceFileDependencyPath(AssetBuilderSDK::SourceFileDependency& sourceDependency, QString& resultDatabaseSourceName, QStringList& resolvedDependencyList)
  3738. {
  3739. resultDatabaseSourceName.clear();
  3740. if (!sourceDependency.m_sourceFileDependencyUUID.IsNull())
  3741. {
  3742. // if the UUID has been provided, we will use that
  3743. auto* uuidInterface = AZ::Interface<IUuidRequests>::Get();
  3744. AZ_Assert(uuidInterface, "Programmer Error - IUuidRequests is not available.");
  3745. // Try to get the canonical UUID for the file in case this is a legacy UUID
  3746. if (auto canonicalUuid = uuidInterface->GetCanonicalUuid(sourceDependency.m_sourceFileDependencyUUID); canonicalUuid)
  3747. {
  3748. sourceDependency.m_sourceFileDependencyUUID = canonicalUuid.value();
  3749. }
  3750. resultDatabaseSourceName = QString::fromUtf8(sourceDependency.m_sourceFileDependencyUUID.ToFixedString().c_str());
  3751. }
  3752. else if (!sourceDependency.m_sourceFileDependencyPath.empty())
  3753. {
  3754. // instead of a UUID, a path has been provided, prepare and use that.
  3755. QString encodedFileData = QString::fromUtf8(sourceDependency.m_sourceFileDependencyPath.c_str());
  3756. encodedFileData = AssetUtilities::NormalizeFilePath(encodedFileData);
  3757. if (sourceDependency.m_sourceDependencyType == AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards)
  3758. {
  3759. int wildcardIndex = encodedFileData.indexOf("*");
  3760. if (wildcardIndex < 0)
  3761. {
  3762. AZ_Warning("AssetProcessorManager", false, "Source File Dependency %s is marked as a wildcard dependency but no wildcard was included."
  3763. " Please change the source dependency type or include a wildcard.", encodedFileData.toUtf8().constData());
  3764. }
  3765. else
  3766. {
  3767. int slashBeforeWildcardIndex = encodedFileData.lastIndexOf("/", wildcardIndex);
  3768. QString knownPathBeforeWildcard = encodedFileData.left(slashBeforeWildcardIndex + 1); // include the slash
  3769. QString relativeSearch = encodedFileData.mid(slashBeforeWildcardIndex + 1); // skip the slash
  3770. const auto& excludedFolders = m_excludedFolderCache->GetExcludedFolders();
  3771. // Absolute path, just check the 1 scan folder
  3772. if (AZ::IO::PathView(encodedFileData.toUtf8().constData()).IsAbsolute())
  3773. {
  3774. auto scanFolderInfo = m_platformConfig->GetScanFolderForFile(encodedFileData);
  3775. if (!m_platformConfig->ConvertToRelativePath(encodedFileData, scanFolderInfo, resultDatabaseSourceName))
  3776. {
  3777. AZ_Warning(
  3778. AssetProcessor::ConsoleChannel, false,
  3779. "'%s' does not appear to be in any input folder. Use relative paths instead.",
  3780. sourceDependency.m_sourceFileDependencyPath.c_str());
  3781. }
  3782. else
  3783. {
  3784. // Make an absolute path that is ScanFolderPath + Part of search path before the wildcard
  3785. QDir rooted(scanFolderInfo->ScanPath());
  3786. QString scanFolderAndKnownSubPath = rooted.absoluteFilePath(knownPathBeforeWildcard);
  3787. resolvedDependencyList.append(m_platformConfig->FindWildcardMatches(
  3788. scanFolderAndKnownSubPath, relativeSearch,
  3789. excludedFolders, false, scanFolderInfo->RecurseSubFolders()));
  3790. }
  3791. }
  3792. else // Relative path, check every scan folder
  3793. {
  3794. for (int i = 0; i < m_platformConfig->GetScanFolderCount(); ++i)
  3795. {
  3796. const ScanFolderInfo* scanFolderInfo = &m_platformConfig->GetScanFolderAt(i);
  3797. if (!scanFolderInfo->RecurseSubFolders() && encodedFileData.contains("/"))
  3798. {
  3799. continue;
  3800. }
  3801. QDir rooted(scanFolderInfo->ScanPath());
  3802. QString absolutePath = rooted.absoluteFilePath(knownPathBeforeWildcard);
  3803. resolvedDependencyList.append(m_platformConfig->FindWildcardMatches(
  3804. absolutePath, relativeSearch,
  3805. excludedFolders, false, scanFolderInfo->RecurseSubFolders()));
  3806. }
  3807. }
  3808. // Filter out any excluded files
  3809. for (auto itr = resolvedDependencyList.begin(); itr != resolvedDependencyList.end();)
  3810. {
  3811. if (m_platformConfig->IsFileExcluded(*itr))
  3812. {
  3813. itr = resolvedDependencyList.erase(itr);
  3814. }
  3815. else
  3816. {
  3817. ++itr;
  3818. }
  3819. }
  3820. // Convert to relative paths
  3821. for (auto dependencyItr = resolvedDependencyList.begin(); dependencyItr != resolvedDependencyList.end();)
  3822. {
  3823. QString relativePath, scanFolder;
  3824. if (m_platformConfig->ConvertToRelativePath(*dependencyItr, relativePath, scanFolder))
  3825. {
  3826. *dependencyItr = relativePath;
  3827. ++dependencyItr;
  3828. }
  3829. else
  3830. {
  3831. AZ_Warning("AssetProcessor", false, "Failed to get relative path for wildcard dependency file %s. Is the file within a scan folder?",
  3832. dependencyItr->toUtf8().constData());
  3833. dependencyItr = resolvedDependencyList.erase(dependencyItr);
  3834. }
  3835. }
  3836. resultDatabaseSourceName = encodedFileData.replace('\\', '/');
  3837. resultDatabaseSourceName = encodedFileData.replace('*', '%');
  3838. }
  3839. }
  3840. else if (QFileInfo(encodedFileData).isAbsolute())
  3841. {
  3842. QString scanFolderName;
  3843. // Verify the path is in a scanfolder
  3844. if (!m_platformConfig->GetScanFolderForFile(encodedFileData))
  3845. {
  3846. AZ_Warning(AssetProcessor::ConsoleChannel, false, "'%s' does not appear to be in any input folder. Use relative paths instead.", sourceDependency.m_sourceFileDependencyPath.c_str());
  3847. }
  3848. else
  3849. {
  3850. // Return the absolute path
  3851. resultDatabaseSourceName = encodedFileData;
  3852. }
  3853. }
  3854. else
  3855. {
  3856. // Return the relative path
  3857. resultDatabaseSourceName = encodedFileData;
  3858. }
  3859. }
  3860. else
  3861. {
  3862. AZ_Warning(AssetProcessor::ConsoleChannel, false, "The dependency fields were empty.");
  3863. }
  3864. return (!resultDatabaseSourceName.isEmpty());
  3865. }
  3866. void AssetProcessorManager::UpdateSourceFileDependenciesDatabase(JobToProcessEntry& entry)
  3867. {
  3868. using namespace AzToolsFramework::AssetDatabase;
  3869. AZ_TraceContext("Source File", entry.m_sourceFileInfo.m_sourceAssetReference.AbsolutePath().c_str());
  3870. // entry is all of the collected CreateJobs responses and other info for a given single source file.
  3871. // we are going to erase the prior entries in the database for this source file and replace them with the new ones
  3872. // we are also going to find any unresolved entries in the database for THIS source, and update them
  3873. // the database contains the following columns
  3874. // ID BuilderID SOURCE WhatItDependsOn TypeOfDependency
  3875. // note that NEITHER columns (source / what it depends on) are database names (ie, they do not have the output prefix prepended)
  3876. // where "whatitdependson" is either a relative path to a source file, or, if the source's UUID is unknown, a UUID in curly braces format.
  3877. // collect all dependencies, of every type of dependency:
  3878. QString sourcePath;
  3879. if (entry.m_sourceFileInfo.m_scanFolder)
  3880. {
  3881. sourcePath = QString("%1/%2").arg(
  3882. entry.m_sourceFileInfo.m_scanFolder->ScanPath(), entry.m_sourceFileInfo.m_sourceAssetReference.RelativePath().c_str());
  3883. }
  3884. else
  3885. {
  3886. sourcePath = entry.m_sourceFileInfo.m_sourceAssetReference.RelativePath().c_str();
  3887. }
  3888. AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer newDependencies;
  3889. struct DependencyDeduplication
  3890. {
  3891. AZ::Uuid m_builderUuid;
  3892. AZStd::string m_source;
  3893. AZStd::string m_dependsOn;
  3894. DependencyDeduplication(AZ::Uuid builderUuid, AZStd::string source, AZStd::string dependsOn)
  3895. : m_builderUuid(std::move(builderUuid)),
  3896. m_source(std::move(source)),
  3897. m_dependsOn(std::move(dependsOn)) {}
  3898. bool operator==(const DependencyDeduplication& other) const
  3899. {
  3900. return m_builderUuid == other.m_builderUuid
  3901. && m_source == other.m_source
  3902. && m_dependsOn == other.m_dependsOn;
  3903. }
  3904. struct Hasher
  3905. {
  3906. size_t operator()(const DependencyDeduplication& val) const
  3907. {
  3908. size_t hash = 0;
  3909. AZStd::hash_combine(hash, val.m_builderUuid, val.m_source, val.m_dependsOn);
  3910. return hash;
  3911. }
  3912. };
  3913. };
  3914. AZStd::unordered_set<DependencyDeduplication, DependencyDeduplication::Hasher> jobDependenciesDeduplication;
  3915. // gather the job dependencies first, since they're more specific and we'll use the dedupe set to check for unnecessary source dependencies
  3916. for (JobDetails& jobToCheck : entry.m_jobsToAnalyze)
  3917. {
  3918. // Since we're dealing with job dependencies here, we're going to be saving these SourceDependencies as JobToJob dependencies
  3919. constexpr SourceFileDependencyEntry::TypeOfDependency JobDependencyType = SourceFileDependencyEntry::DEP_JobToJob;
  3920. const AZ::Uuid& builderId = jobToCheck.m_assetBuilderDesc.m_busId;
  3921. for (AssetProcessor::JobDependencyInternal& jobDependency : jobToCheck.m_jobDependencyList)
  3922. {
  3923. // figure out whether we can resolve the dependency or not:
  3924. QStringList resolvedWildcardDependencies;
  3925. QString resolvedDatabaseName;
  3926. if (!ResolveSourceFileDependencyPath(
  3927. jobDependency.m_jobDependency.m_sourceFile, resolvedDatabaseName, resolvedWildcardDependencies))
  3928. {
  3929. continue;
  3930. }
  3931. AZStd::string subIds = jobDependency.m_jobDependency.ConcatenateSubIds();
  3932. for (const auto& thisEntry : resolvedWildcardDependencies)
  3933. {
  3934. SourceFileDependencyEntry newDependencyEntry(
  3935. builderId, entry.m_sourceFileInfo.m_uuid, PathOrUuid::Create(thisEntry.toUtf8().constData()),
  3936. JobDependencyType,
  3937. false, // Wildcard dependencies never come from an AssetId
  3938. subIds.c_str());
  3939. newDependencies.push_back(AZStd::move(newDependencyEntry));
  3940. }
  3941. // Source dependencies don't have any concept of jobs so if we store an entry for every job, we end up with duplicates.
  3942. // This isn't an issue with the builder, so no error/warning is needed, just check to avoid duplicates.
  3943. if (auto result = jobDependenciesDeduplication.emplace(
  3944. builderId, entry.m_sourceFileInfo.m_sourceAssetReference.RelativePath().c_str(),
  3945. resolvedDatabaseName.toUtf8().constData()); result.second)
  3946. {
  3947. SourceFileDependencyEntry newDependencyEntry(
  3948. builderId, entry.m_sourceFileInfo.m_uuid, PathOrUuid::Create(resolvedDatabaseName.toUtf8().constData()),
  3949. jobDependency.m_jobDependency.m_sourceFile.m_sourceDependencyType ==
  3950. AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards
  3951. ? SourceFileDependencyEntry::DEP_SourceLikeMatch
  3952. : JobDependencyType,
  3953. !jobDependency.m_jobDependency.m_sourceFile.m_sourceFileDependencyUUID.IsNull(),
  3954. subIds.c_str());
  3955. newDependencies.push_back(AZStd::move(newDependencyEntry));
  3956. }
  3957. }
  3958. }
  3959. AZStd::unordered_set<AZStd::string> resolvedSourceDependenciesDeduplication;
  3960. for (AZStd::pair<AZ::Uuid, AssetBuilderSDK::SourceFileDependency>& sourceDependency : entry.m_sourceFileDependencies)
  3961. {
  3962. // figure out whether we can resolve the dependency or not:
  3963. QStringList resolvedWildcardDependencies;
  3964. QString resolvedDatabaseName;
  3965. if (!ResolveSourceFileDependencyPath(sourceDependency.second, resolvedDatabaseName, resolvedWildcardDependencies))
  3966. {
  3967. // ResolveDependencyPath should only fail in a data error, otherwise it always outputs something
  3968. continue;
  3969. }
  3970. constexpr const char* DuplicateJobSourceDependencyMessageFormat =
  3971. "Builder `%s` emitted Source Dependency and Job Dependency on file `%s`. "
  3972. "This is unnecessary and the builder should be updated to only emit the Job Dependency.";
  3973. // Handle multiple resolves (wildcard dependencies)
  3974. for (const auto& thisEntry : resolvedWildcardDependencies)
  3975. {
  3976. if (jobDependenciesDeduplication.contains(
  3977. DependencyDeduplication{ sourceDependency.first,
  3978. entry.m_sourceFileInfo.m_sourceAssetReference.RelativePath().c_str(),
  3979. thisEntry.toUtf8().constData() }))
  3980. {
  3981. for (JobDetails& job : entry.m_jobsToAnalyze)
  3982. {
  3983. job.m_warnings.push_back(
  3984. AZStd::string::format(
  3985. DuplicateJobSourceDependencyMessageFormat,
  3986. job.m_assetBuilderDesc.m_name.c_str(), thisEntry.toUtf8().constData()));
  3987. }
  3988. continue;
  3989. }
  3990. // Sometimes multiple source dependencies can resolve to the same file due to the overrides system
  3991. // Eliminate the duplicates, no warning is needed since the builder can't be expected to handle this
  3992. if (auto result = resolvedSourceDependenciesDeduplication.insert(thisEntry.toUtf8().constData()); result.second)
  3993. {
  3994. // add the new dependency:
  3995. SourceFileDependencyEntry newDependencyEntry(
  3996. sourceDependency.first,
  3997. entry.m_sourceFileInfo.m_uuid,
  3998. PathOrUuid::Create(thisEntry.toUtf8().constData()),
  3999. SourceFileDependencyEntry::DEP_SourceToSource,
  4000. false, // Wildcard dependencies never come from an AssetId
  4001. "");
  4002. newDependencies.push_back(AZStd::move(newDependencyEntry));
  4003. }
  4004. else
  4005. {
  4006. ++m_numOverrides;
  4007. }
  4008. }
  4009. if (jobDependenciesDeduplication.contains(
  4010. DependencyDeduplication{
  4011. sourceDependency.first,
  4012. entry.m_sourceFileInfo.m_sourceAssetReference.RelativePath().c_str(),
  4013. resolvedDatabaseName.toUtf8().constData() }))
  4014. {
  4015. for (JobDetails& job : entry.m_jobsToAnalyze)
  4016. {
  4017. job.m_warnings.push_back(
  4018. AZStd::string::format(
  4019. DuplicateJobSourceDependencyMessageFormat,
  4020. job.m_assetBuilderDesc.m_name.c_str(),
  4021. resolvedDatabaseName.toUtf8().constData()
  4022. ));
  4023. }
  4024. continue;
  4025. }
  4026. // Sometimes multiple source dependencies can resolve to the same file due to the overrides system
  4027. // Eliminate the duplicates, no warning is needed since the builder can't be expected to handle this
  4028. if (auto result = resolvedSourceDependenciesDeduplication.insert(resolvedDatabaseName.toUtf8().constData()); result.second)
  4029. {
  4030. SourceFileDependencyEntry newDependencyEntry(
  4031. sourceDependency.first,
  4032. entry.m_sourceFileInfo.m_uuid,
  4033. PathOrUuid::Create(resolvedDatabaseName.toUtf8().constData()),
  4034. sourceDependency.second.m_sourceDependencyType ==
  4035. AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards
  4036. ? SourceFileDependencyEntry::DEP_SourceLikeMatch
  4037. : SourceFileDependencyEntry::DEP_SourceToSource,
  4038. !sourceDependency.second.m_sourceFileDependencyUUID.IsNull(),
  4039. "");
  4040. // If the UUID is null, then record that this dependency came from a (resolved) path
  4041. newDependencies.push_back(AZStd::move(newDependencyEntry));
  4042. }
  4043. }
  4044. // get all the old dependencies and remove them. This function is comprehensive on all dependencies
  4045. // for a given source file so we can just eliminate all of them from that same source file and replace
  4046. // them with all of the new ones for the given source file:
  4047. AZStd::unordered_set<AZ::s64> oldDependencies;
  4048. m_stateData->QueryDependsOnSourceBySourceDependency(
  4049. entry.m_sourceFileInfo.m_uuid, // find all rows in the database where this is the source column
  4050. SourceFileDependencyEntry::DEP_Any, // significant line in this code block
  4051. [&](SourceFileDependencyEntry& existingEntry)
  4052. {
  4053. oldDependencies.insert(existingEntry.m_sourceDependencyID);
  4054. return true; // return true to keep stepping to additional rows
  4055. });
  4056. m_stateData->RemoveSourceFileDependencies(oldDependencies);
  4057. oldDependencies.clear();
  4058. // set the new dependencies:
  4059. m_stateData->SetSourceFileDependencies(newDependencies);
  4060. }
  4061. AZStd::shared_ptr<AssetDatabaseConnection> AssetProcessorManager::GetDatabaseConnection() const
  4062. {
  4063. return m_stateData;
  4064. }
  4065. void AssetProcessorManager::EmitResolvedDependency(const AZ::Data::AssetId& assetId, const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry& entry)
  4066. {
  4067. Q_EMIT PathDependencyResolved(assetId, entry);
  4068. }
  4069. void AssetProcessorManager::BeginCacheFileUpdate(const char* productPath)
  4070. {
  4071. // Scope the lock for just modifying the processing product info list.
  4072. // This will allow other jobs to lock this list for emitting their own messages.
  4073. // This speeds up asset processing time, by not having jobs holding this longer than they need to.
  4074. {
  4075. QMutexLocker locker(&m_processingJobMutex);
  4076. m_processingProductInfoList.insert(productPath);
  4077. }
  4078. AssetNotificationMessage message(productPath, AssetNotificationMessage::JobFileClaimed, AZ::Data::s_invalidAssetType, "");
  4079. AssetProcessor::ConnectionBus::Broadcast(&AssetProcessor::ConnectionBus::Events::Send, 0, message);
  4080. }
  4081. void AssetProcessorManager::EndCacheFileUpdate(const char* productPath, bool queueAgainForDeletion)
  4082. {
  4083. // Scope the lock for just modifying the processing product info list.
  4084. // This will allow other jobs to lock this list for emitting their own messages.
  4085. // This speeds up asset processing time, by not having jobs holding this longer than they need to.
  4086. {
  4087. QMutexLocker locker(&m_processingJobMutex);
  4088. m_processingProductInfoList.erase(productPath);
  4089. }
  4090. if (queueAgainForDeletion)
  4091. {
  4092. QMetaObject::invokeMethod(this, "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString::fromUtf8(productPath)));
  4093. }
  4094. AssetNotificationMessage message(productPath, AssetNotificationMessage::JobFileReleased, AZ::Data::s_invalidAssetType, "");
  4095. AssetProcessor::ConnectionBus::Broadcast(&AssetProcessor::ConnectionBus::Events::Send, 0, message);
  4096. }
  4097. AZ::u32 AssetProcessorManager::GetJobFingerprint(const AssetProcessor::JobIndentifier& jobIndentifier)
  4098. {
  4099. auto jobFound = m_jobFingerprintMap.find(jobIndentifier);
  4100. if (jobFound == m_jobFingerprintMap.end())
  4101. {
  4102. // fingerprint of this job is missing
  4103. return 0;
  4104. }
  4105. else
  4106. {
  4107. return jobFound->second;
  4108. }
  4109. }
  4110. AZ::s64 AssetProcessorManager::GenerateNewJobRunKey()
  4111. {
  4112. return m_highestJobRunKeySoFar++;
  4113. }
  4114. bool AssetProcessorManager::EraseLogFile(const char* fileName)
  4115. {
  4116. AZ_Assert(fileName, "Invalid call to EraseLogFile with a nullptr filename.");
  4117. if ((!fileName) || (fileName[0] == 0))
  4118. {
  4119. // Sometimes logs are empty / missing already in the DB or empty in the "log" column.
  4120. // this counts as success since there is no log there.
  4121. return true;
  4122. }
  4123. // try removing it immediately - even if it doesn't exist, its quicker to delete it and notice it failed.
  4124. if (!AZ::IO::FileIOBase::GetInstance()->Remove(fileName))
  4125. {
  4126. // we couldn't remove it. Is it because it was already gone? Because in that case, there's no problem.
  4127. // we only worry if we were unable to delete it and it exists
  4128. if (AZ::IO::FileIOBase::GetInstance()->Exists(fileName))
  4129. {
  4130. AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Was unable to delete log file %s...\n", fileName);
  4131. return false;
  4132. }
  4133. }
  4134. return true; // if the file was either successfully removed or never existed in the first place, its gone, so we return true;
  4135. }
  4136. bool AssetProcessorManager::MigrateScanFolders()
  4137. {
  4138. // Migrate Scan Folders retrieves the last list of scan folders from the DB
  4139. // it then finds out what scan folders SHOULD be in the database now, by matching the portable key
  4140. // start with all of the scan folders that are currently in the database.
  4141. m_stateData->QueryScanFoldersTable([this](AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& entry)
  4142. {
  4143. // the database is case-insensitive, so we should emulate that here in our find()
  4144. AZStd::string portableKey = entry.m_portableKey;
  4145. AZStd::to_lower(portableKey.begin(), portableKey.end());
  4146. m_scanFoldersInDatabase.insert(AZStd::make_pair(portableKey, entry));
  4147. return true;
  4148. });
  4149. // now update them based on whats in the config file.
  4150. for (int i = 0; i < m_platformConfig->GetScanFolderCount(); ++i)
  4151. {
  4152. AssetProcessor::ScanFolderInfo& scanFolderFromConfigFile = m_platformConfig->GetScanFolderAt(i);
  4153. // for each scan folder in the config file, see if its port key already exists
  4154. AZStd::string scanFolderFromConfigFileKeyLower = scanFolderFromConfigFile.GetPortableKey().toLower().toUtf8().constData();
  4155. auto found = m_scanFoldersInDatabase.find(scanFolderFromConfigFileKeyLower.c_str());
  4156. AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanFolderToWrite;
  4157. if (found != m_scanFoldersInDatabase.end())
  4158. {
  4159. // portable key was found, this means we have an existing database entry for this config file entry.
  4160. scanFolderToWrite = AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry(
  4161. found->second.m_scanFolderID,
  4162. scanFolderFromConfigFile.ScanPath().toUtf8().constData(),
  4163. scanFolderFromConfigFile.GetDisplayName().toUtf8().constData(),
  4164. scanFolderFromConfigFile.GetPortableKey().toUtf8().constData(),
  4165. scanFolderFromConfigFile.IsRoot());
  4166. //remove this scan path from the scan folders so what is left can deleted
  4167. m_scanFoldersInDatabase.erase(found);
  4168. }
  4169. else
  4170. {
  4171. // no such key exists, its a new entry.
  4172. scanFolderToWrite = AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry(
  4173. scanFolderFromConfigFile.ScanPath().toUtf8().constData(),
  4174. scanFolderFromConfigFile.GetDisplayName().toUtf8().constData(),
  4175. scanFolderFromConfigFile.GetPortableKey().toUtf8().constData(),
  4176. scanFolderFromConfigFile.IsRoot());
  4177. }
  4178. // update the database.
  4179. bool res = m_stateData->SetScanFolder(scanFolderToWrite);
  4180. AZ_Assert(res, "Failed to set a scan folder.");
  4181. if (!res)
  4182. {
  4183. return false;
  4184. }
  4185. // update the in-memory value of the scan folder id from the above query.
  4186. scanFolderFromConfigFile.SetScanFolderID(scanFolderToWrite.m_scanFolderID);
  4187. }
  4188. m_platformConfig->CacheIntermediateAssetsScanFolderId();
  4189. return true;
  4190. }
  4191. bool AssetProcessorManager::SearchSourceInfoBySourceUUID(const AZ::Uuid& sourceUuid, SourceAssetReference& result)
  4192. {
  4193. {
  4194. // check the map first, it will be faster than checking the DB:
  4195. AZStd::lock_guard<AZStd::mutex> lock(m_sourceUUIDToSourceInfoMapMutex);
  4196. // Checking whether AP know about this source file, this map contain uuids of all known sources encountered in this session.
  4197. auto foundSource = m_sourceUUIDToSourceInfoMap.find(sourceUuid);
  4198. if (foundSource != m_sourceUUIDToSourceInfoMap.end())
  4199. {
  4200. result = foundSource->second;
  4201. return true;
  4202. }
  4203. }
  4204. // Try checking the UuidManager, it keeps track of legacy UUIDs
  4205. auto* uuidInterface = AZ::Interface<AssetProcessor::IUuidRequests>::Get();
  4206. AZ_Assert(uuidInterface, "Programmer Error - IUuidRequests interface is not available.");
  4207. if (auto foundFile = uuidInterface->FindHighestPriorityFileByUuid(sourceUuid); foundFile)
  4208. {
  4209. result = SourceAssetReference(foundFile.value());
  4210. return true;
  4211. }
  4212. // try the database next:
  4213. AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceDatabaseEntry;
  4214. if (m_stateData->GetSourceBySourceGuid(sourceUuid, sourceDatabaseEntry))
  4215. {
  4216. AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanFolder;
  4217. if (m_stateData->GetScanFolderByScanFolderID(sourceDatabaseEntry.m_scanFolderPK, scanFolder))
  4218. {
  4219. result = SourceAssetReference(scanFolder.m_scanFolder.c_str(), sourceDatabaseEntry.m_sourceName.c_str());
  4220. {
  4221. // this scope exists to restrict the duration of the below lock.
  4222. AZStd::lock_guard<AZStd::mutex> lock(m_sourceUUIDToSourceInfoMapMutex);
  4223. m_sourceUUIDToSourceInfoMap.insert(AZStd::make_pair(sourceUuid, result));
  4224. }
  4225. }
  4226. return true;
  4227. }
  4228. AZ_TracePrintf(AssetProcessor::DebugChannel, "Unable to find source file having uuid %s", sourceUuid.ToString<AZStd::string>().c_str());
  4229. return false;
  4230. }
  4231. void AssetProcessorManager::AnalyzeJobDetail(JobToProcessEntry& jobEntry)
  4232. {
  4233. // each jobEntry is all the jobs collected for a given single source file, this is our opportunity to update the Job Dependencies table
  4234. // since we need all of the ones for a given source.
  4235. using namespace AzToolsFramework::AssetDatabase;
  4236. for (JobDetails& jobDetail : jobEntry.m_jobsToAnalyze)
  4237. {
  4238. // update the job with whatever info it needs about dependencies to proceed:
  4239. UpdateJobDependency(jobDetail);
  4240. auto jobPair = m_jobsToProcess.insert(AZStd::move(jobDetail));
  4241. if (!jobPair.second)
  4242. {
  4243. // if we are here it means that this job was already found in the jobs to process list
  4244. // and therefore insert failed, we will try to update the iterator manually here.
  4245. // Note that if insert fails the original object is not destroyed and therefore we can use move again.
  4246. // we just replaced a job, so we have to decrement its count.
  4247. UpdateAnalysisTrackerForFile(jobPair.first->m_jobEntry, AnalysisTrackerUpdateType::JobFinished);
  4248. --m_numOfJobsToAnalyze;
  4249. *jobPair.first = AZStd::move(jobDetail);
  4250. }
  4251. }
  4252. }
  4253. QStringList AssetProcessorManager::GetSourceFilesWhichDependOnSourceFile(const QString& sourcePath, const ProductInfoList& updatedProducts)
  4254. {
  4255. // If updatedProducts != empty, we only return dependencies which match a subId in the updatedProducts list (called after process job to start dependencies which do care about specific products)
  4256. // If updatedProducts == empty, we only return dependencies with EMPTY m_subIds (called before create jobs to start dependencies which don't care about specific products)
  4257. // Note that dependencies with subIds are always JOB dependencies, pure source dependencies will never have any subIds
  4258. // The purpose of this function is to find anything that depends on this given file, so that they can be added to the queue.
  4259. // this is NOT a recursive query, because recursion will happen automatically as those files are in turn
  4260. // analyzed.
  4261. // It is generally called when a source file modified in any way, including when it is added or deleted.
  4262. // note that this is a "reverse" dependency query - it looks up what depends on a file, not what the file depends on
  4263. using namespace AzToolsFramework::AssetDatabase;
  4264. QSet<QString> absoluteSourceFilePathQueue;
  4265. QString databasePath;
  4266. QString scanFolder;
  4267. auto callbackFunction = [this, &absoluteSourceFilePathQueue, &updatedProducts](SourceFileDependencyEntry& entry)
  4268. {
  4269. if(updatedProducts.empty() != entry.m_subIds.empty())
  4270. {
  4271. return true;
  4272. }
  4273. if(!updatedProducts.empty())
  4274. {
  4275. // Filter the dependencies to those which match the list of updated products
  4276. bool matched = false;
  4277. AZStd::vector<AZStd::string> dependencyProducts;
  4278. AZ::StringFunc::Tokenize(entry.m_subIds, dependencyProducts, ",", false, false);
  4279. for(const AZStd::string& dependencySubId : dependencyProducts)
  4280. {
  4281. for(const auto& product : updatedProducts)
  4282. {
  4283. int subId = 0;
  4284. if (AZ::StringFunc::LooksLikeInt(dependencySubId.c_str(), &subId) && static_cast<AZ::u32>(subId) == product.first.m_subID)
  4285. {
  4286. matched = true;
  4287. break;
  4288. }
  4289. }
  4290. if(matched)
  4291. {
  4292. break;
  4293. }
  4294. }
  4295. if (!matched)
  4296. {
  4297. return true;
  4298. }
  4299. }
  4300. SourceAssetReference sourceAsset;
  4301. if (SearchSourceInfoBySourceUUID(entry.m_sourceGuid, sourceAsset))
  4302. {
  4303. // add it to the queue for analysis:
  4304. absoluteSourceFilePathQueue.insert(sourceAsset.AbsolutePath().c_str());
  4305. }
  4306. return true;
  4307. };
  4308. if (m_platformConfig->ConvertToRelativePath(sourcePath, databasePath, scanFolder))
  4309. {
  4310. SourceAssetReference sourceAsset(sourcePath);
  4311. AZ::Uuid uuid;
  4312. AzToolsFramework::AssetDatabase::SourceDatabaseEntry databaseEntry;
  4313. if(m_stateData->GetSourceBySourceNameScanFolderId(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderId(), databaseEntry))
  4314. {
  4315. uuid = databaseEntry.m_sourceGuid;
  4316. }
  4317. else
  4318. {
  4319. if (AZ::IO::FileIOBase::GetInstance()->Exists(sourceAsset.AbsolutePath().c_str()))
  4320. {
  4321. auto outcome = AssetUtilities::GetSourceUuid(sourceAsset);
  4322. if (outcome)
  4323. {
  4324. uuid = outcome.GetValue();
  4325. }
  4326. else
  4327. {
  4328. AZ_Error(AssetProcessor::ConsoleChannel, false, "%s", outcome.GetError().c_str());
  4329. return {};
  4330. }
  4331. }
  4332. else
  4333. {
  4334. AZ_TracePrintf(
  4335. AssetProcessor::DebugChannel,
  4336. "GetSourceFilesWhichDependOnSourceFile - source %s is not in asset database and does not exist on disk - "
  4337. "dependency lookup may be incomplete\n",
  4338. sourceAsset.AbsolutePath().c_str());
  4339. }
  4340. }
  4341. m_stateData->QuerySourceDependencyByDependsOnSource(
  4342. uuid,
  4343. databasePath.toUtf8().constData(),
  4344. sourcePath.toUtf8().constData(),
  4345. SourceFileDependencyEntry::DEP_Any,
  4346. callbackFunction);
  4347. }
  4348. return absoluteSourceFilePathQueue.values();
  4349. }
  4350. void AssetProcessorManager::AddSourceToDatabase(AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceDatabaseEntry, const SourceAssetReference& sourceAsset)
  4351. {
  4352. auto outcome = AssetUtilities::GetSourceUuid(sourceAsset);
  4353. if (!outcome)
  4354. {
  4355. AZ_Error(AssetProcessor::ConsoleChannel, false, "%s", outcome.GetError().c_str());
  4356. return;
  4357. }
  4358. sourceDatabaseEntry.m_scanFolderPK = sourceAsset.ScanFolderId();
  4359. sourceDatabaseEntry.m_sourceName = sourceAsset.RelativePath().c_str();
  4360. sourceDatabaseEntry.m_sourceGuid = outcome.GetValue();
  4361. if (sourceDatabaseEntry.m_sourceGuid.IsNull() || !m_stateData->SetSource(sourceDatabaseEntry))
  4362. {
  4363. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to add source to the database!!!");
  4364. return;
  4365. }
  4366. }
  4367. AZStd::optional<AZ::s64> AssetProcessorManager::GetIntermediateAssetScanFolderId() const
  4368. {
  4369. if (!m_platformConfig)
  4370. {
  4371. return AZStd::nullopt;
  4372. }
  4373. return m_platformConfig->GetIntermediateAssetsScanFolderId();
  4374. }
  4375. void AssetProcessorManager::CheckAssetProcessorIdleState()
  4376. {
  4377. Q_EMIT AssetProcessorManagerIdleState(IsIdle());
  4378. }
  4379. void AssetProcessorManager::OnBuildersRegistered()
  4380. {
  4381. ComputeBuilderDirty();
  4382. m_buildersReady = true;
  4383. CheckReadyToAssessScanFiles();
  4384. }
  4385. void AssetProcessorManager::OnCatalogReady()
  4386. {
  4387. m_catalogReady = true;
  4388. CheckReadyToAssessScanFiles();
  4389. }
  4390. void AssetProcessorManager::ComputeBuilderDirty()
  4391. {
  4392. using BuilderInfoEntry = AzToolsFramework::AssetDatabase::BuilderInfoEntry;
  4393. using BuilderInfoEntryContainer = AzToolsFramework::AssetDatabase::BuilderInfoEntryContainer;
  4394. using AssetBuilderDesc = AssetBuilderSDK::AssetBuilderDesc;
  4395. using AssetBuilderPattern = AssetBuilderSDK::AssetBuilderPattern;
  4396. const char* currentAnalysisVersionString = "0";
  4397. AZ_TracePrintf(DebugChannel, "Computing builder differences from last time...\n")
  4398. m_builderDataCache.clear();
  4399. // note that it counts as an addition or removal if the patterns that a builder uses have changed since it may now apply
  4400. // to new files even if the files themselves have not changed.
  4401. m_buildersAddedOrRemoved = false;
  4402. m_anyBuilderChange = false;
  4403. AssetProcessor::BuilderInfoList currentBuilders; // queried from AP
  4404. BuilderInfoEntryContainer priorBuilders; // queried from the DB
  4405. // the following fields are built using the above data.
  4406. BuilderInfoEntryContainer newBuilders;
  4407. // each entr is a pairy of <Fingerprint For Analysis, Pattern Fingerprint>
  4408. using FingerprintPair = AZStd::pair<AZ::Uuid, AZ::Uuid>;
  4409. AZStd::unordered_map<AZ::Uuid, FingerprintPair > newBuilderFingerprints;
  4410. AZStd::unordered_map<AZ::Uuid, FingerprintPair > priorBuilderFingerprints;
  4411. // query the database to retrieve the prior builders:
  4412. m_stateData->QueryBuilderInfoTable([&priorBuilders](BuilderInfoEntry&& result)
  4413. {
  4414. priorBuilders.push_back(result);
  4415. return true;
  4416. });
  4417. // query the AP to retrieve the current builders:
  4418. AssetProcessor::AssetBuilderInfoBus::Broadcast(&AssetProcessor::AssetBuilderInfoBus::Events::GetAllBuildersInfo, currentBuilders);
  4419. auto enabledPlatforms = m_platformConfig->GetEnabledPlatforms();
  4420. AZStd::string platformString = "";
  4421. for(const auto& platform : enabledPlatforms)
  4422. {
  4423. if(!platformString.empty())
  4424. {
  4425. platformString += ",";
  4426. }
  4427. platformString += platform.m_identifier;
  4428. }
  4429. // digest the info into maps for easy lookup
  4430. // the map is of the form
  4431. // [BuilderUUID] = <analysisFingerprint, patternFingerprint>
  4432. // first, digest the current builder info:
  4433. for (const AssetBuilderDesc& currentBuilder : currentBuilders)
  4434. {
  4435. // this makes sure that the version of the builder and enabled platforms are included in the analysis fingerprint data:
  4436. AZStd::string analysisFingerprintString = AZStd::string::format("%i:%s:%s", currentBuilder.m_version, currentBuilder.m_analysisFingerprint.c_str(), platformString.c_str());
  4437. AZStd::string patternFingerprintString;
  4438. for (const AssetBuilderPattern& pattern : currentBuilder.m_patterns)
  4439. {
  4440. patternFingerprintString += pattern.ToString();
  4441. }
  4442. //CreateName hashes the data and makes a UUID out of the hash
  4443. AZ::Uuid newAnalysisFingerprint = AZ::Uuid::CreateName(analysisFingerprintString.c_str());
  4444. AZ::Uuid newPatternFingerprint = AZ::Uuid::CreateName(patternFingerprintString.c_str());
  4445. newBuilderFingerprints[currentBuilder.m_busId] = AZStd::make_pair(newAnalysisFingerprint, newPatternFingerprint);
  4446. // in the end, these are just two fingerprints that are part of the same.
  4447. // its 'data version:analysisfingerprint:patternfingerprint'
  4448. AZStd::string finalFingerprintString = AZStd::string::format("%s:%s:%s",
  4449. currentAnalysisVersionString,
  4450. newAnalysisFingerprint.ToString<AZStd::string>().c_str(),
  4451. newPatternFingerprint.ToString<AZStd::string>().c_str());
  4452. newBuilders.push_back(BuilderInfoEntry(AzToolsFramework::AssetDatabase::InvalidEntryId, currentBuilder.m_busId, finalFingerprintString.c_str()));
  4453. BuilderData newBuilderData;
  4454. newBuilderData.m_fingerprint = AZ::Uuid::CreateName(finalFingerprintString.c_str());
  4455. newBuilderData.m_flags = currentBuilder.m_flags;
  4456. m_builderDataCache[currentBuilder.m_busId] = newBuilderData;
  4457. AZ_TracePrintf(DebugChannel, "Builder %s: %s.\n", currentBuilder.m_flags & AssetBuilderDesc::BF_EmitsNoDependencies ? "does not emit dependencies" : "emits dependencies", currentBuilder.m_name.c_str());
  4458. }
  4459. // now digest the prior builder info from the database:
  4460. for (const BuilderInfoEntry& priorBuilder : priorBuilders)
  4461. {
  4462. AZStd::vector<AZStd::string> tokens;
  4463. AZ::Uuid analysisFingerprint = AZ::Uuid::CreateNull();
  4464. AZ::Uuid patternFingerprint = AZ::Uuid::CreateNull();
  4465. AzFramework::StringFunc::Tokenize(priorBuilder.m_analysisFingerprint.c_str(), tokens, ":");
  4466. // note that the above call to Tokenize will drop empty tokens, so tokens[n] will never be the empty string.
  4467. if ((tokens.size() == 3) && (tokens[0] == currentAnalysisVersionString))
  4468. {
  4469. // CreateString interprets the data as an actual UUID instead of hashing it.
  4470. analysisFingerprint = AZ::Uuid::CreateString(tokens[1].c_str());
  4471. patternFingerprint = AZ::Uuid::CreateString(tokens[2].c_str());
  4472. }
  4473. priorBuilderFingerprints[priorBuilder.m_builderUuid] = AZStd::make_pair(analysisFingerprint, patternFingerprint);
  4474. }
  4475. // now we have the two maps we need to compare and find out which have changed and what is new and old.
  4476. for (const auto& priorBuilderFingerprint : priorBuilderFingerprints)
  4477. {
  4478. const AZ::Uuid& priorBuilderUUID = priorBuilderFingerprint.first;
  4479. const AZ::Uuid& priorBuilderAnalysisFingerprint = priorBuilderFingerprint.second.first;
  4480. const AZ::Uuid& priorBuilderPatternFingerprint = priorBuilderFingerprint.second.second;
  4481. const auto& found = newBuilderFingerprints.find(priorBuilderUUID);
  4482. if (found != newBuilderFingerprints.end())
  4483. {
  4484. const AZ::Uuid& newBuilderAnalysisFingerprint = found->second.first;
  4485. const AZ::Uuid& newBuilderPatternFingerprint = found->second.second;
  4486. bool patternFingerprintIsDirty = (priorBuilderPatternFingerprint != newBuilderPatternFingerprint);
  4487. bool analysisFingerprintIsDirty = (priorBuilderAnalysisFingerprint != newBuilderAnalysisFingerprint);
  4488. bool builderIsDirty = (patternFingerprintIsDirty || analysisFingerprintIsDirty);
  4489. // altering the pattern a builder uses to decide which files it affects counts as builder addition or removal
  4490. // because it causes existing files to potentially map to a new set of builders and thus they need re-analysis
  4491. m_buildersAddedOrRemoved = m_buildersAddedOrRemoved || patternFingerprintIsDirty;
  4492. if (patternFingerprintIsDirty)
  4493. {
  4494. AZ_TracePrintf(DebugChannel, "Builder %s matcher pattern changed. This will cause a full re-analysis of all assets.\n", priorBuilderUUID.ToString<AZStd::string>().c_str());
  4495. }
  4496. else if (analysisFingerprintIsDirty)
  4497. {
  4498. AZ_TracePrintf(DebugChannel, "Builder %s analysis fingerprint changed. Files assigned to it will be re-analyzed.\n", priorBuilderUUID.ToString<AZStd::string>().c_str());
  4499. }
  4500. if (builderIsDirty)
  4501. {
  4502. m_anyBuilderChange = true;
  4503. m_builderDataCache[priorBuilderUUID].m_isDirty = true;
  4504. }
  4505. }
  4506. else
  4507. {
  4508. // if we get here, it means that a prior builder existed, but no longer exists.
  4509. AZ_TracePrintf(DebugChannel, "Builder with UUID %s no longer exists, full analysis will be done.\n", priorBuilderUUID.ToString<AZStd::string>().c_str());
  4510. m_buildersAddedOrRemoved = true;
  4511. m_anyBuilderChange = true;
  4512. }
  4513. }
  4514. for (const auto& newBuilderFingerprint : newBuilderFingerprints)
  4515. {
  4516. const auto& found = priorBuilderFingerprints.find(newBuilderFingerprint.first);
  4517. if (found == priorBuilderFingerprints.end())
  4518. {
  4519. // if we get here, it means that a new builder exists that did not exist before.
  4520. m_buildersAddedOrRemoved = true;
  4521. m_anyBuilderChange = true;
  4522. m_builderDataCache[newBuilderFingerprint.first].m_isDirty = true;
  4523. }
  4524. }
  4525. // note that we do this in this order, so that the data is INVALIDATED before we write the new builders
  4526. // even if power is lost, we are ensured correct database integrity (ie, the worst case scenario is that we re-analyze)
  4527. if (m_buildersAddedOrRemoved)
  4528. {
  4529. AZ_TracePrintf(ConsoleChannel, "At least one builder has been added or removed or has changed its filter - full analysis needs to be performed\n");
  4530. // when this happens we immediately invalidate every source hash of every files so that if hte user
  4531. m_stateData->InvalidateSourceAnalysisFingerprints();
  4532. }
  4533. // update the database:
  4534. m_stateData->SetBuilderInfoTable(newBuilders);
  4535. if (m_anyBuilderChange)
  4536. {
  4537. // notify the console so that logs contain forensics about this.
  4538. for (const AssetBuilderDesc& builder : currentBuilders)
  4539. {
  4540. if (m_builderDataCache[builder.m_busId].m_isDirty)
  4541. {
  4542. AZ_TracePrintf(ConsoleChannel, "Builder is new or has changed: %s (%s)\n", builder.m_name.c_str(), builder.m_busId.ToString<AZStd::string>().c_str());
  4543. }
  4544. }
  4545. }
  4546. }
  4547. AZStd::string AssetProcessorManager::ComputeRecursiveDependenciesFingerprint(const SourceAssetReference& sourceAsset)
  4548. {
  4549. AZStd::string concatenatedFingerprints;
  4550. auto sourceUuid = AssetUtilities::GetSourceUuid(sourceAsset);
  4551. if (!sourceUuid)
  4552. {
  4553. AZ_Error(AssetProcessor::ConsoleChannel, false, "%s", sourceUuid.GetError().c_str());
  4554. return "";
  4555. }
  4556. // QSet is not ordered.
  4557. SourceFilesForFingerprintingContainer knownDependenciesAbsolutePaths;
  4558. // this automatically adds the input file to the list:
  4559. QueryAbsolutePathDependenciesRecursive(sourceUuid.GetValue(), knownDependenciesAbsolutePaths,
  4560. AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any);
  4561. AddMetadataFilesForFingerprinting(QString::fromUtf8(sourceAsset.AbsolutePath().c_str()), knownDependenciesAbsolutePaths);
  4562. // reserve 17 chars for each since its a 64 bit hex number, and then one more for the dash inbetween each.
  4563. constexpr int bytesPerFingerprint = (sizeof(AZ::u64) * 2) + 1; // 2 HEX characters per byte +1 for the `-` we will add between each fingerprint
  4564. concatenatedFingerprints.reserve((knownDependenciesAbsolutePaths.size() * bytesPerFingerprint));
  4565. for (const auto& element : knownDependenciesAbsolutePaths)
  4566. {
  4567. concatenatedFingerprints.append(AssetUtilities::GetFileFingerprint(element.first, element.second));
  4568. concatenatedFingerprints.append("-");
  4569. }
  4570. // to keep this from growing out of hand, we don't use the full string, we use a hash of it:
  4571. return AZ::Uuid::CreateName(concatenatedFingerprints.c_str()).ToString<AZStd::string>();
  4572. }
  4573. void AssetProcessorManager::FinishAnalysis(SourceAssetReference sourceAsset)
  4574. {
  4575. using namespace AzToolsFramework::AssetDatabase;
  4576. auto foundTrackingInfo = m_remainingJobsForEachSourceFile.find(sourceAsset.AbsolutePath().c_str());
  4577. if (foundTrackingInfo == m_remainingJobsForEachSourceFile.end())
  4578. {
  4579. return;
  4580. }
  4581. AnalysisTracker* analysisTracker = &foundTrackingInfo->second;
  4582. if (analysisTracker->failedStatus)
  4583. {
  4584. // We need to clear the analysis fingerprint if it exists. Since this file failed we can't skip processing until it succeeds again
  4585. bool found = false;
  4586. SourceDatabaseEntry source;
  4587. m_stateData->QuerySourceBySourceNameScanFolderID(analysisTracker->m_databaseSourceName.c_str(),
  4588. analysisTracker->m_databaseScanFolderId,
  4589. [&](SourceDatabaseEntry& sourceData)
  4590. {
  4591. source = AZStd::move(sourceData);
  4592. found = true;
  4593. return false; // stop iterating after the first one. There should actually only be one entry anyway.
  4594. });
  4595. if (found)
  4596. {
  4597. source.m_analysisFingerprint = "";
  4598. m_stateData->SetSource(source);
  4599. }
  4600. // if the job failed, we need to wipe the tracking column so that the next time we start the app we will try it again.
  4601. // it may not be necessary to actually alter the database here.
  4602. m_remainingJobsForEachSourceFile.erase(foundTrackingInfo);
  4603. Q_EMIT FinishedAnalysis(static_cast<int>(m_remainingJobsForEachSourceFile.size()));
  4604. return;
  4605. }
  4606. // if we get here, it succeeded, but it may have remaining jobs
  4607. if (analysisTracker->m_remainingJobsSpawned > 0)
  4608. {
  4609. // don't write the fingerprint to the database if there are still remaining jobs to be finished.
  4610. // we only write it when theres no work left to do whatsoever for this asset.
  4611. return;
  4612. }
  4613. // if we get here, we succeeded and there are no more remaining jobs.
  4614. SourceDatabaseEntry source;
  4615. QString databaseSourceName;
  4616. AZ::s64 scanFolderPk = -1;
  4617. bool found = false;
  4618. m_stateData->QuerySourceBySourceNameScanFolderID(analysisTracker->m_databaseSourceName.c_str(),
  4619. analysisTracker->m_databaseScanFolderId,
  4620. [&](SourceDatabaseEntry& sourceData)
  4621. {
  4622. source = AZStd::move(sourceData);
  4623. found = true;
  4624. return false; // stop iterating after the first one. There should actually only be one entry anyway.
  4625. });
  4626. if (found)
  4627. {
  4628. // construct the analysis fingerprint
  4629. // the format for this data is "hashfingerprint:builder0:builder1:builder2:...:buildern"
  4630. source.m_analysisFingerprint = ComputeRecursiveDependenciesFingerprint(sourceAsset);
  4631. for (const AZ::Uuid& builderID : analysisTracker->m_buildersInvolved)
  4632. {
  4633. source.m_analysisFingerprint.append(":");
  4634. // for each builder, we write a combination of
  4635. // its ID and its fingerprint.
  4636. AZ::Uuid builderFP = m_builderDataCache[builderID].m_fingerprint;
  4637. source.m_analysisFingerprint.append(builderID.ToString<AZStd::string>());
  4638. source.m_analysisFingerprint.append("~");
  4639. source.m_analysisFingerprint.append(builderFP.ToString<AZStd::string>());
  4640. }
  4641. m_pathDependencyManager->QueueSourceForDependencyResolution(source);
  4642. m_stateData->SetSource(source);
  4643. databaseSourceName = source.m_sourceName.c_str();
  4644. scanFolderPk = source.m_scanFolderPK;
  4645. }
  4646. else
  4647. {
  4648. databaseSourceName = sourceAsset.RelativePath().c_str();
  4649. scanFolderPk = sourceAsset.ScanFolderId();
  4650. }
  4651. // Record the modtime for the file so we know we processed it
  4652. QFileInfo fileInfo(sourceAsset.AbsolutePath().c_str());
  4653. QDateTime lastModifiedTime = fileInfo.lastModified();
  4654. AZ_Error(AssetProcessor::ConsoleChannel, scanFolderPk > -1 && !sourceAsset.RelativePath().empty(), "FinishAnalysis: Invalid ScanFolderPk (%lld) or databaseSourceName (%s) for file %s. Cannot update file modtime in database.",
  4655. scanFolderPk, sourceAsset.RelativePath().c_str(), sourceAsset.AbsolutePath().c_str());
  4656. m_stateData->UpdateFileModTimeAndHashByFileNameAndScanFolderId(databaseSourceName.toUtf8().constData(), scanFolderPk,
  4657. AssetUtilities::AdjustTimestamp(lastModifiedTime),
  4658. AssetUtilities::GetFileHash(fileInfo.absoluteFilePath().toUtf8().constData()));
  4659. m_remainingJobsForEachSourceFile.erase(foundTrackingInfo);
  4660. Q_EMIT FinishedAnalysis(static_cast<int>(m_remainingJobsForEachSourceFile.size()));
  4661. }
  4662. void AssetProcessorManager::SetEnableModtimeSkippingFeature(bool enable)
  4663. {
  4664. m_allowModtimeSkippingFeature = enable;
  4665. }
  4666. bool AssetProcessorManager::GetModtimeSkippingFeatureEnabled() const
  4667. {
  4668. return m_allowModtimeSkippingFeature;
  4669. }
  4670. void AssetProcessorManager::SetInitialScanSkippingFeature(bool enable)
  4671. {
  4672. m_initialScanSkippingFeature = enable;
  4673. }
  4674. bool AssetProcessorManager::GetInitialScanSkippingFeatureEnabled() const
  4675. {
  4676. return m_initialScanSkippingFeature;
  4677. }
  4678. void AssetProcessorManager::SetQueryLogging(bool enableLogging)
  4679. {
  4680. m_stateData->SetQueryLogging(enableLogging);
  4681. }
  4682. void AssetProcessorManager::SetBuilderDebugFlag(bool enabled)
  4683. {
  4684. m_builderDebugFlag = enabled;
  4685. }
  4686. void AssetProcessorManager::ScanForMissingProductDependencies(QString dbPattern, QString filePattern, const AZStd::vector<AZStd::string>& dependencyAdditionalScanFolders, int maxScanIteration)
  4687. {
  4688. if (!dbPattern.isEmpty())
  4689. {
  4690. AZ_Printf(
  4691. "AssetProcessor",
  4692. "\n----------------\nPerforming dependency scan using database pattern ( %s )"
  4693. "\n(This may be a long running operation)\n----------------\n",
  4694. dbPattern.toUtf8().data());
  4695. // Find all products that match the given pattern.
  4696. m_stateData->QueryProductLikeProductName(
  4697. dbPattern.toStdString().c_str(),
  4698. AssetDatabaseConnection::LikeType::Raw,
  4699. [&](AzToolsFramework::AssetDatabase::ProductDatabaseEntry& entry)
  4700. {
  4701. // Get the full path to the asset, so that it can be loaded by the scanner.
  4702. AZStd::string fullPath;
  4703. AzFramework::StringFunc::Path::Join(
  4704. m_normalizedCacheRootPath.toStdString().c_str(),
  4705. entry.m_productName.c_str(),
  4706. fullPath);
  4707. // Get any existing product dependencies available for the product, so
  4708. // the scanner can cull results based on these existing dependencies.
  4709. AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer container;
  4710. m_stateData->QueryProductDependencyByProductId(
  4711. entry.m_productID,
  4712. [&](AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry& entry)
  4713. {
  4714. container.emplace_back() = AZStd::move(entry);
  4715. return true; // return true to keep iterating over further rows.
  4716. });
  4717. // Scan the file to report anything that looks like a missing product dependency.
  4718. // Don't queue results on the main thread, so the tickbus won't need to be pumped.
  4719. m_missingDependencyScanner.ScanFile(fullPath, maxScanIteration, entry.m_productID, container, m_stateData, false, [](AZStd::string /*relativeDependencyFilePath*/) {});
  4720. return true;
  4721. });
  4722. }
  4723. if (dependencyAdditionalScanFolders.size())
  4724. {
  4725. AZ_Printf(
  4726. "AssetProcessor",
  4727. "\n----------------\nPerforming dependency scan using file pattern ( %s )"
  4728. "\n(This may be a long running operation)\n----------------\n",
  4729. filePattern.toUtf8().data());
  4730. for (const auto& scanFolder : dependencyAdditionalScanFolders)
  4731. {
  4732. QElapsedTimer scanFolderTime;
  4733. scanFolderTime.start();
  4734. AZ_Printf("AssetProcessor", "Scanning folder : ( %s ).\n", scanFolder.c_str());
  4735. auto filesFoundOutcome = AzFramework::FileFunc::FindFileList(scanFolder.c_str(), filePattern.toUtf8().data(), true);
  4736. if (filesFoundOutcome.IsSuccess())
  4737. {
  4738. AZStd::string dependencyTokenName;
  4739. if (!m_missingDependencyScanner.PopulateRulesForScanFolder(scanFolder, m_platformConfig->GetGemsInformation(), dependencyTokenName))
  4740. {
  4741. continue;
  4742. }
  4743. for (const AZStd::string& fullFilePath : filesFoundOutcome.GetValue())
  4744. {
  4745. char resolvedFilePath[AZ_MAX_PATH_LEN] = { 0 };
  4746. AZ::IO::FileIOBase::GetInstance()->ResolvePath(fullFilePath.c_str(), resolvedFilePath, AZ_MAX_PATH_LEN);
  4747. // Scan the file to report anything that looks like a missing product dependency.
  4748. m_missingDependencyScanner.ScanFile(resolvedFilePath, maxScanIteration, m_stateData, dependencyTokenName, false, [](AZStd::string /*relativeDependencyFilePath*/){});
  4749. }
  4750. }
  4751. AZ_Printf("AssetProcessor", "Scan complete, time taken ( %f ) millisecs.\n", static_cast<double>(scanFolderTime.elapsed()));
  4752. scanFolderTime.restart();
  4753. }
  4754. }
  4755. }
  4756. void AssetProcessorManager::QueryAbsolutePathDependenciesRecursive(AZ::Uuid sourceUuid, SourceFilesForFingerprintingContainer& finalDependencyList, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType)
  4757. {
  4758. using namespace AzToolsFramework::AssetDatabase;
  4759. // then we add database dependencies. We have to query this recursively so that we get dependencies of dependencies:
  4760. AZStd::unordered_set<PathOrUuid> results;
  4761. AZStd::unordered_set<PathOrUuid> queryQueue;
  4762. queryQueue.emplace(PathOrUuid(sourceUuid));
  4763. while (!queryQueue.empty())
  4764. {
  4765. PathOrUuid toSearch = *queryQueue.begin();
  4766. queryQueue.erase(toSearch);
  4767. // if we've already queried it, dont do it again (breaks recursion)
  4768. if (results.contains(toSearch))
  4769. {
  4770. continue;
  4771. }
  4772. results.insert(toSearch);
  4773. AZ::Uuid searchUuid;
  4774. if (!toSearch.IsUuid())
  4775. {
  4776. // If the dependency is a path, try to get a UUID for it
  4777. // If the dependency is an asset, this will resolve to a valid UUID
  4778. // If the dependency is not an asset, this will resolve to an invalid UUID which will simply return no results for our
  4779. // search
  4780. if (AZ::IO::PathView(toSearch.GetPath()).IsAbsolute())
  4781. {
  4782. if (AZ::Interface<IFileStateRequests>::Get()->Exists(toSearch.GetPath().c_str()))
  4783. {
  4784. searchUuid = AssetUtilities::GetSourceUuid(SourceAssetReference(toSearch.GetPath().c_str())).GetValueOr(AZ::Uuid());
  4785. }
  4786. }
  4787. else
  4788. {
  4789. const ScanFolderInfo* scanFolder = nullptr;
  4790. m_platformConfig->FindFirstMatchingFile(toSearch.GetPath().c_str(), false, &scanFolder);
  4791. if (scanFolder)
  4792. {
  4793. searchUuid = AssetUtilities::GetSourceUuid(SourceAssetReference(
  4794. scanFolder->ScanFolderID(), scanFolder->ScanPath().toUtf8().constData(), toSearch.GetPath().c_str())).GetValueOr(AZ::Uuid());
  4795. }
  4796. }
  4797. }
  4798. else
  4799. {
  4800. searchUuid = toSearch.GetUuid();
  4801. }
  4802. auto cacheEntry = m_dependencyCache.find(searchUuid);
  4803. if (dependencyType == AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency::DEP_Any &&
  4804. cacheEntry != m_dependencyCache.end())
  4805. {
  4806. queryQueue.insert(cacheEntry->second.begin(), cacheEntry->second.end());
  4807. }
  4808. else
  4809. {
  4810. auto callbackFunction = [this, &queryQueue, dependencyType](SourceFileDependencyEntry& entry)
  4811. {
  4812. queryQueue.emplace(entry.m_dependsOnSource);
  4813. if (m_dependencyCacheEnabled &&
  4814. dependencyType == AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency::DEP_Any)
  4815. {
  4816. m_dependencyCache[entry.m_sourceGuid].emplace_back(entry.m_dependsOnSource);
  4817. }
  4818. return true;
  4819. };
  4820. m_stateData->QueryDependsOnSourceBySourceDependency(searchUuid, dependencyType, callbackFunction);
  4821. }
  4822. }
  4823. for (const PathOrUuid& dep : results)
  4824. {
  4825. QString absolutePath;
  4826. if (dep.IsUuid())
  4827. {
  4828. SourceAssetReference sourceAsset;
  4829. if (!SearchSourceInfoBySourceUUID(dep.GetUuid(), sourceAsset))
  4830. {
  4831. continue;
  4832. }
  4833. absolutePath = sourceAsset.AbsolutePath().c_str();
  4834. }
  4835. else
  4836. {
  4837. if (AZ::IO::PathView(dep.GetPath()).IsAbsolute())
  4838. {
  4839. absolutePath = dep.GetPath().c_str();
  4840. }
  4841. else
  4842. {
  4843. absolutePath = m_platformConfig->FindFirstMatchingFile(dep.GetPath().c_str());
  4844. if (absolutePath.isEmpty())
  4845. {
  4846. continue;
  4847. }
  4848. }
  4849. }
  4850. finalDependencyList.insert(AZStd::make_pair(absolutePath.toUtf8().constData(), dep.ToString().c_str()));
  4851. }
  4852. }
  4853. bool AssetProcessorManager::AreBuildersUnchanged(AZStd::string_view builderEntries, int& numBuildersEmittingSourceDependencies)
  4854. {
  4855. // each entry here is of the format "builderID~builderFingerprint"
  4856. // each part is exactly the size of a UUID, so we can check size instead of having to find or search.
  4857. const size_t sizeOfOneEntry = (s_lengthOfUuid * 2) + 1;
  4858. while (!builderEntries.empty())
  4859. {
  4860. if (builderEntries.size() < sizeOfOneEntry)
  4861. {
  4862. // corrupt data
  4863. return false;
  4864. }
  4865. AZStd::string_view builderFPString(builderEntries.begin() + s_lengthOfUuid + 1, builderEntries.end());
  4866. if ((builderEntries[0] != '{') || (builderFPString[0] != '{'))
  4867. {
  4868. return false; // corrupt or bad format. We chose bracket guids for a reason!
  4869. }
  4870. AZ::Uuid builderID = AZ::Uuid::CreateString(builderEntries.data(), s_lengthOfUuid);
  4871. AZ::Uuid builderFP = AZ::Uuid::CreateString(builderFPString.data(), s_lengthOfUuid);
  4872. if ((builderID.IsNull()) || (builderFP.IsNull()))
  4873. {
  4874. return false;
  4875. }
  4876. // is it different?
  4877. auto foundBuilder = m_builderDataCache.find(builderID);
  4878. if (foundBuilder == m_builderDataCache.end())
  4879. {
  4880. // this file doesn't recognize the builder it was built with last time in the new list of builders, it definitely needs analysis!
  4881. return false;
  4882. }
  4883. const BuilderData& data = foundBuilder->second;
  4884. if (builderFP != data.m_fingerprint)
  4885. {
  4886. return false; // the builder changed!
  4887. }
  4888. // if we get here, its not dirty, but we need to know, does it emit deps?
  4889. if ((data.m_flags & AssetBuilderSDK::AssetBuilderDesc::BF_EmitsNoDependencies) == 0)
  4890. {
  4891. numBuildersEmittingSourceDependencies++;
  4892. }
  4893. // advance to the next one.
  4894. builderEntries = AZStd::string_view(builderEntries.begin() + sizeOfOneEntry, builderEntries.end());
  4895. if (!builderEntries.empty())
  4896. {
  4897. // We add one for the colon that is the token that separates these entries.
  4898. builderEntries = AZStd::string_view(builderEntries.begin() + 1, builderEntries.end());
  4899. }
  4900. }
  4901. return true;
  4902. }
  4903. // given a file, add all the metadata files that could be related to it to an output vector
  4904. void AssetProcessorManager::AddMetadataFilesForFingerprinting(QString absolutePathToFileToCheck, SourceFilesForFingerprintingContainer& outFilesToFingerprint)
  4905. {
  4906. QString metaDataFileName;
  4907. QDir assetRoot;
  4908. AssetUtilities::ComputeAssetRoot(assetRoot);
  4909. QString projectPath = AssetUtilities::ComputeProjectPath();
  4910. QString fullPathToFile(absolutePathToFileToCheck);
  4911. if (!m_cachedMetaFilesExistMap)
  4912. {
  4913. // one-time cache the actually existing metafiles. These are files where its an actual path to a file
  4914. // like "animations/skeletoninfo.xml" as the metafile, not when its a file thats next to each such file of a given type.
  4915. for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
  4916. {
  4917. QPair<QString, QString> metaDataFileType = m_platformConfig->GetMetaDataFileTypeAt(idx);
  4918. QString fullMetaPath = QDir(projectPath).filePath(metaDataFileType.first);
  4919. if (QFileInfo::exists(fullMetaPath))
  4920. {
  4921. m_metaFilesWhichActuallyExistOnDisk.insert(metaDataFileType.first);
  4922. }
  4923. }
  4924. m_cachedMetaFilesExistMap = true;
  4925. }
  4926. for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
  4927. {
  4928. QPair<QString, QString> metaDataFileType = m_platformConfig->GetMetaDataFileTypeAt(idx);
  4929. if (!metaDataFileType.second.isEmpty() && !fullPathToFile.endsWith(metaDataFileType.second, Qt::CaseInsensitive))
  4930. {
  4931. continue;
  4932. }
  4933. if (m_metaFilesWhichActuallyExistOnDisk.find(metaDataFileType.first) != m_metaFilesWhichActuallyExistOnDisk.end())
  4934. {
  4935. QString fullMetaPath = QDir(projectPath).filePath(metaDataFileType.first);
  4936. metaDataFileName = fullMetaPath;
  4937. }
  4938. else
  4939. {
  4940. if (metaDataFileType.second.isEmpty())
  4941. {
  4942. // ADD the metadata file extension to the end of the filename
  4943. metaDataFileName = fullPathToFile + "." + metaDataFileType.first;
  4944. }
  4945. else
  4946. {
  4947. // REPLACE the file's extension with the metadata file extension.
  4948. QFileInfo fileInfo(absolutePathToFileToCheck);
  4949. metaDataFileName = fileInfo.path() + '/' + fileInfo.completeBaseName() + "." + metaDataFileType.first;
  4950. }
  4951. }
  4952. QString databasePath;
  4953. QString scanFolderPath;
  4954. m_platformConfig->ConvertToRelativePath(metaDataFileName, databasePath, scanFolderPath);
  4955. outFilesToFingerprint.insert(AZStd::make_pair(metaDataFileName.toUtf8().constData(), databasePath.toUtf8().constData()));
  4956. }
  4957. }
  4958. // this function gets called whenever something changes about a file being processed, and checks to see
  4959. // if it needs to write the fingerprint to the database.
  4960. void AssetProcessorManager::UpdateAnalysisTrackerForFile(const SourceAssetReference& sourceAsset, AnalysisTrackerUpdateType updateType)
  4961. {
  4962. auto foundTrackingInfo = m_remainingJobsForEachSourceFile.find(sourceAsset.AbsolutePath().c_str());
  4963. if (foundTrackingInfo != m_remainingJobsForEachSourceFile.end())
  4964. {
  4965. // clear our the information about analysis on failed jobs.
  4966. AnalysisTracker& analysisTracker = foundTrackingInfo->second;
  4967. switch (updateType)
  4968. {
  4969. case AnalysisTrackerUpdateType::JobFailed:
  4970. if (!analysisTracker.failedStatus)
  4971. {
  4972. analysisTracker.failedStatus = true;
  4973. analysisTracker.m_remainingJobsSpawned = 0;
  4974. QMetaObject::invokeMethod(this, "FinishAnalysis", Qt::QueuedConnection, Q_ARG(SourceAssetReference, sourceAsset));
  4975. }
  4976. break;
  4977. case AnalysisTrackerUpdateType::JobStarted:
  4978. if (!analysisTracker.failedStatus)
  4979. {
  4980. ++analysisTracker.m_remainingJobsSpawned;
  4981. }
  4982. break;
  4983. case AnalysisTrackerUpdateType::JobFinished:
  4984. {
  4985. if (!analysisTracker.failedStatus)
  4986. {
  4987. --analysisTracker.m_remainingJobsSpawned;
  4988. if (analysisTracker.m_remainingJobsSpawned == 0)
  4989. {
  4990. QMetaObject::invokeMethod(this, "FinishAnalysis", Qt::QueuedConnection, Q_ARG(SourceAssetReference, sourceAsset));
  4991. }
  4992. }
  4993. }
  4994. break;
  4995. }
  4996. }
  4997. }
  4998. void AssetProcessorManager::UpdateAnalysisTrackerForFile(const JobEntry &entry, AnalysisTrackerUpdateType updateType)
  4999. {
  5000. // it is assumed that watch folder path / path relative to watch folder are already normalized and such.
  5001. UpdateAnalysisTrackerForFile(entry.m_sourceAssetReference, updateType);
  5002. }
  5003. void AssetProcessorManager::AutoFailJob([[maybe_unused]] AZStd::string_view consoleMsg, AZStd::string_view autoFailReason, JobEntry jobEntry, AZStd::string_view jobLog)
  5004. {
  5005. if (!consoleMsg.empty())
  5006. {
  5007. AZ_TracePrintf(AssetProcessor::ConsoleChannel, AZ_STRING_FORMAT "\n", AZ_STRING_ARG(consoleMsg));
  5008. }
  5009. JobDetails jobdetail;
  5010. jobdetail.m_jobEntry = AZStd::move(jobEntry);
  5011. jobdetail.m_autoFail = true;
  5012. jobdetail.m_critical = true;
  5013. jobdetail.m_priority = INT_MAX; // front of the queue.
  5014. // the new lines make it easier to copy and paste the file names.
  5015. jobdetail.m_jobParam[AZ_CRC_CE(AutoFailReasonKey)] = autoFailReason;
  5016. if(!jobLog.empty())
  5017. {
  5018. jobdetail.m_jobParam[AZ_CRC_CE(AutoFailLogFile)] = jobLog;
  5019. }
  5020. // this is a failure, so make sure that the system that is tracking files
  5021. // knows that this file must not be skipped next time:
  5022. UpdateAnalysisTrackerForFile(jobEntry, AnalysisTrackerUpdateType::JobFailed);
  5023. Q_EMIT AssetToProcess(jobdetail); // forwarding this job to rccontroller to fail it
  5024. }
  5025. void AssetProcessorManager::AutoFailJob(AZStd::string_view consoleMsg, AZStd::string_view autoFailReason, const AZStd::vector<AssetProcessedEntry>::iterator& assetIter, AZ::s64 failureCauseSourceId, AZ::u32 failureCauseFingerprint)
  5026. {
  5027. JobEntry jobEntry(
  5028. assetIter->m_entry.m_sourceAssetReference,
  5029. assetIter->m_entry.m_builderGuid,
  5030. assetIter->m_entry.m_platformInfo,
  5031. assetIter->m_entry.m_jobKey, 0, GenerateNewJobRunKey(),
  5032. assetIter->m_entry.m_sourceFileUUID);
  5033. jobEntry.m_failureCauseSourceId = failureCauseSourceId;
  5034. jobEntry.m_failureCauseFingerprint = failureCauseFingerprint;
  5035. AutoFailJob(consoleMsg, autoFailReason, jobEntry);
  5036. }
  5037. AZ::u64 AssetProcessorManager::RequestReprocess(const QString& sourcePathRequest)
  5038. {
  5039. QFileInfo dirCheck{ sourcePathRequest };
  5040. auto normalizedSourcePath = AssetUtilities::NormalizeFilePath(sourcePathRequest);
  5041. AZStd::list<AZStd::string> reprocessList;
  5042. if (dirCheck.isDir())
  5043. {
  5044. auto result = AzFramework::FileFunc::FindFilesInPath(sourcePathRequest.toUtf8().constData(), "*", true);
  5045. if (result)
  5046. {
  5047. reprocessList = result.GetValue();
  5048. }
  5049. }
  5050. else
  5051. {
  5052. reprocessList.push_back(normalizedSourcePath.toUtf8().constData());
  5053. }
  5054. return RequestReprocess(reprocessList);
  5055. }
  5056. AZ::u64 AssetProcessorManager::RequestReprocess(const AZStd::list<AZStd::string>& reprocessList)
  5057. {
  5058. AZ::u64 filesFound{ 0 };
  5059. for (const AZStd::string& entry : reprocessList)
  5060. {
  5061. // Remove invalid characters
  5062. QString sourcePath = entry.c_str();
  5063. sourcePath.remove(QRegExp("[\\n\\r]"));
  5064. QString scanFolderName;
  5065. QString relativePathToFile;
  5066. if (!m_platformConfig->ConvertToRelativePath(sourcePath, relativePathToFile, scanFolderName))
  5067. {
  5068. continue;
  5069. }
  5070. auto sources = AssetUtilities::GetAllIntermediateSources(SourceAssetReference(sourcePath.toUtf8().constData()), m_stateData);
  5071. for (const auto& source : sources)
  5072. {
  5073. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs; // should only find one when we specify builder, job key, platform
  5074. m_stateData->GetJobsBySourceName(source, jobs);
  5075. for (auto& job : jobs)
  5076. {
  5077. job.m_fingerprint = 0;
  5078. m_stateData->SetJob(job);
  5079. }
  5080. if (jobs.size())
  5081. {
  5082. filesFound++;
  5083. AssessModifiedFile(sourcePath);
  5084. }
  5085. }
  5086. }
  5087. return filesFound;
  5088. }
  5089. void AssetProcessorManager::SetMetaCreationDelay(AZ::u32 milliseconds)
  5090. {
  5091. m_metaCreationDelayMs = milliseconds;
  5092. }
  5093. void AssetProcessorManager::PrepareForFileMove(AZ::IO::PathView oldPath, AZ::IO::PathView newPath)
  5094. {
  5095. // Note - this code is likely not running on the APM thread, be careful with variable access
  5096. auto* fileStateInterface = AZ::Interface<IFileStateRequests>::Get();
  5097. AZ_Assert(fileStateInterface, "Programmer Error - IFileStateRequests is not available.");
  5098. AssetProcessor::FileStateInfo fileInfo;
  5099. QString oldPathQString = QString::fromUtf8(oldPath.Native().data(), azlossy_caster(oldPath.Native().size()));
  5100. if (fileStateInterface->GetFileInfo(oldPathQString, &fileInfo) && fileInfo.m_isDirectory)
  5101. {
  5102. // If we know the old path is a directory just exit out, a directory rename doesn't have any create/delete issues
  5103. return;
  5104. }
  5105. AZStd::scoped_lock lock(m_pendingMovesMutex);
  5106. m_pendingMoves.emplace(oldPath, false);
  5107. m_pendingMoves.emplace(newPath, true);
  5108. }
  5109. bool AssetProcessorManager::CheckMetadataIsAvailable(AZ::IO::PathView absolutePath)
  5110. {
  5111. auto* fileStateInterface = AZ::Interface<IFileStateRequests>::Get();
  5112. auto* uuidInterface = AZ::Interface<IUuidRequests>::Get();
  5113. AZ_Assert(fileStateInterface, "Programmer Error - IFileStateRequests is not available.");
  5114. AZ_Assert(uuidInterface, "Programmer Error - IUuidRequests is not available.");
  5115. return !uuidInterface->IsGenerationEnabledForFile(absolutePath) ||
  5116. fileStateInterface->Exists(AzToolsFramework::MetadataManager::ToMetadataPath(absolutePath).c_str());
  5117. }
  5118. bool AssetProcessorManager::ShouldIgnorePendingMove(AZ::IO::PathView absolutePath, bool triggeredByMetadata, bool isDelete)
  5119. {
  5120. AZStd::scoped_lock lock(m_pendingMovesMutex);
  5121. auto itr = m_pendingMoves.find(absolutePath);
  5122. if (itr != m_pendingMoves.end())
  5123. {
  5124. const bool isNewFile = itr->second;
  5125. if (!isNewFile)
  5126. {
  5127. if (triggeredByMetadata && isDelete)
  5128. {
  5129. // Deletion of the old metadata file typically would cause the metadata file to be recreated.
  5130. // Since this file is moving, ignore the deletion event.
  5131. m_pendingMoves.erase(itr);
  5132. return true;
  5133. }
  5134. }
  5135. else if (!triggeredByMetadata && !isDelete)
  5136. {
  5137. // The new file has been created.
  5138. m_pendingMoves.erase(itr);
  5139. // If the metadata is not available yet, ignore this event.
  5140. if (!CheckMetadataIsAvailable(absolutePath))
  5141. {
  5142. return true;
  5143. }
  5144. }
  5145. }
  5146. return false;
  5147. }
  5148. bool AssetProcessorManager::HasDelayProcessTimerElapsed(qint64 elapsedTime)
  5149. {
  5150. // QTimer is not a precise timer, it could fire several milliseconds before or after the required wait time.
  5151. // Just check if the elapsed time is relatively close; 30ms is arbitrary but should be sufficient.
  5152. // Precise timing isn't necessary here anyway.
  5153. constexpr double ToleranceMs = 30;
  5154. return elapsedTime + ToleranceMs >= m_metaCreationDelayMs;
  5155. }
  5156. bool AssetProcessorManager::ShouldDelayProcessingFile(
  5157. const AssetProcessorManager::FileEntry& source, QString normalizedFilePath, bool triggeredByMetadata)
  5158. {
  5159. if (m_metaCreationDelayMs > 0)
  5160. {
  5161. AZ::IO::Path absolutePath = normalizedFilePath.toUtf8().constData();
  5162. // There are 7 possible relevant events here:
  5163. // 1) An existing source file is deleted
  5164. // 2) An existing metadata file is deleted
  5165. // 3) A new source file is added
  5166. // 4) A new metadata file is added
  5167. // 5) Delay has expired and the file must be proccessed now
  5168. // 6) A delayed file was deleted
  5169. // 7) A delayed file is updated
  5170. // Normally Events 2, 3 and 7 would cause a new metadata to be generated.
  5171. // Event 1 requires no action (since an orphan metadata file is harmless).
  5172. // Event 2 will need to be delayed if Event 1 has not occurred yet.
  5173. // Event 3 will need to be delayed if Event 4 has not occurred yet.
  5174. // Event 4 will end a delay early if Event 3 has already occurred.
  5175. // Event 5 & 6 will remove the file from the queue and start processing.
  5176. // Event 7 will simply continue waiting.
  5177. // Event 4: Metadata file added, check if Event 3 has already occurred.
  5178. if (triggeredByMetadata && !source.m_isDelete)
  5179. {
  5180. auto itr = m_delayProcessMetadataFiles.find(absolutePath);
  5181. if (itr != m_delayProcessMetadataFiles.end())
  5182. {
  5183. // Events 3 and 4 have occurred, clear to proceed.
  5184. m_delayProcessMetadataFiles.erase(itr);
  5185. Q_EMIT ProcessingResumed(normalizedFilePath);
  5186. }
  5187. }
  5188. else
  5189. {
  5190. // Events 1-3, 5-7
  5191. if (m_delayProcessMetadataFiles.contains(absolutePath))
  5192. {
  5193. auto duration = m_delayProcessMetadataFiles[absolutePath].msecsTo(QDateTime::currentDateTime());
  5194. if (!HasDelayProcessTimerElapsed(duration))
  5195. {
  5196. // Event 7: Already waiting on file, keep waiting
  5197. if (!m_delayProcessMetadataQueued)
  5198. {
  5199. m_delayProcessMetadataQueued = true;
  5200. QTimer::singleShot(m_metaCreationDelayMs, this, SLOT(DelayedMetadataFileCheck()));
  5201. }
  5202. return true;
  5203. }
  5204. // Event 5-6: Times up, process the file
  5205. m_delayProcessMetadataFiles.erase(absolutePath);
  5206. Q_EMIT ProcessingResumed(normalizedFilePath);
  5207. }
  5208. else if ((triggeredByMetadata || !source.m_isDelete) && !CheckMetadataIsAvailable(absolutePath))
  5209. {
  5210. // Events 2-3: File not in queue and invalid metadata, add to queue
  5211. AZ_Trace(
  5212. AssetProcessor::DebugChannel,
  5213. "Source " AZ_STRING_FORMAT " has no metadata file yet, delaying processing to wait for metadata file.\n",
  5214. AZ_STRING_ARG(absolutePath.Native()));
  5215. m_delayProcessMetadataFiles.emplace(absolutePath, QDateTime::currentDateTime());
  5216. if (!m_delayProcessMetadataQueued)
  5217. {
  5218. m_delayProcessMetadataQueued = true;
  5219. QTimer::singleShot(m_metaCreationDelayMs, this, SLOT(DelayedMetadataFileCheck()));
  5220. }
  5221. Q_EMIT ProcessingDelayed(normalizedFilePath);
  5222. return true;
  5223. }
  5224. }
  5225. }
  5226. return false;
  5227. }
  5228. void AssetProcessorManager::DelayedMetadataFileCheck()
  5229. {
  5230. m_delayProcessMetadataQueued = false;
  5231. auto now = QDateTime::currentDateTime();
  5232. bool rerun = false;
  5233. int minWaitTime = m_metaCreationDelayMs;
  5234. for (const auto& [file, time] : m_delayProcessMetadataFiles)
  5235. {
  5236. auto duration = time.msecsTo(now);
  5237. if (HasDelayProcessTimerElapsed(duration))
  5238. {
  5239. // Times up, process it
  5240. auto* fileStateCache = AZ::Interface<IFileStateRequests>::Get();
  5241. AZ_Assert(fileStateCache, "Programmer Error - IFileStateRequests is not available.");
  5242. AssessFileInternal(file.c_str(), !fileStateCache->Exists(file.c_str()));
  5243. }
  5244. else
  5245. {
  5246. rerun = true;
  5247. // Figure out the shortest amount of time left to wait for the next file.
  5248. // This avoids waiting the full duration again in the case that a file was added to the wait list after the timer was already started.
  5249. // Example:
  5250. // t=0, A is added and the timer is started with 5000ms delay
  5251. // t=1000, B is added
  5252. // t=5000, this function runs and A is processed. Timer is started again with 1000ms wait
  5253. minWaitTime = AZ::GetMin(minWaitTime, AZ::GetMax(0, int(m_metaCreationDelayMs - duration)));
  5254. }
  5255. }
  5256. if (rerun)
  5257. {
  5258. m_delayProcessMetadataQueued = true;
  5259. QTimer::singleShot(minWaitTime, this, SLOT(DelayedMetadataFileCheck()));
  5260. }
  5261. }
  5262. QString AssetProcessorManager::AnalysisExtraInfo() const
  5263. {
  5264. if (m_scannerFilesAssessed == m_totalScannerFilesToAssess)
  5265. {
  5266. return "";
  5267. }
  5268. QLocale locale(QLocale::English, QLocale::UnitedStates); // Locale for formatting (US English uses commas)
  5269. QString extraInfo = QString("Assessing %1 of %2: %3 assets with modified builders, %4 unregistered/new, %5 outdated timestamps, %6 "
  5270. "with modified dependencies.")
  5271. .arg(locale.toString(m_scannerFilesAssessed))
  5272. .arg(locale.toString(m_totalScannerFilesToAssess))
  5273. .arg(locale.toString(m_assetsNeedingProcessing_BuildersChanged))
  5274. .arg(locale.toString(m_assetsNeedingProcessing_NewFile))
  5275. .arg(locale.toString(m_assetsNeedingProcessing_TimeStampChanged))
  5276. .arg(locale.toString(m_assetsNeedingProcessing_DependenciesChanged));
  5277. return extraInfo;
  5278. }
  5279. } // namespace AssetProcessor