1
Fork 0
mirror of https://gitlab.com/Kwoth/nadekobot.git synced 2024-10-02 20:13:13 +00:00

- NadekoBot class renamed to Bot

- Implemented grpc based coordinator. Supports restarting, killing single or all shards, as well as getting current shard statuses. (Adaptation of the one used by the public bot)
- Coord is setup via coord.yml file
- Methods from SelfService which deal with shard/bot restart etc have been moved to ICoordinator (with GrpcRemoteCoordinator being the default implementation atm)
- Vastly simplified NadekoBot/Program.cs
This commit is contained in:
Kwoth 2021-06-19 13:13:54 +02:00
parent d8c7cdc7f4
commit c86bf6f300
58 changed files with 1212 additions and 635 deletions

View file

@ -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<CoordinatorRunner>();
services.AddSingleton<IHostedService, CoordinatorRunner>(
serviceProvider => serviceProvider.GetService<CoordinatorRunner>());
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<CoordinatorService>();
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");
});
});
}
}
}

View file

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

View file

@ -1,8 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<RollForward>Major</RollForward>
</PropertyGroup>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\coordinator.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.38.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="YamlDotNet" Version="11.2.0" />
</ItemGroup>
</Project>

View file

@ -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<CoordStartup>();
});
LogSetup.SetupLogger("coord");
Log.Information("Starting coordinator... Pid: {ProcessId}", Environment.ProcessId);
CreateHostBuilder(args).Build().Run();

View file

@ -0,0 +1,13 @@
{
"profiles": {
"Nadeko.Coordinator": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": false,
"applicationUrl": "http://localhost:3442;https://localhost:3443",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

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

View file

@ -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<Config>(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<CoordState>(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<ShardStatus> GetAllStatuses()
{
lock (locker)
{
var toReturn = new List<ShardStatus>(_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<Config>(text);
SaveConfig(in config);
ReloadConfig();
}
}
}

View file

@ -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<HeartbeatReply> 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<ReshardReply> Reshard(ReshardRequest request, ServerCallContext context)
{
_runner.SetShardCount(request.Shards);
return Task.FromResult(new ReshardReply());
}
public override Task<RestartShardReply> RestartShard(RestartShardRequest request, ServerCallContext context)
{
_runner.RestartShard(request.ShardId, request.Queue);
return Task.FromResult(new RestartShardReply());
}
public override Task<ReloadReply> Reload(ReloadRequest request, ServerCallContext context)
{
_runner.ReloadConfig();
return Task.FromResult(new ReloadReply());
}
public override Task<GetStatusReply> GetStatus(GetStatusRequest request, ServerCallContext context)
{
var status = _runner.GetShardStatus(request.ShardId);
return Task.FromResult(StatusToStatusReply(status));
}
public override Task<GetAllStatusesReply> 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<RestartAllReply> RestartAllShards(RestartAllRequest request, ServerCallContext context)
{
_runner.RestartAll(request.Nuke);
return Task.FromResult(new RestartAllReply());
}
public override async Task<DieReply> 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<SetConfigTextReply> 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<GetConfigTextReply> GetConfigText(GetConfigTextRequest request, ServerCallContext context)
{
var text = _runner.GetConfigText();
return Task.FromResult(new GetConfigTextReply()
{
ConfigYml = text,
});
}
}
}

View file

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

View file

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace NadekoBot.Coordinator
{
public class CoordState
{
public List<JsonStatusObject> StatusObjects { get; init; }
}
}

View file

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

View file

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

View file

@ -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"
}
}
}
}

View file

@ -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"
}
}
}
}

View file

@ -0,0 +1,5 @@
TotalShards: 1
RecheckIntervalMs: 5000
ShardStartCommand: dotnet
ShardStartArgs: run -p "<absolute path to NadekoBot.csproj>" --no-build -- {0}
UnresponsiveSec: 30

View file

@ -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

View file

@ -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<ShardComMessage>(x))
.Sum(x => x.Guilds);
public string Mention { get; set; }
public event Func<GuildConfig, Task> 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<ulong> 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<ICoordinator, RemoteGrpcCoordinator>()
.AddSingleton<IReadyExecutor>(x => (IReadyExecutor)x.GetRequiredService<ICoordinator>());
s.AddSingleton<IReadyExecutor>(x => x.GetService<SelfService>());
s.AddSingleton<IReadyExecutor>(x => x.GetService<CustomReactionsService>());
s.AddSingleton<IReadyExecutor>(x => x.GetService<RepeaterService>());
//initialize Services
Services = s.BuildServiceProvider();
var commandHandler = Services.GetService<CommandHandler>();
@ -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<IDataCache>().Redis.GetSubscriber();

View file

@ -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,
};
}
}

View file

@ -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<ShardComMessage>(data));
}, StackExchange.Redis.CommandFlags.FireAndForget);
}
public event Func<ShardComMessage, Task> OnDataReceived = delegate { return Task.CompletedTask; };
}
}

View file

@ -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

View file

@ -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<SelfService>
{
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);

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -16,7 +16,7 @@ namespace NadekoBot.Modules.Administration.Services
private readonly ConcurrentDictionary<ulong, TimeZoneInfo> _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)

View file

@ -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<IPlaceholderProvider> phProviders)
{
_db = db;

View file

@ -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;

View file

@ -20,7 +20,7 @@ namespace NadekoBot.Modules.Administration.Services
private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
public RoleCommandsService(DiscordSocketClient client, DbService db,
NadekoBot bot)
Bot bot)
{
_db = db;
_client = client;

View file

@ -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<ShardComMessage> GetAllShardStatuses()
{
var db = _cache.Redis.GetDatabase();
return db.ListRange(_creds.RedisKey() + "_shardstats")
.Select(x => JsonConvert.DeserializeObject<ShardComMessage>(x));
}
}
}

View file

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

View file

@ -21,7 +21,7 @@ namespace NadekoBot.Modules.Administration.Services
public ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IRole>> VcRoles { get; }
public ConcurrentDictionary<ulong, ConcurrentQueue<(bool, IGuildUser, IRole)>> ToAssign { get; }
public VcRoleService(DiscordSocketClient client, NadekoBot bot, DbService db)
public VcRoleService(DiscordSocketClient client, Bot bot, DbService db)
{
_db = db;
_client = client;

View file

@ -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)
{

View file

@ -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;

View file

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

View file

@ -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;

View file

@ -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)

View file

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

View file

@ -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<ulong, ConcurrentHashSet<CommandCooldown>>(
bot.AllGuildConfigs.ToDictionary(k => k.GuildId,

View file

@ -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"))

View file

@ -24,7 +24,7 @@ namespace NadekoBot.Modules.Searches.Services
private readonly ConcurrentDictionary<string, DateTime> _lastPosts =
new ConcurrentDictionary<string, DateTime>();
public FeedsService(NadekoBot bot, DbService db, DiscordSocketClient client)
public FeedsService(Bot bot, DbService db, DiscordSocketClient client)
{
_db = db;

View file

@ -61,7 +61,7 @@ namespace NadekoBot.Modules.Searches.Services
private readonly List<string> _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;

View file

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

View file

@ -34,7 +34,7 @@ namespace NadekoBot.Modules.Searches
{
var res = await http.GetStringAsync($"{_xkcdUrl}/info.0.json").ConfigureAwait(false);
var comic = JsonConvert.DeserializeObject<XkcdComic>(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<XkcdComic>(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))

View file

@ -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)

View file

@ -23,7 +23,7 @@ namespace NadekoBot.Modules.Utility.Services
private readonly DiscordSocketClient _client;
private readonly ConcurrentDictionary<ulong, StreamRoleSettings> guildSettings;
public StreamRoleService(DiscordSocketClient client, DbService db, NadekoBot bot)
public StreamRoleService(DiscordSocketClient client, DbService db, Bot bot)
{
_db = db;
_client = client;

View file

@ -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;

View file

@ -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]

View file

@ -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)
{

View file

@ -18,6 +18,12 @@
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.52.0.2343" />
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084" />
<PackageReference Include="Google.Protobuf" Version="3.13.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.32.0" />
<PackageReference Include="Grpc.Tools" Version="2.32.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Html2Markdown" Version="4.0.0.427" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.7">
@ -52,6 +58,9 @@
<ItemGroup>
<Compile Remove="credentials.json" />
<Protobuf Include="..\NadekoBot.Coordinator\Protos\coordinator.proto" GrpcServices="Client">
<Link>Protos\coordinator.proto</Link>
</Protobuf>
<None Update="data\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

View file

@ -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();

View file

@ -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<IEarlyBehavior> _earlyBehaviors;
private IEnumerable<IInputTransformer> _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;

View file

@ -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;

View file

@ -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<ShardStatus> 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; }
}
}

View file

@ -22,7 +22,7 @@ namespace NadekoBot.Core.Services.Impl
private static readonly Dictionary<string, CommandData> _commandData = JsonConvert.DeserializeObject<Dictionary<string, CommandData>>(
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;

View file

@ -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<ShardStatus> 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
};
}
}

View file

@ -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()

View file

@ -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<int> _set = new HashSet<int>();
private readonly Queue<int> _queue = new Queue<int>();
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<int> _shardRestartWaitingList =
new ConcurrentHashSet<int>();
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<int>(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<ShardComMessage>(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<bool> tsc = new TaskCompletionSource<bool>();
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<ShardComMessage>(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);
}
}
}

View file

@ -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<SocketReaction, Task> reactionAdded, Func<SocketReaction, Task> reactionRemoved = null)
{