Decent work on config reload ability

It was always annoying to restart whole ASF process just to issue a small config update for one of the bots, with this commit - auto reload has been implemented. ASF will automatically restart given bot instance if it's config file gets changed. With introduction of this commit, we also ditch StartOnLaunch property (because it no longer means sense, any disabled bot can be started through config edit), and we also ditch auto-shutdown of ASF process, because it's possible to edit bot config file to start it.
Some further work might be needed, this is pretty much work in progress. It would be super if ASF was also able to automatically decent new bot configs, and create new bots for them, but this is initial step.
This commit is contained in:
JustArchi
2016-10-07 00:04:08 +02:00
parent 1ea731d878
commit f108829129
9 changed files with 219 additions and 95 deletions

View File

@@ -54,7 +54,6 @@ namespace ArchiSteamFarm {
internal readonly string BotName;
internal readonly ArchiHandler ArchiHandler;
internal readonly ArchiWebHandler ArchiWebHandler;
internal readonly BotConfig BotConfig;
private readonly string SentryFile;
private readonly BotDatabase BotDatabase;
@@ -66,25 +65,28 @@ namespace ArchiSteamFarm {
private readonly ConcurrentHashSet<ulong> HandledGifts = new ConcurrentHashSet<ulong>();
private readonly ConcurrentHashSet<ulong> SteamFamilySharingIDs = new ConcurrentHashSet<ulong>();
private readonly ConcurrentHashSet<uint> OwnedPackageIDs = new ConcurrentHashSet<uint>();
private readonly SemaphoreSlim InitializationSemaphore = new SemaphoreSlim(1);
private readonly SteamApps SteamApps;
private readonly SteamClient SteamClient;
private readonly SteamFriends SteamFriends;
private readonly SteamUser SteamUser;
private readonly Timer AcceptConfirmationsTimer, HeartBeatTimer, SendItemsTimer;
private readonly Timer HeartBeatTimer;
private readonly Trading Trading;
[JsonProperty]
internal bool KeepRunning { get; private set; }
internal BotConfig BotConfig { get; private set; }
internal bool HasMobileAuthenticator => BotDatabase.MobileAuthenticator != null;
internal bool IsConnectedAndLoggedOn => SteamClient.IsConnected && (SteamClient.SteamID != null);
internal bool IsFarmingPossible => !PlayingBlocked && (LibraryLockedBySteamID == 0);
[JsonProperty]
internal bool KeepRunning { get; private set; }
private bool FirstTradeSent, PlayingBlocked, SkipFirstShutdown;
private string AuthCode, TwoFactorCode;
private ulong LibraryLockedBySteamID;
private EResult LastLogOnResult;
private Timer FamilySharingInactivityTimer;
private Timer AcceptConfirmationsTimer, FamilySharingInactivityTimer, SendItemsTimer;
internal static string GetAPIStatus() {
var response = new {
@@ -165,9 +167,9 @@ namespace ArchiSteamFarm {
return;
}
if (!BotConfig.Enabled) {
Logging.LogGenericInfo("Not initializing this instance because it's disabled in config file", botName);
return;
// Register bot as available for ASF
if (!Bots.TryAdd(botName, this)) {
throw new ArgumentException("That bot is already defined!");
}
string botDatabaseFile = botPath + ".db";
@@ -237,7 +239,11 @@ namespace ArchiSteamFarm {
CallbackManager.Subscribe<ArchiHandler.SharedLibraryLockStatusCallback>(OnSharedLibraryLockStatus);
ArchiWebHandler = new ArchiWebHandler(this);
CardsFarmer = new CardsFarmer(this, BotConfig.Paused);
CardsFarmer = new CardsFarmer(this) {
Paused = BotConfig.Paused
};
Trading = new Trading(this);
HeartBeatTimer = new Timer(
@@ -247,43 +253,103 @@ namespace ArchiSteamFarm {
TimeSpan.FromMinutes(1) // Period
);
if (BotConfig.AcceptConfirmationsPeriod > 0) {
AcceptConfirmationsTimer = new Timer(
async e => await AcceptConfirmations(true).ConfigureAwait(false),
null,
TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod) + TimeSpan.FromMinutes(0.2 * Bots.Count), // Delay
TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod) // Period
);
Initialize().Forget();
}
if ((BotConfig.SendTradePeriod > 0) && (BotConfig.SteamMasterID != 0)) {
SendItemsTimer = new Timer(
async e => await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false),
null,
TimeSpan.FromHours(BotConfig.SendTradePeriod) + TimeSpan.FromMinutes(Bots.Count), // Delay
TimeSpan.FromHours(BotConfig.SendTradePeriod) // Period
);
}
private async Task Initialize() {
BotConfig.NewConfigLoaded += OnNewConfigLoaded;
BotConfig.InitializeWatcher();
// Register bot as available for ASF
if (!Bots.TryAdd(botName, this)) {
throw new ArgumentException("That bot is already defined!");
}
if (!BotConfig.StartOnLaunch) {
if (!BotConfig.Enabled) {
Logging.LogGenericInfo("Not starting this instance because it's disabled in config file", BotName);
return;
}
// Start
Start().Forget();
await Start().ConfigureAwait(false);
}
private async void OnNewConfigLoaded(object sender, BotConfig.BotConfigEventArgs args) {
if ((sender == null) || (args == null)) {
Logging.LogNullError(nameof(sender) + " || " + nameof(args), BotName);
return;
}
if (args.BotConfig == null) {
Destroy();
return;
}
if (args.BotConfig == BotConfig) {
return;
}
await InitializationSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (args.BotConfig == BotConfig) {
return;
}
Stop();
BotConfig.NewConfigLoaded -= OnNewConfigLoaded;
BotConfig = args.BotConfig;
CardsFarmer.Paused = BotConfig.Paused;
if (BotConfig.AcceptConfirmationsPeriod > 0) {
TimeSpan delay = TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod) + TimeSpan.FromMinutes(0.2 * Bots.Count);
TimeSpan period = TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod);
if (AcceptConfirmationsTimer == null) {
AcceptConfirmationsTimer = new Timer(
async e => await AcceptConfirmations(true).ConfigureAwait(false),
null,
delay, // Delay
period // Period
);
} else {
AcceptConfirmationsTimer.Change(delay, period);
}
} else {
AcceptConfirmationsTimer?.Change(Timeout.Infinite, Timeout.Infinite);
AcceptConfirmationsTimer?.Dispose();
}
if ((BotConfig.SendTradePeriod > 0) && (BotConfig.SteamMasterID != 0)) {
TimeSpan delay = TimeSpan.FromHours(BotConfig.SendTradePeriod) + TimeSpan.FromMinutes(Bots.Count);
TimeSpan period = TimeSpan.FromHours(BotConfig.SendTradePeriod);
if (SendItemsTimer == null) {
SendItemsTimer = new Timer(
async e => await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false),
null,
delay, // Delay
period // Period
);
} else {
SendItemsTimer.Change(delay, period);
}
} else {
SendItemsTimer?.Change(Timeout.Infinite, Timeout.Infinite);
SendItemsTimer?.Dispose();
}
await Initialize().ConfigureAwait(false);
} finally {
InitializationSemaphore.Release();
}
}
public void Dispose() {
// Those are objects that are always being created if constructor doesn't throw exception
ArchiWebHandler.Dispose();
BotConfig.NewConfigLoaded -= OnNewConfigLoaded;
BotConfig.Dispose();
CardsFarmer.Dispose();
HeartBeatTimer.Dispose();
HandledGifts.Dispose();
InitializationSemaphore.Dispose();
SteamFamilySharingIDs.Dispose();
OwnedPackageIDs.Dispose();
Trading.Dispose();
@@ -380,8 +446,6 @@ namespace ArchiSteamFarm {
if (SteamClient.IsConnected) {
Disconnect();
}
Events.OnBotShutdown();
}
internal async Task LootIfNeeded() {
@@ -533,6 +597,13 @@ namespace ArchiSteamFarm {
}
}
private void Destroy() {
Stop();
Bot ignored;
Bots.TryRemove(BotName, out ignored);
}
private async Task HeartBeat() {
if (!IsConnectedAndLoggedOn) {
return;

View File

@@ -28,13 +28,22 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace ArchiSteamFarm {
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
[SuppressMessage("ReSharper", "ConvertToConstant.Local")]
[SuppressMessage("ReSharper", "ConvertToConstant.Global")]
internal sealed class BotConfig {
internal sealed class BotConfig : IDisposable {
internal sealed class BotConfigEventArgs : EventArgs {
internal readonly BotConfig BotConfig;
internal BotConfigEventArgs(BotConfig botConfig = null) {
BotConfig = botConfig;
}
}
internal enum EFarmingOrder : byte {
Unordered,
AppIDsAscending,
@@ -53,9 +62,6 @@ namespace ArchiSteamFarm {
[JsonProperty(Required = Required.DisallowNull)]
internal readonly bool Paused = false;
[JsonProperty(Required = Required.DisallowNull)]
internal readonly bool StartOnLaunch = true;
[JsonProperty]
internal string SteamLogin { get; set; }
@@ -131,6 +137,12 @@ namespace ArchiSteamFarm {
[JsonProperty(Required = Required.DisallowNull)]
private readonly CryptoHelper.ECryptoMethod PasswordFormat = CryptoHelper.ECryptoMethod.PlainText;
internal event EventHandler<BotConfigEventArgs> NewConfigLoaded;
private string FilePath;
private FileSystemWatcher FileSystemWatcher;
private DateTime LastWriteTime = DateTime.MinValue;
internal static BotConfig Load(string filePath) {
if (string.IsNullOrEmpty(filePath)) {
Logging.LogNullError(nameof(filePath));
@@ -155,6 +167,8 @@ namespace ArchiSteamFarm {
return null;
}
botConfig.FilePath = filePath;
// Support encrypted passwords
if ((botConfig.PasswordFormat != CryptoHelper.ECryptoMethod.PlainText) && !string.IsNullOrEmpty(botConfig.SteamPassword)) {
// In worst case password will result in null, which will have to be corrected by user during runtime
@@ -178,5 +192,78 @@ namespace ArchiSteamFarm {
// This constructor is used only by deserializer
private BotConfig() { }
internal void InitializeWatcher() {
if (FileSystemWatcher != null) {
return;
}
if (string.IsNullOrEmpty(FilePath)) {
Logging.LogNullError(nameof(FilePath));
return;
}
string fileDirectory = Path.GetDirectoryName(FilePath);
if (string.IsNullOrEmpty(fileDirectory)) {
Logging.LogNullError(nameof(fileDirectory));
return;
}
string fileName = Path.GetFileName(FilePath);
if (string.IsNullOrEmpty(fileName)) {
Logging.LogNullError(nameof(fileName));
return;
}
FileSystemWatcher = new FileSystemWatcher(fileDirectory, fileName) {
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite
};
FileSystemWatcher.Changed += OnChanged;
FileSystemWatcher.Deleted += OnDeleted;
FileSystemWatcher.Renamed += OnRenamed;
FileSystemWatcher.EnableRaisingEvents = true;
}
private async void OnChanged(object sender, FileSystemEventArgs e) {
if (NewConfigLoaded == null) {
return;
}
DateTime lastWriteTime;
lock (FileSystemWatcher) {
lastWriteTime = File.GetLastWriteTime(FilePath);
if (LastWriteTime == lastWriteTime) {
return;
}
LastWriteTime = lastWriteTime;
}
// It's entirely possible that some process is still accessing our file, allow at least a second before trying to read it
await Task.Delay(1000).ConfigureAwait(false);
// It's also possible that we got some other event in the meantime
if (lastWriteTime != LastWriteTime) {
return;
}
NewConfigLoaded?.Invoke(this, new BotConfigEventArgs(Load(FilePath)));
}
private void OnDeleted(object sender, FileSystemEventArgs e) => NewConfigLoaded?.Invoke(this, new BotConfigEventArgs());
private void OnRenamed(object sender, RenamedEventArgs e) => NewConfigLoaded?.Invoke(this, new BotConfigEventArgs());
public void Dispose() {
if (FileSystemWatcher == null) {
return;
}
FileSystemWatcher.Changed -= OnChanged;
FileSystemWatcher.Deleted -= OnDeleted;
FileSystemWatcher.Renamed -= OnRenamed;
FileSystemWatcher.Dispose();
}
}
}

View File

@@ -92,17 +92,16 @@ namespace ArchiSteamFarm {
private readonly Timer IdleFarmingTimer;
[JsonProperty]
internal bool Paused { get; private set; }
internal bool Paused { get; set; }
private bool KeepFarming, NowFarming;
internal CardsFarmer(Bot bot, bool paused) {
internal CardsFarmer(Bot bot) {
if (bot == null) {
throw new ArgumentNullException(nameof(bot));
}
Bot = bot;
Paused = paused;
if (Program.GlobalConfig.IdleFarmingPeriod > 0) {
IdleFarmingTimer = new Timer(
@@ -114,17 +113,6 @@ namespace ArchiSteamFarm {
}
}
public void Dispose() {
// Those are objects that are always being created if constructor doesn't throw exception
CurrentGamesFarming.Dispose();
GamesToFarm.Dispose();
FarmingSemaphore.Dispose();
FarmResetEvent.Dispose();
// Those are objects that might be null and the check should be in-place
IdleFarmingTimer?.Dispose();
}
internal async Task Pause() {
Paused = true;
if (NowFarming) {
@@ -670,5 +658,16 @@ namespace ArchiSteamFarm {
Logging.LogGenericInfo("Stopped farming: " + string.Join(", ", games.Select(game => game.AppID)), Bot.BotName);
return success;
}
public void Dispose() {
// Those are objects that are always being created if constructor doesn't throw exception
CurrentGamesFarming.Dispose();
GamesToFarm.Dispose();
FarmingSemaphore.Dispose();
FarmResetEvent.Dispose();
// Those are objects that might be null and the check should be in-place
IdleFarmingTimer?.Dispose();
}
}
}

View File

@@ -1,19 +1,7 @@
using System.Linq;
using System.Threading.Tasks;
using SteamKit2;
using SteamKit2;
namespace ArchiSteamFarm {
internal static class Events {
internal static void OnBotShutdown() {
if (Program.ShutdownSequenceInitialized || Program.WCF.IsServerRunning() || Bot.Bots.Values.Any(bot => bot.KeepRunning)) {
return;
}
Logging.LogGenericInfo("No bots are running, exiting");
Task.Delay(5000).Wait();
Program.Shutdown();
}
internal static void OnStateUpdated(Bot bot, SteamFriends.PersonaStateCallback callback) {
}
}

View File

@@ -34,7 +34,7 @@ namespace ArchiSteamFarm {
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentHashSet<IPEndPoint> Servers = new ConcurrentHashSet<IPEndPoint>();
internal event EventHandler ServerListUpdated = delegate { };
internal event EventHandler ServerListUpdated;
public Task<IEnumerable<IPEndPoint>> FetchServerListAsync() => Task.FromResult<IEnumerable<IPEndPoint>>(Servers);
@@ -50,7 +50,7 @@ namespace ArchiSteamFarm {
return Task.Delay(0);
}
ServerListUpdated(this, EventArgs.Empty);
ServerListUpdated?.Invoke(this, EventArgs.Empty);
return Task.Delay(0);
}

View File

@@ -40,18 +40,18 @@ namespace ArchiSteamFarm {
Server // Normal + WCF server
}
internal static readonly WCF WCF = new WCF();
private static readonly object ConsoleLock = new object();
private static readonly ManualResetEventSlim ShutdownResetEvent = new ManualResetEventSlim(false);
private static readonly WCF WCF = new WCF();
internal static bool IsRunningAsService { get; private set; }
internal static bool ShutdownSequenceInitialized { get; private set; }
internal static EMode Mode { get; private set; } = EMode.Normal;
internal static GlobalConfig GlobalConfig { get; private set; }
internal static GlobalDatabase GlobalDatabase { get; private set; }
internal static WebBrowser WebBrowser { get; private set; }
private static bool ShutdownSequenceInitialized;
internal static void Exit(byte exitCode = 0) {
Shutdown();
Environment.Exit(exitCode);
@@ -131,7 +131,7 @@ namespace ArchiSteamFarm {
return !string.IsNullOrEmpty(result) ? result.Trim() : null;
}
internal static void Shutdown() {
private static void Shutdown() {
if (!InitShutdownSequence()) {
return;
}
@@ -340,8 +340,6 @@ namespace ArchiSteamFarm {
// Before attempting to connect, initialize our list of CMs
Bot.InitializeCMs(GlobalDatabase.CellID, GlobalDatabase.ServerListProvider);
bool isRunning = false;
foreach (string botName in Directory.EnumerateFiles(SharedInfo.ConfigDirectory, "*.json").Select(Path.GetFileNameWithoutExtension)) {
switch (botName) {
case SharedInfo.ASF:
@@ -350,19 +348,7 @@ namespace ArchiSteamFarm {
continue;
}
Bot bot = new Bot(botName);
if ((bot.BotConfig == null) || !bot.BotConfig.Enabled) {
continue;
}
if (bot.BotConfig.StartOnLaunch) {
isRunning = true;
}
}
// Check if we got any bots running
if (!isRunning) {
Events.OnBotShutdown();
new Bot(botName).Forget();
}
}

View File

@@ -27,13 +27,12 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace ArchiSteamFarm {
internal static class Utilities {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[SuppressMessage("ReSharper", "UnusedParameter.Global")]
internal static void Forget(this Task task) { }
internal static void Forget(this object obj) { }
internal static string GetCookieValue(this CookieContainer cookieContainer, string url, string name) {
if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(name)) {

View File

@@ -85,8 +85,6 @@ namespace ArchiSteamFarm {
StopServer();
}
internal bool IsServerRunning() => ServiceHost != null;
internal void StartServer() {
if (ServiceHost != null) {
return;

View File

@@ -61,10 +61,6 @@ namespace ConfigGenerator {
[JsonProperty(Required = Required.DisallowNull)]
public bool Paused { get; set; } = false;
[Category("\tAdvanced")]
[JsonProperty(Required = Required.DisallowNull)]
public bool StartOnLaunch { get; set; } = true;
[Category("\t\tCore")]
[JsonProperty]
public string SteamLogin { get; set; } = null;