Skip to content
Permalink
Browse files

Implemented reconnect config (#484)

  • Loading branch information...
Splamy committed May 8, 2019
1 parent 5fa69bc commit e0a44475d757bc87a01bc3b0c647ea6ebfde3b4b
@@ -1,6 +1,6 @@
<Project>
<ItemGroup>
<PackageReference Include="NLog" Version="4.6.2" />
<PackageReference Include="NLog" Version="4.6.3" />
<PackageReference Include="System.Memory" Version="4.5.2" />
<PackageReference Condition="'$(TargetFramework)'=='net46'" Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
@@ -120,7 +120,6 @@ public void RunBots(bool interactive)
if (id == null)
return "BotManager is shutting down";

// TODO change creation method
var botInjector = new BotInjector(coreInjector);
botInjector.AddModule(botInjector);
botInjector.AddModule(new TS3Client.Helper.Id(id.Value));
@@ -77,7 +77,7 @@ private bool CheckPaths()
return true;
}

public bool Save() => Save(fileName, false);
public bool Save() => Save(fileName, true);

// apply root_path to input path
public string GetFilePath(string file)
@@ -13,6 +13,9 @@ namespace TS3AudioBot.Config
using Nett;
using Playlists;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using TS3AudioBot.Helper;

public partial class ConfRoot : ConfigTable
{
@@ -131,6 +134,7 @@ public partial class ConfBot : ConfigTable

public ConfCommands Commands { get; } = Create<ConfCommands>("commands");
public ConfConnect Connect { get; } = Create<ConfConnect>("connect");
public ConfReconnect Reconnect { get; } = Create<ConfReconnect>("reconnect");
public ConfAudio Audio { get; } = Create<ConfAudio>("audio");
public ConfPlaylists Playlists { get; } = Create<ConfPlaylists>("playlists");
public ConfHistory History { get; } = Create<ConfHistory>("history");
@@ -185,6 +189,16 @@ public class ConfConnect : ConfigTable
public ConfIdentity Identity { get; } = Create<ConfIdentity>("identity");
}

public class ConfReconnect : ConfigTable
{
public ConfigArray<string> OnTimeout { get; } = new ConfigArray<string>("ontimeout", new[] { "1s", "2s", "5s", "10s", "30s", "1m", "5m", "repeat last" }) { Validator = ConfTimeExtensions.ValidateTime };
public ConfigArray<string> OnKick { get; } = new ConfigArray<string>("onkick", Array.Empty<string>()) { Validator = ConfTimeExtensions.ValidateTime };
public ConfigArray<string> OnBan { get; } = new ConfigArray<string>("onban", Array.Empty<string>()) { Validator = ConfTimeExtensions.ValidateTime };
public ConfigArray<string> OnError { get; } = new ConfigArray<string>("onerror", new[] { "30s", "repeat last" }) { Validator = ConfTimeExtensions.ValidateTime };
public ConfigArray<string> OnShutdown { get; } = new ConfigArray<string>("onshutdown", new[] { "5m" }) { Validator = ConfTimeExtensions.ValidateTime };
//public ConfigValue<int> MaxReconnect { get; } = new ConfigValue<int>("max_combined_reconnects", -1, "Each reconnect kind has an own counter and resets when ");
}

public class ConfIdentity : ConfigTable
{
public ConfigValue<string> PrivateKey { get; } = new ConfigValue<string>("key", "",
@@ -226,13 +240,11 @@ public class ConfAudioVolume : ConfigTable

public class ConfPlaylists : ConfigTable
{
protected override TomlTable.TableTypes TableType => TomlTable.TableTypes.Inline;

public ConfigValue<string> Path { get; } = new ConfigValue<string>("path", "playlists",
"Path to the folder where playlist files will be saved.");
public ConfigValue<PlaylistLocation> Share { get; } = new ConfigValue<PlaylistLocation>("share", PlaylistLocation.Bot,
" - Bot : Playlists per bot." +
" - Global : Playlists across all." +
" - Bot : Playlists per bot.\n" +
" - Global : Playlists across all.\n" +
" (- Server)");
}

@@ -298,4 +310,40 @@ public class ConfTsVersion : ConfigTable
public ConfigValue<string> Platform { get; } = new ConfigValue<string>("platform", string.Empty);
public ConfigValue<string> Sign { get; } = new ConfigValue<string>("sign", string.Empty);
}

public static class ConfTimeExtensions
{
public static TimeSpan? GetValueAsTime(this ConfigArray<string> conf, int index)
{
var value = conf.Value;
if (value.Count == 0)
return null;
var last = value[value.Count - 1];
var repeat = last == "repeat" || last == "repeat last"; // "repeat" might get removed for other loops, but for now keep as hidden alternative
var max = repeat ? value.Count - 2 : value.Count - 1;
if (index <= max)
return TomlTools.ParseTime(value[index]);
else
return TomlTools.ParseTime(value[max]);
}

public static E<string> ValidateTime(IReadOnlyList<string> value)
{
if (value.Count == 0)
return R.Ok;
var last = value[value.Count - 1];
var repeat = last == "repeat" || last == "repeat last";
if (repeat && value.Count == 1)
return $"Specified 'repeat' without any previous value.";

var max = repeat ? value.Count - 2 : value.Count - 1;
for (int i = 0; i <= max; i++)
{
var r = TomlTools.ValidateTime(value[i]);
if (!r.Ok)
return r;
}
return R.Ok;
}
}
}
@@ -47,6 +47,7 @@ public T Value
}
}
}
public Func<T, E<string>> Validator { get; set; }

public event EventHandler<ConfigChangedEventArgs<T>> Changed;

@@ -60,13 +61,23 @@ public ConfigValue(string key, T defaultVal, string doc = "") : base(key)

public override void FromToml(TomlObject tomlObject)
{
if (tomlObject != null)
if (tomlObject == null)
return;

if (!tomlObject.TryGetValue<T>(out var tomlValue))
{
if (tomlObject.TryGetValue<T>(out var tomlValue))
Value = tomlValue;
else
Log.Warn("Failed to read '{0}', got {1} with {2}", Key, tomlObject.ReadableTypeName, tomlObject.DumpToJson());
Log.Warn("Failed to read '{0}', got {1} with {2}", Key, tomlObject.ReadableTypeName, tomlObject.DumpToJson());
return;
}

var validate = Validator?.Invoke(tomlValue) ?? R.Ok;
if(!validate.Ok)
{
Log.Warn("Invalid value in '{0}', {1}", Key, validate.Error);
return;
}

Value = tomlValue;
}

public override void ToToml(bool writeDefaults, bool writeDocumentation)
@@ -76,6 +76,7 @@ private E<string> Run(bool interactive = false)
ConfRoot config = configResult.Value;
Config.Deprecated.UpgradeScript.CheckAndUpgrade(config);
ConfigUpgrade2.Upgrade(config.Configs.BotsPath.Value);
config.Save();

var builder = new DependencyBuilder(injector);

@@ -88,6 +89,8 @@ private E<string> Run(bool interactive = false)
builder.AddModule(config.Plugins);
builder.RequestModule<PluginManager>();
builder.AddModule(config.Web);
builder.AddModule(config.Web.Interface);
builder.AddModule(config.Web.Api);
builder.RequestModule<WebServer>();
builder.AddModule(config.Rights);
builder.RequestModule<RightsManager>();
@@ -16,10 +16,13 @@ namespace TS3AudioBot.Helper
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;

public static class TomlTools
{
private static readonly Regex TimeReg = new Regex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?(?:(\d+)ms)?$", Util.DefaultRegexConfig);

// *** Convenience method for getting values out of a toml object. ***

public static T[] TryGetValueArray<T>(this TomlObject tomlObj)
@@ -122,7 +125,7 @@ public static bool TryGetValue<T>(this TomlObject tomlObj, out T value)
{
try
{
value = (T)(object)XmlConvert.ToTimeSpan(((TomlString)tomlObj).Value);
value = (T)(object)ParseTime(((TomlString)tomlObj).Value);
return true;
}
catch (FormatException) { }
@@ -133,6 +136,42 @@ public static bool TryGetValue<T>(this TomlObject tomlObj, out T value)
return false;
}

public static TimeSpan? ParseTime(string value)
{
int AsNum(string svalue)
{
if (string.IsNullOrEmpty(svalue))
return 0;
return int.TryParse(svalue, out var num) ? num : 0;
}

var match = TimeReg.Match(value);
if (match.Success)
{
try
{
return new TimeSpan(0,
AsNum(match.Groups[1].Value),
AsNum(match.Groups[2].Value),
AsNum(match.Groups[3].Value),
AsNum(match.Groups[4].Value));
}
catch { }
}

try { return XmlConvert.ToTimeSpan(value); }
catch (FormatException) { }

return null;
}

public static E<string> ValidateTime(string value)
{
if (TimeReg.IsMatch(value))
return R.Ok;
return $"Value '{value}' is not a valid time.";
}

// *** Convenience method for setting values to a toml object. ***

public static TomlObject Set<T>(this TomlTable tomlTable, string key, T value)
@@ -51,12 +51,8 @@ public sealed class Ts3Client : IPlayerConnection, IDisposable

private bool closed = false;
private TickWorker reconnectTick = null;
public static readonly TimeSpan TooManyClonesReconnectDelay = TimeSpan.FromSeconds(30);
private int reconnectCounter;
private static readonly TimeSpan[] LostConnectionReconnectDelay = new[] {
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(5) };
private static int MaxReconnects { get; } = LostConnectionReconnectDelay.Length;
private ReconnectType? lastReconnect;

private readonly ConfBot config;
private readonly Ts3FullClient tsFullClient;
@@ -141,6 +137,8 @@ public E<string> Connect()
Log.Warn("Invalid config value for 'Level', enter a number between '0' and '160' or '-1' to adapt automatically.");
config.SaveWhenExists();

reconnectCounter = 0;
lastReconnect = null;
tsFullClient.QuitMessage = QuitMessages[Util.Random.Next(0, QuitMessages.Length)];
return ConnectClient();
}
@@ -509,7 +507,7 @@ public E<LocalStr> SetChannelCommander(bool isCommander)

#endregion

#region Event helper
#region Events

private void TsFullClient_OnErrorEvent(object sender, CommandError error)
{
@@ -548,44 +546,79 @@ private void TsFullClient_OnDisconnected(object sender, DisconnectEventArgs e)
break;

case Ts3ErrorCode.client_too_many_clones_connected:
if (reconnectCounter++ < MaxReconnects)
{
Log.Warn("Seems like another client with the same identity is already connected. Waiting {0:0} seconds to reconnect.",
TooManyClonesReconnectDelay.TotalSeconds);
reconnectTick = TickPool.RegisterTickOnce(() => ConnectClient(), TooManyClonesReconnectDelay);
return; // skip triggering event, we want to reconnect
}
Log.Warn("Seems like another client with the same identity is already connected.");
if (TryReconnect(ReconnectType.Error))
return;
break;

case Ts3ErrorCode.connect_failed_banned:
Log.Warn("This bot is banned.");
if (TryReconnect(ReconnectType.Ban))
return;
break;

default:
Log.Warn("Could not connect: {0}", error.ErrorFormat());
if (TryReconnect(ReconnectType.Error))
return;
break;
}
}
else
{
Log.Debug("Bot disconnected. Reason: {0}", e.ExitReason);

if (reconnectCounter < LostConnectionReconnectDelay.Length && !closed)
{
var delay = LostConnectionReconnectDelay[reconnectCounter++];
Log.Info("Trying to reconnect. Delaying reconnect for {0:0} seconds", delay.TotalSeconds);
reconnectTick = TickPool.RegisterTickOnce(() => ConnectClient(), delay);
if (TryReconnect(
e.ExitReason == Reason.Timeout ? ReconnectType.Timeout :
e.ExitReason == Reason.KickedFromServer ? ReconnectType.Kick :
e.ExitReason == Reason.ServerShutdown || e.ExitReason == Reason.ServerStopped ? ReconnectType.ServerShutdown :
e.ExitReason == Reason.Banned ? ReconnectType.Ban :
ReconnectType.None))
return;
}
}

if (reconnectCounter >= LostConnectionReconnectDelay.Length)
OnBotDisconnect?.Invoke(this, e);
}

private bool TryReconnect(ReconnectType type)
{
if (closed)
return false;

if (lastReconnect != type)
reconnectCounter = 0;
lastReconnect = type;

TimeSpan? delay;
switch (type)
{
Log.Warn("Could not (re)connect after {0} tries. Giving up.", reconnectCounter);
case ReconnectType.Timeout: delay = config.Reconnect.OnTimeout.GetValueAsTime(reconnectCounter); break;
case ReconnectType.Kick: delay = config.Reconnect.OnKick.GetValueAsTime(reconnectCounter); break;
case ReconnectType.Ban: delay = config.Reconnect.OnBan.GetValueAsTime(reconnectCounter); break;
case ReconnectType.ServerShutdown: delay = config.Reconnect.OnShutdown.GetValueAsTime(reconnectCounter); break;
case ReconnectType.Error: delay = config.Reconnect.OnError.GetValueAsTime(reconnectCounter); break;
case ReconnectType.None:
return false;
default: throw Util.UnhandledDefault(type);
}
OnBotDisconnect?.Invoke(this, e);
reconnectCounter++;

if (delay == null)
{
Log.Info("Reconnect strategy for '{0}' has reached the end. Closing instance.", type);
return false;
}

Log.Info("Trying to reconnect because of {0}. Delaying reconnect for {1:0} seconds", type, delay.Value.TotalSeconds);
reconnectTick = TickPool.RegisterTickOnce(() => ConnectClient(), delay);
return true;
}

private void TsFullClient_OnConnected(object sender, EventArgs e)
{
StopReconnectTickWorker();
reconnectCounter = 0;
lastReconnect = null;
OnBotConnected?.Invoke(this, EventArgs.Empty);
}

@@ -700,6 +733,16 @@ public void Dispose()
encoderPipe?.Dispose();
tsFullClient.Dispose();
}

enum ReconnectType
{
None,
Timeout,
Kick,
Ban,
ServerShutdown,
Error
}
}

namespace RExtensions
Oops, something went wrong.

0 comments on commit e0a4447

Please sign in to comment.
You can’t perform that action at this time.