Support downloading updates via command.

This commit is contained in:
BruceChen 2022-12-01 22:55:48 +08:00
parent 3713fa2dbe
commit 28827b720a
10 changed files with 2463 additions and 1974 deletions

View file

@ -0,0 +1,50 @@
using System.Collections.Generic;
namespace MinecraftClient.Commands
{
class Upgrade : Command
{
public override string CmdName { get { return "upgrade"; } }
public override string CmdUsage { get { return "upgrade [-f|check|cancel|download]"; } }
public override string CmdDesc { get { return string.Empty; } }
public override string Run(McClient handler, string command, Dictionary<string, object>? localVars)
{
if (HasArg(command))
{
string[] args = GetArgs(command);
return args[0] switch
{
"-f" => DownloadUpdate(force: true),
"-force" => DownloadUpdate(force: true),
"cancel" => CancelDownloadUpdate(),
"check" => CheckUpdate(),
"download" => DownloadUpdate(force: args.Length > 1 && (args[1] == "-f" || args[1] == "-force")),
_ => GetCmdDescTranslated(),
};
}
else
{
return DownloadUpdate(force: false);
}
}
private static string DownloadUpdate(bool force)
{
UpgradeHelper.DownloadLatestBuild(force);
return Translations.mcc_update_start;
}
private static string CancelDownloadUpdate()
{
UpgradeHelper.CancelDownloadUpdate();
return Translations.mcc_update_cancel;
}
private static string CheckUpdate()
{
UpgradeHelper.CheckUpdate(forceUpdate: true);
return Translations.mcc_update_start;
}
}
}

View file

@ -32,6 +32,7 @@
<PackageReference Include="DSharpPlus" Version="4.2.0" />
<PackageReference Include="DynamicExpresso.Core" Version="2.13.0" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="12.2.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="6.0.0" />

View file

@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Inventory.ItemPalettes;
@ -200,6 +198,18 @@ namespace MinecraftClient
return;
}
if (args.Contains("--upgrade"))
{
UpgradeHelper.HandleBlockingUpdate(forceUpgrade: false);
return;
}
if (args.Contains("--force-upgrade"))
{
UpgradeHelper.HandleBlockingUpdate(forceUpgrade: true);
return;
}
if (args.Contains("--generate"))
{
string dataGenerator = "";
@ -284,47 +294,7 @@ namespace MinecraftClient
}
// Check for updates
{
bool needPromptUpdate = true;
if (Settings.CheckUpdate(Config.Head.CurrentVersion, Config.Head.LatestVersion))
{
needPromptUpdate = false;
ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, Settings.GithubReleaseUrl));
}
Task.Run(() =>
{
HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = false };
HttpClient httpClient = new(httpClientHandler);
Task<HttpResponseMessage>? httpWebRequest = null;
try
{
httpWebRequest = httpClient.GetAsync(Settings.GithubLatestReleaseUrl, HttpCompletionOption.ResponseHeadersRead);
httpWebRequest.Wait();
HttpResponseMessage res = httpWebRequest.Result;
if (res.Headers.Location != null)
{
Match match = Regex.Match(res.Headers.Location.ToString(), Settings.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);
if (needPromptUpdate)
if (Settings.CheckUpdate(Config.Head.CurrentVersion, Config.Head.LatestVersion))
ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, Settings.GithubReleaseUrl));
if (latestVersion != Config.Head.LatestVersion)
{
Config.Head.LatestVersion = latestVersion;
WriteBackSettings(false);
}
}
}
}
catch (Exception) { }
finally { httpWebRequest?.Dispose(); }
httpClient.Dispose();
httpClientHandler.Dispose();
});
}
UpgradeHelper.CheckUpdate();
// Load command-line arguments
if (args.Length >= 1)

View file

@ -17,6 +17,9 @@ namespace MinecraftClient.Proxy
[TomlDoNotInlineObject]
public class Configs
{
[TomlInlineComment("$Proxy.Enabled_Update$")]
public bool Enabled_Update = false;
[TomlInlineComment("$Proxy.Enabled_Login$")]
public bool Enabled_Login = false;
@ -79,7 +82,7 @@ namespace MinecraftClient.Proxy
case Configs.ProxyType.SOCKS5: innerProxytype = ProxyType.Socks5; break;
}
if (Config.Username != "" && Config.Password != "")
if (!string.IsNullOrWhiteSpace(Config.Username)&& !string.IsNullOrWhiteSpace(Config.Password))
proxy = factory.CreateProxyClient(innerProxytype, Config.Server.Host, Config.Server.Port, Config.Username, Config.Password);
else
proxy = factory.CreateProxyClient(innerProxytype, Config.Server.Host, Config.Server.Port);

File diff suppressed because it is too large Load diff

View file

@ -1,76 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
@ -89,13 +109,13 @@
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AppVars.Variables" xml:space="preserve">
<value>can be used in some other fields as %yourvar%
@ -723,6 +743,9 @@ If the connection to the Minecraft game server is blocked by the firewall, set E
<data name="Proxy.Enabled_Login" xml:space="preserve">
<value>Whether to connect to the login server through a proxy.</value>
</data>
<data name="Proxy.Enabled_Update" xml:space="preserve">
<value>Whether to download MCC updates via proxy.</value>
</data>
<data name="Proxy.Password" xml:space="preserve">
<value>Only required for password-protected proxies.</value>
</data>
@ -765,4 +788,4 @@ If the connection to the Minecraft game server is blocked by the firewall, set E
<data name="Signature.SignMessageInCommand" xml:space="preserve">
<value>Whether to sign the messages contained in the commands sent by MCC. For example, the message in "/msg" and "/me"</value>
</data>
</root>
</root>

View file

@ -5220,7 +5220,8 @@ namespace MinecraftClient {
}
/// <summary>
/// Looks up a localized string similar to New version of MCC available: {0}.
/// Looks up a localized string similar to A new version of MCC is available and you can download it via /upgrade
///Or download it manually: {0}.
/// </summary>
internal static string mcc_has_update {
get {
@ -5529,6 +5530,132 @@ namespace MinecraftClient {
}
}
/// <summary>
/// Looks up a localized string similar to The current version is already up to date or is a development build..
/// </summary>
internal static string mcc_update_already_latest {
get {
return ResourceManager.GetString("mcc.update.already_latest", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Checking for updates has already started..
/// </summary>
internal static string mcc_update_already_running {
get {
return ResourceManager.GetString("mcc.update.already_running", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The download has been cancelled..
/// </summary>
internal static string mcc_update_cancel {
get {
return ResourceManager.GetString("mcc.update.cancel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unable to check for updates from Github, please check your internet connection or enable proxy..
/// </summary>
internal static string mcc_update_check_fail {
get {
return ResourceManager.GetString("mcc.update.check_fail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Self-updating: Download is complete, decompressing..
/// </summary>
internal static string mcc_update_download_complete {
get {
return ResourceManager.GetString("mcc.update.download_complete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Self-updating: An error occurred while downloading or saving the file:.
/// </summary>
internal static string mcc_update_download_fail {
get {
return ResourceManager.GetString("mcc.update.download_fail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Self-updating: New version detected: &quot;{0}&quot;, download will start soon (platform: {1})..
/// </summary>
internal static string mcc_update_exist_update {
get {
return ResourceManager.GetString("mcc.update.exist_update", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The latest release does not contain a build that matches your operating system and CPU architecture..
/// </summary>
internal static string mcc_update_platform_not_support {
get {
return ResourceManager.GetString("mcc.update.platform_not_support", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Self-updating: {0:00.00}%, Downloaded {1:00.0}MB of {2:00.0}MB, Avg {3:0.0}KB/s.
/// </summary>
internal static string mcc_update_progress {
get {
return ResourceManager.GetString("mcc.update.progress", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Self-updating: Downloaded {0:00.0}MB, Avg {1:0.0}KB/s.
/// </summary>
internal static string mcc_update_progress_type2 {
get {
return ResourceManager.GetString("mcc.update.progress_type2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add &quot;-f&quot; to force the download..
/// </summary>
internal static string mcc_update_promote_force {
get {
return ResourceManager.GetString("mcc.update.promote_force", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use &quot;--force-upgrade&quot; to force the download..
/// </summary>
internal static string mcc_update_promote_force_cmd {
get {
return ResourceManager.GetString("mcc.update.promote_force_cmd", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Self-updating: File saved as: {0}.
/// </summary>
internal static string mcc_update_save_as {
get {
return ResourceManager.GetString("mcc.update.save_as", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Start checking for updates..
/// </summary>
internal static string mcc_update_start {
get {
return ResourceManager.GetString("mcc.update.start", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please use the newly generated MinecraftClient.ini.
/// </summary>

View file

@ -1857,7 +1857,8 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
<value>Handshake successful. (Server ID: {0})</value>
</data>
<data name="mcc.has_update" xml:space="preserve">
<value>New version of MCC available: {0}</value>
<value>A new version of MCC is available and you can download it via /upgrade
Or download it manually: {0}</value>
</data>
<data name="mcc.help_us_translate" xml:space="preserve">
<value>Help us translate MCC: {0}</value>
@ -1962,6 +1963,48 @@ MCC is running with default settings.</value>
<value>Unknown or not supported MC version {0}.
Switching to autodetection mode.</value>
</data>
<data name="mcc.update.already_latest" xml:space="preserve">
<value>The current version is already up to date or is a development build.</value>
</data>
<data name="mcc.update.already_running" xml:space="preserve">
<value>Checking for updates has already started.</value>
</data>
<data name="mcc.update.cancel" xml:space="preserve">
<value>The download has been cancelled.</value>
</data>
<data name="mcc.update.check_fail" xml:space="preserve">
<value>Unable to check for updates from Github, please check your internet connection or enable proxy.</value>
</data>
<data name="mcc.update.download_complete" xml:space="preserve">
<value>Self-updating: Download is complete, decompressing.</value>
</data>
<data name="mcc.update.download_fail" xml:space="preserve">
<value>Self-updating: An error occurred while downloading or saving the file:</value>
</data>
<data name="mcc.update.exist_update" xml:space="preserve">
<value>Self-updating: New version detected: "{0}", download will start soon (platform: {1}).</value>
</data>
<data name="mcc.update.platform_not_support" xml:space="preserve">
<value>The latest release does not contain a build that matches your operating system and CPU architecture.</value>
</data>
<data name="mcc.update.progress" xml:space="preserve">
<value>Self-updating: {0:00.00}%, Downloaded {1:00.0}MB of {2:00.0}MB, Avg {3:0.0}KB/s</value>
</data>
<data name="mcc.update.progress_type2" xml:space="preserve">
<value>Self-updating: Downloaded {0:00.0}MB, Avg {1:0.0}KB/s</value>
</data>
<data name="mcc.update.promote_force" xml:space="preserve">
<value>Add "-f" to force the download.</value>
</data>
<data name="mcc.update.promote_force_cmd" xml:space="preserve">
<value>Use "--force-upgrade" to force the download.</value>
</data>
<data name="mcc.update.save_as" xml:space="preserve">
<value>Self-updating: File saved as: {0}</value>
</data>
<data name="mcc.update.start" xml:space="preserve">
<value>Start checking for updates.</value>
</data>
<data name="mcc.use_new_config" xml:space="preserve">
<value>Please use the newly generated MinecraftClient.ini</value>
</data>
@ -1978,4 +2021,4 @@ Logging in...</value>
<data name="proxy.connected" xml:space="preserve">
<value>Connected to proxy {0}:{1}</value>
</data>
</root>
</root>

View file

@ -37,8 +37,6 @@ namespace MinecraftClient
public static string TranslationsFile_Website_Download = "http://resources.download.minecraft.net";
public const string TranslationProjectUrl = "https://crwd.in/minecraft-console-client";
public const string GithubReleaseUrl = "https://github.com/MCCTeam/Minecraft-Console-Client/releases";
public const string GithubLatestReleaseUrl = GithubReleaseUrl + "/latest";
public static GlobalConfig Config = new();
@ -1688,61 +1686,6 @@ namespace MinecraftClient
}
}
public static bool CheckUpdate(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;
}
public static int DoubleToTick(double time)
{
time = Math.Min(int.MaxValue / 10, time);

View file

@ -0,0 +1,321 @@
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 void DownloadLatestBuild(bool forceUpdate, bool isCommandLine = false)
{
if (Interlocked.Exchange(ref running, 1) == 1)
{
ConsoleIO.WriteLine(Translations.mcc_update_already_running);
}
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)
);
}
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";
using HttpResponseMessage response = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
downloadStartTime = DateTime.Now;
lastNotifyTime = DateTime.MinValue;
lastBytesTransferred = 0;
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);
}
}
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<HttpResponseMessage>? 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);
}
}
}
}