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
+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
}