AtomToolsApplication.cpp 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  3. *
  4. * SPDX-License-Identifier: Apache-2.0 OR MIT
  5. *
  6. */
  7. #include <Atom/RPI.Edit/Common/AssetUtils.h>
  8. #include <Atom/RPI.Public/RPISystemInterface.h>
  9. #include <AtomToolsFramework/Application/AtomToolsApplication.h>
  10. #include <AtomToolsFramework/Util/Util.h>
  11. #include <AtomToolsFramework/Window/AtomToolsMainWindowRequestBus.h>
  12. #include <AzCore/Component/ComponentApplicationLifecycle.h>
  13. #include <AzCore/IO/Path/Path.h>
  14. #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
  15. #include <AzCore/Utils/Utils.h>
  16. #include <AzFramework/Asset/AssetSystemComponent.h>
  17. #include <AzFramework/AzFrameworkNativeUIModule.h>
  18. #include <AzFramework/Components/NativeUISystemComponent.h>
  19. #include <AzFramework/IO/LocalFileIO.h>
  20. #include <AzFramework/Network/AssetProcessorConnection.h>
  21. #include <AzFramework/StringFunc/StringFunc.h>
  22. #include <AzQtComponents/Components/GlobalEventFilter.h>
  23. #include <AzToolsFramework/API/EditorPythonConsoleBus.h>
  24. #include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
  25. #include <AzToolsFramework/ActionManager/ActionManagerSystemComponent.h>
  26. #include <AzToolsFramework/Asset/AssetSystemComponent.h>
  27. #include <AzToolsFramework/AssetBrowser/AssetBrowserComponent.h>
  28. #include <AzToolsFramework/AssetBrowser/AssetBrowserEntry.h>
  29. #include <AzToolsFramework/AzToolsFrameworkModule.h>
  30. #include <AzToolsFramework/SourceControl/PerforceComponent.h>
  31. #include <AzToolsFramework/SourceControl/SourceControlAPI.h>
  32. #include <AzToolsFramework/Thumbnails/ThumbnailerComponent.h>
  33. #include <AzToolsFramework/UI/PropertyEditor/PropertyManagerComponent.h>
  34. #include <AzToolsFramework/UI/UICore/QTreeViewStateSaver.hxx>
  35. #include <AzToolsFramework/UI/UICore/QWidgetSavedState.h>
  36. #include "AtomToolsFramework_Traits_Platform.h"
  37. AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
  38. #include <QClipboard>
  39. #include <QMessageBox>
  40. #include <QObject>
  41. AZ_POP_DISABLE_WARNING
  42. namespace AtomToolsFramework
  43. {
  44. AtomToolsApplication* AtomToolsApplication::m_instance = {};
  45. AtomToolsApplication::AtomToolsApplication(const char* targetName, int* argc, char*** argv)
  46. : AtomToolsApplication(targetName, argc, argv, {})
  47. {
  48. }
  49. AtomToolsApplication::AtomToolsApplication(const char* targetName, int* argc, char*** argv, AZ::ComponentApplicationSettings componentAppSettings)
  50. : AzQtApplication(*argc, *argv)
  51. , Application(argc, argv, AZStd::move(componentAppSettings))
  52. , m_targetName(targetName)
  53. , m_toolId(targetName)
  54. {
  55. m_instance = this;
  56. // The settings registry has been created at this point, so add the CMake target
  57. AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddBuildSystemTargetSpecialization(
  58. *AZ::SettingsRegistry::Get(), m_targetName);
  59. // Suppress spam from the Source Control system
  60. m_traceLogger.AddWindowFilter(AzToolsFramework::SCC_WINDOW);
  61. installEventFilter(new AzQtComponents::GlobalEventFilter(this));
  62. const AZ::IO::FixedMaxPath engineRootPath(
  63. GetSettingsValue(AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder, AZStd::string()));
  64. m_styleManager.reset(new AzQtComponents::StyleManager(this));
  65. m_styleManager->initialize(this, engineRootPath);
  66. }
  67. AtomToolsApplication ::~AtomToolsApplication()
  68. {
  69. m_instance = {};
  70. m_styleManager.reset();
  71. AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler::BusDisconnect();
  72. AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusDisconnect();
  73. }
  74. void AtomToolsApplication::CreateReflectionManager()
  75. {
  76. Base::CreateReflectionManager();
  77. GetSerializeContext()->CreateEditContext();
  78. }
  79. const char* AtomToolsApplication::GetCurrentConfigurationName() const
  80. {
  81. #if defined(_RELEASE)
  82. return "ReleaseAtomTools";
  83. #elif defined(_DEBUG)
  84. return "DebugAtomTools";
  85. #else
  86. return "ProfileAtomTools";
  87. #endif
  88. }
  89. void AtomToolsApplication::Reflect(AZ::ReflectContext* context)
  90. {
  91. Base::Reflect(context);
  92. AzToolsFramework::QTreeViewWithStateSaving::Reflect(context);
  93. AzToolsFramework::QWidgetSavedState::Reflect(context);
  94. ReflectUtilFunctions(context);
  95. if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  96. {
  97. // This will put these methods into the 'azlmbr.atomtools.general' module
  98. auto addGeneral = [](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder)
  99. {
  100. methodBuilder->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  101. ->Attribute(AZ::Script::Attributes::Category, "Editor")
  102. ->Attribute(AZ::Script::Attributes::Module, "atomtools.general");
  103. };
  104. addGeneral(behaviorContext->Method(
  105. "idle_wait_frames", &AtomToolsApplication::PyIdleWaitFrames, nullptr,
  106. "Waits idling for a frames. Primarily used for auto-testing."));
  107. addGeneral(behaviorContext->Method(
  108. "exit", &AtomToolsApplication::PyExit, nullptr,
  109. "Exit application. Primarily used for auto-testing."));
  110. addGeneral(behaviorContext->Method(
  111. "crash", &AtomToolsApplication::PyCrash, nullptr,
  112. "Crashes the application, useful for testing crash reporting and other automation tools."));
  113. addGeneral(behaviorContext->Method(
  114. "test_output", &AtomToolsApplication::PyTestOutput, nullptr,
  115. "Report test information."));
  116. }
  117. }
  118. void AtomToolsApplication::RegisterCoreComponents()
  119. {
  120. Base::RegisterCoreComponents();
  121. RegisterComponentDescriptor(AzToolsFramework::AssetBrowser::AssetBrowserComponent::CreateDescriptor());
  122. RegisterComponentDescriptor(AzToolsFramework::Thumbnailer::ThumbnailerComponent::CreateDescriptor());
  123. RegisterComponentDescriptor(AzToolsFramework::Components::PropertyManagerComponent::CreateDescriptor());
  124. RegisterComponentDescriptor(AzToolsFramework::AssetSystem::AssetSystemComponent::CreateDescriptor());
  125. RegisterComponentDescriptor(AzToolsFramework::PerforceComponent::CreateDescriptor());
  126. }
  127. AZ::ComponentTypeList AtomToolsApplication::GetRequiredSystemComponents() const
  128. {
  129. AZ::ComponentTypeList components = Base::GetRequiredSystemComponents();
  130. components.insert(
  131. components.end(),
  132. {
  133. azrtti_typeid<AzToolsFramework::ActionManagerSystemComponent>(),
  134. azrtti_typeid<AzToolsFramework::AssetBrowser::AssetBrowserComponent>(),
  135. azrtti_typeid<AzToolsFramework::AssetSystem::AssetSystemComponent>(),
  136. azrtti_typeid<AzToolsFramework::Components::PropertyManagerComponent>(),
  137. azrtti_typeid<AzToolsFramework::PerforceComponent>(),
  138. azrtti_typeid<AzToolsFramework::Thumbnailer::ThumbnailerComponent>(),
  139. azrtti_typeid<AzFramework::NativeUISystemComponent>(),
  140. });
  141. return components;
  142. }
  143. void AtomToolsApplication::CreateStaticModules(AZStd::vector<AZ::Module*>& outModules)
  144. {
  145. Base::CreateStaticModules(outModules);
  146. outModules.push_back(aznew AzToolsFramework::AzToolsFrameworkModule);
  147. outModules.push_back(aznew AzFramework::AzFrameworkNativeUIModule());
  148. }
  149. void AtomToolsApplication::StartCommon(AZ::Entity* systemEntity)
  150. {
  151. AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusConnect();
  152. Base::StartCommon(systemEntity);
  153. // Before serializing data to the log file, determine if it should be cleared first.
  154. const bool clearLogFile = GetSettingsValue("/O3DE/AtomToolsFramework/Application/ClearLogOnStart", false);
  155. // Now that the base application is initialized, open the file to record any log messages and dump any pending content into it.
  156. if (m_commandLine.HasSwitch("logfile"))
  157. {
  158. // If a custom log file name was supplied via command line, redirect output to it.
  159. m_traceLogger.OpenLogFile(m_commandLine.GetSwitchValue("logfile", 0), clearLogFile);
  160. }
  161. else
  162. {
  163. m_traceLogger.OpenLogFile(m_targetName + ".log", clearLogFile);
  164. }
  165. ConnectToAssetProcessor();
  166. AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler::BusConnect();
  167. AzToolsFramework::AssetBrowser::AssetDatabaseLocationNotificationBus::Broadcast(
  168. &AzToolsFramework::AssetBrowser::AssetDatabaseLocationNotifications::OnDatabaseInitialized);
  169. // Disabling source control integration by default to disable messages and menus if no supported source control system is active
  170. const bool enableSourceControl = GetSettingsValue("/O3DE/AtomToolsFramework/Application/EnableSourceControl", false);
  171. AzToolsFramework::SourceControlConnectionRequestBus::Broadcast(
  172. &AzToolsFramework::SourceControlConnectionRequests::EnableSourceControl, enableSourceControl);
  173. auto rpiInterface = AZ::RPI::RPISystemInterface::Get();
  174. if (rpiInterface && !rpiInterface->IsInitialized())
  175. {
  176. AZ::RPI::RPISystemInterface::Get()->InitializeSystemAssets();
  177. }
  178. LoadSettings();
  179. m_assetBrowserInteractions.reset(aznew AtomToolsFramework::AtomToolsAssetBrowserInteractions);
  180. auto editorPythonEventsInterface = AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Get();
  181. if (editorPythonEventsInterface)
  182. {
  183. // The PythonSystemComponent does not call StartPython to allow for lazy python initialization, so start it here
  184. // The PythonSystemComponent will call StopPython when it deactivates, so we do not need our own corresponding call to
  185. // StopPython
  186. editorPythonEventsInterface->StartPython();
  187. }
  188. // Handle command line options for setting up a test environment that should not be affected forwarding commands from other
  189. // instances of an application
  190. if (m_commandLine.HasSwitch("autotest_mode") || m_commandLine.HasSwitch("runpythontest"))
  191. {
  192. // Nullroute all stdout to null for automated tests, this way we make sure
  193. // that the test result output is not polluted with unrelated output data.
  194. RedirectStdoutToNull();
  195. }
  196. else
  197. {
  198. // Enable native UI for some low level system popup message when it's not in automated test mode
  199. if (auto nativeUI = AZ::Interface<AZ::NativeUI::NativeUIRequests>::Get(); nativeUI != nullptr)
  200. {
  201. nativeUI->SetMode(AZ::NativeUI::Mode::ENABLED);
  202. }
  203. }
  204. // Per Qt documentation, forcing Stop to be called when the application is about to quit in case exit bypasses Stop or destructor
  205. connect(this, &QApplication::aboutToQuit, this, [this] {
  206. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop);
  207. Stop();
  208. });
  209. }
  210. void AtomToolsApplication::Destroy()
  211. {
  212. // Clearing graph canvas clipboard mime data for copied nodes before exiting the application to prevent a crash in qt_call_post_routines
  213. QApplication::clipboard()->clear();
  214. m_assetBrowserInteractions.reset();
  215. m_styleManager.reset();
  216. // Save application registry settings to a target specific settings file. The file must be named so that it is only loaded for an
  217. // application with the corresponding target name.
  218. AZStd::string settingsFileName(AZStd::string::format("usersettings.%s.setreg", m_targetName.c_str()));
  219. AZStd::to_lower(settingsFileName.begin(), settingsFileName.end());
  220. const AZ::IO::FixedMaxPath settingsFilePath(
  221. AZStd::string::format("%s/user/Registry/%s", AZ::Utils::GetProjectPath().c_str(), settingsFileName.c_str()));
  222. // This will only save modified registry settings that match the following filters
  223. const AZStd::vector<AZStd::string> filters = {
  224. "/O3DE/AtomToolsFramework", "/O3DE/Atom/Tools", AZStd::string::format("/O3DE/Atom/%s", m_targetName.c_str()) };
  225. SaveSettingsToFile(settingsFilePath, filters);
  226. // Handler for serializing legacy user settings
  227. UnloadSettings();
  228. AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusDisconnect();
  229. AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler::BusDisconnect();
  230. AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::StartDisconnectingAssetProcessor);
  231. #if AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY
  232. ::_exit(0);
  233. #else
  234. Base::Destroy();
  235. #endif
  236. }
  237. void AtomToolsApplication::RunMainLoop()
  238. {
  239. // Start initial command line processing and application update as part of the Qt event loop
  240. QTimer::singleShot(0, this, [this]() { OnIdle(); ProcessCommandLine(m_commandLine); });
  241. exec();
  242. }
  243. void AtomToolsApplication::OnIdle()
  244. {
  245. // Process a single application tick unless exit was requested
  246. if (!WasExitMainLoopRequested())
  247. {
  248. PumpSystemEventLoopUntilEmpty();
  249. TickSystem();
  250. Tick();
  251. // Rescheduling the update every frame with an interval based on the state of the application.
  252. // This allows the tool to free up resources for other processes when it's not in focus.
  253. const int updateInterval = (applicationState() & Qt::ApplicationActive)
  254. ? aznumeric_cast<int>(GetSettingsValue<AZ::u64>("/O3DE/AtomToolsFramework/Application/UpdateIntervalWhenActive", 1))
  255. : aznumeric_cast<int>(GetSettingsValue<AZ::u64>("/O3DE/AtomToolsFramework/Application/UpdateIntervalWhenNotActive", 250));
  256. QTimer::singleShot(updateInterval, this, &AtomToolsApplication::OnIdle);
  257. return;
  258. }
  259. quit();
  260. }
  261. AZStd::vector<AZStd::string> AtomToolsApplication::GetCriticalAssetFilters() const
  262. {
  263. return AZStd::vector<AZStd::string>({});
  264. }
  265. void AtomToolsApplication::ConnectToAssetProcessor()
  266. {
  267. bool connectedToAssetProcessor = false;
  268. // When the AssetProcessor is already launched it should take less than a second to perform a connection
  269. // but when the AssetProcessor needs to be launch it could take up to 15 seconds to have the AssetProcessor initialize
  270. // and able to negotiate a connection when running a debug build
  271. // and to negotiate a connection
  272. AzFramework::AssetSystem::ConnectionSettings connectionSettings;
  273. AzFramework::AssetSystem::ReadConnectionSettingsFromSettingsRegistry(connectionSettings);
  274. connectionSettings.m_connectionDirection =
  275. AzFramework::AssetSystem::ConnectionSettings::ConnectionDirection::ConnectToAssetProcessor;
  276. connectionSettings.m_connectionIdentifier = m_targetName;
  277. connectionSettings.m_loggingCallback = [targetName = m_targetName]([[maybe_unused]] AZStd::string_view logData)
  278. {
  279. AZ_UNUSED(targetName); // Prevent unused warning in release builds
  280. AZ_TracePrintf(targetName.c_str(), "%.*s", aznumeric_cast<int>(logData.size()), logData.data());
  281. };
  282. AzFramework::AssetSystemRequestBus::BroadcastResult(
  283. connectedToAssetProcessor, &AzFramework::AssetSystemRequestBus::Events::EstablishAssetProcessorConnection, connectionSettings);
  284. if (connectedToAssetProcessor)
  285. {
  286. CompileCriticalAssets();
  287. }
  288. }
  289. void AtomToolsApplication::CompileCriticalAssets()
  290. {
  291. AZ_TracePrintf(m_targetName.c_str(), "Compiling critical assets.\n");
  292. QStringList failedAssets;
  293. // Forced asset processor to synchronously process all critical assets
  294. // Note: with AssetManager's current implementation, a compiled asset won't be added in asset registry until next system tick.
  295. // So the asset id won't be found right after CompileAssetSync call.
  296. for (const AZStd::string& assetFilters : GetCriticalAssetFilters())
  297. {
  298. AZ_TracePrintf(m_targetName.c_str(), "Compiling critical asset matching: %s.\n", assetFilters.c_str());
  299. // Wait for the asset be compiled
  300. AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus_Unknown;
  301. AzFramework::AssetSystemRequestBus::BroadcastResult(
  302. status, &AzFramework::AssetSystemRequestBus::Events::CompileAssetSync, assetFilters);
  303. if (status != AzFramework::AssetSystem::AssetStatus_Compiled && status != AzFramework::AssetSystem::AssetStatus_Unknown)
  304. {
  305. failedAssets.append(assetFilters.c_str());
  306. }
  307. }
  308. if (!failedAssets.empty())
  309. {
  310. QMessageBox::critical(
  311. GetToolMainWindow(),
  312. QString("Failed to compile critical assets"),
  313. QString("Failed to compile the following critical assets:\n%1\n%2")
  314. .arg(failedAssets.join(",\n"))
  315. .arg("Make sure this is an Atom project."));
  316. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop);
  317. }
  318. // Reload the assetcatalog.xml at this point again
  319. // Start monitoring asset changes over the network and load the AssetCatalog
  320. auto LoadCatalog = [settingsRegistry = m_settingsRegistry.get()](AZ::Data::AssetCatalogRequests* assetCatalogRequests)
  321. {
  322. if (AZ::IO::FixedMaxPath assetCatalogPath;
  323. settingsRegistry->Get(assetCatalogPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder))
  324. {
  325. assetCatalogPath /= "assetcatalog.xml";
  326. assetCatalogRequests->LoadCatalog(assetCatalogPath.c_str());
  327. }
  328. };
  329. AZ::Data::AssetCatalogRequestBus::Broadcast(AZStd::move(LoadCatalog));
  330. // Only signal the event *after* the asset catalog has been loaded.
  331. AZ::ComponentApplicationLifecycle::SignalEvent(*m_settingsRegistry, "CriticalAssetsCompiled", R"({})");
  332. }
  333. void AtomToolsApplication::SaveSettings()
  334. {
  335. if (m_activatedLocalUserSettings)
  336. {
  337. AZ::SerializeContext* context = nullptr;
  338. AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext);
  339. AZ_Assert(context, "No serialize context");
  340. char resolvedPath[AZ_MAX_PATH_LEN] = "";
  341. AZStd::string fileName = "@user@/" + m_targetName + "UserSettings.xml";
  342. AZ::IO::FileIOBase::GetInstance()->ResolvePath(
  343. fileName.c_str(), resolvedPath, AZ_ARRAY_SIZE(resolvedPath));
  344. m_localUserSettings.Save(resolvedPath, context);
  345. }
  346. }
  347. void AtomToolsApplication::LoadSettings()
  348. {
  349. AZ::SerializeContext* context = nullptr;
  350. AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext);
  351. AZ_Assert(context, "No serialize context");
  352. char resolvedPath[AZ_MAX_PATH_LEN] = "";
  353. AZStd::string fileName = "@user@/" + m_targetName + "UserSettings.xml";
  354. AZ::IO::FileIOBase::GetInstance()->ResolvePath(fileName.c_str(), resolvedPath, AZ_MAX_PATH_LEN);
  355. m_localUserSettings.Load(resolvedPath, context);
  356. m_localUserSettings.Activate(AZ::UserSettings::CT_LOCAL);
  357. AZ::UserSettingsOwnerRequestBus::Handler::BusConnect(AZ::UserSettings::CT_LOCAL);
  358. m_activatedLocalUserSettings = true;
  359. }
  360. void AtomToolsApplication::UnloadSettings()
  361. {
  362. if (m_activatedLocalUserSettings)
  363. {
  364. SaveSettings();
  365. m_localUserSettings.Deactivate();
  366. AZ::UserSettingsOwnerRequestBus::Handler::BusDisconnect();
  367. m_activatedLocalUserSettings = false;
  368. }
  369. }
  370. void AtomToolsApplication::ProcessCommandLine(const AZ::CommandLine& commandLine)
  371. {
  372. if (commandLine.HasSwitch("activatewindow"))
  373. {
  374. AtomToolsMainWindowRequestBus::Event(m_toolId, &AtomToolsMainWindowRequestBus::Handler::ActivateWindow);
  375. }
  376. const AZStd::string timeoutSwitchName = "timeout";
  377. if (commandLine.HasSwitch(timeoutSwitchName ))
  378. {
  379. const AZStd::string& timeoutValue = commandLine.GetSwitchValue(timeoutSwitchName , 0);
  380. const uint32_t timeoutInMs = atoi(timeoutValue.c_str());
  381. AZ_Printf(m_targetName.c_str(), "Timeout scheduled, shutting down in %u ms", timeoutInMs);
  382. QTimer::singleShot(timeoutInMs, this, [targetName = m_targetName]{
  383. AZ_Printf(targetName.c_str(), "Timeout reached, shutting down");
  384. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop);
  385. });
  386. }
  387. // Process command line options for running one or more python scripts on startup
  388. const size_t pythonScriptCount = commandLine.GetNumSwitchValues("runpython");
  389. AZStd::vector<AZStd::string_view> pythonScripts;
  390. pythonScripts.reserve(pythonScriptCount);
  391. for (size_t pythonScriptIndex = 0; pythonScriptIndex < pythonScriptCount; ++pythonScriptIndex)
  392. {
  393. pythonScripts.push_back(commandLine.GetSwitchValue("runpython", pythonScriptIndex));
  394. }
  395. const size_t pythonTestScriptCount = commandLine.GetNumSwitchValues("runpythontest");
  396. AZStd::vector<AZStd::string_view> pythonTestScripts;
  397. pythonTestScripts.reserve(pythonTestScriptCount);
  398. for (size_t pythonTestScriptIndex = 0; pythonTestScriptIndex < pythonTestScriptCount; ++pythonTestScriptIndex)
  399. {
  400. pythonTestScripts.push_back(commandLine.GetSwitchValue("runpythontest", pythonTestScriptIndex));
  401. }
  402. const char* pythonArgSwitchName = "runpythonargs";
  403. const size_t pythonArgCount = commandLine.GetNumSwitchValues(pythonArgSwitchName);
  404. AZStd::vector<AZStd::string_view> pythonArgs;
  405. pythonArgs.reserve(pythonArgCount);
  406. for (size_t pythonArgIndex = 0; pythonArgIndex < pythonArgCount; ++pythonArgIndex)
  407. {
  408. pythonArgs.push_back(commandLine.GetSwitchValue(pythonArgSwitchName, pythonArgIndex));
  409. }
  410. const char* pythonTestCaseSwitchName = "runpythontestcase";
  411. const size_t pythonTestCaseCount = commandLine.GetNumSwitchValues(pythonTestCaseSwitchName);
  412. AZStd::vector<AZStd::string_view> pythonTestCases;
  413. pythonTestCases.reserve(pythonTestCaseCount);
  414. for (size_t pythonTestCaseIndex = 0; pythonTestCaseIndex < pythonTestCaseCount; ++pythonTestCaseIndex)
  415. {
  416. pythonTestCases.push_back(commandLine.GetSwitchValue(pythonTestCaseSwitchName, pythonTestCaseIndex));
  417. }
  418. // The number of test case strings must be identical to the number of test scripts even if they are empty
  419. pythonTestCases.resize(pythonTestScripts.size());
  420. if (!pythonTestScripts.empty())
  421. {
  422. bool success = true;
  423. AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(
  424. [&](AzToolsFramework::EditorPythonRunnerRequests* pythonRunnerRequests)
  425. {
  426. for (int i = 0; i < pythonTestScripts.size(); ++i)
  427. {
  428. bool cur_success =
  429. pythonRunnerRequests->ExecuteByFilenameAsTest(pythonTestScripts[i], pythonTestCases[i], pythonArgs);
  430. success = success && cur_success;
  431. }
  432. });
  433. if (success)
  434. {
  435. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop);
  436. }
  437. else
  438. {
  439. // Close down the application with 0xF exit code indicating failure of the test
  440. AZ::Debug::Trace::Terminate(0xF);
  441. }
  442. }
  443. if (!pythonScripts.empty())
  444. {
  445. AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(
  446. [&](AzToolsFramework::EditorPythonRunnerRequests* pythonRunnerRequests)
  447. {
  448. for (const auto& filename : pythonScripts)
  449. {
  450. pythonRunnerRequests->ExecuteByFilenameWithArgs(filename, pythonArgs);
  451. }
  452. });
  453. }
  454. if (commandLine.HasSwitch("autotest_mode") ||
  455. commandLine.HasSwitch("runpythontest") ||
  456. commandLine.HasSwitch("exitaftercommands"))
  457. {
  458. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop);
  459. }
  460. }
  461. void AtomToolsApplication::PrintAlways(const AZStd::string& output)
  462. {
  463. m_stdoutRedirection.WriteBypassingRedirect(output.c_str(), static_cast<unsigned int>(output.size()));
  464. }
  465. void AtomToolsApplication::RedirectStdoutToNull()
  466. {
  467. m_stdoutRedirection.RedirectTo(AZ::IO::SystemFile::GetNullFilename());
  468. }
  469. bool AtomToolsApplication::LaunchLocalServer()
  470. {
  471. // The socket and server are currently used to forward all requests to an existing application process if one is already running.
  472. // These additional settings will allow multiple instances to be launched in automated testing batch mode and other scenarios.
  473. const bool allowMultipleInstances = GetSettingsValue("/O3DE/AtomToolsFramework/Application/AllowMultipleInstances", false);
  474. if (allowMultipleInstances || m_commandLine.HasSwitch("allowMultipleInstances") || m_commandLine.HasSwitch("batchmode"))
  475. {
  476. return true;
  477. }
  478. // Determine if this is the first launch of the tool by attempting to connect to a running server
  479. if (m_socket.Connect(QApplication::applicationName()))
  480. {
  481. // If the server was located, the application is already running.
  482. // Forward commandline options to other application instance.
  483. QByteArray buffer;
  484. buffer.append("ProcessCommandLine:");
  485. // Add the command line options from this process to the message, skipping the executable path
  486. for (int argi = 1; argi < m_argC; ++argi)
  487. {
  488. buffer.append(QString(m_argV[argi]).append("\n").toUtf8());
  489. }
  490. // Inject command line option to always bring the main window to the foreground
  491. buffer.append("--activatewindow\n");
  492. m_socket.Send(buffer);
  493. m_socket.Disconnect();
  494. return false;
  495. }
  496. // Setup server to handle basic commands
  497. m_server.SetReadHandler(
  498. [this](const QByteArray& buffer)
  499. {
  500. // Handle commmand line params from connected socket
  501. if (buffer.startsWith("ProcessCommandLine:"))
  502. {
  503. // Remove header and parse commands
  504. AZStd::string params(buffer.data(), buffer.size());
  505. params = params.substr(strlen("ProcessCommandLine:"));
  506. AZStd::vector<AZStd::string> tokens;
  507. AZ::StringFunc::Tokenize(params, tokens, "\n");
  508. if (!tokens.empty())
  509. {
  510. AZ::CommandLine commandLine;
  511. commandLine.Parse(tokens);
  512. QTimer::singleShot(0, this, [this, commandLine]() { ProcessCommandLine(commandLine); });
  513. }
  514. }
  515. });
  516. // Launch local server
  517. if (!m_server.Connect(QApplication::applicationName()))
  518. {
  519. return false;
  520. }
  521. return true;
  522. }
  523. bool AtomToolsApplication::GetAssetDatabaseLocation(AZStd::string& result)
  524. {
  525. AZ::SettingsRegistryInterface* settingsRegistry = AZ::SettingsRegistry::Get();
  526. AZ::IO::FixedMaxPath assetDatabaseSqlitePath;
  527. if (settingsRegistry &&
  528. settingsRegistry->Get(assetDatabaseSqlitePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_CacheProjectRootFolder))
  529. {
  530. assetDatabaseSqlitePath /= "assetdb.sqlite";
  531. result = AZStd::string_view(assetDatabaseSqlitePath.Native());
  532. return true;
  533. }
  534. return false;
  535. }
  536. void AtomToolsApplication::QueryApplicationType(AZ::ApplicationTypeQuery& appType) const
  537. {
  538. appType.m_maskValue = AZ::ApplicationTypeQuery::Masks::Tool;
  539. }
  540. void AtomToolsApplication::OnTraceMessage([[maybe_unused]] AZStd::string_view message)
  541. {
  542. #if defined(AZ_ENABLE_TRACING)
  543. AZStd::vector<AZStd::string> lines;
  544. AzFramework::StringFunc::Tokenize(
  545. message, lines, "\n",
  546. false, // Keep empty strings
  547. false // Keep space strings
  548. );
  549. for (auto& line : lines)
  550. {
  551. AZ_TracePrintf(m_targetName.c_str(), "Python: %s\n", line.c_str());
  552. }
  553. #endif
  554. }
  555. void AtomToolsApplication::OnErrorMessage(AZStd::string_view message)
  556. {
  557. // Use AZ_TracePrintf instead of AZ_Error or AZ_Warning to avoid all the metadata noise
  558. OnTraceMessage(message);
  559. }
  560. void AtomToolsApplication::OnExceptionMessage([[maybe_unused]] AZStd::string_view message)
  561. {
  562. AZ_Error(m_targetName.c_str(), false, "Python: " AZ_STRING_FORMAT, AZ_STRING_ARG(message));
  563. }
  564. void AtomToolsApplication::PyIdleWaitFrames(uint32_t frames)
  565. {
  566. // Create a child event loop that takes control of updating the application for a set number of frames.
  567. // When executed from a script, this continues to update the application but allows the script to pause until the number of frames
  568. // have passed.
  569. QEventLoop loop;
  570. QTimer timer;
  571. uint32_t frame = 0;
  572. QObject::connect(&timer, &QTimer::timeout, &loop, [&]() {
  573. auto app = AtomToolsApplication::GetInstance();
  574. if (app && !app->WasExitMainLoopRequested() && frame++ < frames)
  575. {
  576. app->PumpSystemEventLoopUntilEmpty();
  577. app->TickSystem();
  578. app->Tick();
  579. return;
  580. }
  581. timer.stop();
  582. loop.quit();
  583. });
  584. timer.setInterval(0);
  585. timer.start();
  586. loop.exec();
  587. }
  588. void AtomToolsApplication::PyExit()
  589. {
  590. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop);
  591. }
  592. void AtomToolsApplication::PyCrash()
  593. {
  594. AZ_Crash();
  595. }
  596. void AtomToolsApplication::PyTestOutput(const AZStd::string& output)
  597. {
  598. AtomToolsApplication::GetInstance()->PrintAlways(output);
  599. }
  600. AtomToolsApplication* AtomToolsApplication::GetInstance()
  601. {
  602. return m_instance;
  603. }
  604. } // namespace AtomToolsFramework