AtomToolsApplication.cpp 30 KB

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