diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index b2231d9cc..89ef78cd2 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -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 HandledGifts = new ConcurrentHashSet(); private readonly ConcurrentHashSet SteamFamilySharingIDs = new ConcurrentHashSet(); private readonly ConcurrentHashSet OwnedPackageIDs = new ConcurrentHashSet(); + 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(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; diff --git a/ArchiSteamFarm/BotConfig.cs b/ArchiSteamFarm/BotConfig.cs index 6dabd1a2d..45d4da21c 100644 --- a/ArchiSteamFarm/BotConfig.cs +++ b/ArchiSteamFarm/BotConfig.cs @@ -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 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(); + } } } diff --git a/ArchiSteamFarm/CardsFarmer.cs b/ArchiSteamFarm/CardsFarmer.cs index 7f92608e4..326076363 100755 --- a/ArchiSteamFarm/CardsFarmer.cs +++ b/ArchiSteamFarm/CardsFarmer.cs @@ -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(); + } } } diff --git a/ArchiSteamFarm/Events.cs b/ArchiSteamFarm/Events.cs index dc84442eb..ede6d1cac 100644 --- a/ArchiSteamFarm/Events.cs +++ b/ArchiSteamFarm/Events.cs @@ -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) { } } diff --git a/ArchiSteamFarm/InMemoryServerListProvider.cs b/ArchiSteamFarm/InMemoryServerListProvider.cs index 65905f866..a2e947eb1 100644 --- a/ArchiSteamFarm/InMemoryServerListProvider.cs +++ b/ArchiSteamFarm/InMemoryServerListProvider.cs @@ -34,7 +34,7 @@ namespace ArchiSteamFarm { [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentHashSet Servers = new ConcurrentHashSet(); - internal event EventHandler ServerListUpdated = delegate { }; + internal event EventHandler ServerListUpdated; public Task> FetchServerListAsync() => Task.FromResult>(Servers); @@ -50,7 +50,7 @@ namespace ArchiSteamFarm { return Task.Delay(0); } - ServerListUpdated(this, EventArgs.Empty); + ServerListUpdated?.Invoke(this, EventArgs.Empty); return Task.Delay(0); } diff --git a/ArchiSteamFarm/Program.cs b/ArchiSteamFarm/Program.cs index ccc83b988..e05b53b2b 100644 --- a/ArchiSteamFarm/Program.cs +++ b/ArchiSteamFarm/Program.cs @@ -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(); } } diff --git a/ArchiSteamFarm/Utilities.cs b/ArchiSteamFarm/Utilities.cs index 84424008e..7a1657026 100644 --- a/ArchiSteamFarm/Utilities.cs +++ b/ArchiSteamFarm/Utilities.cs @@ -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)) { diff --git a/ArchiSteamFarm/WCF.cs b/ArchiSteamFarm/WCF.cs index 27b37013a..08b8dae7a 100644 --- a/ArchiSteamFarm/WCF.cs +++ b/ArchiSteamFarm/WCF.cs @@ -85,8 +85,6 @@ namespace ArchiSteamFarm { StopServer(); } - internal bool IsServerRunning() => ServiceHost != null; - internal void StartServer() { if (ServiceHost != null) { return; diff --git a/ConfigGenerator/BotConfig.cs b/ConfigGenerator/BotConfig.cs index 9119748f3..213470c56 100644 --- a/ConfigGenerator/BotConfig.cs +++ b/ConfigGenerator/BotConfig.cs @@ -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;