Added a deferred queue to the AZ Console class (#3298)

* Added a deferred queue to the AZ Console class

An AZ Console instance will now store any console commands that could be
dispatched from a configuration file into a deferred queue, that can be
invoked later.

This can be used to defer execution of console commands in configuration files such as
.cfg, .setreg and .setregpatch files that are defined in gem modules that
have not been loaded yet.
The defered execution can then be invoked at any point later in the
application

Updated the Component Application CreateCommon function to invoke deferred console commands
after all the gems have loaded

fixes #2062

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Fixed variable shadowing in the Console Deferred Command Test

Updated commit for the ClearDeferredQueue function to just mention
clearing the queue

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Filtered out execution of the ConsoleRootCommandKey as a console command

The AZ::Console notification handler is tracking changes to the fields
of "/Amazon/AzCore/Runtime/ConsoleCommands" and it's children.

Now the "/Amazon/AzCore/Runtime/ConsoleCommands" field is the
ConsoleRootCommandKey and not an actual console command so it shouldn't
attempt to be invoked

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Moved the execution of deferred console commands after linking deferred
functors

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Moved the execution of deferred console commands into CreateModuleClass
hook

Any module that loads using the ModuleManager system will attempt to
execute any deferred console commands to allow newly registered commands
from that module to be dispatched.

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>
This commit is contained in:
lumberyard-employee-dm
2021-08-18 20:58:57 -05:00
committed by GitHub
parent 90845313fb
commit 586678a5f9
5 changed files with 149 additions and 21 deletions
@@ -174,12 +174,28 @@ namespace AZ
}
}
bool Console::HasCommand(const char* command)
bool Console::ExecuteDeferredConsoleCommands()
{
auto DeferredCommandCallable = [this](const DeferredCommand& deferredCommand)
{
return this->DispatchCommand(deferredCommand.m_command, deferredCommand.m_arguments, deferredCommand.m_silentMode,
deferredCommand.m_invokedFrom, deferredCommand.m_requiredSet, deferredCommand.m_requiredClear);
};
// Attempt to invoke the deferred command and remove it from the queue if successful
return AZStd::erase_if(m_deferredCommands, DeferredCommandCallable) != 0;
}
void Console::ClearDeferredConsoleCommands()
{
m_deferredCommands = {};
}
bool Console::HasCommand(AZStd::string_view command)
{
return FindCommand(command) != nullptr;
}
ConsoleFunctorBase* Console::FindCommand(const char* command)
ConsoleFunctorBase* Console::FindCommand(AZStd::string_view command)
{
CVarFixedString lowerName(command);
AZStd::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](char value) { return std::tolower(value); });
@@ -200,11 +216,9 @@ namespace AZ
return nullptr;
}
AZStd::string Console::AutoCompleteCommand(const char* command, AZStd::vector<AZStd::string>* matches)
AZStd::string Console::AutoCompleteCommand(AZStd::string_view command, AZStd::vector<AZStd::string>* matches)
{
const size_t commandLength = strlen(command);
if (commandLength <= 0)
if (command.empty())
{
return command;
}
@@ -219,7 +233,7 @@ namespace AZ
continue;
}
if (StringFunc::Equal(curr->m_name, command, false, commandLength))
if (StringFunc::StartsWith(curr->m_name, command, false))
{
AZLOG_INFO("- %s : %s\n", curr->m_name, curr->m_desc);
commandSubset.push_back(curr->m_name);
@@ -498,7 +512,8 @@ namespace AZ
AZ::IO::PathView consoleRootCommandKey{ IConsole::ConsoleRootCommandKey, AZ::IO::PosixPathSeparator };
AZ::IO::PathView inputKey{ path, AZ::IO::PosixPathSeparator };
if (inputKey.IsRelativeTo(consoleRootCommandKey))
// The ConsoleRootComamndKey is not a command itself so strictly children keys are being examined
if (inputKey.IsRelativeTo(consoleRootCommandKey) && inputKey != consoleRootCommandKey)
{
FixedValueString command = inputKey.LexicallyRelative(consoleRootCommandKey).Native();
ConsoleCommandContainer commandArgs;
@@ -560,7 +575,24 @@ namespace AZ
commandTrace += commandArg;
}
m_console.PerformCommand(command, commandArgs, ConsoleSilentMode::NotSilent, ConsoleInvokedFrom::AzConsole, ConsoleFunctorFlags::Null, ConsoleFunctorFlags::Null);
if (!m_console.PerformCommand(command, commandArgs, ConsoleSilentMode::NotSilent,
ConsoleInvokedFrom::AzConsole, ConsoleFunctorFlags::Null, ConsoleFunctorFlags::Null))
{
// If the command could not be dispatched at this time add it to the
// deferred commands queue
using DeferredCommand = Console::DeferredCommand;
DeferredCommand deferredCommand
{
AZStd::string_view{command},
DeferredCommand::DeferredArguments{commandArgs.begin(), commandArgs.end()},
ConsoleSilentMode::NotSilent,
ConsoleInvokedFrom::AzConsole,
ConsoleFunctorFlags::Null,
ConsoleFunctorFlags::Null
};
m_console.m_deferredCommands.emplace_back(AZStd::move(deferredCommand));
}
}
}
+20 -3
View File
@@ -60,9 +60,13 @@ namespace AZ
) override;
void ExecuteConfigFile(AZStd::string_view configFileName) override;
void ExecuteCommandLine(const AZ::CommandLine& commandLine) override;
bool HasCommand(const char* command) override;
ConsoleFunctorBase* FindCommand(const char* command) override;
AZStd::string AutoCompleteCommand(const char* command, AZStd::vector<AZStd::string>* matches = nullptr) override;
bool ExecuteDeferredConsoleCommands() override;
void ClearDeferredConsoleCommands() override;
bool HasCommand(AZStd::string_view command) override;
ConsoleFunctorBase* FindCommand(AZStd::string_view command) override;
AZStd::string AutoCompleteCommand(AZStd::string_view command, AZStd::vector<AZStd::string>* matches = nullptr) override;
void VisitRegisteredFunctors(const FunctorVisitor& visitor) override;
void RegisterFunctor(ConsoleFunctorBase* functor) override;
void UnregisterFunctor(ConsoleFunctorBase* functor) override;
@@ -98,7 +102,20 @@ namespace AZ
using CommandMap = AZStd::unordered_map<CVarFixedString, AZStd::vector<ConsoleFunctorBase*>>;
CommandMap m_commands;
AZ::SettingsRegistryInterface::NotifyEventHandler m_consoleCommandKeyHandler;
struct DeferredCommand
{
using DeferredArguments = AZStd::vector<AZStd::string>;
AZStd::string m_command;
DeferredArguments m_arguments;
ConsoleSilentMode m_silentMode;
ConsoleInvokedFrom m_invokedFrom;
ConsoleFunctorFlags m_requiredSet;
ConsoleFunctorFlags m_requiredClear;
};
using DeferredCommandQueue = AZStd::deque<DeferredCommand>;
DeferredCommandQueue m_deferredCommands;
friend struct ConsoleCommandKeyNotificationHandler;
friend class ConsoleFunctorBase;
};
}
@@ -94,22 +94,30 @@ namespace AZ
//! @param commandLine the concatenated command-line string to execute
virtual void ExecuteCommandLine(const AZ::CommandLine& commandLine) = 0;
//! Attempts to invoke a "deferred console command", which is a console command
//! that has failed to execute previously due to the command not being registered yet.
//! @return boolean true if any deferred console commands have executed, false otherwise
virtual bool ExecuteDeferredConsoleCommands() = 0;
//! Clear out any deferred console commands queue
virtual void ClearDeferredConsoleCommands() = 0;
//! HasCommand is used to determine if the console knows about a command.
//! @param command the command we are checking for
//! @return boolean true on if the command is registered, false otherwise
virtual bool HasCommand(const char* command) = 0;
virtual bool HasCommand(AZStd::string_view command) = 0;
//! FindCommand finds the console command with the specified console string.
//! @param command the command that is being searched for
//! @return non-null pointer to the console command if found
virtual ConsoleFunctorBase* FindCommand(const char* command) = 0;
virtual ConsoleFunctorBase* FindCommand(AZStd::string_view command) = 0;
//! Finds all commands where the input command is a prefix and returns
//! the longest matching substring prefix the results have in common.
//! @param command The prefix string to find all matching commands for.
//! @param matches The list of all commands that match the input prefix.
//! @return The longest matching substring prefix the results have in common.
virtual AZStd::string AutoCompleteCommand(const char* command,
virtual AZStd::string AutoCompleteCommand(AZStd::string_view command,
AZStd::vector<AZStd::string>* matches = nullptr) = 0;
//! Retrieves the value of the requested cvar.
@@ -117,7 +125,7 @@ namespace AZ
//! @param outValue reference to the instance to write the current cvar value to
//! @return GetValueResult::Success if the operation succeeded, or an error result if the operation failed
template<typename RETURN_TYPE>
GetValueResult GetCvarValue(const char* command, RETURN_TYPE& outValue);
GetValueResult GetCvarValue(AZStd::string_view command, RETURN_TYPE& outValue);
//! Visits all registered console functors.
//! @param visitor the instance to visit all functors with
@@ -176,7 +184,7 @@ namespace AZ
}
template<typename RETURN_TYPE>
inline GetValueResult IConsole::GetCvarValue(const char* command, RETURN_TYPE& outValue)
inline GetValueResult IConsole::GetCvarValue(AZStd::string_view command, RETURN_TYPE& outValue)
{
ConsoleFunctorBase* cvarFunctor = FindCommand(command);
if (cvarFunctor == nullptr)
+11 -4
View File
@@ -98,19 +98,26 @@ namespace AZ
///
/// \param MODULE_NAME Name of module.
/// \param MODULE_CLASSNAME Name of AZ::Module class (include namespace).
///
/// Execute any deferred console commands after linking any new deferred functors
/// This allows deferred console commands defined within the module to now execute
/// at this point now that the module has been loaded
#if defined(AZ_MONOLITHIC_BUILD)
# define AZ_DECLARE_MODULE_CLASS(MODULE_NAME, MODULE_CLASSNAME) \
extern "C" AZ::Module * CreateModuleClass_##MODULE_NAME() { return aznew MODULE_CLASSNAME; }
#else
# define AZ_DECLARE_MODULE_CLASS(MODULE_NAME, MODULE_CLASSNAME) \
AZ_DECLARE_MODULE_INITIALIZATION \
extern "C" AZ_DLL_EXPORT AZ::Module * CreateModuleClass() \
extern "C" AZ_DLL_EXPORT AZ::Module* CreateModuleClass() \
{ \
AZ::ConsoleFunctorBase*& deferredHead = AZ::ConsoleFunctorBase::GetDeferredHead(); \
AZ::Interface<AZ::IConsole>::Get()->LinkDeferredFunctors(deferredHead); \
if (auto console = AZ::Interface<AZ::IConsole>::Get(); console != nullptr) \
{ \
console->LinkDeferredFunctors(AZ::ConsoleFunctorBase::GetDeferredHead()); \
console->ExecuteDeferredConsoleCommands(); \
} \
return aznew MODULE_CLASSNAME; \
} \
extern "C" AZ_DLL_EXPORT void DestroyModuleClass(AZ::Module * module) { delete module; }
extern "C" AZ_DLL_EXPORT void DestroyModuleClass(AZ::Module* module) { delete module; }
#endif
#endif // AZCORE_MODULE_INCLUDE_H
@@ -507,6 +507,70 @@ namespace ConsoleSettingsRegistryTests
AZ::Interface<AZ::IConsole>::Unregister(&testConsole);
}
template<typename T>
using ConsoleDataWrapper = AZ::ConsoleDataWrapper<T, ConsoleThreadSafety<T>>;
TEST_P(ConsoleSettingsRegistryFixture, Console_RecordsUnregisteredCommands_And_IsAbleToDeferDispatchCommand_Successfully)
{
AZ::Console testConsole(*m_registry);
AZ::Interface<AZ::IConsole>::Register(&testConsole);
// GetDeferredHead is invoked for the side effect of to set the s_deferredHeadInvoked value to true
// This allows scoped console variables to be attached immediately
[[maybe_unused]] auto deferredHead = AZ::ConsoleFunctorBase::GetDeferredHead();
ConsoleDataWrapper<int32_t> localTestInit{ {}, nullptr, "testInit", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<char> localTestChar{ {}, nullptr, "testChar", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<bool> localTestBool{ {}, nullptr, "testBool", "", AZ::ConsoleFunctorFlags::Null };
s_consoleFreeFunctionInvoked = false;
// Invoke the Commands for Scoped CVar variables above
auto configFileParams = GetParam();
auto testFilePath = m_testFolder / configFileParams.m_testConfigFileName;
EXPECT_TRUE(AZ::IO::SystemFile::Exists(testFilePath.c_str()));
testConsole.ExecuteConfigFile(testFilePath.Native());
EXPECT_EQ(3, localTestInit);
EXPECT_TRUE(static_cast<bool>(localTestBool));
EXPECT_EQ('Q', localTestChar);
// The following commands from the config files should have been deferred
ConsoleDataWrapper<int8_t> localTestInt8{ {}, nullptr, "testInt8", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<int16_t> localTestInt16{ {}, nullptr, "testInt16", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<int32_t> localTestInt32{ {}, nullptr, "testInt32", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<int64_t> localTestInt64{ {}, nullptr, "testInt64", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<uint8_t> localTestUInt8{ {}, nullptr, "testUInt8", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<uint16_t> localTestUInt16{ {}, nullptr, "testUInt16", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<uint32_t> localTestUInt32{ {}, nullptr, "testUInt32", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<uint64_t> localTestUInt64{ {}, nullptr, "testUInt64", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<float> localTestFloat{ {}, nullptr, "testFloat", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<double> localTestDouble{ {}, nullptr, "testDouble", "", AZ::ConsoleFunctorFlags::Null };
ConsoleDataWrapper<AZ::CVarFixedString> localTestString{ {}, nullptr, "testString", "", AZ::ConsoleFunctorFlags::Null };
// The scoped cvars just above should have all been deferred for execution
// Each of them should have executed resulting in the expected return value
EXPECT_TRUE(testConsole.ExecuteDeferredConsoleCommands());
EXPECT_EQ(24, localTestInt8);
EXPECT_EQ(-32, localTestInt16);
EXPECT_EQ(41, localTestInt32);
EXPECT_EQ(-51, localTestInt64);
EXPECT_EQ(3, localTestUInt8);
EXPECT_EQ(5, localTestUInt16);
EXPECT_EQ(6, localTestUInt32);
EXPECT_EQ(0xFFFF'FFFF'FFFF'FFFF, localTestUInt64);
EXPECT_FLOAT_EQ(1.0f, localTestFloat);
EXPECT_DOUBLE_EQ(2, localTestDouble);
EXPECT_STREQ("Stable", static_cast<AZ::CVarFixedString>(localTestString).c_str());
// All of the deferred console commands should have executed at this point
// Therefore this invocation should return false
EXPECT_FALSE(testConsole.ExecuteDeferredConsoleCommands());
AZ::Interface<AZ::IConsole>::Unregister(&testConsole);
}
static constexpr AZStd::string_view UserINIStyleContent =
R"(