private async Task <ICommandResult> ExecuteAsyncInternal(ICommandContext context, CancellationToken cancellationToken = default) { if (!this._started) { throw new InvalidOperationException($"This {this.GetType().Name} is not started yet"); } using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this._cts.Token)) { IEnumerable <KeyValuePair <ICommandInstanceDescriptor, ICommandInstance> > commandsCopy; // copying might not be the fastest thing to do, but it'll ensure that commands won't be changed out of the lock // locking only copying to prevent hangs if user is not careful with their commands, while still preventing race conditions with StartAsync await this._lock.WaitAsync(cts.Token).ConfigureAwait(false); try { // order commands by priority // try to get from concrete Descriptor if possible, as it should be precached and avoid additional reflection and thus faster commandsCopy = this._commands.OrderByDescending(kvp => (kvp.Key is CommandInstanceDescriptor cid) ? cid.Priority : kvp.Key.GetPriority()); } finally { this._lock.Release(); } using (IServiceScope serviceScope = this._services.CreateScope()) { // Because scope won't have fallback services, and commands might need them, combine scope with fallback services // TODO: think of a nicer way to solve fallback services problem - I don't like it as it is now IServiceProvider services = serviceScope.ServiceProvider; if (!object.ReferenceEquals(services, this._fallbackServices)) { services = CombinedServiceProvider.Combine(serviceScope.ServiceProvider, this._fallbackServices); } foreach (KeyValuePair <ICommandInstanceDescriptor, ICommandInstance> commandKvp in commandsCopy) { ICommandInstanceDescriptor command = commandKvp.Key; ICommandInstance instance = commandKvp.Value; using (this._log.BeginCommandScope(context, command.GetHandlerType().Name, command.Method.Name)) { ICommandsHandlerProviderResult handlerResult = null; try { cts.Token.ThrowIfCancellationRequested(); // check if the command should run at all - if not, skip ICommandResult matchResult = await instance.CheckMatchAsync(context, services, cts.Token).ConfigureAwait(false); if (!matchResult.IsSuccess) { continue; } // initialize handler this._log?.LogTrace("Initializing handler handler {Handler} for command {Name}", command.GetHandlerType().Name, command.Method.Name); handlerResult = this._handlerProvider.GetCommandHandler(command, services); if (handlerResult?.HandlerInstance == null) { this._log?.LogError("Retrieving handler {Handler} for command {Name} has failed, command execution aborting", command.GetHandlerType().Name, command.Method.Name); return(CommandExecutionResult.FromException(new ArgumentNullException(nameof(ICommandsHandlerProviderResult.HandlerInstance), $"Retrieving handler {command.GetHandlerType().Name} for command {command.Method.Name} has failed, command execution aborting"))); } this._log?.LogTrace("Executing command {Name} from handler {Handler}", command.Method.Name, command.GetHandlerType().Name); // execute the command ICommandResult executeResult = await instance.ExecuteAsync(context, services, matchResult, handlerResult.HandlerInstance, cts.Token).ConfigureAwait(false); if (executeResult.Exception != null) { this._log?.LogError(executeResult.Exception, "Exception when executing command {Name} from handler {Handler}", command.Method.Name, command.GetHandlerType().Name); return(executeResult); } if (executeResult is IMessagesCommandResult messagesResult && messagesResult.Messages?.Any() == true) { this._log?.LogTrace("Sending command results messages as a command response"); await context.ReplyTextAsync(string.Join("\n", messagesResult.Messages), cts.Token).ConfigureAwait(false); } return(executeResult); } // special error case: operation canceled // operation canceled is normal, so it shouldn't be logged as error catch (OperationCanceledException ex) { this._log?.LogWarning("Execution of command {Name} from handler {Handler} was cancelled", command.Method.Name, command.GetHandlerType().Name); return(CommandExecutionResult.FromException(ex)); } // special error case: responding when silenced // bots almost always respond to a command - but if they're silenced, an exception will be thrown // this is normal - so it shouldn't be logged as error. Warning max catch (MessageSendingException ex) when (this.LogSilencedException(ex, context, "Unhandled Exception when executing command {Name} from handler {Handler} - likely due to being silenced or spam filtered", command.Method.Name, command.GetHandlerType().Name)) { return(CommandExecutionResult.FromException(ex)); } // normal error case catch (Exception ex) when(ex.LogAsError(_log, "Unhandled Exception when executing command {Name} from handler {Handler}", command.Method.Name, command.GetHandlerType().Name)) { return(CommandExecutionResult.FromException(ex)); } finally { // if handler is allocated, not persistent and disposable, let's dispose it if (handlerResult?.Descriptor?.Attribute?.IsPersistent != true && handlerResult?.HandlerInstance is IDisposable disposableHandler) { try { disposableHandler?.Dispose(); } catch { } } } } } } return(CommandExecutionResult.Failure); } }
/// <summary>Gets command's pre-execute checks.</summary> /// <param name="descriptor">Command descriptor.</param> /// <returns>Enumerable on all pre-execute checks on the command's method and handler type.</returns> public static IEnumerable <CommandRequirementAttribute> GetRequirements(this ICommandInstanceDescriptor descriptor) => descriptor.Method.GetCustomAttributes <CommandRequirementAttribute>(true) .Union(descriptor.GetHandlerType().GetCustomAttributes <CommandRequirementAttribute>(true));
/// <summary>Checks if the command is overriding default case sensitivity.</summary> /// <remarks>See <see cref="CaseSensitivityAttribute"/> for more information about command case senstivity.</remarks> /// <returns>True/false if command is overriding case sensitivity - true to be case sensitive, false to be case insensitive; null if not overriding.</returns> public static bool?GetCaseSensitivityOverride(this ICommandInstanceDescriptor descriptor) => descriptor.Method.GetCustomAttribute <CaseSensitivityAttribute>(true)?.CaseSensitive ?? descriptor.GetHandlerType().GetCustomAttribute <CaseSensitivityAttribute>(true)?.CaseSensitive;
/// <summary>Gets command's prefix requirement override.</summary> /// <param name="descriptor">Command descriptor.</param> /// <returns>Prefix requirement value.</returns> public static PrefixRequirement?GetPrefixRequirementOverride(this ICommandInstanceDescriptor descriptor) => descriptor.Method.GetCustomAttribute <PrefixAttribute>(true)?.PrefixRequirementOverride ?? descriptor.GetHandlerType().GetCustomAttribute <PrefixAttribute>(true)?.PrefixRequirementOverride;
/// <inheritdoc/> public ICommandsHandlerProviderResult GetCommandHandler(ICommandInstanceDescriptor descriptor, IServiceProvider services) { Type handlerType = descriptor.GetHandlerType(); CommandHandlerDescriptor handlerDescriptor = null; lock (_lock) { // check if persistent if (_persistentHandlers.TryGetValue(handlerType, out CommandsHandlerProviderResult handler)) { return(handler); } // keep resolved services to avoid recreating them in case of multiple constructors IDictionary <Type, object> resolvedServices = new Dictionary <Type, object>(); // if no persistent handler was found, we need to create a new one - check if constructor is known yet if (!_knownConstructors.TryGetValue(handlerType, out ConstructorInfo handlerConstructor)) { // if descriptor not cached, create new one // start with grabbing all constructors IEnumerable <ConstructorInfo> allConstructors = handlerType .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); // check if any of the constructors are specifically designated to be used by Commands System IEnumerable <ConstructorInfo> selectedConstructors = allConstructors .Select(ctor => (constructor: ctor, attribute: ctor.GetCustomAttribute <CommandsHandlerConstructorAttribute>(false))) .Where(ctor => ctor.attribute != null) .OrderByDescending(ctor => ctor.attribute.Priority) .ThenByDescending(ctor => ctor.constructor.GetParameters().Length) .Select(ctor => ctor.constructor); // if no explicitly-attributed constructor found, grab all that are public if (!selectedConstructors.Any()) { selectedConstructors = allConstructors .Where(ctor => ctor.IsPublic) .OrderByDescending(ctor => ctor.GetParameters().Length); } // try to resolve dependencies for each constructor. First one that can be resolved wins foreach (ConstructorInfo ctor in selectedConstructors) { if (TryCreateHandlerDescriptor(ctor, services, out handlerDescriptor, ref resolvedServices)) { // cache found descriptor _knownConstructors.Add(handlerType, ctor); handlerConstructor = ctor; break; } } // throw if we didn't find any constructor we can resolve if (handlerDescriptor == null) { throw new InvalidOperationException($"Cannot create descriptor for type {handlerType.FullName} - none of the constructors can have its dependencies resolved"); } } // if constructor is already known, just create a descriptor else { TryCreateHandlerDescriptor(handlerConstructor, services, out handlerDescriptor, ref resolvedServices); } // now that we have a descriptor, let's create an instance handler = new CommandsHandlerProviderResult(handlerDescriptor, handlerDescriptor.CreateInstance()); // if it's a persistent instance, store it if (handlerDescriptor.IsPersistent()) { _persistentHandlers.Add(handlerType, handler); } // finally, return the fresh handler return(handler); } }