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 OnProgress; public Action 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 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 ParseIncrementalUpdates(string json) { List updates = new List(); string arrayJson = ConfigManager.ExtractJsonArray(json, "incremental_updates"); if (string.IsNullOrEmpty(arrayJson)) return updates; List 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 PerformFullUpdate(UpdateCheckResult updateInfo) { return await PerformFullUpdate(updateInfo, false); } public async Task 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 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 PerformMultiIncrementalUpdate(List 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 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 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 ParseModifiedArray(string json) { List list = new List(); string arrayJson = ConfigManager.ExtractJsonArray(json, "modified"); if (string.IsNullOrEmpty(arrayJson)) return list; List 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 ParseNewFilesArray(string json) { List list = new List(); string arrayJson = ConfigManager.ExtractJsonArray(json, "new_files"); if (string.IsNullOrEmpty(arrayJson)) return list; List 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 ParseDeletedArray(string json) { string arrayJson = ConfigManager.ExtractJsonArray(json, "deleted"); if (string.IsNullOrEmpty(arrayJson)) return new List(); 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 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 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(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(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(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 ReadStreamAsync(Stream stream, byte[] buffer, int offset, int count) { return Task.Factory.FromAsync( (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 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 modified = new List(); public List new_files = new List(); public List deleted = new List(); } 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 }