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:
parent
d8c7cdc7f4
commit
c86bf6f300
58 changed files with 1212 additions and 635 deletions
49
src/NadekoBot.Coordinator/CoordStartup.cs
Normal file
49
src/NadekoBot.Coordinator/CoordStartup.cs
Normal 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");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
37
src/NadekoBot.Coordinator/LogSetup.cs
Normal file
37
src/NadekoBot.Coordinator/LogSetup.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
20
src/NadekoBot.Coordinator/Program.cs
Normal file
20
src/NadekoBot.Coordinator/Program.cs
Normal 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();
|
13
src/NadekoBot.Coordinator/Properties/launchSettings.json
Normal file
13
src/NadekoBot.Coordinator/Properties/launchSettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
127
src/NadekoBot.Coordinator/Protos/coordinator.proto
Normal file
127
src/NadekoBot.Coordinator/Protos/coordinator.proto
Normal 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;
|
||||
}
|
448
src/NadekoBot.Coordinator/Services/CoordinatorRunner.cs
Normal file
448
src/NadekoBot.Coordinator/Services/CoordinatorRunner.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
147
src/NadekoBot.Coordinator/Services/CoordinatorService.cs
Normal file
147
src/NadekoBot.Coordinator/Services/CoordinatorService.cs
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
21
src/NadekoBot.Coordinator/Shared/Config.cs
Normal file
21
src/NadekoBot.Coordinator/Shared/Config.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
9
src/NadekoBot.Coordinator/Shared/CoordState.cs
Normal file
9
src/NadekoBot.Coordinator/Shared/CoordState.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Coordinator
|
||||
{
|
||||
public class CoordState
|
||||
{
|
||||
public List<JsonStatusObject> StatusObjects { get; init; }
|
||||
}
|
||||
}
|
11
src/NadekoBot.Coordinator/Shared/JsonStatusObject.cs
Normal file
11
src/NadekoBot.Coordinator/Shared/JsonStatusObject.cs
Normal 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; }
|
||||
}
|
||||
}
|
15
src/NadekoBot.Coordinator/Shared/ShardStatus.cs
Normal file
15
src/NadekoBot.Coordinator/Shared/ShardStatus.cs
Normal 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
|
||||
);
|
||||
}
|
20
src/NadekoBot.Coordinator/appsettings.Development.json
Normal file
20
src/NadekoBot.Coordinator/appsettings.Development.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
src/NadekoBot.Coordinator/appsettings.json
Normal file
20
src/NadekoBot.Coordinator/appsettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
src/NadekoBot.Coordinator/coord.yml
Normal file
5
src/NadekoBot.Coordinator/coord.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
TotalShards: 1
|
||||
RecheckIntervalMs: 5000
|
||||
ShardStartCommand: dotnet
|
||||
ShardStartArgs: run -p "<absolute path to NadekoBot.csproj>" --no-build -- {0}
|
||||
UnresponsiveSec: 30
|
|
@ -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
|
||||
|
|
|
@ -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();
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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; };
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
22
src/NadekoBot/Services/ICoordinator.cs
Normal file
22
src/NadekoBot/Services/ICoordinator.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
138
src/NadekoBot/Services/Impl/RemoteGrpcCoordinator.cs
Normal file
138
src/NadekoBot/Services/Impl/RemoteGrpcCoordinator.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue