/// <summary>Initializes a command service.</summary> /// <param name="client">WOLF client. Required.</param> /// <param name="options">Commands options that will be used as default when running a command. Required.</param> /// <param name="services">Services provider that will be used by all commands. Null will cause a backup provider to be used.</param> /// <param name="log">Logger to log messages and errors to. If null, all logging will be disabled.</param> /// <param name="cancellationToken">Cancellation token that can be used for cancelling all tasks.</param> public CommandsService(IWolfClient client, CommandsOptions options, ILogger log, IServiceProvider services = null, CancellationToken cancellationToken = default) { // init private this._commands = new Dictionary <ICommandInstanceDescriptor, ICommandInstance>(); this._lock = new SemaphoreSlim(1, 1); this._cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); this._disposableServices = new List <IDisposable>(2); this._started = false; // init required this._client = client ?? services?.GetService <IWolfClient>() ?? throw new ArgumentNullException(nameof(client)); this._options = options ?? services?.GetService <CommandsOptions>() ?? throw new ArgumentNullException(nameof(options)); // init optionals this._log = log ?? services?.GetService <ILogger <CommandsService> >() ?? services?.GetService <ILogger <ICommandsService> >() ?? services.GetService <ILogger>(); this._argumentConverterProvider = services?.GetService <IArgumentConverterProvider>() ?? CreateAsDisposable <ArgumentConverterProvider>(); this._handlerProvider = services?.GetService <ICommandsHandlerProvider>() ?? CreateAsDisposable <CommandsHandlerProvider>(); this._argumentsParser = services?.GetService <IArgumentsParser>() ?? new ArgumentsParser(); this._parameterBuilder = services?.GetService <IParameterBuilder>() ?? new ParameterBuilder(); this._initializers = services?.GetService <ICommandInitializerProvider>() ?? new CommandInitializerProvider(); this._commandsLoader = services?.GetService <ICommandsLoader>() ?? new CommandsLoader(this._initializers, this._log); // init service provider - use combine, to use fallback one as well this._fallbackServices = this.CreateFallbackServiceProvider(); this._services = CombinedServiceProvider.Combine(services, this._fallbackServices); // register event handlers this._client.AddMessageListener <ChatMessage>(OnMessageReceived); }
private IServiceProvider BuildServices() { IServiceProvider?services = null; while (_serviceProviders.Count > 0) { if (services == null) { services = _serviceProviders.Last(); } else { services = new CombinedServiceProvider( _serviceProviders.Last(), services); } _serviceProviders.RemoveAt(_serviceProviders.Count - 1); } if (_services.Count > 0) { var localServices = new DictionaryServiceProvider(_services); return(services is null ? (IServiceProvider)localServices : new CombinedServiceProvider(localServices, services)); } if (services is null) { throw new InvalidOperationException( "There was no service provider or service specified."); } return(services); }
/// <summary> /// Adds a custom transaction scope handler to the schema. /// </summary> /// <param name="builder"> /// The request executor builder. /// </param> /// <param name="create"> /// A factory to create the transaction scope. /// </param> /// <returns> /// The request executor builder. /// </returns> /// <exception cref="ArgumentNullException"></exception> public static IRequestExecutorBuilder AddTransactionScopeHandler( this IRequestExecutorBuilder builder, Func <IServiceProvider, ITransactionScopeHandler> create) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } return(ConfigureSchemaServices( builder, services => { services.RemoveAll(typeof(ITransactionScopeHandler)); services.AddSingleton(sp => { var combined = new CombinedServiceProvider( sp.GetApplicationServices(), sp); return create(combined); }); })); }
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); } }