private void TraceExecution(ICommandInstance instance) { Trace.WriteLine($"Executing class command: {Name}"); Trace.Indent(); foreach (var parameter in Parameters) { Trace.WriteLine($"{parameter.Name}: {parameter.Property.GetValue(instance) ?? "<null>"}"); } Trace.Unindent(); }
/// <inheritdoc/> /// <remarks>Once already started, calling this method again will cause commands to be re-loaded.</remarks> public async Task StartAsync(CancellationToken cancellationToken = default) { using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this._cts.Token)) { await this._lock.WaitAsync(cts.Token).ConfigureAwait(false); try { this._log?.LogDebug("Initializing commands"); // dispose commands since we're reloading them this.DisposeCommands(); // ask loader to load from all specified assemblies and types IEnumerable <ICommandInstanceDescriptor> descriptors = await _commandsLoader.LoadFromAssembliesAsync(_options.Assemblies ?? Enumerable.Empty <Assembly>(), cts.Token).ConfigureAwait(false); descriptors = descriptors.Union(await _commandsLoader.LoadFromTypesAsync(_options.Classes.Select(t => t.GetTypeInfo()) ?? Enumerable.Empty <TypeInfo>(), cts.Token).ConfigureAwait(false)); // make sure there's no duplicates descriptors = descriptors.Distinct(); // for each loaded command, handle pre-initialization and caching foreach (ICommandInstanceDescriptor descriptor in descriptors) { CommandsHandlerAttribute handlerAttribute = descriptor.GetHandlerAttribute(); // check if handler is persistent. If so, request it from provider to pre-initialize if (handlerAttribute?.IsPersistent == true) { this._log?.LogDebug("Pre-initializing command handler {Handler}", descriptor.GetHandlerType().Name); this._handlerProvider.GetCommandHandler(descriptor, this._services); } // create all command instances this._log?.LogDebug("Creating command instance {Name} from handler {Handler}", descriptor.Method.Name, descriptor.GetHandlerType().Name); ICommandInitializer initializer = this._initializers.GetInitializer(descriptor.Attribute.GetType()); ICommandInstance instance = initializer.InitializeCommand(descriptor, _options); this._commands.Add(descriptor, instance); } // mark as started this._started = true; this._log?.LogDebug("{Count} commands loaded", _commands.Count); } finally { this._lock.Release(); } } }
public Task CmdAliasesAsync(CommandContext context, ICommandInstance instance) { if (instance is StandardCommandInstance standardInstance) { return(context.ReplyTextAsync($"Executed alias `{standardInstance.Text}`")); } else if (instance is RegexCommandInstance regexInstance) { return(context.ReplyTextAsync($"Executed alias `{regexInstance.Pattern}`")); } else { return(context.ReplyTextAsync($"Executed some alias ({instance.GetType().Name})")); } }
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); } }