diff --git a/src/NadekoBot.Coordinator/CoordStartup.cs b/src/NadekoBot.Coordinator/CoordStartup.cs new file mode 100644 index 000000000..250453e1c --- /dev/null +++ b/src/NadekoBot.Coordinator/CoordStartup.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace NadekoBot.Coordinator +{ + public class CoordStartup + { + public IConfiguration Configuration { get; } + + public CoordStartup(IConfiguration config) + { + Configuration = config; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddGrpc(); + services.AddSingleton(); + services.AddSingleton( + serviceProvider => serviceProvider.GetService()); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + + endpoints.MapGet("/", + async context => + { + await context.Response.WriteAsync( + "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); + }); + }); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot.Coordinator/LogSetup.cs b/src/NadekoBot.Coordinator/LogSetup.cs new file mode 100644 index 000000000..aa6f91cb5 --- /dev/null +++ b/src/NadekoBot.Coordinator/LogSetup.cs @@ -0,0 +1,37 @@ +using System; +using System.Text; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; + +namespace NadekoBot.Core.Services +{ + public static class LogSetup + { + public static void SetupLogger(object source) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Override("System", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console(LogEventLevel.Information, + theme: GetTheme(), + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}") + .Enrich.WithProperty("LogSource", source) + .CreateLogger(); + + System.Console.OutputEncoding = Encoding.UTF8; + } + + private static ConsoleTheme GetTheme() + { + if(Environment.OSVersion.Platform == PlatformID.Unix) + return AnsiConsoleTheme.Code; +#if DEBUG + return AnsiConsoleTheme.Code; +#endif + return ConsoleTheme.None; + } + } +} diff --git a/src/NadekoBot.Coordinator/NadekoBot.Coordinator.csproj b/src/NadekoBot.Coordinator/NadekoBot.Coordinator.csproj index feb8aacb7..4f3a48d2e 100644 --- a/src/NadekoBot.Coordinator/NadekoBot.Coordinator.csproj +++ b/src/NadekoBot.Coordinator/NadekoBot.Coordinator.csproj @@ -1,8 +1,18 @@ - + net5.0 - 9.0 - Major - + + + + + + + + + + + + + diff --git a/src/NadekoBot.Coordinator/Program.cs b/src/NadekoBot.Coordinator/Program.cs new file mode 100644 index 000000000..f79075a4b --- /dev/null +++ b/src/NadekoBot.Coordinator/Program.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using NadekoBot.Coordinator; +using NadekoBot.Core.Services; +using Serilog; + +// Additional configuration is required to successfully run gRPC on macOS. +// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + +LogSetup.SetupLogger("coord"); +Log.Information("Starting coordinator... Pid: {ProcessId}", Environment.ProcessId); + +CreateHostBuilder(args).Build().Run(); \ No newline at end of file diff --git a/src/NadekoBot.Coordinator/Properties/launchSettings.json b/src/NadekoBot.Coordinator/Properties/launchSettings.json new file mode 100644 index 000000000..66fd09226 --- /dev/null +++ b/src/NadekoBot.Coordinator/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Nadeko.Coordinator": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": false, + "applicationUrl": "http://localhost:3442;https://localhost:3443", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/NadekoBot.Coordinator/Protos/coordinator.proto b/src/NadekoBot.Coordinator/Protos/coordinator.proto new file mode 100644 index 000000000..39c01e405 --- /dev/null +++ b/src/NadekoBot.Coordinator/Protos/coordinator.proto @@ -0,0 +1,127 @@ +syntax = "proto3"; +import "google/protobuf/timestamp.proto"; + +option csharp_namespace = "NadekoBot.Coordinator"; + +package nadekobot; + +service Coordinator { + // sends update to coordinator to let it know that the shard is alive + rpc Heartbeat(HeartbeatRequest) returns (HeartbeatReply); + // restarts a shard given the id + rpc RestartShard(RestartShardRequest) returns (RestartShardReply); + // reshards given the new number of shards + rpc Reshard(ReshardRequest) returns (ReshardReply); + // Reload config + rpc Reload(ReloadRequest) returns (ReloadReply); + // Gets status of a single shard + rpc GetStatus(GetStatusRequest) returns (GetStatusReply); + // Get status of all shards + rpc GetAllStatuses(GetAllStatusesRequest) returns (GetAllStatusesReply); + // Restarts all shards. Queues them to be restarted at a normal rate. Setting Nuke to true will kill all shards right + // away + rpc RestartAllShards(RestartAllRequest) returns (RestartAllReply); + + // kill coordinator (and all shards as a consequence) + rpc Die(DieRequest) returns (DieReply); + + rpc SetConfigText(SetConfigTextRequest) returns (SetConfigTextReply); + + rpc GetConfigText(GetConfigTextRequest) returns (GetConfigTextReply); +} + +enum ConnState { + Disconnected = 0; + Connecting = 1; + Connected = 2; +} + +message HeartbeatRequest { + int32 shardId = 1; + int32 guildCount = 2; + ConnState state = 3; +} + +message HeartbeatReply { + bool gracefulImminent = 1; +} + +message RestartShardRequest { + int32 shardId = 1; + // should it be queued for restart, set false to kill it and restart immediately with priority + bool queue = 2; +} + +message RestartShardReply { + +} + +message ReshardRequest { + int32 shards = 1; +} + +message ReshardReply { + +} + +message ReloadRequest { + +} + +message ReloadReply { + +} + +message GetStatusRequest { + int32 shardId = 1; +} + +message GetStatusReply { + int32 shardId = 1; + ConnState state = 2; + int32 guildCount = 3; + google.protobuf.Timestamp lastUpdate = 4; + bool scheduledForRestart = 5; + google.protobuf.Timestamp startedAt = 6; +} + +message GetAllStatusesRequest { + +} + +message GetAllStatusesReply { + repeated GetStatusReply Statuses = 1; +} + +message RestartAllRequest { + bool nuke = 1; +} + +message RestartAllReply { + +} + +message DieRequest { + bool graceful = 1; +} + +message DieReply { + +} + +message GetConfigTextRequest { + +} + +message GetConfigTextReply { + string configYml = 1; +} + +message SetConfigTextRequest { + string configYml = 1; +} + +message SetConfigTextReply { + bool success = 1; + string error = 2; +} \ No newline at end of file diff --git a/src/NadekoBot.Coordinator/Services/CoordinatorRunner.cs b/src/NadekoBot.Coordinator/Services/CoordinatorRunner.cs new file mode 100644 index 000000000..4f4f2bedd --- /dev/null +++ b/src/NadekoBot.Coordinator/Services/CoordinatorRunner.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Serilog; +using YamlDotNet.Serialization; + +namespace NadekoBot.Coordinator +{ + // todo future: test graceful and update bot to not wait for coord exit + public sealed class CoordinatorRunner : BackgroundService + { + private const string CONFIG_PATH = "coord.yml"; + + private const string GRACEFUL_STATE_PATH = "graceful.json"; + private const string GRACEFUL_STATE_BACKUP_PATH = "graceful_old.json"; + + private readonly Serializer _serializer; + private readonly Deserializer _deserializer; + + private Config _config; + private ShardStatus[] _shardStatuses; + + private readonly object locker = new object(); + private readonly Random _rng; + private bool _gracefulImminent; + + public CoordinatorRunner(IConfiguration configuration) + { + _serializer = new(); + _deserializer = new(); + _config = LoadConfig(); + _rng = new Random(); + + if(!TryRestoreOldState()) + InitAll(); + } + + private Config LoadConfig() + { + lock (locker) + { + return _deserializer.Deserialize(File.ReadAllText(CONFIG_PATH)); + } + } + + private void SaveConfig(in Config config) + { + lock (locker) + { + var output = _serializer.Serialize(config); + File.WriteAllText(CONFIG_PATH, output); + } + } + + public void ReloadConfig() + { + lock (locker) + { + var oldConfig = _config; + var newConfig = LoadConfig(); + if (oldConfig.TotalShards != newConfig.TotalShards) + { + KillAll(); + } + _config = newConfig; + if (oldConfig.TotalShards != newConfig.TotalShards) + { + InitAll(); + } + } + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Information("Executing"); + + bool first = true; + while (!stoppingToken.IsCancellationRequested) + { + try + { + bool hadAction = false; + lock (locker) + { + var shardIds = Enumerable.Range(0, 1) // shard 0 is always first + .Append((int)((117523346618318850 >> 22) % _config.TotalShards)) // then nadeko server shard + .Concat(Enumerable.Range(1, _config.TotalShards - 1) + .OrderBy(x => _rng.Next())) // then all other shards in a random order + .Distinct() + .ToList(); + + if (first) + { + Log.Information("Startup order: {StartupOrder}",string.Join(' ', shardIds)); + first = false; + } + + foreach (var shardId in shardIds) + { + if (stoppingToken.IsCancellationRequested) + break; + + var status = _shardStatuses[shardId]; + + if (status.ShouldRestart) + { + Log.Warning("Shard {ShardId} is restarting (scheduled)...", shardId); + hadAction = true; + StartShard(shardId); + break; + } + + if (status.Process is null or {HasExited: true}) + { + Log.Warning("Shard {ShardId} is starting (process)...", shardId); + hadAction = true; + StartShard(shardId); + break; + } + + if (DateTime.UtcNow - status.LastUpdate > + TimeSpan.FromSeconds(_config.UnresponsiveSec)) + { + Log.Warning("Shard {ShardId} is restarting (unresponsive)...", shardId); + hadAction = true; + StartShard(shardId); + break; + } + + if (status.StateCounter > 8 && status.State != ConnState.Connected) + { + Log.Warning("Shard {ShardId} is restarting (stuck)...", shardId); + hadAction = true; + StartShard(shardId); + break; + } + } + } + + if (hadAction) + { + await Task.Delay(_config.RecheckIntervalMs, stoppingToken).ConfigureAwait(false); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error in coordinator: {Message}", ex.Message); + } + + await Task.Delay(5000, stoppingToken).ConfigureAwait(false); + } + } + + private void StartShard(int shardId) + { + var status = _shardStatuses[shardId]; + if (status.Process is {HasExited: false} p) + { + try + { + p.Kill(true); + } + catch + { + } + } + + status.Process?.Dispose(); + + var proc = StartShardProcess(shardId); + _shardStatuses[shardId] = status with + { + Process = proc, + LastUpdate = DateTime.UtcNow, + State = ConnState.Disconnected, + ShouldRestart = false, + StateCounter = 0, + }; + } + + private Process StartShardProcess(int shardId) + { + return Process.Start(new ProcessStartInfo() + { + FileName = _config.ShardStartCommand, + Arguments = string.Format(_config.ShardStartArgs, + shardId, + Environment.ProcessId), + // CreateNoWindow = true, + // UseShellExecute = false, + }); + } + + public bool Heartbeat(int shardId, int guildCount, ConnState state) + { + lock (locker) + { + if (shardId >= _shardStatuses.Length) + throw new ArgumentOutOfRangeException(nameof(shardId)); + + var status = _shardStatuses[shardId]; + status = _shardStatuses[shardId] = status with + { + GuildCount = guildCount, + State = state, + LastUpdate = DateTime.UtcNow, + StateCounter = status.State == state + ? status.StateCounter + 1 + : 1 + }; + if (status.StateCounter > 1 && status.State == ConnState.Disconnected) + { + Log.Warning("Shard {ShardId} is in DISCONNECTED state! ({StateCounter})", + status.ShardId, + status.StateCounter); + } + + return _gracefulImminent; + } + } + + public void SetShardCount(int totalShards) + { + lock (locker) + { + ref var toSave = ref _config; + SaveConfig(new Config( + totalShards, + _config.RecheckIntervalMs, + _config.ShardStartCommand, + _config.ShardStartArgs, + _config.UnresponsiveSec)); + } + } + + public void RestartShard(int shardId, bool queue) + { + lock (locker) + { + if (shardId >= _shardStatuses.Length) + throw new ArgumentOutOfRangeException(nameof(shardId)); + + _shardStatuses[shardId] = _shardStatuses[shardId] with + { + ShouldRestart = true, + StateCounter = 0, + }; + } + } + + public void RestartAll(bool nuke) + { + lock (locker) + { + if (nuke) + { + KillAll(); + } + + QueueAll(); + } + } + + private void KillAll() + { + lock (locker) + { + for (var shardId = 0; shardId < _shardStatuses.Length; shardId++) + { + var status = _shardStatuses[shardId]; + if (status.Process is Process p) + { + p.Kill(); + p.Dispose(); + _shardStatuses[shardId] = status with + { + Process = null, + ShouldRestart = true, + LastUpdate = DateTime.UtcNow, + State = ConnState.Disconnected, + StateCounter = 0, + }; + } + } + } + } + + public void SaveState() + { + var coordState = new CoordState() + { + StatusObjects = _shardStatuses + .Select(x => new JsonStatusObject() + { + Pid = x.Process?.Id, + ConnectionState = x.State, + GuildCount = x.GuildCount, + }) + .ToList() + }; + var jsonState = JsonSerializer.Serialize(coordState, new () + { + WriteIndented = true, + }); + File.WriteAllText(GRACEFUL_STATE_PATH, jsonState); + } + private bool TryRestoreOldState() + { + lock (locker) + { + if (!File.Exists(GRACEFUL_STATE_PATH)) + return false; + + Log.Information("Restoring old coordinator state..."); + + CoordState savedState; + try + { + savedState = JsonSerializer.Deserialize(File.ReadAllText(GRACEFUL_STATE_PATH)); + + if (savedState is null) + throw new Exception("Old state is null?!"); + } + catch (Exception ex) + { + Log.Error(ex, "Error deserializing old state: {Message}", ex.Message); + File.Move(GRACEFUL_STATE_PATH, GRACEFUL_STATE_BACKUP_PATH, overwrite: true); + return false; + } + + if (savedState.StatusObjects.Count != _config.TotalShards) + { + Log.Error("Unable to restore old state because shard count doesn't match."); + File.Move(GRACEFUL_STATE_PATH, GRACEFUL_STATE_BACKUP_PATH, overwrite: true); + return false; + } + + _shardStatuses = new ShardStatus[_config.TotalShards]; + + for (int shardId = 0; shardId < _shardStatuses.Length; shardId++) + { + var statusObj = savedState.StatusObjects[shardId]; + Process p = null; + if (statusObj.Pid is int pid) + { + try + { + p = Process.GetProcessById(pid); + } + catch (Exception ex) + { + Log.Warning(ex, $"Process for shard {shardId} is not runnning."); + } + } + + _shardStatuses[shardId] = new( + shardId, + DateTime.UtcNow, + statusObj.GuildCount, + statusObj.ConnectionState, + p is null, + p); + } + + File.Move(GRACEFUL_STATE_PATH, GRACEFUL_STATE_BACKUP_PATH, overwrite: true); + Log.Information("Old state restored!"); + return true; + } + } + + private void InitAll() + { + lock (locker) + { + _shardStatuses = new ShardStatus[_config.TotalShards]; + for (var shardId = 0; shardId < _shardStatuses.Length; shardId++) + { + _shardStatuses[shardId] = new ShardStatus(shardId, DateTime.UtcNow); + } + } + } + + private void QueueAll() + { + lock (locker) + { + for (var shardId = 0; shardId < _shardStatuses.Length; shardId++) + { + _shardStatuses[shardId] = _shardStatuses[shardId] with + { + ShouldRestart = true + }; + } + } + } + + + public ShardStatus GetShardStatus(int shardId) + { + lock (locker) + { + if (shardId >= _shardStatuses.Length) + throw new ArgumentOutOfRangeException(nameof(shardId)); + + return _shardStatuses[shardId]; + } + } + + public List GetAllStatuses() + { + lock (locker) + { + var toReturn = new List(_shardStatuses.Length); + toReturn.AddRange(_shardStatuses); + return toReturn; + } + } + + public void PrepareGracefulShutdown() + { + lock (locker) + { + _gracefulImminent = true; + } + } + + public string GetConfigText() + { + return File.ReadAllText(CONFIG_PATH); + } + + public void SetConfigText(string text) + { + if (string.IsNullOrWhiteSpace(text)) + throw new ArgumentNullException(nameof(text), "coord.yml can't be empty"); + var config = _deserializer.Deserialize(text); + SaveConfig(in config); + ReloadConfig(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot.Coordinator/Services/CoordinatorService.cs b/src/NadekoBot.Coordinator/Services/CoordinatorService.cs new file mode 100644 index 000000000..3ade834f9 --- /dev/null +++ b/src/NadekoBot.Coordinator/Services/CoordinatorService.cs @@ -0,0 +1,147 @@ +using System; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; + +namespace NadekoBot.Coordinator +{ + public sealed class CoordinatorService : NadekoBot.Coordinator.Coordinator.CoordinatorBase + { + private readonly CoordinatorRunner _runner; + + public CoordinatorService(CoordinatorRunner runner) + { + _runner = runner; + } + + public override Task Heartbeat(HeartbeatRequest request, ServerCallContext context) + { + var gracefulImminent = _runner.Heartbeat(request.ShardId, request.GuildCount, request.State); + return Task.FromResult(new HeartbeatReply() + { + GracefulImminent = gracefulImminent + }); + } + + public override Task Reshard(ReshardRequest request, ServerCallContext context) + { + _runner.SetShardCount(request.Shards); + return Task.FromResult(new ReshardReply()); + } + + public override Task RestartShard(RestartShardRequest request, ServerCallContext context) + { + _runner.RestartShard(request.ShardId, request.Queue); + return Task.FromResult(new RestartShardReply()); + } + + public override Task Reload(ReloadRequest request, ServerCallContext context) + { + _runner.ReloadConfig(); + return Task.FromResult(new ReloadReply()); + } + + public override Task GetStatus(GetStatusRequest request, ServerCallContext context) + { + var status = _runner.GetShardStatus(request.ShardId); + + + return Task.FromResult(StatusToStatusReply(status)); + } + + public override Task GetAllStatuses(GetAllStatusesRequest request, + ServerCallContext context) + { + var statuses = _runner + .GetAllStatuses(); + + var reply = new GetAllStatusesReply(); + foreach (var status in statuses) + reply.Statuses.Add(StatusToStatusReply(status)); + + return Task.FromResult(reply); + } + + private static GetStatusReply StatusToStatusReply(ShardStatus status) + { + DateTime startTime; + try + { + startTime = status.Process is null or {HasExited: true} + ? DateTime.MinValue.ToUniversalTime() + : status.Process.StartTime.ToUniversalTime(); + } + catch + { + startTime = DateTime.MinValue.ToUniversalTime(); + } + + var reply = new GetStatusReply() + { + State = status.State, + GuildCount = status.GuildCount, + ShardId = status.ShardId, + LastUpdate = Timestamp.FromDateTime(status.LastUpdate), + ScheduledForRestart = status.ShouldRestart, + StartedAt = Timestamp.FromDateTime(startTime) + }; + + return reply; + } + + public override Task RestartAllShards(RestartAllRequest request, ServerCallContext context) + { + _runner.RestartAll(request.Nuke); + return Task.FromResult(new RestartAllReply()); + } + + public override async Task Die(DieRequest request, ServerCallContext context) + { + if (request.Graceful) + { + _runner.PrepareGracefulShutdown(); + await Task.Delay(10_000); + } + + _runner.SaveState(); + _ = Task.Run(async () => + { + await Task.Delay(250); + Environment.Exit(0); + }); + + return new DieReply(); + } + + public override async Task SetConfigText(SetConfigTextRequest request, ServerCallContext context) + { + await Task.Yield(); + string error = string.Empty; + bool success = true; + try + { + _runner.SetConfigText(request.ConfigYml); + } + catch (Exception ex) + { + error = ex.Message; + success = false; + } + + return new(new() + { + Success = success, + Error = error + }); + } + + public override Task GetConfigText(GetConfigTextRequest request, ServerCallContext context) + { + var text = _runner.GetConfigText(); + return Task.FromResult(new GetConfigTextReply() + { + ConfigYml = text, + }); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot.Coordinator/Shared/Config.cs b/src/NadekoBot.Coordinator/Shared/Config.cs new file mode 100644 index 000000000..9a700bc36 --- /dev/null +++ b/src/NadekoBot.Coordinator/Shared/Config.cs @@ -0,0 +1,21 @@ +namespace NadekoBot.Coordinator +{ + public readonly struct Config + { + public int TotalShards { get; init; } + public int RecheckIntervalMs { get; init; } + public string ShardStartCommand { get; init; } + public string ShardStartArgs { get; init; } + public double UnresponsiveSec { get; init; } + + public Config(int totalShards, int recheckIntervalMs, string shardStartCommand, string shardStartArgs, double unresponsiveSec) + { + TotalShards = totalShards; + RecheckIntervalMs = recheckIntervalMs; + ShardStartCommand = shardStartCommand; + ShardStartArgs = shardStartArgs; + UnresponsiveSec = unresponsiveSec; + } + + } +} \ No newline at end of file diff --git a/src/NadekoBot.Coordinator/Shared/CoordState.cs b/src/NadekoBot.Coordinator/Shared/CoordState.cs new file mode 100644 index 000000000..f2336dc12 --- /dev/null +++ b/src/NadekoBot.Coordinator/Shared/CoordState.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace NadekoBot.Coordinator +{ + public class CoordState + { + public List StatusObjects { get; init; } + } +} \ No newline at end of file diff --git a/src/NadekoBot.Coordinator/Shared/JsonStatusObject.cs b/src/NadekoBot.Coordinator/Shared/JsonStatusObject.cs new file mode 100644 index 000000000..6a98c3a85 --- /dev/null +++ b/src/NadekoBot.Coordinator/Shared/JsonStatusObject.cs @@ -0,0 +1,11 @@ +using System; + +namespace NadekoBot.Coordinator +{ + public class JsonStatusObject + { + public int? Pid { get; init; } + public int GuildCount { get; init; } + public ConnState ConnectionState { get; init; } + } +} \ No newline at end of file diff --git a/src/NadekoBot.Coordinator/Shared/ShardStatus.cs b/src/NadekoBot.Coordinator/Shared/ShardStatus.cs new file mode 100644 index 000000000..aedc5c201 --- /dev/null +++ b/src/NadekoBot.Coordinator/Shared/ShardStatus.cs @@ -0,0 +1,15 @@ +using System; +using System.Diagnostics; + +namespace NadekoBot.Coordinator +{ + public sealed record ShardStatus( + int ShardId, + DateTime LastUpdate, + int GuildCount = 0, + ConnState State = ConnState.Disconnected, + bool ShouldRestart = false, + Process Process = null, + int StateCounter = 0 + ); +} \ No newline at end of file diff --git a/src/NadekoBot.Coordinator/appsettings.Development.json b/src/NadekoBot.Coordinator/appsettings.Development.json new file mode 100644 index 000000000..bfd9af70b --- /dev/null +++ b/src/NadekoBot.Coordinator/appsettings.Development.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" + }, + "Endpoints": { + "Http": { + "Url": "https://localhost:3443" + } + } + } +} diff --git a/src/NadekoBot.Coordinator/appsettings.json b/src/NadekoBot.Coordinator/appsettings.json new file mode 100644 index 000000000..c80c727da --- /dev/null +++ b/src/NadekoBot.Coordinator/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" + }, + "Endpoints": { + "Http": { + "Url": "http://localhost:3443" + } + } + } +} diff --git a/src/NadekoBot.Coordinator/coord.yml b/src/NadekoBot.Coordinator/coord.yml new file mode 100644 index 000000000..d630e781f --- /dev/null +++ b/src/NadekoBot.Coordinator/coord.yml @@ -0,0 +1,5 @@ +TotalShards: 1 +RecheckIntervalMs: 5000 +ShardStartCommand: dotnet +ShardStartArgs: run -p "" --no-build -- {0} +UnresponsiveSec: 30 diff --git a/src/NadekoBot.Tests/BotStringsTests.cs b/src/NadekoBot.Tests/BotStringsTests.cs index 68ee64bdb..7b199bf1e 100644 --- a/src/NadekoBot.Tests/BotStringsTests.cs +++ b/src/NadekoBot.Tests/BotStringsTests.cs @@ -43,7 +43,7 @@ namespace Nadeko.Tests } private static string[] GetCommandMethodNames() - => typeof(NadekoBot.NadekoBot).Assembly + => typeof(NadekoBot.Bot).Assembly .GetExportedTypes() .Where(type => type.IsClass && !type.IsAbstract) .Where(type => typeof(NadekoModule).IsAssignableFrom(type) // if its a top level module diff --git a/src/NadekoBot/Services/NadekoBot.cs b/src/NadekoBot/Bot.cs similarity index 88% rename from src/NadekoBot/Services/NadekoBot.cs rename to src/NadekoBot/Bot.cs index 5b0b4a6be..e4962ffe9 100644 --- a/src/NadekoBot/Services/NadekoBot.cs +++ b/src/NadekoBot/Bot.cs @@ -3,7 +3,6 @@ using Discord.Commands; using Discord.WebSocket; using Microsoft.Extensions.DependencyInjection; using NadekoBot.Common; -using NadekoBot.Common.ShardCom; using NadekoBot.Core.Services; using NadekoBot.Core.Services.Database.Models; using NadekoBot.Core.Services.Impl; @@ -26,16 +25,16 @@ using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Core.Common; using NadekoBot.Core.Common.Configs; using NadekoBot.Db; -using NadekoBot.Modules.Administration; using NadekoBot.Modules.Gambling.Services; using NadekoBot.Modules.Administration.Services; using NadekoBot.Modules.CustomReactions.Services; using NadekoBot.Modules.Utility.Services; using Serilog; +using NadekoBot.Services; namespace NadekoBot { - public class NadekoBot + public class Bot { public BotCredentials Credentials { get; } public DiscordSocketClient Client { get; } @@ -54,22 +53,15 @@ namespace NadekoBot public IServiceProvider Services { get; private set; } public IDataCache Cache { get; private set; } - public int GuildCount => - Cache.Redis.GetDatabase() - .ListRange(Credentials.RedisKey() + "_shardstats") - .Select(x => JsonConvert.DeserializeObject(x)) - .Sum(x => x.Guilds); - public string Mention { get; set; } public event Func JoinedGuild = delegate { return Task.CompletedTask; }; - public NadekoBot(int shardId, int parentProcessId) + public Bot(int shardId) { if (shardId < 0) throw new ArgumentOutOfRangeException(nameof(shardId)); - - LogSetup.SetupLogger(shardId); + TerribleElevatedPermissionCheck(); Credentials = new BotCredentials(); @@ -99,36 +91,11 @@ namespace NadekoBot DefaultRunMode = RunMode.Sync, }); - SetupShard(parentProcessId); - #if GLOBAL_NADEKO || DEBUG Client.Log += Client_Log; #endif } - private void StartSendingData() - { - Task.Run(async () => - { - while (true) - { - var data = new ShardComMessage() - { - ConnectionState = Client.ConnectionState, - Guilds = Client.ConnectionState == ConnectionState.Connected ? Client.Guilds.Count : 0, - ShardId = Client.ShardId, - Time = DateTime.UtcNow, - }; - - var sub = Cache.Redis.GetSubscriber(); - var msg = JsonConvert.SerializeObject(data); - - await sub.PublishAsync(Credentials.RedisKey() + "_shardcoord_send", msg).ConfigureAwait(false); - await Task.Delay(7500).ConfigureAwait(false); - } - }); - } - public List GetCurrentGuildIds() { return Client.Guilds.Select(x => x.Id).ToList(); @@ -180,9 +147,15 @@ namespace NadekoBot s.LoadFrom(Assembly.GetAssembly(typeof(CommandHandler))); + // todo if sharded + s + .AddSingleton() + .AddSingleton(x => (IReadyExecutor)x.GetRequiredService()); + s.AddSingleton(x => x.GetService()); s.AddSingleton(x => x.GetService()); s.AddSingleton(x => x.GetService()); + //initialize Services Services = s.BuildServiceProvider(); var commandHandler = Services.GetService(); @@ -194,7 +167,7 @@ namespace NadekoBot //what the fluff commandHandler.AddServices(s); - _ = LoadTypeReaders(typeof(NadekoBot).Assembly); + _ = LoadTypeReaders(typeof(Bot).Assembly); sw.Stop(); Log.Information($"All services loaded in {sw.Elapsed.TotalSeconds:F2}s"); @@ -355,7 +328,6 @@ namespace NadekoBot .ConfigureAwait(false); HandleStatusChanges(); - StartSendingData(); Ready.TrySetResult(true); _ = Task.Run(ExecuteReadySubscriptions); Log.Information("Shard {ShardId} ready", Client.ShardId); @@ -412,22 +384,6 @@ namespace NadekoBot } } - private static void SetupShard(int parentProcessId) - { - new Thread(new ThreadStart(() => - { - try - { - var p = Process.GetProcessById(parentProcessId); - p.WaitForExit(); - } - finally - { - Environment.Exit(7); - } - })).Start(); - } - private void HandleStatusChanges() { var sub = Services.GetService().Redis.GetSubscriber(); diff --git a/src/NadekoBot/Common/ShardCom/ShardComMessage.cs b/src/NadekoBot/Common/ShardCom/ShardComMessage.cs deleted file mode 100644 index 2baf9f0e9..000000000 --- a/src/NadekoBot/Common/ShardCom/ShardComMessage.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Discord; - -namespace NadekoBot.Common.ShardCom -{ - public class ShardComMessage - { - public int ShardId { get; set; } - public ConnectionState ConnectionState { get; set; } - public int Guilds { get; set; } - public DateTime Time { get; set; } - - public ShardComMessage Clone() => - new ShardComMessage - { - ShardId = ShardId, - ConnectionState = ConnectionState, - Guilds = Guilds, - Time = Time, - }; - } -} diff --git a/src/NadekoBot/Common/ShardCom/ShardComServer.cs b/src/NadekoBot/Common/ShardCom/ShardComServer.cs deleted file mode 100644 index fe8eb8922..000000000 --- a/src/NadekoBot/Common/ShardCom/ShardComServer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Threading.Tasks; -using Newtonsoft.Json; -using NadekoBot.Core.Services; - -namespace NadekoBot.Common.ShardCom -{ - public class ShardComServer - { - private readonly IDataCache _cache; - - public ShardComServer(IDataCache cache) - { - _cache = cache; - } - - public void Start() - { - var sub = _cache.Redis.GetSubscriber(); - sub.SubscribeAsync("shardcoord_send", (ch, data) => - { - var _ = OnDataReceived(JsonConvert.DeserializeObject(data)); - }, StackExchange.Redis.CommandFlags.FireAndForget); - } - - public event Func OnDataReceived = delegate { return Task.CompletedTask; }; - } -} diff --git a/src/NadekoBot/Db/Extensions/WaifuExtensions.cs b/src/NadekoBot/Db/Extensions/WaifuExtensions.cs index 303cdacd9..cb78f2e39 100644 --- a/src/NadekoBot/Db/Extensions/WaifuExtensions.cs +++ b/src/NadekoBot/Db/Extensions/WaifuExtensions.cs @@ -1,11 +1,9 @@ - -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using NadekoBot.Core.Services.Database; using NadekoBot.Core.Services.Database.Models; -using NadekoBot.Migrations; using NadekoBot.Db.Models; namespace NadekoBot.Db diff --git a/src/NadekoBot/Modules/Administration/SelfCommands.cs b/src/NadekoBot/Modules/Administration/SelfCommands.cs index d3cce1717..fb15bfa8b 100644 --- a/src/NadekoBot/Modules/Administration/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/SelfCommands.cs @@ -12,6 +12,7 @@ using System; using System.Linq; using System.Threading.Tasks; using NadekoBot.Core.Services; +using NadekoBot.Services; using Serilog; namespace NadekoBot.Modules.Administration @@ -22,14 +23,16 @@ namespace NadekoBot.Modules.Administration public class SelfCommands : NadekoSubmodule { private readonly DiscordSocketClient _client; - private readonly NadekoBot _bot; + private readonly Bot _bot; private readonly IBotStrings _strings; + private readonly ICoordinator _coord; - public SelfCommands(DiscordSocketClient client, NadekoBot bot, IBotStrings strings) + public SelfCommands(DiscordSocketClient client, Bot bot, IBotStrings strings, ICoordinator coord) { _client = client; _bot = bot; _strings = strings; + _coord = coord; } [NadekoCommand, Usage, Description, Aliases] @@ -251,7 +254,7 @@ namespace NadekoBot.Modules.Administration if (--page < 0) return; - var statuses = _service.GetAllShardStatuses(); + var statuses = _coord.GetAllShardStatuses(); var status = string.Join(", ", statuses .GroupBy(x => x.ConnectionState) @@ -289,7 +292,7 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task RestartShard(int shardId) { - var success = _service.RestartShard(shardId); + var success = _coord.RestartShard(shardId); if (success) { await ReplyConfirmLocalizedAsync("shard_reconnecting", Format.Bold("#" + shardId)).ConfigureAwait(false); @@ -321,14 +324,14 @@ namespace NadekoBot.Modules.Administration // ignored } await Task.Delay(2000).ConfigureAwait(false); - _service.Die(); + _coord.Die(); } [NadekoCommand, Usage, Description, Aliases] [OwnerOnly] public async Task Restart() { - bool success = _service.RestartBot(); + bool success = _coord.RestartBot(); if (!success) { await ReplyErrorLocalizedAsync("restart_fail").ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs b/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs index c721cde39..f48eadbf6 100644 --- a/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs +++ b/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Modules.Administration.Services private readonly DbService _db; private readonly LogCommandService _logService; - public AdministrationService(NadekoBot bot, CommandHandler cmdHandler, DbService db, + public AdministrationService(Bot bot, CommandHandler cmdHandler, DbService db, LogCommandService logService) { _db = db; diff --git a/src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs b/src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs index b40b291f5..f79c479d2 100644 --- a/src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs +++ b/src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs @@ -31,7 +31,7 @@ namespace NadekoBot.Modules.Administration.Services SingleWriter = false, }); - public AutoAssignRoleService(DiscordSocketClient client, NadekoBot bot, DbService db) + public AutoAssignRoleService(DiscordSocketClient client, Bot bot, DbService db) { _client = client; _db = db; diff --git a/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs b/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs index 1af4ec1eb..8962202dd 100644 --- a/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs +++ b/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Modules.Administration.Services private readonly DbService _db; private readonly DiscordSocketClient _client; - public GameVoiceChannelService(DiscordSocketClient client, DbService db, NadekoBot bot) + public GameVoiceChannelService(DiscordSocketClient client, DbService db, Bot bot) { _db = db; _client = client; diff --git a/src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs b/src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs index 5d8aa88f1..6cdd7a7d5 100644 --- a/src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs +++ b/src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs @@ -16,7 +16,7 @@ namespace NadekoBot.Modules.Administration.Services private readonly ConcurrentDictionary _timezones; private readonly DbService _db; - public GuildTimezoneService(DiscordSocketClient client, NadekoBot bot, DbService db) + public GuildTimezoneService(DiscordSocketClient client, Bot bot, DbService db) { _timezones = bot.AllGuildConfigs .Select(GetTimzezoneTuple) diff --git a/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs b/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs index 998476a0a..4e6eb62e7 100644 --- a/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs +++ b/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs @@ -20,14 +20,14 @@ namespace NadekoBot.Modules.Administration.Services private readonly BotConfigService _bss; private readonly Replacer _rep; private readonly DbService _db; - private readonly NadekoBot _bot; + private readonly Bot _bot; private class TimerState { public int Index { get; set; } } - public PlayingRotateService(DiscordSocketClient client, DbService db, NadekoBot bot, + public PlayingRotateService(DiscordSocketClient client, DbService db, Bot bot, BotConfigService bss, IEnumerable phProviders) { _db = db; diff --git a/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs b/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs index 6278f7231..34d5753a6 100644 --- a/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs +++ b/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs @@ -42,7 +42,7 @@ namespace NadekoBot.Modules.Administration.Services SingleWriter = false }); - public ProtectionService(DiscordSocketClient client, NadekoBot bot, + public ProtectionService(DiscordSocketClient client, Bot bot, MuteService mute, DbService db, UserPunishService punishService) { _client = client; diff --git a/src/NadekoBot/Modules/Administration/Services/RoleCommandsService.cs b/src/NadekoBot/Modules/Administration/Services/RoleCommandsService.cs index f64071c60..e26bb62c5 100644 --- a/src/NadekoBot/Modules/Administration/Services/RoleCommandsService.cs +++ b/src/NadekoBot/Modules/Administration/Services/RoleCommandsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Modules.Administration.Services private readonly ConcurrentDictionary> _models; public RoleCommandsService(DiscordSocketClient client, DbService db, - NadekoBot bot) + Bot bot) { _db = db; _client = client; diff --git a/src/NadekoBot/Modules/Administration/Services/SelfService.cs b/src/NadekoBot/Modules/Administration/Services/SelfService.cs index f733488a3..3542b65c6 100644 --- a/src/NadekoBot/Modules/Administration/Services/SelfService.cs +++ b/src/NadekoBot/Modules/Administration/Services/SelfService.cs @@ -9,14 +9,13 @@ using NadekoBot.Core.Services; using StackExchange.Redis; using System.Collections.Generic; using System.Diagnostics; -using Newtonsoft.Json; -using NadekoBot.Common.ShardCom; using Microsoft.EntityFrameworkCore; using NadekoBot.Core.Services.Database.Models; using System.Threading; using System.Collections.Concurrent; using System; using System.Net.Http; +using NadekoBot.Services; using Serilog; namespace NadekoBot.Modules.Administration.Services @@ -41,10 +40,11 @@ namespace NadekoBot.Modules.Administration.Services private readonly IImageCache _imgs; private readonly IHttpClientFactory _httpFactory; private readonly BotConfigService _bss; + private readonly ICoordinator _coord; public SelfService(DiscordSocketClient client, CommandHandler cmdHandler, DbService db, IBotStrings strings, IBotCredentials creds, IDataCache cache, IHttpClientFactory factory, - BotConfigService bss) + BotConfigService bss, ICoordinator coord) { _redis = cache.Redis; _cmdHandler = cmdHandler; @@ -56,6 +56,7 @@ namespace NadekoBot.Modules.Administration.Services _imgs = cache.LocalImages; _httpFactory = factory; _bss = bss; + _coord = coord; var sub = _redis.GetSubscriber(); if (_client.ShardId == 0) @@ -281,18 +282,6 @@ namespace NadekoBot.Modules.Administration.Services } } - public bool RestartBot() - { - var cmd = _creds.RestartCommand; - if (string.IsNullOrWhiteSpace(cmd?.Cmd)) - { - return false; - } - - Restart(); - return true; - } - public bool RemoveStartupCommand(int index, out AutoCommand cmd) { using (var uow = _db.GetDbContext()) @@ -385,32 +374,6 @@ namespace NadekoBot.Modules.Administration.Services sub.Publish(_creds.RedisKey() + "_reload_images", ""); } - public void Die() - { - var sub = _cache.Redis.GetSubscriber(); - sub.Publish(_creds.RedisKey() + "_die", "", CommandFlags.FireAndForget); - } - - public void Restart() - { - Process.Start(_creds.RestartCommand.Cmd, _creds.RestartCommand.Args); - var sub = _cache.Redis.GetSubscriber(); - sub.Publish(_creds.RedisKey() + "_die", "", CommandFlags.FireAndForget); - } - - public bool RestartShard(int shardId) - { - if (shardId < 0 || shardId >= _creds.TotalShards) - return false; - - var pub = _cache.Redis.GetSubscriber(); - pub.Publish(_creds.RedisKey() + "_shardcoord_stop", - JsonConvert.SerializeObject(shardId), - CommandFlags.FireAndForget); - - return true; - } - public bool ForwardMessages() { var isForwarding = false; @@ -425,12 +388,5 @@ namespace NadekoBot.Modules.Administration.Services _bss.ModifyConfig(config => { isToAll = config.ForwardToAllOwners = !config.ForwardToAllOwners; }); return isToAll; } - - public IEnumerable GetAllShardStatuses() - { - var db = _cache.Redis.GetDatabase(); - return db.ListRange(_creds.RedisKey() + "_shardstats") - .Select(x => JsonConvert.DeserializeObject(x)); - } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/UserPunishService.cs b/src/NadekoBot/Modules/Administration/Services/UserPunishService.cs index 898843129..38ccf77f9 100644 --- a/src/NadekoBot/Modules/Administration/Services/UserPunishService.cs +++ b/src/NadekoBot/Modules/Administration/Services/UserPunishService.cs @@ -456,7 +456,7 @@ WHERE GuildId={guildId} { template = JsonConvert.SerializeObject(new { - color = NadekoBot.ErrorColor.RawValue, + color = Bot.ErrorColor.RawValue, description = defaultMessage }); @@ -477,7 +477,7 @@ WHERE GuildId={guildId} { template = JsonConvert.SerializeObject(new { - color = NadekoBot.ErrorColor.RawValue, + color = Bot.ErrorColor.RawValue, description = replacer.Replace(template) }); diff --git a/src/NadekoBot/Modules/Administration/Services/VcRoleService.cs b/src/NadekoBot/Modules/Administration/Services/VcRoleService.cs index 0cf1e4aee..61487d9f8 100644 --- a/src/NadekoBot/Modules/Administration/Services/VcRoleService.cs +++ b/src/NadekoBot/Modules/Administration/Services/VcRoleService.cs @@ -21,7 +21,7 @@ namespace NadekoBot.Modules.Administration.Services public ConcurrentDictionary> VcRoles { get; } public ConcurrentDictionary> ToAssign { get; } - public VcRoleService(DiscordSocketClient client, NadekoBot bot, DbService db) + public VcRoleService(DiscordSocketClient client, Bot bot, DbService db) { _db = db; _client = client; diff --git a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs index 1c30e6fbd..85eed499e 100644 --- a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs +++ b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs @@ -57,13 +57,13 @@ namespace NadekoBot.Modules.CustomReactions.Services private readonly PermissionService _perms; private readonly CommandHandler _cmd; private readonly IBotStrings _strings; - private readonly NadekoBot _bot; + private readonly Bot _bot; private readonly GlobalPermissionService _gperm; private readonly CmdCdService _cmdCds; private readonly IPubSub _pubSub; private readonly Random _rng; - public CustomReactionsService(PermissionService perms, DbService db, IBotStrings strings, NadekoBot bot, + public CustomReactionsService(PermissionService perms, DbService db, IBotStrings strings, Bot bot, DiscordSocketClient client, CommandHandler cmd, GlobalPermissionService gperm, CmdCdService cmdCds, IPubSub pubSub) { diff --git a/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs b/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs index 479c9a0da..c66dce464 100644 --- a/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs @@ -21,7 +21,7 @@ namespace NadekoBot.Modules.Gambling.Services { private readonly DbService _db; private readonly ICurrencyService _cs; - private readonly NadekoBot _bot; + private readonly Bot _bot; private readonly DiscordSocketClient _client; private readonly IDataCache _cache; private readonly GamblingConfigService _gss; @@ -31,7 +31,7 @@ namespace NadekoBot.Modules.Gambling.Services private readonly Timer _decayTimer; - public GamblingService(DbService db, NadekoBot bot, ICurrencyService cs, + public GamblingService(DbService db, Bot bot, ICurrencyService cs, DiscordSocketClient client, IDataCache cache, GamblingConfigService gss) { _db = db; diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index cb95a73c9..aefb01de7 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -48,7 +48,7 @@ namespace NadekoBot.Modules.Games return; var res = _service.GetEightballResponse(ctx.User.Id, question); - await ctx.Channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) + await ctx.Channel.EmbedAsync(new EmbedBuilder().WithColor(Bot.OkColor) .WithDescription(ctx.User.ToString()) .AddField(efb => efb.WithName("❓ " + GetText("question")).WithValue(question).WithIsInline(false)) .AddField("🎱 " + GetText("8ball"), res, false)); diff --git a/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs b/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs index 7682d855c..08da8d1e3 100644 --- a/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs +++ b/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs @@ -30,7 +30,7 @@ namespace NadekoBot.Modules.Games.Services public ModuleBehaviorType BehaviorType => ModuleBehaviorType.Executor; public ChatterBotService(DiscordSocketClient client, PermissionService perms, - NadekoBot bot, CommandHandler cmd, IBotStrings strings, IHttpClientFactory factory, + Bot bot, CommandHandler cmd, IBotStrings strings, IHttpClientFactory factory, IBotCredentials creds) { _client = client; diff --git a/src/NadekoBot/Modules/Help/Services/HelpService.cs b/src/NadekoBot/Modules/Help/Services/HelpService.cs index ce355402d..c2e406ebc 100644 --- a/src/NadekoBot/Modules/Help/Services/HelpService.cs +++ b/src/NadekoBot/Modules/Help/Services/HelpService.cs @@ -83,7 +83,7 @@ namespace NadekoBot.Modules.Help.Services arg => Format.Code(arg)))) .WithIsInline(false)) .WithFooter(efb => efb.WithText(GetText("module", guild, com.Module.GetTopLevelModule().Name))) - .WithColor(NadekoBot.OkColor); + .WithColor(Bot.OkColor); var opt = ((NadekoOptionsAttribute)com.Attributes.FirstOrDefault(x => x is NadekoOptionsAttribute))?.OptionType; if (opt != null) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 1775c4507..5d02c55dd 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -619,7 +619,7 @@ namespace NadekoBot.Modules.Music .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{from + 1}").WithIsInline(true)) .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{to + 1}").WithIsInline(true)) - .WithColor(NadekoBot.OkColor); + .WithColor(Bot.OkColor); if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute)) embed.WithUrl(track.Url); diff --git a/src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs b/src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs index cc081fb35..0ffb32bfe 100644 --- a/src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs @@ -18,7 +18,7 @@ namespace NadekoBot.Modules.Permissions.Services public int Priority { get; } = 0; - public CmdCdService(NadekoBot bot) + public CmdCdService(Bot bot) { CommandCooldowns = new ConcurrentDictionary>( bot.AllGuildConfigs.ToDictionary(k => k.GuildId, diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index 69f817546..3553706a2 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -325,7 +325,7 @@ namespace NadekoBot.Modules.Searches } await ctx.Channel.EmbedAsync(new EmbedBuilder() - .WithColor(NadekoBot.OkColor) + .WithColor(Bot.OkColor) .AddField(efb => efb.WithName(GetText("original_url")) .WithValue($"<{query}>")) .AddField(efb => efb.WithName(GetText("short_url")) diff --git a/src/NadekoBot/Modules/Searches/Services/FeedsService.cs b/src/NadekoBot/Modules/Searches/Services/FeedsService.cs index aff87297b..704907288 100644 --- a/src/NadekoBot/Modules/Searches/Services/FeedsService.cs +++ b/src/NadekoBot/Modules/Searches/Services/FeedsService.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Modules.Searches.Services private readonly ConcurrentDictionary _lastPosts = new ConcurrentDictionary(); - public FeedsService(NadekoBot bot, DbService db, DiscordSocketClient client) + public FeedsService(Bot bot, DbService db, DiscordSocketClient client) { _db = db; diff --git a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs index 428c962a0..a1701dbc9 100644 --- a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs +++ b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs @@ -61,7 +61,7 @@ namespace NadekoBot.Modules.Searches.Services private readonly List _yomamaJokes; public SearchesService(DiscordSocketClient client, IGoogleApiService google, - DbService db, NadekoBot bot, IDataCache cache, IHttpClientFactory factory, + DbService db, Bot bot, IDataCache cache, IHttpClientFactory factory, FontProvider fonts, IBotCredentials creds) { _httpFactory = factory; diff --git a/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs b/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs index 25c78fcab..7cc6b9253 100644 --- a/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs +++ b/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs @@ -46,7 +46,7 @@ namespace NadekoBot.Modules.Searches.Services public StreamNotificationService(DbService db, DiscordSocketClient client, IBotStrings strings, IDataCache cache, IBotCredentials creds, IHttpClientFactory httpFactory, - NadekoBot bot) + Bot bot) { _db = db; _client = client; @@ -457,7 +457,7 @@ namespace NadekoBot.Modules.Searches.Services .AddField(efb => efb.WithName(GetText(guildId, "viewers")) .WithValue(status.IsLive ? status.Viewers.ToString() : "-") .WithIsInline(true)) - .WithColor(status.IsLive ? NadekoBot.OkColor : NadekoBot.ErrorColor); + .WithColor(status.IsLive ? Bot.OkColor : Bot.ErrorColor); if (!string.IsNullOrWhiteSpace(status.Title)) embed.WithAuthor(status.Title); diff --git a/src/NadekoBot/Modules/Searches/XkcdCommands.cs b/src/NadekoBot/Modules/Searches/XkcdCommands.cs index 94fb17b5d..548a001d0 100644 --- a/src/NadekoBot/Modules/Searches/XkcdCommands.cs +++ b/src/NadekoBot/Modules/Searches/XkcdCommands.cs @@ -34,7 +34,7 @@ namespace NadekoBot.Modules.Searches { var res = await http.GetStringAsync($"{_xkcdUrl}/info.0.json").ConfigureAwait(false); var comic = JsonConvert.DeserializeObject(res); - var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor) + var embed = new EmbedBuilder().WithColor(Bot.OkColor) .WithImageUrl(comic.ImageLink) .WithAuthor(eab => eab.WithName(comic.Title).WithUrl($"{_xkcdUrl}/{comic.Num}").WithIconUrl("https://xkcd.com/s/919f27.ico")) .AddField(efb => efb.WithName(GetText("comic_number")).WithValue(comic.Num.ToString()).WithIsInline(true)) @@ -69,7 +69,7 @@ namespace NadekoBot.Modules.Searches var res = await http.GetStringAsync($"{_xkcdUrl}/{num}/info.0.json").ConfigureAwait(false); var comic = JsonConvert.DeserializeObject(res); - var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor) + var embed = new EmbedBuilder().WithColor(Bot.OkColor) .WithImageUrl(comic.ImageLink) .WithAuthor(eab => eab.WithName(comic.Title).WithUrl($"{_xkcdUrl}/{num}").WithIconUrl("https://xkcd.com/s/919f27.ico")) .AddField(efb => efb.WithName(GetText("comic_number")).WithValue(comic.Num.ToString()).WithIsInline(true)) diff --git a/src/NadekoBot/Modules/Utility/InfoCommands.cs b/src/NadekoBot/Modules/Utility/InfoCommands.cs index f23ad59ae..3de72c515 100644 --- a/src/NadekoBot/Modules/Utility/InfoCommands.cs +++ b/src/NadekoBot/Modules/Utility/InfoCommands.cs @@ -58,7 +58,7 @@ namespace NadekoBot.Modules.Utility .AddField(fb => fb.WithName(GetText("region")).WithValue(guild.VoiceRegionId.ToString()).WithIsInline(true)) .AddField(fb => fb.WithName(GetText("roles")).WithValue((guild.Roles.Count - 1).ToString()).WithIsInline(true)) .AddField(fb => fb.WithName(GetText("features")).WithValue(features).WithIsInline(true)) - .WithColor(NadekoBot.OkColor); + .WithColor(Bot.OkColor); if (Uri.IsWellFormedUriString(guild.IconUrl, UriKind.Absolute)) embed.WithThumbnailUrl(guild.IconUrl); if (guild.Emotes.Any()) @@ -89,7 +89,7 @@ namespace NadekoBot.Modules.Utility .AddField(fb => fb.WithName(GetText("id")).WithValue(ch.Id.ToString()).WithIsInline(true)) .AddField(fb => fb.WithName(GetText("created_at")).WithValue($"{createdAt:dd.MM.yyyy HH:mm}").WithIsInline(true)) .AddField(fb => fb.WithName(GetText("users")).WithValue(usercount.ToString()).WithIsInline(true)) - .WithColor(NadekoBot.OkColor); + .WithColor(Bot.OkColor); await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); } @@ -112,7 +112,7 @@ namespace NadekoBot.Modules.Utility .AddField(fb => fb.WithName(GetText("joined_server")).WithValue($"{user.JoinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}").WithIsInline(true)) .AddField(fb => fb.WithName(GetText("joined_discord")).WithValue($"{user.CreatedAt:dd.MM.yyyy HH:mm}").WithIsInline(true)) .AddField(fb => fb.WithName(GetText("roles")).WithValue($"**({user.RoleIds.Count - 1})** - {string.Join("\n", user.GetRoles().Take(10).Where(r => r.Id != r.Guild.EveryoneRole.Id).Select(r => r.Name)).SanitizeMentions(true)}").WithIsInline(true)) - .WithColor(NadekoBot.OkColor); + .WithColor(Bot.OkColor); var av = user.RealAvatarUrl(); if (av != null && av.IsAbsoluteUri) diff --git a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs index 4bf35d0b0..4fd9db6c3 100644 --- a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs +++ b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs @@ -23,7 +23,7 @@ namespace NadekoBot.Modules.Utility.Services private readonly DiscordSocketClient _client; private readonly ConcurrentDictionary guildSettings; - public StreamRoleService(DiscordSocketClient client, DbService db, NadekoBot bot) + public StreamRoleService(DiscordSocketClient client, DbService db, Bot bot) { _db = db; _client = client; diff --git a/src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs b/src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs index 5d7410d7a..06752ea67 100644 --- a/src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs +++ b/src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs @@ -18,7 +18,7 @@ namespace NadekoBot.Modules.Utility.Services private readonly CommandHandler _ch; private readonly HelpService _hs; - public VerboseErrorsService(NadekoBot bot, DbService db, CommandHandler ch, HelpService hs) + public VerboseErrorsService(Bot bot, DbService db, CommandHandler ch, HelpService hs) { _db = db; _ch = ch; diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 877ba6d20..158df8caf 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -16,6 +16,7 @@ using System.Threading; using System.Threading.Tasks; using NadekoBot.Common.Replacements; using NadekoBot.Core.Common; +using NadekoBot.Services; using Serilog; namespace NadekoBot.Modules.Utility @@ -23,18 +24,18 @@ namespace NadekoBot.Modules.Utility public partial class Utility : NadekoModule { private readonly DiscordSocketClient _client; + private readonly ICoordinator _coord; private readonly IStatsService _stats; private readonly IBotCredentials _creds; - private readonly NadekoBot _bot; private readonly DownloadTracker _tracker; - public Utility(NadekoBot nadeko, DiscordSocketClient client, + public Utility(DiscordSocketClient client, ICoordinator coord, IStatsService stats, IBotCredentials creds, DownloadTracker tracker) { _client = client; + _coord = coord; _stats = stats; _creds = creds; - _bot = nadeko; _tracker = tracker; } @@ -276,7 +277,7 @@ namespace NadekoBot.Modules.Utility .AddField(efb => efb.WithName(GetText("uptime")).WithValue(_stats.GetUptimeString("\n")).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("presence")).WithValue( GetText("presence_txt", - _bot.GuildCount, _stats.TextChannels, _stats.VoiceChannels)).WithIsInline(true))).ConfigureAwait(false); + _coord.GetGuildCount(), _stats.TextChannels, _stats.VoiceChannels)).WithIsInline(true))).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index 5256326ea..9e4b8fd1f 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -62,7 +62,7 @@ namespace NadekoBot.Modules.Xp.Services private XpTemplate _template; private readonly DiscordSocketClient _client; - public XpService(DiscordSocketClient client, CommandHandler cmd, NadekoBot bot, DbService db, + public XpService(DiscordSocketClient client, CommandHandler cmd, Bot bot, DbService db, IBotStrings strings, IDataCache cache, FontProvider fonts, IBotCredentials creds, ICurrencyService cs, IHttpClientFactory http, XpConfigService xpConfig) { diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 4f3079ffd..59a755b77 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -18,6 +18,12 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -52,6 +58,9 @@ + + Protos\coordinator.proto + PreserveNewest diff --git a/src/NadekoBot/Program.cs b/src/NadekoBot/Program.cs index a676ebb27..94ff6ca12 100644 --- a/src/NadekoBot/Program.cs +++ b/src/NadekoBot/Program.cs @@ -1,34 +1,14 @@ -using NadekoBot.Core.Services; -using System.Diagnostics; -using System.Threading.Tasks; +using NadekoBot; +using NadekoBot.Core.Services; +using Serilog; -namespace NadekoBot -{ - public sealed class Program - { - public static async Task Main(string[] args) - { - var pid = Process.GetCurrentProcess().Id; - System.Console.WriteLine($"Pid: {pid}"); - if (args.Length == 2 - && int.TryParse(args[0], out int shardId) - && int.TryParse(args[1], out int parentProcessId)) - { - await new NadekoBot(shardId, parentProcessId == 0 ? pid : parentProcessId) - .RunAndBlockAsync(); - } - else - { - await new ShardsCoordinator() - .RunAsync() - .ConfigureAwait(false); -#if DEBUG - await new NadekoBot(0, pid) - .RunAndBlockAsync(); -#else - await Task.Delay(-1); -#endif - } - } - } -} +var pid = System.Environment.ProcessId; + +var shardId = 0; +if (args.Length == 1) + int.TryParse(args[0], out shardId); + +LogSetup.SetupLogger(shardId); +Log.Information($"Pid: {pid}"); + +await new Bot(shardId).RunAndBlockAsync(); \ No newline at end of file diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 5a53dfb29..ead24944e 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -37,7 +37,7 @@ namespace NadekoBot.Core.Services private readonly DiscordSocketClient _client; private readonly CommandService _commandService; private readonly BotConfigService _bss; - private readonly NadekoBot _bot; + private readonly Bot _bot; private IServiceProvider _services; private IEnumerable _earlyBehaviors; private IEnumerable _inputTransformers; @@ -57,7 +57,7 @@ namespace NadekoBot.Core.Services private readonly Timer _clearUsersOnShortCooldown; public CommandHandler(DiscordSocketClient client, DbService db, CommandService commandService, - BotConfigService bss, NadekoBot bot, IServiceProvider services) + BotConfigService bss, Bot bot, IServiceProvider services) { _client = client; _commandService = commandService; diff --git a/src/NadekoBot/Services/GreetSettingsService.cs b/src/NadekoBot/Services/GreetSettingsService.cs index 2833d2adc..e67a43694 100644 --- a/src/NadekoBot/Services/GreetSettingsService.cs +++ b/src/NadekoBot/Services/GreetSettingsService.cs @@ -27,7 +27,7 @@ namespace NadekoBot.Core.Services private readonly BotConfigService _bss; public bool GroupGreets => _bss.Data.GroupGreets; - public GreetSettingsService(DiscordSocketClient client, NadekoBot bot, DbService db, + public GreetSettingsService(DiscordSocketClient client, Bot bot, DbService db, BotConfigService bss) { _db = db; diff --git a/src/NadekoBot/Services/ICoordinator.cs b/src/NadekoBot/Services/ICoordinator.cs new file mode 100644 index 000000000..e104ea5d9 --- /dev/null +++ b/src/NadekoBot/Services/ICoordinator.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace NadekoBot.Services +{ + public interface ICoordinator + { + bool RestartBot(); + void Die(); + bool RestartShard(int shardId); + IEnumerable GetAllShardStatuses(); + int GetGuildCount(); + } + + public class ShardStatus + { + public Discord.ConnectionState ConnectionState { get; set; } + public DateTime Time { get; set; } + public int ShardId { get; set; } + public int Guilds { get; set; } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Impl/Localization.cs b/src/NadekoBot/Services/Impl/Localization.cs index 230dc01c5..82fadf843 100644 --- a/src/NadekoBot/Services/Impl/Localization.cs +++ b/src/NadekoBot/Services/Impl/Localization.cs @@ -22,7 +22,7 @@ namespace NadekoBot.Core.Services.Impl private static readonly Dictionary _commandData = JsonConvert.DeserializeObject>( File.ReadAllText("./data/strings/commands/commands.en-US.json")); - public Localization(BotConfigService bss, NadekoBot bot, DbService db) + public Localization(BotConfigService bss, Bot bot, DbService db) { _bss = bss; _db = db; diff --git a/src/NadekoBot/Services/Impl/RemoteGrpcCoordinator.cs b/src/NadekoBot/Services/Impl/RemoteGrpcCoordinator.cs new file mode 100644 index 000000000..9525a58af --- /dev/null +++ b/src/NadekoBot/Services/Impl/RemoteGrpcCoordinator.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using Grpc.Core; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Coordinator; +using NadekoBot.Core.Services; +using NadekoBot.Extensions; +using Serilog; + +namespace NadekoBot.Services +{ + public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor + { + private readonly Coordinator.Coordinator.CoordinatorClient _coordClient; + private readonly DiscordSocketClient _client; + + public RemoteGrpcCoordinator(IBotCredentials creds, DiscordSocketClient client) + { + // todo should use credentials + var channel = Grpc.Net.Client.GrpcChannel.ForAddress("https://localhost:3443"); + _coordClient = new(channel); + _client = client; + } + + public bool RestartBot() + { + _coordClient.RestartAllShards(new RestartAllRequest + { + + }); + + return true; + } + + public void Die() + { + _coordClient.Die(new DieRequest() + { + Graceful = false + }); + } + + public bool RestartShard(int shardId) + { + _coordClient.RestartShard(new RestartShardRequest + { + ShardId = shardId, + }); + + return true; + } + + public IEnumerable GetAllShardStatuses() + { + var res = _coordClient.GetAllStatuses(new GetAllStatusesRequest()); + + return res.Statuses + .ToArray() + .Map(s => new ShardStatus() + { + ConnectionState = FromCoordConnState(s.State), + Guilds = s.GuildCount, + ShardId = s.ShardId, + Time = s.LastUpdate.ToDateTime(), + }); + } + + public int GetGuildCount() + { + var res = _coordClient.GetAllStatuses(new GetAllStatusesRequest()); + + return res.Statuses.Sum(x => x.GuildCount); + } + + public Task OnReadyAsync() + { + Task.Run(async () => + { + var gracefulImminent = false; + while (true) + { + try + { + var reply = await _coordClient.HeartbeatAsync(new HeartbeatRequest + { + State = ToCoordConnState(_client.ConnectionState), + GuildCount = _client.ConnectionState == Discord.ConnectionState.Connected ? _client.Guilds.Count : 0, + ShardId = _client.ShardId, + }, deadline: DateTime.UtcNow + TimeSpan.FromSeconds(10)); + gracefulImminent = reply.GracefulImminent; + } + catch (RpcException ex) + { + if (!gracefulImminent) + { + Log.Warning(ex, "Hearbeat failed and graceful shutdown was not expected: {Message}", + ex.Message); + break; + } + + await Task.Delay(22500).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Error(ex, "Unexpected heartbeat exception: {Message}", ex.Message); + break; + } + + await Task.Delay(7500).ConfigureAwait(false); + } + + Environment.Exit(5); + }); + + return Task.CompletedTask; + } + + private ConnState ToCoordConnState(Discord.ConnectionState state) + => state switch + { + Discord.ConnectionState.Connecting => ConnState.Connecting, + Discord.ConnectionState.Connected => ConnState.Connected, + _ => ConnState.Disconnected + }; + + private Discord.ConnectionState FromCoordConnState(ConnState state) + => state switch + { + ConnState.Connecting => Discord.ConnectionState.Connecting, + ConnState.Connected => Discord.ConnectionState.Connected, + _ => Discord.ConnectionState.Disconnected + }; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Settings/BotConfigService.cs b/src/NadekoBot/Services/Settings/BotConfigService.cs index 23cb202c2..35caa6175 100644 --- a/src/NadekoBot/Services/Settings/BotConfigService.cs +++ b/src/NadekoBot/Services/Settings/BotConfigService.cs @@ -37,9 +37,9 @@ namespace NadekoBot.Core.Services var error = _data.Color.Error; var pend = _data.Color.Pending; // todo future remove these static props once cleanup is done - NadekoBot.OkColor = new Color(ok.R, ok.G, ok.B); - NadekoBot.ErrorColor = new Color(error.R, error.G, error.B); - NadekoBot.PendingColor = new Color(pend.R, pend.G, pend.B); + Bot.OkColor = new Color(ok.R, ok.G, ok.B); + Bot.ErrorColor = new Color(error.R, error.G, error.B); + Bot.PendingColor = new Color(pend.R, pend.G, pend.B); } protected override void OnStateUpdate() diff --git a/src/NadekoBot/Services/ShardsCoordinator.cs b/src/NadekoBot/Services/ShardsCoordinator.cs deleted file mode 100644 index e288cf66c..000000000 --- a/src/NadekoBot/Services/ShardsCoordinator.cs +++ /dev/null @@ -1,388 +0,0 @@ -using NadekoBot.Common.Collections; -using NadekoBot.Common.ShardCom; -using NadekoBot.Core.Services.Impl; -using NadekoBot.Extensions; -using Newtonsoft.Json; -using StackExchange.Redis; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using NadekoBot.Core.Common; -using Serilog; - -namespace NadekoBot.Core.Services -{ - public class ShardsCoordinator - { - private class ShardsCoordinatorQueue - { - private readonly object _locker = new object(); - private readonly HashSet _set = new HashSet(); - private readonly Queue _queue = new Queue(); - public int Count => _queue.Count; - - public void Enqueue(int i) - { - lock (_locker) - { - if (_set.Add(i)) - _queue.Enqueue(i); - } - } - - public bool TryPeek(out int id) - { - lock (_locker) - { - return _queue.TryPeek(out id); - } - } - - public bool TryDequeue(out int id) - { - lock (_locker) - { - if (_queue.TryDequeue(out id)) - { - _set.Remove(id); - return true; - } - } - return false; - } - } - - private readonly BotCredentials _creds; - private readonly string _key; - private readonly Process[] _shardProcesses; - - private readonly int _curProcessId; - private readonly ConnectionMultiplexer _redis; - private ShardComMessage _defaultShardState; - - private ShardsCoordinatorQueue _shardStartQueue = - new ShardsCoordinatorQueue(); - - private ConcurrentHashSet _shardRestartWaitingList = - new ConcurrentHashSet(); - - public ShardsCoordinator() - { - //load main stuff - LogSetup.SetupLogger("coord"); - _creds = new BotCredentials(); - - Log.Information("Starting NadekoBot v" + StatsService.BotVersion); - - _key = _creds.RedisKey(); - - var conf = ConfigurationOptions.Parse(_creds.RedisOptions); - try - { - _redis = ConnectionMultiplexer.Connect(conf); - } - catch (RedisConnectionException ex) - { - Log.Error(ex, "Redis error. Make sure Redis is installed and running as a service"); - Helpers.ReadErrorAndExit(11); - } - - var imgCache = new RedisImagesCache(_redis, _creds); //reload images into redis - if (!imgCache.AllKeysExist().GetAwaiter().GetResult()) // but only if the keys don't exist. If images exist, you have to reload them manually - { - imgCache.Reload().GetAwaiter().GetResult(); - } - else - { - Log.Information("Images are already present in redis. Use .imagesreload to force update if needed"); - } - - //setup initial shard statuses - _defaultShardState = new ShardComMessage() - { - ConnectionState = Discord.ConnectionState.Disconnected, - Guilds = 0, - Time = DateTime.UtcNow - }; - var db = _redis.GetDatabase(); - //clear previous statuses - db.KeyDelete(_key + "_shardstats"); - - _shardProcesses = new Process[_creds.TotalShards]; - -#if GLOBAL_NADEKO - var shardIdsEnum = Enumerable.Range(1, 31) - .Concat(Enumerable.Range(33, _creds.TotalShards - 33)) - .Shuffle() - .Prepend(32) - .Prepend(0); -#else - var shardIdsEnum = Enumerable.Range(1, _creds.TotalShards - 1) - .Shuffle() - .Prepend(0); -#endif - - var shardIds = shardIdsEnum - .ToArray(); - for (var i = 0; i < shardIds.Length; i++) - { - var id = shardIds[i]; - //add it to the list of shards which should be started -#if DEBUG - if (id > 0) - _shardStartQueue.Enqueue(id); - else - _shardProcesses[id] = Process.GetCurrentProcess(); -#else - _shardStartQueue.Enqueue(id); -#endif - //set the shard's initial state in redis cache - var msg = _defaultShardState.Clone(); - msg.ShardId = id; - //this is to avoid the shard coordinator thinking that - //the shard is unresponsive while starting up - var delay = 45; -#if GLOBAL_NADEKO - delay = 180; -#endif - msg.Time = DateTime.UtcNow + TimeSpan.FromSeconds(delay * (id + 1)); - db.ListRightPush(_key + "_shardstats", - JsonConvert.SerializeObject(msg), - flags: CommandFlags.FireAndForget); - } - - _curProcessId = Process.GetCurrentProcess().Id; - - //subscribe to shardcoord events - var sub = _redis.GetSubscriber(); - - //send is called when shard status is updated. Every 7.5 seconds atm - sub.Subscribe(_key + "_shardcoord_send", - OnDataReceived, - CommandFlags.FireAndForget); - - //called to stop the shard, although the shard will start again when it finds out it's dead - sub.Subscribe(_key + "_shardcoord_stop", - OnStop, - CommandFlags.FireAndForget); - - //called kill the bot - sub.Subscribe(_key + "_die", - (ch, x) => Environment.Exit(0), - CommandFlags.FireAndForget); - } - - private void OnStop(RedisChannel ch, RedisValue data) - { - var shardId = JsonConvert.DeserializeObject(data); - OnStop(shardId); - } - - private void OnStop(int shardId) - { - var db = _redis.GetDatabase(); - var msg = _defaultShardState.Clone(); - msg.ShardId = shardId; - db.ListSetByIndex(_key + "_shardstats", - shardId, - JsonConvert.SerializeObject(msg), - CommandFlags.FireAndForget); - var p = _shardProcesses[shardId]; - if (p is null) - return; // ignore - _shardProcesses[shardId] = null; - try - { - p.KillTree(); - p.Dispose(); - } - catch { } - } - - private void OnDataReceived(RedisChannel ch, RedisValue data) - { - var msg = JsonConvert.DeserializeObject(data); - if (msg is null) - return; - var db = _redis.GetDatabase(); - //sets the shard state - db.ListSetByIndex(_key + "_shardstats", - msg.ShardId, - data, - CommandFlags.FireAndForget); - if (msg.ConnectionState == Discord.ConnectionState.Disconnected - || msg.ConnectionState == Discord.ConnectionState.Disconnecting) - { - Log.Error("!!! SHARD {0} IS IN {1} STATE !!!", msg.ShardId, msg.ConnectionState.ToString()); - - OnShardUnavailable(msg.ShardId); - } - else - { - // remove the shard from the waiting list if it's on it, - // because it's connected/connecting now - _shardRestartWaitingList.TryRemove(msg.ShardId); - } - return; - } - - private void OnShardUnavailable(int shardId) - { - //if the shard is dc'd, add it to the restart waiting list - if (!_shardRestartWaitingList.Add(shardId)) - { - //if it's already on the waiting list - //stop the shard - OnStop(shardId); - //add it to the start queue (start the shard) - _shardStartQueue.Enqueue(shardId); - //remove it from the waiting list - _shardRestartWaitingList.TryRemove(shardId); - } - } - - public async Task RunAsync() - { - //this task will complete when the initial start of the shards - //is complete, but will keep running in order to restart shards - //which are disconnected for too long - TaskCompletionSource tsc = new TaskCompletionSource(); - var _ = Task.Run(async () => - { - do - { - //start a shard which is scheduled for start every 6 seconds - while (_shardStartQueue.TryPeek(out var id)) - { - // if the shard is on the waiting list again - // remove it since it's starting up now - - _shardRestartWaitingList.TryRemove(id); - //if the task is already completed, - //it means the initial shard starting is done, - //and this is an auto-restart - if (tsc.Task.IsCompleted) - { - Log.Warning("Auto-restarting shard {0}, {1} more in queue.", id, _shardStartQueue.Count); - } - else - { - Log.Warning("Starting shard {0}, {1} more in queue.", id, _shardStartQueue.Count - 1); - } - var rem = _shardProcesses[id]; - if (rem != null) - { - try - { - rem.KillTree(); - rem.Dispose(); - } - catch { } - } - _shardProcesses[id] = StartShard(id); - _shardStartQueue.TryDequeue(out var __); - await Task.Delay(10000).ConfigureAwait(false); - } - tsc.TrySetResult(true); - await Task.Delay(6000).ConfigureAwait(false); - } - while (true); - // ^ keep checking for shards which need to be restarted - }); - - //restart unresponsive shards - _ = Task.Run(async () => - { - //after all shards have started initially - await tsc.Task.ConfigureAwait(false); - while (true) - { - await Task.Delay(15000).ConfigureAwait(false); - try - { - var db = _redis.GetDatabase(); - //get all shards which didn't communicate their status in the last 30 seconds - var all = db.ListRange(_creds.RedisKey() + "_shardstats") - .Select(x => JsonConvert.DeserializeObject(x)); - var statuses = all - .Where(x => x.Time < DateTime.UtcNow - TimeSpan.FromSeconds(30)) - .ToArray(); - - if (!statuses.Any()) - { -#if DEBUG - for (var i = 0; i < _shardProcesses.Length; i++) - { - var p = _shardProcesses[i]; - if (p is null || p.HasExited) - { - Log.Warning("Scheduling shard {0} for restart because it's process is stopped.", i); - _shardStartQueue.Enqueue(i); - } - } -#endif - } - else - { - for (var i = 0; i < statuses.Length; i++) - { - var s = statuses[i]; - OnStop(s.ShardId); - _shardStartQueue.Enqueue(s.ShardId); - - //to prevent shards which are already scheduled for restart to be scheduled again - s.Time = DateTime.UtcNow + TimeSpan.FromSeconds(60 * _shardStartQueue.Count); - db.ListSetByIndex(_key + "_shardstats", s.ShardId, - JsonConvert.SerializeObject(s), CommandFlags.FireAndForget); - Log.Warning("Shard {0} is scheduled for a restart because it's unresponsive.", s.ShardId); - } - } - } - catch (Exception ex) { Log.Error(ex, "Error in RunAsync"); throw; } - } - }); - - await tsc.Task.ConfigureAwait(false); - return; - } - - private Process StartShard(int shardId) - { - return Process.Start(new ProcessStartInfo() - { - FileName = _creds.ShardRunCommand, - Arguments = string.Format(_creds.ShardRunArguments, shardId, _curProcessId, "") - }); - // last "" in format is for backwards compatibility - // because current startup commands have {2} in them probably - } - - public async Task RunAndBlockAsync() - { - try - { - await RunAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Error(ex, "Unhandled exception in RunAsync"); - foreach (var p in _shardProcesses) - { - if (p is null) - continue; - try - { - p.KillTree(); - p.Dispose(); - } - catch { } - } - return; - } - - await Task.Delay(-1).ConfigureAwait(false); - } - } -} diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index aed7f838c..7e760caf2 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -184,13 +184,13 @@ namespace NadekoBot.Extensions } public static EmbedBuilder WithOkColor(this EmbedBuilder eb) => - eb.WithColor(NadekoBot.OkColor); + eb.WithColor(Bot.OkColor); public static EmbedBuilder WithPendingColor(this EmbedBuilder eb) => - eb.WithColor(NadekoBot.PendingColor); + eb.WithColor(Bot.PendingColor); public static EmbedBuilder WithErrorColor(this EmbedBuilder eb) => - eb.WithColor(NadekoBot.ErrorColor); + eb.WithColor(Bot.ErrorColor); public static ReactionEventWrapper OnReaction(this IUserMessage msg, DiscordSocketClient client, Func reactionAdded, Func reactionRemoved = null) {