From 34d934dd829a0d7051170388bab1df00672fa616 Mon Sep 17 00:00:00 2001 From: JustArchi Date: Mon, 17 Sep 2018 00:42:24 +0200 Subject: [PATCH] Add Update/Restart actions --- ArchiSteamFarm/ASF.cs | 323 +++++++++--------- ArchiSteamFarm/Actions.cs | 22 ++ ArchiSteamFarm/Commands.cs | 15 +- .../IPC/Controllers/Api/ASFController.cs | 12 + .../IPC/Responses/GenericResponse.cs | 18 +- ArchiSteamFarm/Program.cs | 2 +- 6 files changed, 216 insertions(+), 176 deletions(-) diff --git a/ArchiSteamFarm/ASF.cs b/ArchiSteamFarm/ASF.cs index f6ac382a0..0a9fa2125 100644 --- a/ArchiSteamFarm/ASF.cs +++ b/ArchiSteamFarm/ASF.cs @@ -36,150 +36,11 @@ namespace ArchiSteamFarm { internal static readonly ArchiLogger ArchiLogger = new ArchiLogger(SharedInfo.ASF); private static readonly ConcurrentDictionary LastWriteTimes = new ConcurrentDictionary(); + private static readonly SemaphoreSlim UpdateSemaphore = new SemaphoreSlim(1, 1); private static Timer AutoUpdatesTimer; private static FileSystemWatcher FileSystemWatcher; - internal static async Task CheckAndUpdateProgram(bool updateOverride = false) { - if (!SharedInfo.BuildInfo.CanUpdate || (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None)) { - return null; - } - - if ((AutoUpdatesTimer == null) && (Program.GlobalConfig.UpdatePeriod > 0)) { - TimeSpan autoUpdatePeriod = TimeSpan.FromHours(Program.GlobalConfig.UpdatePeriod); - - AutoUpdatesTimer = new Timer( - async e => await CheckAndUpdateProgram().ConfigureAwait(false), - null, - autoUpdatePeriod, // Delay - autoUpdatePeriod // Period - ); - - ArchiLogger.LogGenericInfo(string.Format(Strings.AutoUpdateCheckInfo, autoUpdatePeriod.ToHumanReadable())); - } - - ArchiLogger.LogGenericInfo(Strings.UpdateCheckingNewVersion); - - // Cleanup from previous update - update directory for old in-use runtime files - string backupDirectory = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.UpdateDirectory); - if (Directory.Exists(backupDirectory)) { - // It's entirely possible that old process is still running, wait a short moment for eventual cleanup - await Task.Delay(5000).ConfigureAwait(false); - - try { - Directory.Delete(backupDirectory, true); - } catch (Exception e) { - ArchiLogger.LogGenericException(e); - return null; - } - } - - // Cleanup from previous update - old non-runtime in-use files - try { - foreach (string file in Directory.EnumerateFiles(SharedInfo.HomeDirectory, "*.old", SearchOption.AllDirectories)) { - File.Delete(file); - } - } catch (Exception e) { - ArchiLogger.LogGenericException(e); - return null; - } - - string releaseURL = SharedInfo.GithubReleaseURL + (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.Stable ? "/latest" : "?per_page=1"); - - GitHub.ReleaseResponse releaseResponse; - - if (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.Stable) { - WebBrowser.ObjectResponse objectResponse = await Program.WebBrowser.UrlGetToJsonObject(releaseURL).ConfigureAwait(false); - if (objectResponse?.Content == null) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - return null; - } - - releaseResponse = objectResponse.Content; - } else { - WebBrowser.ObjectResponse> objectResponse = await Program.WebBrowser.UrlGetToJsonObject>(releaseURL).ConfigureAwait(false); - if ((objectResponse?.Content == null) || (objectResponse.Content.Count == 0)) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - return null; - } - - releaseResponse = objectResponse.Content[0]; - } - - if (string.IsNullOrEmpty(releaseResponse.Tag)) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - return null; - } - - Version newVersion = new Version(releaseResponse.Tag); - - ArchiLogger.LogGenericInfo(string.Format(Strings.UpdateVersionInfo, SharedInfo.Version, newVersion)); - - if (SharedInfo.Version == newVersion) { - return SharedInfo.Version; - } - - if (SharedInfo.Version > newVersion) { - ArchiLogger.LogGenericWarning(Strings.WarningPreReleaseVersion); - await Task.Delay(15 * 1000).ConfigureAwait(false); - return SharedInfo.Version; - } - - if (!updateOverride && (Program.GlobalConfig.UpdatePeriod == 0)) { - ArchiLogger.LogGenericInfo(Strings.UpdateNewVersionAvailable); - await Task.Delay(5000).ConfigureAwait(false); - return null; - } - - // Auto update logic starts here - if (releaseResponse.Assets == null) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssets); - return null; - } - - string targetFile = SharedInfo.ASF + "-" + SharedInfo.BuildInfo.Variant + ".zip"; - GitHub.ReleaseResponse.Asset binaryAsset = releaseResponse.Assets.FirstOrDefault(asset => asset.Name.Equals(targetFile, StringComparison.OrdinalIgnoreCase)); - - if (binaryAsset == null) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssetForThisVersion); - return null; - } - - if (string.IsNullOrEmpty(binaryAsset.DownloadURL)) { - ArchiLogger.LogNullError(nameof(binaryAsset.DownloadURL)); - return null; - } - - ArchiLogger.LogGenericInfo(string.Format(Strings.UpdateDownloadingNewVersion, newVersion, binaryAsset.Size / 1024 / 1024)); - - WebBrowser.BinaryResponse response = await Program.WebBrowser.UrlGetToBinaryWithProgress(binaryAsset.DownloadURL).ConfigureAwait(false); - if (response?.Content == null) { - return null; - } - - try { - using (ZipArchive zipArchive = new ZipArchive(new MemoryStream(response.Content))) { - if (!UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory)) { - ArchiLogger.LogGenericError(Strings.WarningFailed); - } - } - } catch (Exception e) { - ArchiLogger.LogGenericException(e); - return null; - } - - if (OS.IsUnix) { - string executable = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.AssemblyName); - if (File.Exists(executable)) { - OS.UnixSetFileAccessExecutable(executable); - } - } - - ArchiLogger.LogGenericInfo(Strings.UpdateFinished); - await RestartOrExit().ConfigureAwait(false); - return newVersion; - } - internal static async Task InitBots() { if (Bot.Bots.Count != 0) { return; @@ -236,6 +97,176 @@ namespace ArchiSteamFarm { return (steamID == Program.GlobalConfig.SteamOwnerID) || (Debugging.IsDebugBuild && (steamID == SharedInfo.ArchiSteamID)); } + internal static async Task RestartOrExit() { + if (Program.RestartAllowed && Program.GlobalConfig.AutoRestart) { + ArchiLogger.LogGenericInfo(Strings.Restarting); + await Task.Delay(5000).ConfigureAwait(false); + await Program.Restart().ConfigureAwait(false); + } else { + ArchiLogger.LogGenericInfo(Strings.Exiting); + await Task.Delay(5000).ConfigureAwait(false); + await Program.Exit().ConfigureAwait(false); + } + } + + internal static async Task Update(bool updateOverride = false) { + if (!SharedInfo.BuildInfo.CanUpdate || (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None)) { + return null; + } + + await UpdateSemaphore.WaitAsync().ConfigureAwait(false); + + try { + ArchiLogger.LogGenericInfo(Strings.UpdateCheckingNewVersion); + + // Cleanup from previous update - update directory for old in-use runtime files + string backupDirectory = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.UpdateDirectory); + if (Directory.Exists(backupDirectory)) { + // It's entirely possible that old process is still running, wait a short moment for eventual cleanup + await Task.Delay(5000).ConfigureAwait(false); + + try { + Directory.Delete(backupDirectory, true); + } catch (Exception e) { + ArchiLogger.LogGenericException(e); + return null; + } + } + + // Cleanup from previous update - old non-runtime in-use files + try { + foreach (string file in Directory.EnumerateFiles(SharedInfo.HomeDirectory, "*.old", SearchOption.AllDirectories)) { + File.Delete(file); + } + } catch (Exception e) { + ArchiLogger.LogGenericException(e); + return null; + } + + string releaseURL = SharedInfo.GithubReleaseURL + (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.Stable ? "/latest" : "?per_page=1"); + + GitHub.ReleaseResponse releaseResponse; + + if (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.Stable) { + WebBrowser.ObjectResponse objectResponse = await Program.WebBrowser.UrlGetToJsonObject(releaseURL).ConfigureAwait(false); + if (objectResponse?.Content == null) { + ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); + return null; + } + + releaseResponse = objectResponse.Content; + } else { + WebBrowser.ObjectResponse> objectResponse = await Program.WebBrowser.UrlGetToJsonObject>(releaseURL).ConfigureAwait(false); + if ((objectResponse?.Content == null) || (objectResponse.Content.Count == 0)) { + ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); + return null; + } + + releaseResponse = objectResponse.Content[0]; + } + + if (string.IsNullOrEmpty(releaseResponse.Tag)) { + ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); + return null; + } + + Version newVersion = new Version(releaseResponse.Tag); + + ArchiLogger.LogGenericInfo(string.Format(Strings.UpdateVersionInfo, SharedInfo.Version, newVersion)); + + if (SharedInfo.Version == newVersion) { + return SharedInfo.Version; + } + + if (SharedInfo.Version > newVersion) { + ArchiLogger.LogGenericWarning(Strings.WarningPreReleaseVersion); + await Task.Delay(15 * 1000).ConfigureAwait(false); + return SharedInfo.Version; + } + + if (!updateOverride && (Program.GlobalConfig.UpdatePeriod == 0)) { + ArchiLogger.LogGenericInfo(Strings.UpdateNewVersionAvailable); + await Task.Delay(5000).ConfigureAwait(false); + return null; + } + + // Auto update logic starts here + if (releaseResponse.Assets == null) { + ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssets); + return null; + } + + string targetFile = SharedInfo.ASF + "-" + SharedInfo.BuildInfo.Variant + ".zip"; + GitHub.ReleaseResponse.Asset binaryAsset = releaseResponse.Assets.FirstOrDefault(asset => asset.Name.Equals(targetFile, StringComparison.OrdinalIgnoreCase)); + + if (binaryAsset == null) { + ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssetForThisVersion); + return null; + } + + if (string.IsNullOrEmpty(binaryAsset.DownloadURL)) { + ArchiLogger.LogNullError(nameof(binaryAsset.DownloadURL)); + return null; + } + + ArchiLogger.LogGenericInfo(string.Format(Strings.UpdateDownloadingNewVersion, newVersion, binaryAsset.Size / 1024 / 1024)); + + WebBrowser.BinaryResponse response = await Program.WebBrowser.UrlGetToBinaryWithProgress(binaryAsset.DownloadURL).ConfigureAwait(false); + if (response?.Content == null) { + return null; + } + + try { + using (ZipArchive zipArchive = new ZipArchive(new MemoryStream(response.Content))) { + if (!UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory)) { + ArchiLogger.LogGenericError(Strings.WarningFailed); + } + } + } catch (Exception e) { + ArchiLogger.LogGenericException(e); + return null; + } + + if (OS.IsUnix) { + string executable = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.AssemblyName); + if (File.Exists(executable)) { + OS.UnixSetFileAccessExecutable(executable); + } + } + + ArchiLogger.LogGenericInfo(Strings.UpdateFinished); + return newVersion; + } finally { + UpdateSemaphore.Release(); + } + } + + internal static async Task UpdateAndRestart() { + if (!SharedInfo.BuildInfo.CanUpdate || (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None)) { + return; + } + + if ((AutoUpdatesTimer == null) && (Program.GlobalConfig.UpdatePeriod > 0)) { + TimeSpan autoUpdatePeriod = TimeSpan.FromHours(Program.GlobalConfig.UpdatePeriod); + + AutoUpdatesTimer = new Timer( + async e => await UpdateAndRestart().ConfigureAwait(false), + null, + autoUpdatePeriod, // Delay + autoUpdatePeriod // Period + ); + + ArchiLogger.LogGenericInfo(string.Format(Strings.AutoUpdateCheckInfo, autoUpdatePeriod.ToHumanReadable())); + } + + Version newVersion = await Update().ConfigureAwait(false); + if ((newVersion == null) || (newVersion <= SharedInfo.Version)) { + return; + } + + await RestartOrExit().ConfigureAwait(false); + } + private static bool IsValidBotName(string botName) { if (string.IsNullOrEmpty(botName)) { ArchiLogger.LogNullError(nameof(botName)); @@ -518,18 +549,6 @@ namespace ArchiSteamFarm { await OnCreatedFile(e.Name, e.FullPath).ConfigureAwait(false); } - private static async Task RestartOrExit() { - if (Program.RestartAllowed && Program.GlobalConfig.AutoRestart) { - ArchiLogger.LogGenericInfo(Strings.Restarting); - await Task.Delay(5000).ConfigureAwait(false); - await Program.Restart().ConfigureAwait(false); - } else { - ArchiLogger.LogGenericInfo(Strings.Exiting); - await Task.Delay(5000).ConfigureAwait(false); - await Program.Exit().ConfigureAwait(false); - } - } - private static bool UpdateFromArchive(ZipArchive archive, string targetDirectory) { if ((archive == null) || string.IsNullOrEmpty(targetDirectory)) { ArchiLogger.LogNullError(nameof(archive) + " || " + nameof(targetDirectory)); diff --git a/ArchiSteamFarm/Actions.cs b/ArchiSteamFarm/Actions.cs index d5300b363..bd5db4223 100644 --- a/ArchiSteamFarm/Actions.cs +++ b/ArchiSteamFarm/Actions.cs @@ -238,8 +238,30 @@ namespace ArchiSteamFarm { return await Bot.ArchiHandler.RedeemKey(key).ConfigureAwait(false); } + internal static (bool Success, string Output) Restart() { + // Schedule the task after some time so user can receive response + Utilities.InBackground( + async () => { + await Task.Delay(1000).ConfigureAwait(false); + await Program.Restart().ConfigureAwait(false); + } + ); + + return (true, Strings.Done); + } + internal bool SwitchLootingAllowed() => LootingAllowed = !LootingAllowed; + internal static async Task<(bool Success, Version Version)> Update() { + Version version = await ASF.Update(true).ConfigureAwait(false); + if ((version == null) || (version <= SharedInfo.Version)) { + return (false, version); + } + + Utilities.InBackground(ASF.RestartOrExit); + return (true, version); + } + private ulong GetFirstSteamMasterID() => Bot.BotConfig.SteamUserPermissions.Where(kv => (kv.Key != 0) && (kv.Value == BotConfig.EPermission.Master)).Select(kv => kv.Key).OrderByDescending(steamID => steamID != Bot.CachedSteamID).ThenBy(steamID => steamID).FirstOrDefault(); private static async Task LimitGiftsRequestsAsync() { diff --git a/ArchiSteamFarm/Commands.cs b/ArchiSteamFarm/Commands.cs index c8b8a3f56..2e80822e4 100644 --- a/ArchiSteamFarm/Commands.cs +++ b/ArchiSteamFarm/Commands.cs @@ -2243,15 +2243,8 @@ namespace ArchiSteamFarm { return null; } - // Schedule the task after some time so user can receive response - Utilities.InBackground( - async () => { - await Task.Delay(1000).ConfigureAwait(false); - await Program.Restart().ConfigureAwait(false); - } - ); - - return FormatStaticResponse(Strings.Done); + (bool success, string output) = Actions.Exit(); + return FormatStaticResponse(success ? output : string.Format(Strings.WarningFailedWithError, output)); } private string ResponseResume(ulong steamID) { @@ -2695,8 +2688,8 @@ namespace ArchiSteamFarm { return null; } - Version version = await ASF.CheckAndUpdateProgram(true).ConfigureAwait(false); - return FormatStaticResponse(version != null ? (version > SharedInfo.Version ? Strings.Success : Strings.Done) : Strings.WarningFailed); + (bool success, Version version) = await Actions.Update().ConfigureAwait(false); + return FormatStaticResponse((success ? Strings.Success : Strings.WarningFailed) + (version != null ? " V" + version : "")); } private string ResponseVersion(ulong steamID) { diff --git a/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs b/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs index 476f54212..1c668e945 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs @@ -76,5 +76,17 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { (bool success, string output) = Actions.Exit(); return Ok(new GenericResponse(success, output)); } + + [HttpPost("Restart")] + public ActionResult PostRestart() { + (bool success, string output) = Actions.Restart(); + return Ok(new GenericResponse(success, output)); + } + + [HttpPost("Update")] + public async Task>> PostUpdate() { + (bool success, Version version) = await Actions.Update().ConfigureAwait(false); + return Ok(new GenericResponse(success, version)); + } } } diff --git a/ArchiSteamFarm/IPC/Responses/GenericResponse.cs b/ArchiSteamFarm/IPC/Responses/GenericResponse.cs index 4ea6fff51..4010383bd 100644 --- a/ArchiSteamFarm/IPC/Responses/GenericResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/GenericResponse.cs @@ -19,7 +19,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Responses { @@ -27,8 +26,9 @@ namespace ArchiSteamFarm.IPC.Responses { [JsonProperty] private readonly T Result; - internal GenericResponse(T result) : base(true, "OK") => Result = result ?? throw new ArgumentNullException(nameof(result)); + internal GenericResponse(T result) : base(result != null) => Result = result; internal GenericResponse(bool success, string message) : base(success, message) { } + internal GenericResponse(bool success, T result) : base(success) => Result = result; } public class GenericResponse { @@ -39,20 +39,14 @@ namespace ArchiSteamFarm.IPC.Responses { private readonly bool Success; internal GenericResponse(bool success) { - if (!success) { - // Returning failed generic response without a message should never happen - throw new ArgumentException(nameof(success)); - } + Success = success; - Success = true; - Message = "OK"; + if (success) { + Message = "OK"; + } } internal GenericResponse(bool success, string message) { - if (string.IsNullOrEmpty(message)) { - throw new ArgumentNullException(nameof(message)); - } - Success = success; Message = message; } diff --git a/ArchiSteamFarm/Program.cs b/ArchiSteamFarm/Program.cs index d31749008..908802b25 100644 --- a/ArchiSteamFarm/Program.cs +++ b/ArchiSteamFarm/Program.cs @@ -203,7 +203,7 @@ namespace ArchiSteamFarm { await InitGlobalDatabaseAndServices().ConfigureAwait(false); - await ASF.CheckAndUpdateProgram().ConfigureAwait(false); + await ASF.UpdateAndRestart().ConfigureAwait(false); await ASF.InitBots().ConfigureAwait(false); ASF.InitEvents();