#if UNITY_EDITOR || !UNITY_WEBGL #define THREADS_SUPPORTED #endif using QFSW.QC.Internal; using QFSW.QC.Utilities; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using UnityEngine; namespace QFSW.QC { public enum LoggingLevel { None = 0, Errors = 1, Warnings = 2, Full = 3 } /// The core processor of Quantum Console handling command table generation and invocation. public static partial class QuantumConsoleProcessor { /// The logging level to use during operation of the Quantum Console Processor. public static LoggingLevel loggingLevel = LoggingLevel.Full; private static readonly QuantumParser _parser = new QuantumParser(); private static readonly QuantumPreprocessor _preprocessor = new QuantumPreprocessor(); private static readonly QuantumScanRuleset _scanRuleset = new QuantumScanRuleset(); private static readonly ConcurrentDictionary _commandTable = new ConcurrentDictionary(); private static readonly List _commandCache = new List(); public static bool TableGenerated { get; private set; } public static bool TableIsGenerating { get; private set; } [Command("command-count", "Gets the number of loaded commands")] public static int LoadedCommandCount => _loadedCommandCount; private static int _loadedCommandCount = 0; private static bool _commandCacheDirty = true; private static readonly Assembly[] _loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); /// /// Gets all loaded commands. /// /// All loaded commands. public static IEnumerable GetAllCommands() { if (_commandCacheDirty) { lock (_commandCache) { _commandCache.Clear(); _commandCache.AddRange(_commandTable.Values); _commandCacheDirty = false; } } return _commandCache; } #region Table Generation /// /// Generates the command table so that commands can be invoked. /// /// If set to true a second thread will be deployed for the table generation. /// If set to true then the table will be cleared and generated again. public static void GenerateCommandTable(bool deployThread = false, bool forceReload = false) { #if THREADS_SUPPORTED if (deployThread) { ThreadPool.QueueUserWorkItem(state => { try { GenerateCommandTable(false, forceReload); } catch (Exception e) { Debug.LogException(e); } }); return; } #endif lock (_commandTable) { if (!TableGenerated || forceReload) { TableIsGenerating = true; { if (forceReload && TableGenerated) { _commandTable.Clear(); _loadedCommandCount = 0; } #if THREADS_SUPPORTED Parallel.ForEach(_loadedAssemblies, LoadCommandsFromAssembly); #else foreach (Assembly assembly in _loadedAssemblies) { LoadCommandsFromAssembly(assembly); } #endif } TableIsGenerating = false; TableGenerated = true; GC.Collect(3, GCCollectionMode.Forced, false, true); } } } private static IEnumerable<(MethodInfo method, MemberInfo member)> ExtractCommandMethods(Type type) { const BindingFlags flags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; MethodInfo[] methods = type.GetMethods(flags); PropertyInfo[] properties = type.GetProperties(flags); FieldInfo[] fields = type.GetFields(flags); foreach (MethodInfo method in methods) { yield return (method, method); } foreach (PropertyInfo property in properties) { if (property.CanWrite) { yield return (property.SetMethod, property); } if (property.CanRead) { yield return (property.GetMethod, property); } } foreach (FieldInfo field in fields) { if (field.HasAttribute()) { if (field.IsDelegate()) { if (field.IsStrongDelegate()) { FieldDelegateMethod executer = new FieldDelegateMethod(field); yield return (executer, field); } else if (loggingLevel >= LoggingLevel.Warnings) { Debug.LogWarning($"Quantum Processor Warning: Could not add '{field.Name}' from {field.DeclaringType} to the table as it is an invalid delegate type."); } } else { FieldAutoMethod reader = new FieldAutoMethod(field, FieldAutoMethod.AccessType.Read); yield return (reader, field); if (!(field.IsLiteral || field.IsInitOnly)) { FieldAutoMethod writer = new FieldAutoMethod(field, FieldAutoMethod.AccessType.Write); yield return (writer, field); } } } } } private static bool GetCommandSupported(CommandData command, out string unsupportedReason) { for (int i = 0; i < command.ParamCount; i++) { ParameterInfo param = command.MethodParamData[i]; Type paramType = param.ParameterType; if (!_parser.CanParse(paramType) && !paramType.IsGenericParameter) { unsupportedReason = $"Parameter type {paramType} is not supported by the Quantum Parser."; return false; } } if (command.MonoTarget != MonoTargetType.Registry && !command.MethodData.IsStatic && !command.MethodData.DeclaringType.IsDerivedTypeOf(typeof(MonoBehaviour))) { unsupportedReason = $"Non static non MonoBehaviour commands are incompatible with MonoTargetType.{command.MonoTarget}."; return false; } unsupportedReason = string.Empty; return true; } private static void LoadCommandsFromAssembly(Assembly assembly) { if (!_scanRuleset.ShouldScan(assembly)) { return; } Type[] loadedTypes = assembly.GetTypes(); foreach (Type type in loadedTypes) { try { LoadCommandsFromType(type); } catch (TypeLoadException) { // Issue under investigation /* if (loggingLevel >= LoggingLevel.Warnings) { Debug.LogWarning($"Unable to extract command data from type {type} in assembly {assembly.GetName().Name} as it may be corrupted. The following exception was thrown: {e.Message}"); } */ } catch (BadImageFormatException) { // Confirmed to be an issue on Unity/Mono's side // Extremely unlikely that it will ever occur in user code, so for this reason it is ignored silently // QC Issue: https://bitbucket.org/QFSW/quantum-console/issues/67/add-protection-against-corrupt-dlls // Unity Issue: https://issuetracker.unity3d.com/issues/badimageformatexception-is-thrown-when-calling-getcustomattributes-on-certain-memberinfo-instances // Mono Issue: https://github.com/mono/mono/issues/17278 /* if (loggingLevel >= LoggingLevel.Warnings) { Debug.LogWarning($"Unable to extract command data from type {type} in assembly {assembly.GetName().Name} as it may be corrupted. The following exception was thrown: {e.Message}"); } */ } } } private static void LoadCommandsFromType(Type type) { if (!_scanRuleset.ShouldScan(type)) { return; } foreach ((MethodInfo method, MemberInfo member) in ExtractCommandMethods(type)) { if (member.DeclaringType == type) { LoadCommandsFromMember(member, method); } } } private static void LoadCommandsFromMember(MemberInfo member, MethodInfo method) { if (!_scanRuleset.ShouldScan(member)) { return; } IEnumerable commandAttributes = member.GetCustomAttributes(); CommandDescriptionAttribute descriptionAttribute = member.GetCustomAttribute(); foreach (CommandAttribute commandAttribute in commandAttributes) { if (!commandAttribute.Valid) { if (loggingLevel >= LoggingLevel.Warnings) { Debug.LogWarning($"Quantum Processor Warning: Could not add '{commandAttribute.Alias}' to the table as it is invalid."); } } else { CommandPlatformAttribute platformAttribute = member.GetCustomAttribute(); Platform commandPlatforms = platformAttribute?.SupportedPlatforms ?? commandAttribute.SupportedPlatforms; if (commandPlatforms.HasFlag(Application.platform.ToPlatform())) { IEnumerable newCommands = CreateCommandOverloads(method, commandAttribute, descriptionAttribute); foreach (CommandData command in newCommands) { TryAddCommand(command); } } } } } private static IEnumerable CreateCommandOverloads(MethodInfo method, CommandAttribute commandAttribute, CommandDescriptionAttribute descriptionAttribute) { int defaultParameters = method.GetParameters().Count(x => x.HasDefaultValue); for (int i = 0; i < defaultParameters + 1; i++) { CommandData command = new CommandData(method, commandAttribute, descriptionAttribute, i); yield return command; } } private static string GenerateCommandKey(CommandData command) { return $"{command.CommandName}({command.ParamCount})"; } /// /// Registers a new command. /// /// The command to register. /// If the addition was successful. public static bool TryAddCommand(CommandData command) { if (!GetCommandSupported(command, out string reason)) { if (loggingLevel >= LoggingLevel.Warnings) { Debug.LogWarning($"Quantum Processor Warning: Could not add '{command.CommandSignature}' from {command.MethodData.DeclaringType.GetDisplayName()} " + $"to the table as it is not supported. {reason}"); } return false; } string key = GenerateCommandKey(command); bool alreadyExists = !_commandTable.TryAdd(key, command); if (alreadyExists) { if (loggingLevel >= LoggingLevel.Warnings) { string fullMethodName = $"{command.MethodData.DeclaringType.FullName}.{command.MethodData.Name}"; Debug.LogWarning($"Quantum Processor Warning: Could not add {fullMethodName} to the table as another method with the same alias and parameter count, {key}, already exists."); } return false; } _commandCacheDirty = true; Interlocked.Increment(ref _loadedCommandCount); return true; } /// /// Removes an existing command. /// /// The command to remove. /// If the removal was successful. public static bool TryRemoveCommand(CommandData command) { string key = GenerateCommandKey(command); if (_commandTable.TryRemove(key, out _)) { _commandCacheDirty = true; Interlocked.Decrement(ref _loadedCommandCount); return true; } return false; } #endregion #region Command Invocation /// Invokes a command on the QuantumConsoleProcessor. /// Return value of the invocation. /// The command to invoke. public static object InvokeCommand(string commandString) { GenerateCommandTable(); commandString = commandString.Trim(); commandString = _preprocessor.Process(commandString); if (string.IsNullOrWhiteSpace(commandString)) { throw new ArgumentException("Cannot parse an empty string."); } string[] commandParts = commandString.SplitScoped(' '); commandParts = commandParts.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); string commandName = commandParts[0]; string[] commandParams = commandParts.SubArray(1, commandParts.Length - 1); int paramCount = commandParams.Length; string[] commandNameParts = commandName.Split(new[] { '<' }, 2); string genericSignature = commandNameParts.Length > 1 ? $"<{commandNameParts[1]}" : ""; commandName = commandNameParts[0]; string keyName = $"{commandName}({paramCount})"; if (!_commandTable.ContainsKey(keyName)) { bool overloadExists = _commandTable.Keys.Any(key => key.Contains($"{commandName}(") && _commandTable[key].CommandName == commandName); if (overloadExists) { throw new ArgumentException($"No overload of '{commandName}' with {paramCount} parameters could be found."); } else { throw new ArgumentException($"Command '{commandName}' could not be found."); } } CommandData command = _commandTable[keyName]; Type[] genericTypes = Array.Empty(); if (command.IsGeneric) { int expectedArgCount = command.GenericParamTypes.Length; string[] genericArgNames = genericSignature.ReduceScope('<', '>').SplitScoped(','); if (genericArgNames.Length == expectedArgCount) { genericTypes = new Type[genericArgNames.Length]; for (int i = 0; i < genericTypes.Length; i++) { genericTypes[i] = QuantumParser.ParseType(genericArgNames[i]); } } else { throw new ArgumentException($"Generic command '{commandName}' requires {expectedArgCount} generic parameter{(expectedArgCount == 1 ? "" : "s")} but was supplied with {genericArgNames.Length}."); } } else if (genericSignature != string.Empty) { throw new ArgumentException($"Command '{commandName}' is not a generic command and cannot be invoked as such."); } #if !UNITY_EDITOR && ENABLE_IL2CPP && !UNITY_2022_2_OR_NEWER if (genericTypes.Any((Type x) => x.IsValueType)) { throw new NotSupportedException("Value types in generic commands are not supported in IL2CPP before Unity 2022.2"); } #endif object[] parsedCommandParams = ParseParamData(command.MakeGenericArguments(genericTypes), commandParams); return command.Invoke(parsedCommandParams, genericTypes); } private static object[] ParseParamData(Type[] paramTypes, string[] paramData) { object[] parsedData = new object[paramData.Length]; for (int i = 0; i < parsedData.Length; i++) { parsedData[i] = _parser.Parse(paramData[i], paramTypes[i]); } return parsedData; } #endregion } }