Initial commit: R0Installer source, patch generator, API backend and build scripts
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user