Files

945 lines
38 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}