Initial commit: R0Installer source, patch generator, API backend and build scripts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-27 18:03:44 +08:00
commit 6124d6060c
33 changed files with 8389 additions and 0 deletions
+577
View File
@@ -0,0 +1,577 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace R0Installer
{
public static class ConfigManager
{
private const string BASE_URL = "https://a-p-i.r0csgo.com";
private const string INSTALLER_CONFIG_API = "/v3/config/installer";
private const string PROJECT_CONFIG_API = "/v3/config/project/{0}";
private const string VERSION_INDEX_API = "/v3/version/index?project={0}";
private const string USER_AGENT = "r0_installer";
private static string installerConfigCache = null;
private static string projectConfigCache = null;
private static string projectConfigCacheId = null;
#region HTTP
private static string HttpGet(string url)
{
using (WebClient client = new WebClient())
{
client.Encoding = Encoding.UTF8;
client.Headers.Add("User-Agent", USER_AGENT);
return client.DownloadString(url);
}
}
private static async Task<string> HttpGetAsync(string url)
{
using (WebClient client = new WebClient())
{
client.Encoding = Encoding.UTF8;
client.Headers.Add("User-Agent", USER_AGENT);
return await client.DownloadStringTaskAsync(url);
}
}
#endregion
#region JSON helpers
public static string ExtractJsonValue(string json, string key)
{
string searchKey = String.Format("\"{0}\":", key);
int startIndex = json.IndexOf(searchKey);
if (startIndex < 0) return "";
startIndex += searchKey.Length;
while (startIndex < json.Length && (json[startIndex] == ' ' || json[startIndex] == '\n' || json[startIndex] == '\r' || json[startIndex] == '\t'))
startIndex++;
if (startIndex >= json.Length) return "";
if (json[startIndex] == '"')
{
startIndex++;
int endIndex = json.IndexOf('"', startIndex);
if (endIndex < 0) return "";
return json.Substring(startIndex, endIndex - startIndex);
}
else
{
int endIndex = startIndex;
while (endIndex < json.Length && json[endIndex] != ',' && json[endIndex] != '}' && json[endIndex] != ']')
endIndex++;
return json.Substring(startIndex, endIndex - startIndex).Trim();
}
}
public static bool ExtractJsonBool(string json, string key)
{
string value = ExtractJsonValue(json, key);
return value.ToLower() == "true";
}
public static int ExtractJsonInt(string json, string key, int defaultValue)
{
string value = ExtractJsonValue(json, key);
int result;
if (int.TryParse(value, out result)) return result;
return defaultValue;
}
public static string ExtractNestedJsonValue(string json, string parentKey, string childKey)
{
string searchParentKey = String.Format("\"{0}\":", parentKey);
int parentStartIndex = json.IndexOf(searchParentKey);
if (parentStartIndex < 0) return "";
parentStartIndex += searchParentKey.Length;
while (parentStartIndex < json.Length && (json[parentStartIndex] == ' ' || json[parentStartIndex] == '\n' || json[parentStartIndex] == '\r'))
parentStartIndex++;
if (parentStartIndex >= json.Length || json[parentStartIndex] != '{') return "";
int braceCount = 1;
int parentEndIndex = parentStartIndex + 1;
while (parentEndIndex < json.Length && braceCount > 0)
{
if (json[parentEndIndex] == '{') braceCount++;
else if (json[parentEndIndex] == '}') braceCount--;
parentEndIndex++;
}
string parentJson = json.Substring(parentStartIndex, parentEndIndex - parentStartIndex);
return ExtractJsonValue(parentJson, childKey);
}
public static string ExtractJsonObject(string json, string key)
{
string searchKey = String.Format("\"{0}\":", key);
int startIndex = json.IndexOf(searchKey);
if (startIndex < 0) return "";
startIndex += searchKey.Length;
while (startIndex < json.Length && (json[startIndex] == ' ' || json[startIndex] == '\n' || json[startIndex] == '\r'))
startIndex++;
if (startIndex >= json.Length) return "";
if (json[startIndex] == '{')
{
int braceCount = 1;
int endIndex = startIndex + 1;
while (endIndex < json.Length && braceCount > 0)
{
if (json[endIndex] == '{') braceCount++;
else if (json[endIndex] == '}') braceCount--;
endIndex++;
}
return json.Substring(startIndex, endIndex - startIndex);
}
return "";
}
public static string ExtractJsonArray(string json, string key)
{
string searchKey = String.Format("\"{0}\":", key);
int startIndex = json.IndexOf(searchKey);
if (startIndex < 0) return "";
startIndex += searchKey.Length;
while (startIndex < json.Length && (json[startIndex] == ' ' || json[startIndex] == '\n' || json[startIndex] == '\r'))
startIndex++;
if (startIndex >= json.Length || json[startIndex] != '[') return "";
int bracketCount = 1;
int endIndex = startIndex + 1;
while (endIndex < json.Length && bracketCount > 0)
{
if (json[endIndex] == '[') bracketCount++;
else if (json[endIndex] == ']') bracketCount--;
endIndex++;
}
return json.Substring(startIndex, endIndex - startIndex);
}
public static List<string> ParseJsonObjectArray(string arrayJson)
{
List<string> objects = new List<string>();
if (string.IsNullOrEmpty(arrayJson) || arrayJson[0] != '[') return objects;
string inner = arrayJson.Substring(1, arrayJson.Length - 2);
int pos = 0;
while (pos < inner.Length)
{
int objStart = inner.IndexOf('{', pos);
if (objStart < 0) break;
int braceCount = 1;
int objEnd = objStart + 1;
while (objEnd < inner.Length && braceCount > 0)
{
if (inner[objEnd] == '{') braceCount++;
else if (inner[objEnd] == '}') braceCount--;
objEnd++;
}
objects.Add(inner.Substring(objStart, objEnd - objStart));
pos = objEnd;
}
return objects;
}
public static List<string> ParseJsonStringArray(string arrayJson)
{
List<string> strings = new List<string>();
if (string.IsNullOrEmpty(arrayJson) || arrayJson[0] != '[') return strings;
string inner = arrayJson.Substring(1, arrayJson.Length - 2);
int pos = 0;
while (pos < inner.Length)
{
int quoteStart = inner.IndexOf('"', pos);
if (quoteStart < 0) break;
int quoteEnd = inner.IndexOf('"', quoteStart + 1);
if (quoteEnd < 0) break;
strings.Add(inner.Substring(quoteStart + 1, quoteEnd - quoteStart - 1));
pos = quoteEnd + 1;
}
return strings;
}
#endregion
#region Config loading
private static string GetCachePath(string name)
{
return Path.Combine(Path.GetTempPath(), "r0_installer_" + name + ".json");
}
private static void SaveCache(string name, string json)
{
try
{
File.WriteAllText(GetCachePath(name), json, Encoding.UTF8);
}
catch (Exception ex)
{
Debug.WriteLine("缓存写入失败: " + ex.Message);
}
}
private static string LoadCache(string name)
{
try
{
string path = GetCachePath(name);
if (File.Exists(path))
return File.ReadAllText(path, Encoding.UTF8);
}
catch (Exception ex)
{
Debug.WriteLine("缓存读取失败: " + ex.Message);
}
return null;
}
// 下载的安装包/运行库统一放到固定的厂商目录下,而不是 %TEMP%。
// 从 %TEMP% 写入并立即执行 EXE 是杀软启发式判定下载器木马的主要特征,
// 改用 ProgramData 下的固定目录可显著降低误报,且符合正规安装器的行为。
public static string GetWorkDirectory()
{
string baseDir = null;
try { baseDir = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); }
catch { }
if (string.IsNullOrEmpty(baseDir)) baseDir = Path.GetTempPath();
string dir = Path.Combine(baseDir, "R0Arena", "cache");
try
{
Directory.CreateDirectory(dir);
return dir;
}
catch
{
return Path.GetTempPath();
}
}
// 目标 exe 已被占用无法覆盖时,改用同目录下的新 exe 文件名(必须保持 .exe 后缀以便运行)。
public static string ResolveWritableDownloadPath(string preferredPath)
{
if (string.IsNullOrEmpty(preferredPath))
preferredPath = Path.Combine(GetWorkDirectory(), "download_" + Guid.NewGuid().ToString("N") + ".exe");
if (!File.Exists(preferredPath))
return preferredPath;
try
{
File.Delete(preferredPath);
if (!File.Exists(preferredPath))
return preferredPath;
}
catch { }
string dir = Path.GetDirectoryName(preferredPath);
if (string.IsNullOrEmpty(dir))
dir = GetWorkDirectory();
string nameWithoutExt = Path.GetFileNameWithoutExtension(preferredPath);
string ext = Path.GetExtension(preferredPath);
if (string.IsNullOrEmpty(ext))
ext = ".exe";
return Path.Combine(dir, nameWithoutExt + "_" + Guid.NewGuid().ToString("N").Substring(0, 8) + ext);
}
public static void PrepareDownloadFile(string path)
{
if (!File.Exists(path)) return;
try { File.Delete(path); } catch { }
}
// 执行前确认下载结果确实是有效文件:避免把服务器返回的错误页 / 半截下载
// 当作程序去运行(既是健壮性,也是“正规更新器”才有的行为)。
public static bool IsDownloadedFileValid(string path)
{
try
{
FileInfo fi = new FileInfo(path);
if (!fi.Exists || fi.Length < 1) return false;
if (path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
using (FileStream fs = File.OpenRead(path))
{
return fs.ReadByte() == 0x4D && fs.ReadByte() == 0x5A; // 'MZ'
}
}
return true;
}
catch
{
return false;
}
}
public static string GetInstallerConfig()
{
if (installerConfigCache != null) return installerConfigCache;
try
{
string json = HttpGet(BASE_URL + INSTALLER_CONFIG_API);
string data = ExtractJsonObject(json, "data");
if (!string.IsNullOrEmpty(data))
{
installerConfigCache = data;
SaveCache("installer_config", data);
return data;
}
}
catch (Exception ex)
{
Debug.WriteLine("获取安装器配置失败: " + ex.Message);
}
string cached = LoadCache("installer_config");
if (cached != null)
{
installerConfigCache = cached;
return cached;
}
return null;
}
public static async Task<string> GetProjectConfig(string projectId)
{
if (projectConfigCache != null && projectConfigCacheId == projectId)
return projectConfigCache;
try
{
string url = BASE_URL + String.Format(PROJECT_CONFIG_API, Uri.EscapeDataString(projectId));
string json = await HttpGetAsync(url);
string data = ExtractJsonObject(json, "data");
if (!string.IsNullOrEmpty(data))
{
projectConfigCache = data;
projectConfigCacheId = projectId;
SaveCache("project_" + projectId, data);
return data;
}
}
catch (Exception ex)
{
Debug.WriteLine("获取项目配置失败: " + ex.Message);
}
string cached = LoadCache("project_" + projectId);
if (cached != null)
{
projectConfigCache = cached;
projectConfigCacheId = projectId;
return cached;
}
return null;
}
public static string GetProjectConfigSync(string projectId)
{
if (projectConfigCache != null && projectConfigCacheId == projectId)
return projectConfigCache;
try
{
string url = BASE_URL + String.Format(PROJECT_CONFIG_API, Uri.EscapeDataString(projectId));
string json = HttpGet(url);
string data = ExtractJsonObject(json, "data");
if (!string.IsNullOrEmpty(data))
{
projectConfigCache = data;
projectConfigCacheId = projectId;
SaveCache("project_" + projectId, data);
return data;
}
}
catch (Exception ex)
{
Debug.WriteLine("获取项目配置失败: " + ex.Message);
}
string cached = LoadCache("project_" + projectId);
if (cached != null)
{
projectConfigCache = cached;
projectConfigCacheId = projectId;
return cached;
}
return null;
}
public static async Task<string> GetVersionIndex(string projectId)
{
try
{
string url = BASE_URL + String.Format(VERSION_INDEX_API, Uri.EscapeDataString(projectId));
string json = await HttpGetAsync(url);
string data = ExtractJsonObject(json, "data");
return data;
}
catch (Exception ex)
{
Debug.WriteLine("获取版本信息失败: " + ex.Message);
return null;
}
}
public struct UpdateSummary
{
public bool HasUpdate;
public bool HasSilentField; // 服务端是否在本次响应中显式返回了 silent 字段
public bool Silent; // 该版本是否应静默更新(仅当 HasSilentField 为 true 时有效)
}
// 轻量预检:在决定“弹界面 / 静默”之前,先同步拿到 has_update 以及(可选的)按版本 silent 标志。
// silent 由服务端在 /v3/update/check 响应中按目标版本下发,从而实现“云端指定哪个版本静默更新”。
public static UpdateSummary GetUpdateSummarySync(string projectId, string checkApiTemplate, string currentVersion)
{
UpdateSummary summary = new UpdateSummary();
try
{
string apiUrl = BASE_URL + String.Format(checkApiTemplate, Uri.EscapeDataString(currentVersion));
string json = HttpGet(apiUrl);
string data = ExtractJsonObject(json, "data");
string body = string.IsNullOrEmpty(data) ? json : data;
summary.HasUpdate = ExtractJsonBool(body, "has_update");
string silentRaw = ExtractJsonValue(body, "silent");
summary.HasSilentField = !string.IsNullOrEmpty(silentRaw);
summary.Silent = silentRaw.Trim().ToLower() == "true";
}
catch (Exception)
{
// 网络异常时保守认为有更新(与旧逻辑一致);是否静默回退到本地命令/更新目标配置。
summary.HasUpdate = true;
summary.HasSilentField = false;
}
return summary;
}
#endregion
#region Installer config matching
public struct MatchResult
{
public string ProjectId;
public string InstallMode;
public string UpdateTarget;
public bool Silent;
}
public static MatchResult MatchFilename(string installerConfig, string exeName)
{
MatchResult result = new MatchResult();
result.ProjectId = ExtractJsonValue(installerConfig, "default_project_id");
result.InstallMode = ExtractJsonValue(installerConfig, "default_install_mode");
if (string.IsNullOrEmpty(result.InstallMode)) result.InstallMode = "full";
string rulesArray = ExtractJsonArray(installerConfig, "filename_rules");
if (string.IsNullOrEmpty(rulesArray)) return result;
List<string> rules = ParseJsonObjectArray(rulesArray);
foreach (string rule in rules)
{
string pattern = ExtractJsonValue(rule, "pattern");
string matchType = ExtractJsonValue(rule, "match_type");
bool caseSensitive = ExtractJsonBool(rule, "case_sensitive");
if (string.IsNullOrEmpty(pattern)) continue;
bool matched = false;
string nameToCheck = caseSensitive ? exeName : exeName.ToLower();
string patternToCheck = caseSensitive ? pattern : pattern.ToLower();
switch (matchType)
{
case "starts_with":
matched = nameToCheck.StartsWith(patternToCheck);
break;
case "contains":
matched = nameToCheck.Contains(patternToCheck);
break;
case "exact":
matched = nameToCheck == patternToCheck;
break;
default:
matched = nameToCheck.StartsWith(patternToCheck);
break;
}
if (matched)
{
result.ProjectId = ExtractJsonValue(rule, "project_id");
result.InstallMode = ExtractJsonValue(rule, "install_mode");
return result;
}
}
return result;
}
public static MatchResult MatchCommand(string installerConfig, string command)
{
MatchResult result = new MatchResult();
string rulesArray = ExtractJsonArray(installerConfig, "command_rules");
if (string.IsNullOrEmpty(rulesArray)) return result;
List<string> rules = ParseJsonObjectArray(rulesArray);
foreach (string rule in rules)
{
string cmd = ExtractJsonValue(rule, "command");
if (cmd.ToLower() == command.ToLower())
{
result.ProjectId = ExtractJsonValue(rule, "project_id");
result.UpdateTarget = ExtractJsonValue(rule, "update_target");
result.Silent = ExtractJsonBool(rule, "silent");
return result;
}
}
return result;
}
public static string GetUpdateConfig(string projectConfig, string updateTarget)
{
return ExtractJsonObject(projectConfig, updateTarget);
}
#endregion
}
}
File diff suppressed because it is too large Load Diff
+171
View File
@@ -0,0 +1,171 @@
using System;
using System.IO;
using System.Net;
using System.Windows.Forms;
namespace R0Installer
{
public enum UpdateMode
{
MainProgram,
AntiCheat
}
public enum InstallMode
{
Full,
ClientOnly,
AcOnly
}
static class Program
{
[STAThread]
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
ServicePointManager.DefaultConnectionLimit = 64;
string installerConfig = ConfigManager.GetInstallerConfig();
if (args.Length >= 2)
{
string command = args[0].ToLower();
string currentVersion = args[1];
ConfigManager.MatchResult cmdMatch = new ConfigManager.MatchResult();
if (installerConfig != null)
{
cmdMatch = ConfigManager.MatchCommand(installerConfig, command);
}
if (!string.IsNullOrEmpty(cmdMatch.ProjectId))
{
string projectConfig = ConfigManager.GetProjectConfigSync(cmdMatch.ProjectId);
if (projectConfig == null)
{
Console.WriteLine("ERROR: cannot load project config");
return;
}
string updateTarget = cmdMatch.UpdateTarget;
if (string.IsNullOrEmpty(updateTarget)) updateTarget = "update";
string updateConfig = ConfigManager.GetUpdateConfig(projectConfig, updateTarget);
if (string.IsNullOrEmpty(updateConfig))
{
Console.WriteLine("ERROR: update target not found");
return;
}
string checkApi = ConfigManager.ExtractJsonValue(updateConfig, "check_api");
if (string.IsNullOrEmpty(checkApi))
{
Console.WriteLine("ERROR: check_api not configured");
return;
}
ConfigManager.UpdateSummary summary = ConfigManager.GetUpdateSummarySync(cmdMatch.ProjectId, checkApi, currentVersion);
if (!summary.HasUpdate)
{
Console.WriteLine("OK");
return;
}
// 是否静默更新,优先级从高到低:
// 1) 命令行 --silent:调用方强制静默
// 2) 云端按版本下发的 silent/v3/update/check 响应里的 silent 字段,可开可关)
// 3) 命令规则 / 更新目标配置中的 silent(对该命令的所有版本生效)
bool silent;
if (HasSilentFlag(args))
silent = true;
else if (summary.HasSilentField)
silent = summary.Silent;
else
silent = cmdMatch.Silent || ConfigManager.ExtractJsonBool(updateConfig, "silent");
if (silent)
{
new SilentUpdater(currentVersion, true, cmdMatch.ProjectId, updateTarget).Run();
}
else
{
Application.Run(new UpdateForm(currentVersion, true, cmdMatch.ProjectId, updateTarget));
}
}
else
{
RunInstallMode(installerConfig);
}
}
else
{
RunInstallMode(installerConfig);
}
}
// 检测命令行中是否带有静默更新标志。支持 --silent / -s / /silent / /s 几种写法,
// 可出现在任意位置(通常作为第三个参数:R0Installer.exe update <version> --silent)。
private static bool HasSilentFlag(string[] args)
{
if (args == null) return false;
foreach (string arg in args)
{
if (string.IsNullOrEmpty(arg)) continue;
switch (arg.Trim().ToLowerInvariant())
{
case "--silent":
case "-s":
case "/silent":
case "/s":
return true;
}
}
return false;
}
private static void RunInstallMode(string installerConfig)
{
string projectId = "r0_arena";
InstallMode mode = InstallMode.Full;
if (installerConfig != null)
{
string exePath = System.Reflection.Assembly.GetExecutingAssembly().Location;
string exeName = Path.GetFileNameWithoutExtension(exePath);
ConfigManager.MatchResult fileMatch = ConfigManager.MatchFilename(installerConfig, exeName);
if (!string.IsNullOrEmpty(fileMatch.ProjectId))
projectId = fileMatch.ProjectId;
switch (fileMatch.InstallMode)
{
case "client_only": mode = InstallMode.ClientOnly; break;
case "ac_only": mode = InstallMode.AcOnly; break;
default: mode = InstallMode.Full; break;
}
}
else
{
try
{
string exePath = System.Reflection.Assembly.GetExecutingAssembly().Location;
string exeName = Path.GetFileNameWithoutExtension(exePath);
if (exeName.StartsWith("R0ClientInstaller", StringComparison.OrdinalIgnoreCase))
mode = InstallMode.ClientOnly;
else if (exeName.StartsWith("R0AcInstaller", StringComparison.OrdinalIgnoreCase))
mode = InstallMode.AcOnly;
}
catch { }
}
Application.Run(new MainForm(mode, projectId));
}
}
}
+16
View File
@@ -0,0 +1,16 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("R0对战平台安装程序")]
[assembly: AssemblyDescription("R0对战平台环境检查与安装程序")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("R0 Arena")]
[assembly: AssemblyProduct("R0Installer")]
[assembly: AssemblyCopyright("Copyright © 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("a1b2c3d4-e5f6-7890-abcd-ef1234567890")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
+39
View File
@@ -0,0 +1,39 @@
namespace R0Installer.Properties {
using System;
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("R0Installer.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<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="0" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<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=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>R0Installer</RootNamespace>
<AssemblyName>R0Installer</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<ApplicationIcon>..\icon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="ConfigManager.cs" />
<Compile Include="MainForm.cs" />
<Compile Include="UpdateForm.cs" />
<Compile Include="SilentUpdater.cs" />
<Compile Include="UpdateManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="..\icon.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
+23
View File
@@ -0,0 +1,23 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "R0Installer", "R0Installer.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
+232
View File
@@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace R0Installer
{
/// <summary>
/// 静默更新执行器:不创建任何窗口,直接在后台完成检查/下载/应用更新。
/// 复用与 UpdateForm 完全相同的配置加载与 UpdateManager 逻辑,
/// 仅去除界面展示与纯用于观感的等待,因此更新行为与有界面模式一致。
/// </summary>
public class SilentUpdater
{
private readonly string currentVersion;
private readonly bool updateConfirmed;
private readonly string projectId;
private readonly string updateTarget;
private readonly string installPath;
private UpdateManager updateManager;
private string projectConfig;
private string updateConfig;
private string[] processesToClose = new string[0];
private string launchAfterUpdate = "";
private string holdFile = "";
public SilentUpdater(string version, bool updateConfirmed, string projectId, string updateTarget)
{
this.currentVersion = version;
this.updateConfirmed = updateConfirmed;
this.projectId = projectId;
this.updateTarget = updateTarget;
this.installPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
LoadConfig();
}
/// <summary>同步入口:阻塞当前线程直到静默更新完成,返回是否成功。</summary>
public bool Run()
{
return RunAsync().GetAwaiter().GetResult();
}
private void LoadConfig()
{
projectConfig = ConfigManager.GetProjectConfigSync(projectId);
if (projectConfig != null)
updateConfig = ConfigManager.GetUpdateConfig(projectConfig, updateTarget);
if (!string.IsNullOrEmpty(updateConfig))
{
launchAfterUpdate = ConfigManager.ExtractJsonValue(updateConfig, "launch_after_update");
holdFile = ConfigManager.ExtractJsonValue(updateConfig, "hold_file");
string procArray = ConfigManager.ExtractJsonArray(updateConfig, "processes_to_close");
if (!string.IsNullOrEmpty(procArray))
processesToClose = ConfigManager.ParseJsonStringArray(procArray).ToArray();
updateManager = new UpdateManager(updateConfig, projectConfig);
}
else
{
string fallbackUpdateJson = "{\"check_api\":\"/v3/update/check?project=" + projectId + "&v={0}\",\"temp_filename_template\":\"update_v{version}.exe\"}";
updateManager = new UpdateManager(fallbackUpdateJson, "{}");
}
// 静默模式下没有界面,进度/状态仅写入调试输出,便于排查问题。
updateManager.OnProgress = (percent, message, speed) =>
Debug.WriteLine(String.Format("[静默更新] {0}% {1} {2}", percent, message, speed));
updateManager.OnStatus = (message) => Debug.WriteLine("[静默更新] " + message);
}
public async Task<bool> RunAsync()
{
bool hasHoldFile = !string.IsNullOrEmpty(holdFile) && holdFile != "null";
try
{
UpdateCheckResult result = await updateManager.CheckForUpdate(currentVersion);
if (result == null)
{
Console.WriteLine("ERROR");
return false;
}
if (!updateConfirmed && !result.has_update)
{
Console.WriteLine("OK");
return true;
}
if (hasHoldFile) CreateHoldFile();
CloseProcesses();
// 给系统一点时间释放文件句柄,确保增量补丁能成功覆盖文件。
await Task.Delay(500);
bool success = false;
switch (result.update_type)
{
case "full":
// 全量包(Inno Setup)静默安装。
success = await updateManager.PerformFullUpdate(result, true);
break;
case "incremental":
success = await updateManager.PerformIncrementalUpdate(result.download_url, installPath);
break;
case "multi_incremental":
success = await updateManager.PerformMultiIncrementalUpdate(result.incremental_updates, installPath);
break;
default:
Debug.WriteLine("未知的更新类型: " + result.update_type);
if (hasHoldFile) DeleteHoldFile();
Console.WriteLine("ERROR");
return false;
}
// 无论全量/增量,静默更新由本程序负责全流程,完成后统一清理 hold 文件并拉起主程序。
if (hasHoldFile) DeleteHoldFile();
if (success)
LaunchMainProgram();
Console.WriteLine(success ? "OK" : "ERROR");
return success;
}
catch (Exception ex)
{
Debug.WriteLine("静默更新出错: " + ex);
if (hasHoldFile) DeleteHoldFile();
Console.WriteLine("ERROR: " + ex.Message);
return false;
}
}
private void CloseProcesses()
{
foreach (string name in processesToClose)
{
try
{
Process[] processes = Process.GetProcessesByName(name);
foreach (Process proc in processes)
{
try
{
proc.Kill();
proc.WaitForExit(5000);
}
catch (Exception ex)
{
Debug.WriteLine("关闭进程失败: " + ex.Message);
}
finally
{
proc.Dispose();
}
}
}
catch (Exception ex)
{
Debug.WriteLine("获取进程失败: " + ex.Message);
}
}
}
private string GetHoldFilePath()
{
return Path.Combine(Path.GetTempPath(), holdFile);
}
private void CreateHoldFile()
{
try
{
File.WriteAllText(GetHoldFilePath(), DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
}
catch (Exception ex)
{
Debug.WriteLine("创建 hold 文件失败: " + ex.Message);
}
}
private void DeleteHoldFile()
{
try
{
string holdPath = GetHoldFilePath();
if (File.Exists(holdPath))
File.Delete(holdPath);
}
catch (Exception ex)
{
Debug.WriteLine("删除 hold 文件失败: " + ex.Message);
}
}
private void LaunchMainProgram()
{
if (string.IsNullOrEmpty(launchAfterUpdate)) return;
try
{
string mainExe = Path.Combine(installPath, launchAfterUpdate);
if (File.Exists(mainExe))
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = mainExe;
psi.UseShellExecute = true;
psi.WorkingDirectory = installPath;
Process.Start(psi);
}
else
{
Debug.WriteLine("程序未找到: " + mainExe);
}
}
catch (Exception ex)
{
Debug.WriteLine("启动程序失败: " + ex.Message);
}
}
}
}
+492
View File
@@ -0,0 +1,492 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace R0Installer
{
public class UpdateForm : Form
{
private Panel progressBar;
private Panel progressFill;
private Label titleLabel;
private Label statusLabel;
private Label percentLabel;
private Label versionLabel;
private string currentVersion;
private UpdateManager updateManager;
private readonly Color primaryGreen = Color.FromArgb(76, 175, 80);
private readonly Color darkBg = Color.FromArgb(18, 18, 18);
private readonly Color panelBg = Color.FromArgb(28, 28, 28);
private readonly Color textGray = Color.FromArgb(180, 180, 180);
private readonly Color textDark = Color.FromArgb(100, 100, 100);
private bool isDragging = false;
private Point dragStart;
private string installPath;
private bool updateConfirmed;
private string projectId;
private string updateTarget;
private string projectConfig;
private string updateConfig;
private string windowTitle;
private string[] processesToClose;
private string launchAfterUpdate;
private string holdFile;
public UpdateForm(string version, bool updateConfirmed, string projectId, string updateTarget)
{
this.currentVersion = version;
this.updateConfirmed = updateConfirmed;
this.projectId = projectId;
this.updateTarget = updateTarget;
this.installPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
LoadConfig();
InitializeComponent();
SetupUI();
this.Load += UpdateForm_Load;
}
private void LoadConfig()
{
projectConfig = ConfigManager.GetProjectConfigSync(projectId);
if (projectConfig != null)
{
updateConfig = ConfigManager.GetUpdateConfig(projectConfig, updateTarget);
}
if (updateConfig != null)
{
windowTitle = ConfigManager.ExtractJsonValue(updateConfig, "window_title");
launchAfterUpdate = ConfigManager.ExtractJsonValue(updateConfig, "launch_after_update");
holdFile = ConfigManager.ExtractJsonValue(updateConfig, "hold_file");
string procArray = ConfigManager.ExtractJsonArray(updateConfig, "processes_to_close");
if (!string.IsNullOrEmpty(procArray))
{
List<string> procs = ConfigManager.ParseJsonStringArray(procArray);
processesToClose = procs.ToArray();
}
this.updateManager = new UpdateManager(updateConfig, projectConfig);
}
else
{
windowTitle = "更新程序";
processesToClose = new string[0];
launchAfterUpdate = "";
holdFile = "";
string fallbackUpdateJson = "{\"check_api\":\"/v3/update/check?project=" + projectId + "&v={0}\",\"temp_filename_template\":\"update_v{version}.exe\"}";
string fallbackProjectJson = "{}";
this.updateManager = new UpdateManager(fallbackUpdateJson, fallbackProjectJson);
}
if (string.IsNullOrEmpty(windowTitle)) windowTitle = "更新程序";
if (processesToClose == null) processesToClose = new string[0];
this.updateManager.OnProgress = OnProgressUpdate;
this.updateManager.OnStatus = OnStatusUpdate;
}
private async void UpdateForm_Load(object sender, EventArgs e)
{
await StartUpdateProcess();
}
private void InitializeComponent()
{
this.SuspendLayout();
this.Text = windowTitle;
this.Size = new Size(500, 200);
this.StartPosition = FormStartPosition.CenterScreen;
this.FormBorderStyle = FormBorderStyle.None;
this.BackColor = darkBg;
this.DoubleBuffered = true;
try
{
this.Icon = Icon.ExtractAssociatedIcon(System.Reflection.Assembly.GetExecutingAssembly().Location);
}
catch { }
this.Region = CreateRoundedRegion(this.Width, this.Height, 12);
this.MouseDown += Form_MouseDown;
this.MouseMove += Form_MouseMove;
this.MouseUp += Form_MouseUp;
this.ResumeLayout(false);
}
private Region CreateRoundedRegion(int width, int height, int radius)
{
GraphicsPath path = new GraphicsPath();
path.AddArc(0, 0, radius * 2, radius * 2, 180, 90);
path.AddArc(width - radius * 2, 0, radius * 2, radius * 2, 270, 90);
path.AddArc(width - radius * 2, height - radius * 2, radius * 2, radius * 2, 0, 90);
path.AddArc(0, height - radius * 2, radius * 2, radius * 2, 90, 90);
path.CloseAllFigures();
return new Region(path);
}
private void SetupUI()
{
Label closeBtn = new Label();
closeBtn.Text = "×";
closeBtn.Font = new Font("Segoe UI", 14, FontStyle.Regular);
closeBtn.ForeColor = textGray;
closeBtn.Size = new Size(36, 36);
closeBtn.Location = new Point(this.Width - 40, 4);
closeBtn.TextAlign = ContentAlignment.MiddleCenter;
closeBtn.Cursor = Cursors.Hand;
closeBtn.Click += CloseBtn_Click;
closeBtn.MouseEnter += (s, e) => ((Label)s).ForeColor = Color.White;
closeBtn.MouseLeave += (s, e) => ((Label)s).ForeColor = textGray;
this.Controls.Add(closeBtn);
titleLabel = new Label();
titleLabel.Text = windowTitle.Replace("程序", "");
titleLabel.Font = new Font("Microsoft YaHei", 16, FontStyle.Bold);
titleLabel.ForeColor = primaryGreen;
titleLabel.AutoSize = true;
titleLabel.BackColor = Color.Transparent;
titleLabel.Location = new Point((this.Width - 180) / 2, 30);
this.Controls.Add(titleLabel);
versionLabel = new Label();
versionLabel.Text = "当前版本: " + currentVersion;
versionLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Regular);
versionLabel.ForeColor = textGray;
versionLabel.AutoSize = true;
versionLabel.Location = new Point(40, 75);
this.Controls.Add(versionLabel);
statusLabel = new Label();
statusLabel.Text = "正在检查更新...";
statusLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Regular);
statusLabel.ForeColor = textGray;
statusLabel.AutoSize = false;
statusLabel.Size = new Size(300, 25);
statusLabel.Location = new Point(40, 110);
this.Controls.Add(statusLabel);
percentLabel = new Label();
percentLabel.Text = "";
percentLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Bold);
percentLabel.ForeColor = primaryGreen;
percentLabel.AutoSize = false;
percentLabel.Size = new Size(160, 25);
percentLabel.TextAlign = ContentAlignment.MiddleRight;
percentLabel.Location = new Point(this.Width - 200, 110);
this.Controls.Add(percentLabel);
progressBar = new Panel();
progressBar.Size = new Size(this.Width - 80, 8);
progressBar.Location = new Point(40, 140);
progressBar.BackColor = panelBg;
this.Controls.Add(progressBar);
progressFill = new Panel();
progressFill.Size = new Size(0, 8);
progressFill.Location = new Point(0, 0);
progressFill.BackColor = primaryGreen;
progressBar.Controls.Add(progressFill);
this.Paint += UpdateForm_Paint;
}
private void UpdateForm_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
using (Pen pen = new Pen(Color.FromArgb(50, 255, 255, 255), 1))
{
Rectangle rect = new Rectangle(0, 0, this.Width - 1, this.Height - 1);
using (GraphicsPath path = CreateRoundedPath(rect, 12))
{
g.DrawPath(pen, path);
}
}
}
private GraphicsPath CreateRoundedPath(Rectangle rect, int radius)
{
GraphicsPath path = new GraphicsPath();
path.AddArc(rect.X, rect.Y, radius * 2, radius * 2, 180, 90);
path.AddArc(rect.Right - radius * 2, rect.Y, radius * 2, radius * 2, 270, 90);
path.AddArc(rect.Right - radius * 2, rect.Bottom - radius * 2, radius * 2, radius * 2, 0, 90);
path.AddArc(rect.X, rect.Bottom - radius * 2, radius * 2, radius * 2, 90, 90);
path.CloseAllFigures();
return path;
}
#region Window events
private void CloseBtn_Click(object sender, EventArgs e) { Application.Exit(); }
private void Form_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left) { isDragging = true; dragStart = e.Location; }
}
private void Form_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
Point diff = new Point(e.X - dragStart.X, e.Y - dragStart.Y);
this.Location = new Point(this.Location.X + diff.X, this.Location.Y + diff.Y);
}
}
private void Form_MouseUp(object sender, MouseEventArgs e) { isDragging = false; }
#endregion
#region Progress
private void OnProgressUpdate(int percent, string message, string speedText)
{
if (this.InvokeRequired) { this.Invoke(new Action<int, string, string>(OnProgressUpdate), percent, message, speedText); return; }
int targetWidth = (int)(progressBar.Width * percent / 100.0);
progressFill.Width = targetWidth;
if (message != null)
statusLabel.Text = message;
if (!string.IsNullOrEmpty(speedText))
percentLabel.Text = String.Format("{0}% {1}", percent, speedText);
else
percentLabel.Text = String.Format("{0}%", percent);
}
private void OnStatusUpdate(string message)
{
if (this.InvokeRequired) { this.Invoke(new Action<string>(OnStatusUpdate), message); return; }
statusLabel.Text = message;
}
private void UpdateVersionLabel(string newVersion)
{
if (this.InvokeRequired) { this.Invoke(new Action<string>(UpdateVersionLabel), newVersion); return; }
versionLabel.Text = String.Format("更新: {0} → {1}", currentVersion, newVersion);
}
#endregion
#region Update process
private async Task StartUpdateProcess()
{
try
{
OnStatusUpdate("正在获取更新信息...");
UpdateCheckResult result = await updateManager.CheckForUpdate(currentVersion);
if (result == null)
{
OnStatusUpdate("获取更新信息失败,请检查网络连接");
Console.WriteLine("ERROR");
await Task.Delay(3000);
Application.Exit();
return;
}
if (!updateConfirmed && !result.has_update)
{
OnStatusUpdate("当前已是最新版本");
Console.WriteLine("OK");
Application.Exit();
return;
}
bool hasHoldFile = !string.IsNullOrEmpty(holdFile) && holdFile != "null";
if (hasHoldFile)
{
CreateHoldFile();
}
OnStatusUpdate("正在关闭相关程序...");
CloseProcesses();
await Task.Delay(500);
UpdateVersionLabel(result.latest_version);
bool success = false;
bool isIncrementalUpdate = false;
switch (result.update_type)
{
case "full":
OnStatusUpdate("正在准备全量更新...");
success = await updateManager.PerformFullUpdate(result);
if (success)
{
OnProgressUpdate(100, "更新完成,正在启动安装程序...", "");
await Task.Delay(1000);
}
break;
case "incremental":
isIncrementalUpdate = true;
OnStatusUpdate("正在准备增量更新...");
success = await updateManager.PerformIncrementalUpdate(result.download_url, installPath);
if (success)
{
OnProgressUpdate(100, "更新完成!", "");
await Task.Delay(1000);
if (hasHoldFile) DeleteHoldFile();
LaunchMainProgram();
}
break;
case "multi_incremental":
isIncrementalUpdate = true;
OnStatusUpdate("正在准备增量更新...");
success = await updateManager.PerformMultiIncrementalUpdate(result.incremental_updates, installPath);
if (success)
{
OnProgressUpdate(100, "更新完成!", "");
await Task.Delay(1000);
if (hasHoldFile) DeleteHoldFile();
LaunchMainProgram();
}
break;
default:
OnStatusUpdate("未知的更新类型: " + result.update_type);
await Task.Delay(3000);
break;
}
if (!success)
{
OnStatusUpdate("更新失败,请稍后重试");
if (hasHoldFile && isIncrementalUpdate) DeleteHoldFile();
await Task.Delay(3000);
}
Application.Exit();
}
catch (Exception ex)
{
OnStatusUpdate("更新出错: " + ex.Message);
Debug.WriteLine("更新错误: " + ex.ToString());
Console.WriteLine("ERROR: " + ex.Message);
System.Threading.Thread.Sleep(5000);
Application.Exit();
}
}
private void CloseProcesses()
{
foreach (string name in processesToClose)
{
try
{
Process[] processes = Process.GetProcessesByName(name);
foreach (Process proc in processes)
{
try
{
proc.Kill();
proc.WaitForExit(5000);
}
catch (Exception ex)
{
Debug.WriteLine("关闭进程失败: " + ex.Message);
}
finally
{
proc.Dispose();
}
}
}
catch (Exception ex)
{
Debug.WriteLine("获取进程失败: " + ex.Message);
}
}
}
private string GetHoldFilePath()
{
return Path.Combine(Path.GetTempPath(), holdFile);
}
private void CreateHoldFile()
{
try
{
string holdPath = GetHoldFilePath();
File.WriteAllText(holdPath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
}
catch (Exception ex)
{
Debug.WriteLine("创建 hold 文件失败: " + ex.Message);
}
}
private void DeleteHoldFile()
{
try
{
string holdPath = GetHoldFilePath();
if (File.Exists(holdPath))
{
File.Delete(holdPath);
}
}
catch (Exception ex)
{
Debug.WriteLine("删除 hold 文件失败: " + ex.Message);
}
}
private void LaunchMainProgram()
{
if (string.IsNullOrEmpty(launchAfterUpdate)) return;
try
{
string mainExe = Path.Combine(installPath, launchAfterUpdate);
if (File.Exists(mainExe))
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = mainExe;
psi.UseShellExecute = true;
psi.WorkingDirectory = installPath;
Process.Start(psi);
}
else
{
Debug.WriteLine("程序未找到: " + mainExe);
}
}
catch (Exception ex)
{
Debug.WriteLine("启动程序失败: " + ex.Message);
}
}
#endregion
}
}
+944
View File
@@ -0,0 +1,944 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace R0Installer
{
public class UpdateManager
{
private string checkApiTemplate;
private string userAgent;
private int maxThreads;
private string tempFilenameTemplate;
private string fullSilentArgs;
public Action<int, string, string> OnProgress;
public Action<string> OnStatus;
private long speedCalcLastBytes = 0;
private DateTime speedCalcLastTime = DateTime.Now;
private double currentSpeed = 0;
private string hpatchzPath;
public UpdateManager(string updateConfigJson, string projectConfigJson)
{
checkApiTemplate = ConfigManager.ExtractJsonValue(updateConfigJson, "check_api");
tempFilenameTemplate = ConfigManager.ExtractJsonValue(updateConfigJson, "temp_filename_template");
string downloadConfig = ConfigManager.ExtractJsonObject(projectConfigJson, "download_config");
if (!string.IsNullOrEmpty(downloadConfig))
{
maxThreads = ConfigManager.ExtractJsonInt(downloadConfig, "max_threads", 8);
userAgent = ConfigManager.ExtractJsonValue(downloadConfig, "user_agent");
}
else
{
maxThreads = 8;
}
if (string.IsNullOrEmpty(userAgent)) userAgent = "r0_installer";
if (string.IsNullOrEmpty(tempFilenameTemplate)) tempFilenameTemplate = "update_v{version}.exe";
// 全量安装包(Inno Setup 打包)静默安装参数,可由更新配置覆盖。
// /VERYSILENT 完全无界面;/SUPPRESSMSGBOXES 抑制弹窗;/NORESTART 安装后不自动重启。
fullSilentArgs = ConfigManager.ExtractJsonValue(updateConfigJson, "silent_install_args");
if (string.IsNullOrEmpty(fullSilentArgs)) fullSilentArgs = "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART";
string exeDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
hpatchzPath = Path.Combine(exeDir, "hpatchz.exe");
}
#region Version check
public async Task<UpdateCheckResult> CheckForUpdate(string currentVersion)
{
try
{
string apiUrl = "https://a-p-i.r0csgo.com" + String.Format(checkApiTemplate, Uri.EscapeDataString(currentVersion));
using (WebClient client = new WebClient())
{
client.Encoding = Encoding.UTF8;
client.Headers.Add("User-Agent", userAgent);
string json = await client.DownloadStringTaskAsync(apiUrl);
string data = ConfigManager.ExtractJsonObject(json, "data");
if (!string.IsNullOrEmpty(data))
return ParseUpdateCheckResult(data);
return ParseUpdateCheckResult(json);
}
}
catch (Exception ex)
{
Debug.WriteLine("检查更新失败: " + ex.Message);
return null;
}
}
private UpdateCheckResult ParseUpdateCheckResult(string json)
{
UpdateCheckResult result = new UpdateCheckResult();
result.has_update = ConfigManager.ExtractJsonBool(json, "has_update");
result.silent = ConfigManager.ExtractJsonBool(json, "silent");
result.latest_version = ConfigManager.ExtractJsonValue(json, "latest_version");
result.current_version = ConfigManager.ExtractJsonValue(json, "current_version");
result.update_type = ConfigManager.ExtractJsonValue(json, "update_type");
string rawUrl = ConfigManager.ExtractJsonValue(json, "download_url");
result.download_url = string.IsNullOrEmpty(rawUrl) ? "" : rawUrl.Replace("\\/", "/");
result.file_size = ConfigManager.ExtractJsonValue(json, "file_size");
result.changelog = ConfigManager.ExtractJsonValue(json, "changelog");
result.total_size = ConfigManager.ExtractJsonValue(json, "total_size");
string threadsStr = ConfigManager.ExtractJsonValue(json, "threads");
if (!string.IsNullOrEmpty(threadsStr))
{
int parsedThreads;
if (int.TryParse(threadsStr, out parsedThreads) && parsedThreads > 0)
{
maxThreads = parsedThreads;
}
}
if (result.update_type == "multi_incremental")
{
result.incremental_updates = ParseIncrementalUpdates(json);
}
return result;
}
private List<IncrementalUpdate> ParseIncrementalUpdates(string json)
{
List<IncrementalUpdate> updates = new List<IncrementalUpdate>();
string arrayJson = ConfigManager.ExtractJsonArray(json, "incremental_updates");
if (string.IsNullOrEmpty(arrayJson)) return updates;
List<string> objects = ConfigManager.ParseJsonObjectArray(arrayJson);
foreach (string objJson in objects)
{
IncrementalUpdate update = new IncrementalUpdate();
update.from_version = ConfigManager.ExtractJsonValue(objJson, "from_version");
update.to_version = ConfigManager.ExtractJsonValue(objJson, "to_version");
string rawUpdateUrl = ConfigManager.ExtractJsonValue(objJson, "download_url");
update.download_url = string.IsNullOrEmpty(rawUpdateUrl) ? "" : rawUpdateUrl.Replace("\\/", "/");
update.file_size = ConfigManager.ExtractJsonValue(objJson, "file_size");
update.changelog = ConfigManager.ExtractJsonValue(objJson, "changelog");
updates.Add(update);
}
return updates;
}
#endregion
#region Full update
public async Task<bool> PerformFullUpdate(UpdateCheckResult updateInfo)
{
return await PerformFullUpdate(updateInfo, false);
}
public async Task<bool> PerformFullUpdate(UpdateCheckResult updateInfo, bool silent)
{
try
{
ReportStatus("正在下载完整安装包...");
string filename = tempFilenameTemplate.Replace("{version}", updateInfo.latest_version);
string tempPath = ConfigManager.ResolveWritableDownloadPath(
Path.Combine(ConfigManager.GetWorkDirectory(), filename));
string actualPath = await DownloadFileMultiThread(updateInfo.download_url, tempPath, "安装包");
if (!ConfigManager.IsDownloadedFileValid(actualPath))
throw new Exception("下载的安装包无效");
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = actualPath;
psi.UseShellExecute = true;
psi.WorkingDirectory = Path.GetDirectoryName(actualPath);
if (silent)
{
// Inno Setup 安装包静默安装:传入 /VERYSILENT 等参数,等待安装结束。
// 本程序 manifest 已要求管理员权限,子进程继承提权,Inno 不会再二次提权重启,
// 因此 WaitForExit 能准确反映安装完成。
ReportStatus("正在静默安装更新...");
psi.Arguments = fullSilentArgs;
using (Process process = Process.Start(psi))
{
if (process != null)
{
await Task.Run(new Action(process.WaitForExit));
int exitCode = process.ExitCode;
// Inno Setup 退出码:0=成功,3010=成功但需重启。
if (exitCode != 0 && exitCode != 3010)
throw new Exception(String.Format("静默安装失败,退出代码: {0}", exitCode));
}
}
ReportProgress(100, "安装完成");
}
else
{
ReportStatus("正在启动安装程序...");
Process.Start(psi);
}
return true;
}
catch (Exception ex)
{
Debug.WriteLine("全量更新失败: " + ex.Message);
throw;
}
}
#endregion
#region Incremental update
public async Task<bool> PerformIncrementalUpdate(string downloadUrl, string installPath)
{
try
{
ReportStatus("正在下载增量更新包...");
string workDir = ConfigManager.GetWorkDirectory();
string tempZipPath = Path.Combine(workDir, "r0_patch_" + Guid.NewGuid().ToString("N") + ".zip");
string actualZipPath = await DownloadFileMultiThread(downloadUrl, tempZipPath, "增量包");
ReportStatus("正在解压增量包...");
string extractPath = Path.Combine(workDir, "r0_patch_" + Guid.NewGuid().ToString("N"));
ZipFile.ExtractToDirectory(actualZipPath, extractPath);
ReportStatus("正在应用更新...");
bool success = await ApplyPatch(extractPath, installPath);
try
{
if (File.Exists(actualZipPath))
File.Delete(actualZipPath);
if (Directory.Exists(extractPath))
Directory.Delete(extractPath, true);
}
catch { }
return success;
}
catch (Exception ex)
{
Debug.WriteLine("增量更新失败: " + ex.Message);
throw;
}
}
public async Task<bool> PerformMultiIncrementalUpdate(List<IncrementalUpdate> updates, string installPath)
{
for (int i = 0; i < updates.Count; i++)
{
IncrementalUpdate update = updates[i];
ReportStatus(String.Format("正在应用更新 {0}/{1}: {2} -> {3}", i + 1, updates.Count, update.from_version, update.to_version));
bool success = await PerformIncrementalUpdate(update.download_url, installPath);
if (!success)
{
return false;
}
}
return true;
}
private async Task<bool> ApplyPatch(string patchPath, string installPath)
{
try
{
string manifestPath = Path.Combine(patchPath, "patch.json");
if (!File.Exists(manifestPath))
{
throw new Exception("增量包格式错误:找不到 patch.json");
}
string manifestJson = File.ReadAllText(manifestPath, Encoding.UTF8);
PatchManifest manifest = ParsePatchManifest(manifestJson);
int totalOperations = manifest.modified.Count + manifest.new_files.Count + manifest.deleted.Count;
int currentOperation = 0;
foreach (PatchDiffInfo diff in manifest.modified)
{
currentOperation++;
int percent = (int)(currentOperation * 100.0 / totalOperations);
ReportProgress(percent, "正在更新: " + diff.path);
string targetFile = Path.Combine(installPath, diff.path);
string patchFile = Path.Combine(patchPath, diff.patch_file);
if (!File.Exists(targetFile))
{
Debug.WriteLine("警告: 目标文件不存在,跳过: " + diff.path);
continue;
}
if (!File.Exists(patchFile))
{
Debug.WriteLine("警告: 补丁文件不存在,跳过: " + diff.path);
continue;
}
string currentHash = CalculateMD5(targetFile);
if (currentHash != diff.old_hash)
{
Debug.WriteLine("警告: 原文件hash不匹配,跳过: " + diff.path);
continue;
}
string tempOutputFile = targetFile + ".new";
bool patchSuccess = await ApplyHDiff(targetFile, patchFile, tempOutputFile);
if (patchSuccess && File.Exists(tempOutputFile))
{
string newHash = CalculateMD5(tempOutputFile);
if (newHash == diff.new_hash)
{
File.Delete(targetFile);
File.Move(tempOutputFile, targetFile);
}
else
{
Debug.WriteLine("警告: 新文件hash不匹配: " + diff.path);
File.Delete(tempOutputFile);
}
}
else
{
Debug.WriteLine("警告: 应用补丁失败: " + diff.path);
if (File.Exists(tempOutputFile))
File.Delete(tempOutputFile);
}
}
string newFilesDir = Path.Combine(patchPath, "new");
foreach (PatchFileInfo newFile in manifest.new_files)
{
currentOperation++;
int percent = (int)(currentOperation * 100.0 / totalOperations);
ReportProgress(percent, "正在添加: " + newFile.path);
string sourceFile = Path.Combine(newFilesDir, newFile.path);
string targetFile = Path.Combine(installPath, newFile.path);
if (File.Exists(sourceFile))
{
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
if (File.Exists(targetFile))
File.Delete(targetFile);
File.Copy(sourceFile, targetFile, true);
}
else
{
Debug.WriteLine("警告: 新文件不存在于增量包: " + newFile.path);
}
}
foreach (string deletePath in manifest.deleted)
{
currentOperation++;
int percent = (int)(currentOperation * 100.0 / totalOperations);
ReportProgress(percent, "正在删除: " + deletePath);
string targetFile = Path.Combine(installPath, deletePath);
if (File.Exists(targetFile))
{
try
{
File.Delete(targetFile);
}
catch (Exception ex)
{
Debug.WriteLine("警告: 删除文件失败: " + deletePath + " - " + ex.Message);
}
}
}
ReportProgress(100, "更新完成");
return true;
}
catch (Exception ex)
{
Debug.WriteLine("应用增量补丁失败: " + ex.Message);
throw;
}
}
private async Task<bool> ApplyHDiff(string oldFile, string patchFile, string outputFile)
{
if (!File.Exists(hpatchzPath))
{
string altPath = Path.Combine(Path.GetDirectoryName(oldFile), "hpatchz.exe");
if (File.Exists(altPath))
{
hpatchzPath = altPath;
}
else
{
Debug.WriteLine("hpatchz.exe 未找到");
return false;
}
}
try
{
return await Task.Run(() =>
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = hpatchzPath;
psi.Arguments = String.Format("\"{0}\" \"{1}\" \"{2}\"", oldFile, patchFile, outputFile);
psi.UseShellExecute = false;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.CreateNoWindow = true;
using (Process process = Process.Start(psi))
{
process.WaitForExit(60000);
return process.ExitCode == 0;
}
});
}
catch (Exception ex)
{
Debug.WriteLine("hpatchz执行失败: " + ex.Message);
return false;
}
}
private PatchManifest ParsePatchManifest(string json)
{
PatchManifest manifest = new PatchManifest();
manifest.from_version = ConfigManager.ExtractJsonValue(json, "from_version");
manifest.to_version = ConfigManager.ExtractJsonValue(json, "to_version");
manifest.modified = ParseModifiedArray(json);
manifest.new_files = ParseNewFilesArray(json);
manifest.deleted = ParseDeletedArray(json);
return manifest;
}
private List<PatchDiffInfo> ParseModifiedArray(string json)
{
List<PatchDiffInfo> list = new List<PatchDiffInfo>();
string arrayJson = ConfigManager.ExtractJsonArray(json, "modified");
if (string.IsNullOrEmpty(arrayJson)) return list;
List<string> objects = ConfigManager.ParseJsonObjectArray(arrayJson);
foreach (string objJson in objects)
{
PatchDiffInfo info = new PatchDiffInfo();
info.path = ConfigManager.ExtractJsonValue(objJson, "path");
info.patch_file = ConfigManager.ExtractJsonValue(objJson, "patch_file");
info.old_hash = ConfigManager.ExtractJsonValue(objJson, "old_hash");
info.new_hash = ConfigManager.ExtractJsonValue(objJson, "new_hash");
long.TryParse(ConfigManager.ExtractJsonValue(objJson, "new_size"), out info.new_size);
list.Add(info);
}
return list;
}
private List<PatchFileInfo> ParseNewFilesArray(string json)
{
List<PatchFileInfo> list = new List<PatchFileInfo>();
string arrayJson = ConfigManager.ExtractJsonArray(json, "new_files");
if (string.IsNullOrEmpty(arrayJson)) return list;
List<string> objects = ConfigManager.ParseJsonObjectArray(arrayJson);
foreach (string objJson in objects)
{
PatchFileInfo info = new PatchFileInfo();
info.path = ConfigManager.ExtractJsonValue(objJson, "path");
info.hash = ConfigManager.ExtractJsonValue(objJson, "hash");
long.TryParse(ConfigManager.ExtractJsonValue(objJson, "size"), out info.size);
list.Add(info);
}
return list;
}
private List<string> ParseDeletedArray(string json)
{
string arrayJson = ConfigManager.ExtractJsonArray(json, "deleted");
if (string.IsNullOrEmpty(arrayJson)) return new List<string>();
return ConfigManager.ParseJsonStringArray(arrayJson);
}
private string CalculateMD5(string filePath)
{
using (var md5 = MD5.Create())
using (var stream = File.OpenRead(filePath))
{
byte[] hash = md5.ComputeHash(stream);
StringBuilder sb = new StringBuilder();
foreach (byte b in hash)
sb.Append(b.ToString("x2"));
return sb.ToString();
}
}
#endregion
#region Download with retry and resume
private const int DOWNLOAD_MAX_RETRIES = 10;
private const int RETRY_DELAY_BASE_MS = 2000;
private class ChunkInfo
{
public int Index;
public long StartPos;
public long EndPos;
public long Downloaded;
}
private async Task<string> DownloadFileMultiThread(string url, string savePath, string displayName)
{
string downloadPath = ConfigManager.ResolveWritableDownloadPath(savePath);
for (int attempt = 1; ; attempt++)
{
Exception caught = null;
try
{
ConfigManager.PrepareDownloadFile(downloadPath);
await DownloadFileInternal(url, downloadPath, displayName);
return downloadPath;
}
catch (Exception ex)
{
caught = ex;
if (IsFileAccessError(ex) && attempt < DOWNLOAD_MAX_RETRIES)
downloadPath = ConfigManager.ResolveWritableDownloadPath(savePath);
}
Debug.WriteLine(String.Format("下载失败(第{0}次): {1}\nURL: {2}", attempt, caught.Message, url));
if (attempt >= DOWNLOAD_MAX_RETRIES)
throw new Exception(String.Format("下载{0}失败,已重试{1}次: {2}", displayName, DOWNLOAD_MAX_RETRIES, caught.Message));
int delay = Math.Min(RETRY_DELAY_BASE_MS * attempt, 15000);
ReportStatus(String.Format("下载中断({0}){1}秒后第{2}次重试...", caught.Message, delay / 1000, attempt));
await Task.Delay(delay);
}
}
private static bool IsFileAccessError(Exception ex)
{
while (ex != null)
{
if (ex is UnauthorizedAccessException || ex is IOException)
return true;
ex = ex.InnerException;
}
return false;
}
private class ProbeResult
{
public long FileSize = -1;
public bool SupportsRange = false;
}
private async Task DownloadFileInternal(string url, string savePath, string displayName)
{
speedCalcLastBytes = 0;
speedCalcLastTime = DateTime.Now;
currentSpeed = 0;
if (maxThreads <= 1)
{
await DownloadSingleThreadNoRange(url, savePath, displayName);
return;
}
ProbeResult probe = await ProbeUrl(url);
if (probe.FileSize <= 0 || !probe.SupportsRange)
{
await DownloadSingleThreadNoRange(url, savePath, displayName);
return;
}
long fileSize = probe.FileSize;
int chunkCount = maxThreads;
long chunkBytes = fileSize / chunkCount;
ChunkInfo[] allChunks = new ChunkInfo[chunkCount];
for (int i = 0; i < chunkCount; i++)
{
allChunks[i] = new ChunkInfo
{
Index = i,
StartPos = (long)i * chunkBytes,
EndPos = (i == chunkCount - 1) ? fileSize - 1 : (long)i * chunkBytes + chunkBytes - 1,
Downloaded = 0,
};
}
if (File.Exists(savePath))
{
try
{
if (new FileInfo(savePath).Length != fileSize)
{
File.Delete(savePath);
using (FileStream fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Write))
fs.SetLength(fileSize);
}
}
catch
{
if (File.Exists(savePath)) File.Delete(savePath);
using (FileStream fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Write))
fs.SetLength(fileSize);
}
}
else
{
using (FileStream fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Write))
fs.SetLength(fileSize);
}
object progressLock = new object();
Task[] workers = new Task[chunkCount];
for (int i = 0; i < chunkCount; i++)
{
ChunkInfo chunk = allChunks[i];
workers[i] = Task.Run(async () =>
{
await DownloadChunkWithRetry(url, savePath, chunk, allChunks, progressLock, fileSize, displayName);
});
}
await Task.WhenAll(workers);
}
private async Task<ProbeResult> ProbeUrl(string url)
{
ProbeResult result = new ProbeResult();
try
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
req.Method = "HEAD"; req.Timeout = 15000; req.UserAgent = userAgent;
using (HttpWebResponse resp = (HttpWebResponse)await Task.Factory.FromAsync<WebResponse>(req.BeginGetResponse, req.EndGetResponse, null))
{
result.FileSize = resp.ContentLength;
string acceptRanges = resp.Headers["Accept-Ranges"];
result.SupportsRange = !string.IsNullOrEmpty(acceptRanges)
&& acceptRanges.IndexOf("none", StringComparison.OrdinalIgnoreCase) < 0;
}
}
catch { }
return result;
}
private async Task DownloadChunkWithRetry(string url, string savePath, ChunkInfo chunk, ChunkInfo[] allChunks, object progressLock, long totalSize, string displayName)
{
for (int attempt = 0; attempt < DOWNLOAD_MAX_RETRIES; attempt++)
{
HttpWebResponse response = null;
Stream responseStream = null;
FileStream fs = null;
try
{
long resumeFrom = chunk.StartPos + chunk.Downloaded;
if (resumeFrom > chunk.EndPos) return;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.AddRange(resumeFrom, chunk.EndPos);
request.Timeout = 60000;
request.ReadWriteTimeout = 60000;
request.KeepAlive = true;
request.UserAgent = userAgent;
response = (HttpWebResponse)await Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse, request.EndGetResponse, null);
responseStream = response.GetResponseStream();
byte[] buffer = new byte[65536];
fs = new FileStream(savePath, FileMode.Open, FileAccess.Write, FileShare.Write);
fs.Seek(resumeFrom, SeekOrigin.Begin);
int bytesRead;
while ((bytesRead = await ReadStreamAsync(responseStream, buffer, 0, buffer.Length)) > 0)
{
await WriteStreamAsync(fs, buffer, 0, bytesRead);
chunk.Downloaded += bytesRead;
lock (progressLock)
{
long totalDownloaded = 0;
foreach (ChunkInfo c in allChunks) totalDownloaded += c.Downloaded;
DateTime now = DateTime.Now;
double elapsed = (now - speedCalcLastTime).TotalSeconds;
if (elapsed >= 0.3)
{
long delta = totalDownloaded - speedCalcLastBytes;
if (delta > 0)
{
double instantSpeed = delta / elapsed;
currentSpeed = currentSpeed <= 0 ? instantSpeed : currentSpeed * 0.3 + instantSpeed * 0.7;
}
speedCalcLastBytes = totalDownloaded;
speedCalcLastTime = now;
}
int percent = (int)(totalDownloaded * 100 / totalSize);
string sizeInfo = String.Format("{0}/{1}", FormatFileSize(totalDownloaded), FormatFileSize(totalSize));
string speedInfo = FormatSpeed(currentSpeed);
ReportProgress(percent, String.Format("正在下载{0} {1}", displayName, sizeInfo), speedInfo);
}
}
return;
}
catch (Exception ex)
{
Debug.WriteLine(String.Format("分片{0}下载失败(第{1}次): {2}", chunk.Index, attempt + 1, ex.Message));
WebException webEx = ex as WebException;
if (webEx == null && ex.InnerException != null)
webEx = ex.InnerException as WebException;
if (webEx != null && webEx.Response is HttpWebResponse)
{
HttpStatusCode code = ((HttpWebResponse)webEx.Response).StatusCode;
if (code == HttpStatusCode.NotFound || code == HttpStatusCode.Forbidden || code == HttpStatusCode.Unauthorized)
throw new Exception(String.Format("服务器返回 {0}URL: {1}", (int)code, url));
}
}
finally
{
if (fs != null) { try { fs.Close(); fs.Dispose(); } catch { } }
if (responseStream != null) { try { responseStream.Close(); responseStream.Dispose(); } catch { } }
if (response != null) { try { response.Close(); } catch { } }
}
if (attempt + 1 < DOWNLOAD_MAX_RETRIES)
await Task.Delay(Math.Min(RETRY_DELAY_BASE_MS * (attempt + 1), 10000));
}
throw new Exception(String.Format("分片{0}下载失败,已达最大重试次数", chunk.Index));
}
private async Task DownloadSingleThreadNoRange(string url, string savePath, string displayName)
{
speedCalcLastBytes = 0;
speedCalcLastTime = DateTime.Now;
currentSpeed = 0;
if (File.Exists(savePath)) { try { File.Delete(savePath); } catch { } }
for (int attempt = 0; ; attempt++)
{
HttpWebResponse response = null;
Stream responseStream = null;
FileStream fs = null;
Exception caught = null;
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Timeout = 60000; request.ReadWriteTimeout = 60000; request.UserAgent = userAgent;
response = (HttpWebResponse)await Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse, request.EndGetResponse, null);
long totalSize = response.ContentLength > 0 ? response.ContentLength : 0;
fs = new FileStream(savePath, FileMode.Create, FileAccess.Write);
responseStream = response.GetResponseStream();
byte[] buffer = new byte[65536];
long downloaded = 0;
int bytesRead;
while ((bytesRead = await ReadStreamAsync(responseStream, buffer, 0, buffer.Length)) > 0)
{
await WriteStreamAsync(fs, buffer, 0, bytesRead);
downloaded += bytesRead;
DateTime now = DateTime.Now;
double elapsed = (now - speedCalcLastTime).TotalSeconds;
if (elapsed >= 0.3)
{
long delta = downloaded - speedCalcLastBytes;
if (delta > 0)
{
double instantSpeed = delta / elapsed;
currentSpeed = currentSpeed <= 0 ? instantSpeed : currentSpeed * 0.3 + instantSpeed * 0.7;
}
speedCalcLastBytes = downloaded;
speedCalcLastTime = now;
}
int percent = totalSize > 0 ? (int)(downloaded * 100 / totalSize) : 0;
string sizeInfo = totalSize > 0 ? String.Format("{0}/{1}", FormatFileSize(downloaded), FormatFileSize(totalSize)) : FormatFileSize(downloaded);
string speedInfo = FormatSpeed(currentSpeed);
ReportProgress(percent, String.Format("正在下载{0} {1}", displayName, sizeInfo), speedInfo);
}
return;
}
catch (Exception ex)
{
caught = ex;
}
finally
{
if (fs != null) { try { fs.Close(); fs.Dispose(); } catch { } }
if (responseStream != null) { try { responseStream.Close(); responseStream.Dispose(); } catch { } }
if (response != null) { try { response.Close(); } catch { } }
}
WebException singleWebEx = caught as WebException;
if (singleWebEx == null && caught.InnerException != null)
singleWebEx = caught.InnerException as WebException;
if (singleWebEx != null && singleWebEx.Response is HttpWebResponse)
{
HttpStatusCode code = ((HttpWebResponse)singleWebEx.Response).StatusCode;
if (code == HttpStatusCode.NotFound || code == HttpStatusCode.Forbidden || code == HttpStatusCode.Unauthorized)
throw new Exception(String.Format("服务器返回 {0}URL: {1}", (int)code, url));
}
Debug.WriteLine(String.Format("单线程下载失败(第{0}次): {1}", attempt + 1, caught.Message));
if (attempt >= DOWNLOAD_MAX_RETRIES - 1) throw caught;
int delay = Math.Min(RETRY_DELAY_BASE_MS * (attempt + 1), 15000);
ReportStatus(String.Format("下载中断({0}){1}秒后重试...", caught.Message, delay / 1000));
await Task.Delay(delay);
}
}
private Task<int> ReadStreamAsync(Stream stream, byte[] buffer, int offset, int count)
{
return Task.Factory.FromAsync<int>(
(callback, state) => stream.BeginRead(buffer, offset, count, callback, state),
stream.EndRead, null);
}
private Task WriteStreamAsync(Stream stream, byte[] buffer, int offset, int count)
{
return Task.Factory.FromAsync(
(callback, state) => stream.BeginWrite(buffer, offset, count, callback, state),
stream.EndWrite, null);
}
#endregion
#region Helpers
private void ReportProgress(int percent, string message)
{
ReportProgress(percent, message, "");
}
private void ReportProgress(int percent, string message, string speedText)
{
if (OnProgress != null)
{
OnProgress(percent, message, speedText);
}
}
private void ReportStatus(string message)
{
if (OnStatus != null)
{
OnStatus(message);
}
}
private string FormatFileSize(long bytes)
{
if (bytes >= 1073741824)
return String.Format("{0:F2} GB", bytes / 1073741824.0);
if (bytes >= 1048576)
return String.Format("{0:F2} MB", bytes / 1048576.0);
if (bytes >= 1024)
return String.Format("{0:F2} KB", bytes / 1024.0);
return String.Format("{0} B", bytes);
}
private string FormatSpeed(double bytesPerSecond)
{
if (bytesPerSecond <= 0) return "";
if (bytesPerSecond >= 1073741824)
return String.Format("{0:F2} GB/s", bytesPerSecond / 1073741824.0);
if (bytesPerSecond >= 1048576)
return String.Format("{0:F2} MB/s", bytesPerSecond / 1048576.0);
if (bytesPerSecond >= 1024)
return String.Format("{0:F2} KB/s", bytesPerSecond / 1024.0);
return String.Format("{0:F0} B/s", bytesPerSecond);
}
#endregion
}
#region Data structures
public class UpdateCheckResult
{
public bool has_update;
public bool silent;
public string latest_version;
public string current_version;
public string update_type;
public string download_url;
public string file_size;
public string changelog;
public string total_size;
public List<IncrementalUpdate> incremental_updates;
}
public class IncrementalUpdate
{
public string from_version;
public string to_version;
public string download_url;
public string file_size;
public string changelog;
}
public class PatchManifest
{
public string from_version;
public string to_version;
public List<PatchDiffInfo> modified = new List<PatchDiffInfo>();
public List<PatchFileInfo> new_files = new List<PatchFileInfo>();
public List<string> deleted = new List<string>();
}
public class PatchDiffInfo
{
public string path;
public string patch_file;
public string old_hash;
public string new_hash;
public long new_size;
}
public class PatchFileInfo
{
public string path;
public string hash;
public long size;
}
#endregion
}
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="R0Installer"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- 请求管理员权限 -->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 / Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
</application>
</compatibility>
</assembly>