using System; using System.IO; using System.IO.Compression; using System.Net; using System.Net.Http; using System.Net.Http.Handlers; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace MinecraftClient { internal static class UpgradeHelper { private const string GithubReleaseUrl = "https://github.com/MCCTeam/Minecraft-Console-Client/releases"; private static int running = 0; // Type: bool; 1 for running; 0 for stopped; private static CancellationTokenSource cancellationTokenSource = new(); private static CancellationToken cancellationToken = CancellationToken.None; private static long lastBytesTransferred = 0, minNotifyThreshold = 5 * 1024 * 1024; private static DateTime downloadStartTime = DateTime.Now, lastNotifyTime = DateTime.Now; private static TimeSpan minNotifyInterval = TimeSpan.FromMilliseconds(3000); public static void CheckUpdate(bool forceUpdate = false) { bool needPromptUpdate = true; if (!forceUpdate && CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion)) { needPromptUpdate = false; ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, GithubReleaseUrl), true); } Task.Run(() => { DoCheckUpdate(CancellationToken.None); if (needPromptUpdate) { if (CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion)) { ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, GithubReleaseUrl), true); } else if (forceUpdate) { ConsoleIO.WriteLine(Translations.mcc_update_already_latest + ' ' + Translations.mcc_update_promote_force_cmd); } } }); } public static bool DownloadLatestBuild(bool forceUpdate, bool isCommandLine = false) { if (Interlocked.Exchange(ref running, 1) == 1) { return false; } else { if (!cancellationTokenSource.TryReset()) cancellationTokenSource = new(); cancellationToken = cancellationTokenSource.Token; Task.Run(async () => { string OSInfo = GetOSIdentifier(); if (Settings.Config.Logging.DebugMessages || string.IsNullOrEmpty(OSInfo)) ConsoleIO.WriteLine(string.Format("OS: {0}, Arch: {1}, Framework: {2}", RuntimeInformation.OSDescription, RuntimeInformation.ProcessArchitecture, RuntimeInformation.FrameworkDescription)); if (string.IsNullOrEmpty(OSInfo)) { ConsoleIO.WriteLine(Translations.mcc_update_platform_not_support); } else { string latestVersion = DoCheckUpdate(cancellationToken); if (cancellationToken.IsCancellationRequested) { } else if (string.IsNullOrEmpty(latestVersion)) { ConsoleIO.WriteLine(Translations.mcc_update_check_fail); } else if (!forceUpdate && !CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion)) { ConsoleIO.WriteLine(Translations.mcc_update_already_latest + ' ' + (isCommandLine ? Translations.mcc_update_promote_force_cmd : Translations.mcc_update_promote_force)); } else { ConsoleIO.WriteLine(string.Format(Translations.mcc_update_exist_update, latestVersion, OSInfo)); HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = true }; AddProxySettings(httpClientHandler); ProgressMessageHandler progressMessageHandler = new(httpClientHandler); progressMessageHandler.HttpReceiveProgress += (_, info) => { DateTime now = DateTime.Now; if (now - lastNotifyTime > minNotifyInterval || info.BytesTransferred - lastBytesTransferred > minNotifyThreshold) { lastNotifyTime = now; lastBytesTransferred = info.BytesTransferred; if (info.TotalBytes.HasValue) { ConsoleIO.WriteLine(string.Format(Translations.mcc_update_progress, (double)info.BytesTransferred / info.TotalBytes * 100.0, (double)info.BytesTransferred / 1024 / 1024, (double)info.TotalBytes / 1024 / 1024, (double)info.BytesTransferred / 1024 / (now - downloadStartTime).TotalSeconds, TimeSpan.FromMilliseconds( (double)(info.TotalBytes - info.BytesTransferred) / (info.BytesTransferred / (now - downloadStartTime).TotalMilliseconds) ).ToString("hh\\:mm\\:ss")) ); } else { ConsoleIO.WriteLine(string.Format(Translations.mcc_update_progress_type2, (double)info.BytesTransferred / 1024 / 1024, (double)info.BytesTransferred / 1024 / (now - downloadStartTime).TotalSeconds) ); } } }; HttpClient httpClient = new(progressMessageHandler); try { string downloadUrl = $"{GithubReleaseUrl}/download/{latestVersion}/MinecraftClient-{OSInfo}.zip"; downloadStartTime = DateTime.Now; lastNotifyTime = DateTime.MinValue; lastBytesTransferred = 0; using HttpResponseMessage response = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); using Stream zipFileStream = await response.Content.ReadAsStreamAsync(cancellationToken); if (!cancellationToken.IsCancellationRequested) { using ZipArchive zipArchive = new(zipFileStream, ZipArchiveMode.Read); ConsoleIO.WriteLine(Translations.mcc_update_download_complete); foreach (var archiveEntry in zipArchive.Entries) { if (archiveEntry.Name.StartsWith("MinecraftClient")) { string fileName = $"MinecraftClient-{latestVersion}{Path.GetExtension(archiveEntry.Name)}"; archiveEntry.ExtractToFile(fileName, true); ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_update_save_as, fileName), true); break; } } zipArchive.Dispose(); } zipFileStream.Dispose(); response.Dispose(); } catch (TaskCanceledException) { } catch (Exception e) { ConsoleIO.WriteLine($"{Translations.mcc_update_download_fail}\n{e.GetType().Name}: {e.Message}\n{e.StackTrace}"); } httpClient.Dispose(); progressMessageHandler.Dispose(); httpClientHandler.Dispose(); } } Interlocked.Exchange(ref running, 0); }, cancellationToken); return true; } } public static void CancelDownloadUpdate() { if (!cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Cancel(); } public static void HandleBlockingUpdate(bool forceUpgrade) { minNotifyInterval = TimeSpan.FromMilliseconds(500); DownloadLatestBuild(forceUpgrade, true); while (running == 1) Thread.Sleep(500); } private static string DoCheckUpdate(CancellationToken cancellationToken) { string latestBuildInfo = string.Empty; HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = false }; AddProxySettings(httpClientHandler); HttpClient httpClient = new(httpClientHandler); Task? httpWebRequest = null; try { httpWebRequest = httpClient.GetAsync(GithubReleaseUrl + "/latest", HttpCompletionOption.ResponseHeadersRead, cancellationToken); httpWebRequest.Wait(); if (!cancellationToken.IsCancellationRequested) { HttpResponseMessage res = httpWebRequest.Result; if (res.Headers.Location != null) { Match match = Regex.Match(res.Headers.Location.ToString(), GithubReleaseUrl + @"/tag/(\d{4})(\d{2})(\d{2})-(\d+)"); if (match.Success && match.Groups.Count == 5) { string year = match.Groups[1].Value, month = match.Groups[2].Value, day = match.Groups[3].Value, run = match.Groups[4].Value; string latestVersion = string.Format("GitHub build {0}, built on {1}-{2}-{3}", run, year, month, day); latestBuildInfo = string.Format("{0}{1}{2}-{3}", year, month, day, run); if (latestVersion != Settings.Config.Head.LatestVersion) { Settings.Config.Head.LatestVersion = latestVersion; Program.WriteBackSettings(false); } } } res.Dispose(); } httpWebRequest.Dispose(); } catch (Exception) { } finally { httpWebRequest?.Dispose(); } httpClient.Dispose(); httpClientHandler.Dispose(); return latestBuildInfo; } private static string GetOSIdentifier() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) return "linux-arm64"; else if (RuntimeInformation.ProcessArchitecture == Architecture.X64) return "linux"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { if (RuntimeInformation.ProcessArchitecture == Architecture.X64) return "osx"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { if (RuntimeInformation.ProcessArchitecture == Architecture.X64) return "windows-x64"; else if (RuntimeInformation.ProcessArchitecture == Architecture.X86) return "windows-x86"; } return string.Empty; } private static bool CompareVersionInfo(string? current, string? latest) { if (current == null || latest == null) return false; Regex reg = new(@"\w+\sbuild\s(\d+),\sbuilt\son\s(\d{4})[-\/\.\s]?(\d{2})[-\/\.\s]?(\d{2}).*"); Regex reg2 = new(@"\w+\sbuild\s(\d+),\sbuilt\son\s\w+\s(\d{2})[-\/\.\s]?(\d{2})[-\/\.\s]?(\d{4}).*"); DateTime? curTime = null, latestTime = null; Match curMatch = reg.Match(current); if (curMatch.Success && curMatch.Groups.Count == 5) { try { curTime = new(int.Parse(curMatch.Groups[2].Value), int.Parse(curMatch.Groups[3].Value), int.Parse(curMatch.Groups[4].Value)); } catch { curTime = null; } } if (curTime == null) { curMatch = reg2.Match(current); try { curTime = new(int.Parse(curMatch.Groups[4].Value), int.Parse(curMatch.Groups[3].Value), int.Parse(curMatch.Groups[2].Value)); } catch { curTime = null; } } if (curTime == null) return false; Match latestMatch = reg.Match(latest); if (latestMatch.Success && latestMatch.Groups.Count == 5) { try { latestTime = new(int.Parse(latestMatch.Groups[2].Value), int.Parse(latestMatch.Groups[3].Value), int.Parse(latestMatch.Groups[4].Value)); } catch { latestTime = null; } } if (latestTime == null) { latestMatch = reg2.Match(latest); try { latestTime = new(int.Parse(latestMatch.Groups[4].Value), int.Parse(latestMatch.Groups[3].Value), int.Parse(latestMatch.Groups[2].Value)); } catch { latestTime = null; } } if (latestTime == null) return false; int curBuildId, latestBuildId; try { curBuildId = int.Parse(curMatch.Groups[1].Value); latestBuildId = int.Parse(latestMatch.Groups[1].Value); } catch { return false; } if (latestTime > curTime) return true; else if (latestTime >= curTime && latestBuildId > curBuildId) return true; else return false; } private static void AddProxySettings(HttpClientHandler httpClientHandler) { if (Settings.Config.Proxy.Enabled_Update) { string proxyAddress; if (!string.IsNullOrWhiteSpace(Settings.Config.Proxy.Username) && !string.IsNullOrWhiteSpace(Settings.Config.Proxy.Password)) proxyAddress = string.Format("{0}://{3}:{4}@{1}:{2}", Settings.Config.Proxy.Proxy_Type.ToString().ToLower(), Settings.Config.Proxy.Server.Host, Settings.Config.Proxy.Server.Port, Settings.Config.Proxy.Username, Settings.Config.Proxy.Password); else proxyAddress = string.Format("{0}://{1}:{2}", Settings.Config.Proxy.Proxy_Type.ToString().ToLower(), Settings.Config.Proxy.Server.Host, Settings.Config.Proxy.Server.Port); httpClientHandler.Proxy = new WebProxy(proxyAddress, true); } } } }