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); } }