6124d6060c
Co-authored-by: Cursor <cursoragent@cursor.com>
1162 lines
51 KiB
C#
1162 lines
51 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Drawing;
|
||
using System.Drawing.Drawing2D;
|
||
using System.IO;
|
||
using System.Net;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using System.Windows.Forms;
|
||
using Microsoft.Win32;
|
||
|
||
namespace R0Installer
|
||
{
|
||
public class MainForm : Form
|
||
{
|
||
private Panel totalProgressBar;
|
||
private Panel totalProgressFill;
|
||
private Panel stepProgressBar;
|
||
private Panel stepProgressFill;
|
||
private Label titleLabel;
|
||
private Label totalStatusLabel;
|
||
private Label stepStatusLabel;
|
||
private Label totalPercentLabel;
|
||
private Label stepPercentLabel;
|
||
private Label versionLabel;
|
||
|
||
private Label[] stepLabels;
|
||
private Panel[] stepIndicators;
|
||
|
||
private string projectId;
|
||
private string projectConfig;
|
||
private string installConfigJson;
|
||
private string versionData;
|
||
|
||
private int maxThreads = 8;
|
||
private string userAgent = "r0_installer";
|
||
|
||
private string mainVersion = "";
|
||
private InstallMode installMode = InstallMode.Full;
|
||
|
||
private List<StepInfo> enabledSteps = new List<StepInfo>();
|
||
private List<string> runtimeConfigs = new List<string>();
|
||
|
||
private readonly Color primaryGreen = Color.FromArgb(76, 175, 80);
|
||
private readonly Color brightGreen = Color.FromArgb(111, 255, 99);
|
||
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 int currentStep = 0;
|
||
private int totalStepCount = 3;
|
||
|
||
private string windowTitle = "安装程序";
|
||
|
||
// total progress tracking: each step occupies a fraction of [0,100]
|
||
private int currentStepIndex = 0;
|
||
private double currentStepSubProgress = 0; // 0..1 within current step
|
||
|
||
private class StepInfo
|
||
{
|
||
public string ConfigJson;
|
||
public string DisplayName;
|
||
public string StepType;
|
||
public bool ShouldExecute;
|
||
}
|
||
|
||
public MainForm(InstallMode mode, string projectId)
|
||
{
|
||
this.installMode = mode;
|
||
this.projectId = projectId;
|
||
InitializeComponent();
|
||
SetupUI();
|
||
this.Load += MainForm_Load;
|
||
}
|
||
|
||
private async void MainForm_Load(object sender, EventArgs e)
|
||
{
|
||
await StartInstallProcess();
|
||
}
|
||
|
||
private void InitializeComponent()
|
||
{
|
||
this.SuspendLayout();
|
||
this.Text = windowTitle;
|
||
this.Size = new Size(500, 380);
|
||
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 += (s, ev) => Application.Exit();
|
||
closeBtn.MouseEnter += (s, ev) => ((Label)s).ForeColor = Color.White;
|
||
closeBtn.MouseLeave += (s, ev) => ((Label)s).ForeColor = textGray;
|
||
this.Controls.Add(closeBtn);
|
||
|
||
titleLabel = new Label();
|
||
titleLabel.Text = windowTitle;
|
||
titleLabel.Font = new Font("Microsoft YaHei", 20, FontStyle.Bold);
|
||
titleLabel.ForeColor = primaryGreen;
|
||
titleLabel.AutoSize = true;
|
||
titleLabel.BackColor = Color.Transparent;
|
||
this.Controls.Add(titleLabel);
|
||
titleLabel.Location = new Point((this.Width - 240) / 2, 40);
|
||
|
||
this.Paint += MainForm_Paint;
|
||
|
||
int progressY = 180;
|
||
|
||
totalStatusLabel = new Label();
|
||
totalStatusLabel.Text = "总进度";
|
||
totalStatusLabel.Font = new Font("Microsoft YaHei", 10, FontStyle.Regular);
|
||
totalStatusLabel.ForeColor = textGray;
|
||
totalStatusLabel.AutoSize = true;
|
||
totalStatusLabel.Location = new Point(40, progressY);
|
||
this.Controls.Add(totalStatusLabel);
|
||
|
||
totalPercentLabel = new Label();
|
||
totalPercentLabel.Text = "0%";
|
||
totalPercentLabel.Font = new Font("Microsoft YaHei", 10, FontStyle.Bold);
|
||
totalPercentLabel.ForeColor = primaryGreen;
|
||
totalPercentLabel.AutoSize = true;
|
||
totalPercentLabel.Location = new Point(this.Width - 80, progressY);
|
||
this.Controls.Add(totalPercentLabel);
|
||
|
||
totalProgressBar = new Panel();
|
||
totalProgressBar.Size = new Size(this.Width - 80, 8);
|
||
totalProgressBar.Location = new Point(40, progressY + 25);
|
||
totalProgressBar.BackColor = panelBg;
|
||
this.Controls.Add(totalProgressBar);
|
||
|
||
totalProgressFill = new Panel();
|
||
totalProgressFill.Size = new Size(0, 8);
|
||
totalProgressFill.Location = new Point(0, 0);
|
||
totalProgressFill.BackColor = primaryGreen;
|
||
totalProgressBar.Controls.Add(totalProgressFill);
|
||
|
||
int stepProgressY = progressY + 60;
|
||
|
||
stepStatusLabel = new Label();
|
||
stepStatusLabel.Text = "正在初始化...";
|
||
stepStatusLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Regular);
|
||
stepStatusLabel.ForeColor = textGray;
|
||
stepStatusLabel.AutoSize = false;
|
||
stepStatusLabel.Size = new Size(300, 20);
|
||
stepStatusLabel.Location = new Point(40, stepProgressY);
|
||
this.Controls.Add(stepStatusLabel);
|
||
|
||
stepPercentLabel = new Label();
|
||
stepPercentLabel.Text = "";
|
||
stepPercentLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Regular);
|
||
stepPercentLabel.ForeColor = brightGreen;
|
||
stepPercentLabel.AutoSize = false;
|
||
stepPercentLabel.Size = new Size(160, 20);
|
||
stepPercentLabel.TextAlign = ContentAlignment.MiddleRight;
|
||
stepPercentLabel.Location = new Point(this.Width - 200, stepProgressY);
|
||
this.Controls.Add(stepPercentLabel);
|
||
|
||
stepProgressBar = new Panel();
|
||
stepProgressBar.Size = new Size(this.Width - 80, 4);
|
||
stepProgressBar.Location = new Point(40, stepProgressY + 22);
|
||
stepProgressBar.BackColor = panelBg;
|
||
this.Controls.Add(stepProgressBar);
|
||
|
||
stepProgressFill = new Panel();
|
||
stepProgressFill.Size = new Size(0, 4);
|
||
stepProgressFill.Location = new Point(0, 0);
|
||
stepProgressFill.BackColor = brightGreen;
|
||
stepProgressBar.Controls.Add(stepProgressFill);
|
||
|
||
versionLabel = new Label();
|
||
versionLabel.Text = "v1.0";
|
||
versionLabel.Font = new Font("Segoe UI", 8, FontStyle.Regular);
|
||
versionLabel.ForeColor = textDark;
|
||
versionLabel.AutoSize = true;
|
||
versionLabel.Location = new Point(this.Width - 50, this.Height - 25);
|
||
versionLabel.BackColor = Color.Transparent;
|
||
this.Controls.Add(versionLabel);
|
||
}
|
||
|
||
private void BuildStepIndicators()
|
||
{
|
||
if (this.InvokeRequired) { this.Invoke(new Action(BuildStepIndicators)); return; }
|
||
|
||
if (stepLabels != null)
|
||
{
|
||
foreach (Label l in stepLabels) this.Controls.Remove(l);
|
||
foreach (Panel p in stepIndicators) this.Controls.Remove(p);
|
||
}
|
||
|
||
totalStepCount = enabledSteps.Count;
|
||
if (totalStepCount == 0) totalStepCount = 1;
|
||
|
||
stepLabels = new Label[totalStepCount];
|
||
stepIndicators = new Panel[totalStepCount];
|
||
|
||
int stepY = 100;
|
||
int maxStepWidth = (this.Width - 40) / totalStepCount;
|
||
int stepWidth = Math.Min(140, maxStepWidth);
|
||
int startX = (this.Width - stepWidth * totalStepCount) / 2;
|
||
|
||
for (int i = 0; i < totalStepCount; i++)
|
||
{
|
||
stepIndicators[i] = new Panel();
|
||
stepIndicators[i].Size = new Size(24, 24);
|
||
stepIndicators[i].Location = new Point(startX + i * stepWidth + (stepWidth - 24) / 2, stepY);
|
||
stepIndicators[i].BackColor = Color.Transparent;
|
||
stepIndicators[i].Tag = i;
|
||
stepIndicators[i].Paint += StepIndicator_Paint;
|
||
this.Controls.Add(stepIndicators[i]);
|
||
|
||
string name = i < enabledSteps.Count ? enabledSteps[i].DisplayName : "";
|
||
stepLabels[i] = new Label();
|
||
stepLabels[i].Text = String.Format("{0}. {1}", i + 1, name);
|
||
stepLabels[i].Font = new Font("Microsoft YaHei", 9, FontStyle.Regular);
|
||
stepLabels[i].ForeColor = textDark;
|
||
stepLabels[i].AutoSize = true;
|
||
stepLabels[i].BackColor = Color.Transparent;
|
||
this.Controls.Add(stepLabels[i]);
|
||
stepLabels[i].Location = new Point(startX + i * stepWidth + (stepWidth - stepLabels[i].Width) / 2, stepY + 30);
|
||
}
|
||
|
||
titleLabel.Text = windowTitle;
|
||
this.Text = windowTitle;
|
||
this.Invalidate();
|
||
}
|
||
|
||
private void StepIndicator_Paint(object sender, PaintEventArgs e)
|
||
{
|
||
Panel panel = (Panel)sender;
|
||
int index = (int)panel.Tag;
|
||
Graphics g = e.Graphics;
|
||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||
Rectangle rect = new Rectangle(2, 2, 20, 20);
|
||
|
||
if (index < currentStep)
|
||
{
|
||
using (SolidBrush brush = new SolidBrush(primaryGreen)) { g.FillEllipse(brush, rect); }
|
||
using (Pen pen = new Pen(Color.White, 2)) { g.DrawLine(pen, 7, 12, 10, 15); g.DrawLine(pen, 10, 15, 16, 8); }
|
||
}
|
||
else if (index == currentStep)
|
||
{
|
||
using (Pen pen = new Pen(primaryGreen, 2)) { g.DrawEllipse(pen, rect); }
|
||
using (SolidBrush brush = new SolidBrush(primaryGreen)) { g.FillEllipse(brush, 8, 8, 8, 8); }
|
||
}
|
||
else
|
||
{
|
||
using (Pen pen = new Pen(textDark, 2)) { g.DrawEllipse(pen, rect); }
|
||
}
|
||
}
|
||
|
||
private void MainForm_Paint(object sender, PaintEventArgs e)
|
||
{
|
||
Graphics g = e.Graphics;
|
||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||
|
||
if (stepIndicators != null && totalStepCount > 1)
|
||
{
|
||
int stepY = 112;
|
||
int maxStepWidth = (this.Width - 40) / totalStepCount;
|
||
int stepWidth = Math.Min(140, maxStepWidth);
|
||
int startX = (this.Width - stepWidth * totalStepCount) / 2;
|
||
|
||
for (int i = 0; i < totalStepCount - 1; i++)
|
||
{
|
||
int x1 = startX + i * stepWidth + (stepWidth + 24) / 2;
|
||
int x2 = startX + (i + 1) * stepWidth + (stepWidth - 24) / 2;
|
||
Color lineColor = i < currentStep ? primaryGreen : textDark;
|
||
using (Pen pen = new Pen(lineColor, 2)) { g.DrawLine(pen, x1, stepY, x2, stepY); }
|
||
}
|
||
}
|
||
|
||
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 drag
|
||
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) { this.Location = new Point(this.Location.X + e.X - dragStart.X, this.Location.Y + e.Y - dragStart.Y); } }
|
||
private void Form_MouseUp(object sender, MouseEventArgs e) { isDragging = false; }
|
||
#endregion
|
||
|
||
#region UI updates
|
||
|
||
private void SetCurrentStep(int step)
|
||
{
|
||
if (this.InvokeRequired) { this.Invoke(new Action<int>(SetCurrentStep), step); return; }
|
||
currentStep = step;
|
||
currentStepIndex = step;
|
||
currentStepSubProgress = 0;
|
||
if (stepIndicators != null)
|
||
{
|
||
for (int i = 0; i < totalStepCount; i++)
|
||
{
|
||
stepIndicators[i].Invalidate();
|
||
stepLabels[i].ForeColor = i <= currentStep ? primaryGreen : textDark;
|
||
}
|
||
}
|
||
this.Invalidate();
|
||
}
|
||
|
||
private void RecalcTotalProgress()
|
||
{
|
||
if (totalStepCount <= 0) return;
|
||
double total = (currentStepIndex + currentStepSubProgress) / totalStepCount * 100.0;
|
||
int pct = Math.Min(100, Math.Max(0, (int)total));
|
||
UpdateTotalProgress(pct);
|
||
}
|
||
|
||
private void UpdateTotalProgress(int percent)
|
||
{
|
||
if (this.InvokeRequired) { this.Invoke(new Action<int>(UpdateTotalProgress), percent); return; }
|
||
int targetWidth = (int)(totalProgressBar.Width * percent / 100.0);
|
||
totalProgressFill.Width = targetWidth;
|
||
totalPercentLabel.Text = String.Format("{0}%", percent);
|
||
}
|
||
|
||
private void UpdateStepProgress(int percent, string message)
|
||
{
|
||
UpdateStepProgress(percent, message, null);
|
||
}
|
||
|
||
private void UpdateStepProgress(int percent, string message, string rightText)
|
||
{
|
||
if (this.InvokeRequired) { this.Invoke(new Action<int, string, string>(UpdateStepProgress), percent, message, rightText); return; }
|
||
int targetWidth = (int)(stepProgressBar.Width * percent / 100.0);
|
||
stepProgressFill.Width = targetWidth;
|
||
if (message != null) stepStatusLabel.Text = message;
|
||
if (rightText != null)
|
||
stepPercentLabel.Text = rightText;
|
||
else
|
||
stepPercentLabel.Text = percent > 0 ? String.Format("{0}%", percent) : "";
|
||
|
||
currentStepSubProgress = percent / 100.0;
|
||
RecalcTotalProgress();
|
||
}
|
||
|
||
private void UpdateStepStatus(string message)
|
||
{
|
||
if (this.InvokeRequired) { this.Invoke(new Action<string>(UpdateStepStatus), message); return; }
|
||
stepStatusLabel.Text = message;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Install process
|
||
|
||
private void CloseProjectProcesses()
|
||
{
|
||
CloseProcessesFromUpdateTarget("update");
|
||
CloseProcessesFromUpdateTarget("update_ac");
|
||
}
|
||
|
||
private void CloseProcessesFromUpdateTarget(string updateTargetKey)
|
||
{
|
||
if (projectConfig == null) return;
|
||
string updateJson = ConfigManager.ExtractJsonObject(projectConfig, updateTargetKey);
|
||
if (string.IsNullOrEmpty(updateJson)) return;
|
||
string procArray = ConfigManager.ExtractJsonArray(updateJson, "processes_to_close");
|
||
if (string.IsNullOrEmpty(procArray)) return;
|
||
List<string> processNames = ConfigManager.ParseJsonStringArray(procArray);
|
||
foreach (string name in processNames)
|
||
{
|
||
try
|
||
{
|
||
Process[] processes = Process.GetProcessesByName(name);
|
||
foreach (Process proc in processes)
|
||
{
|
||
try { proc.Kill(); proc.WaitForExit(5000); } catch { } finally { proc.Dispose(); }
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
|
||
private async Task StartInstallProcess()
|
||
{
|
||
Exception installError = null;
|
||
try
|
||
{
|
||
UpdateStepStatus("正在获取项目配置...");
|
||
projectConfig = await ConfigManager.GetProjectConfig(projectId);
|
||
if (projectConfig == null)
|
||
{
|
||
MessageBox.Show("无法获取项目配置,请检查网络连接", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
Application.Exit();
|
||
return;
|
||
}
|
||
|
||
installConfigJson = ConfigManager.ExtractJsonObject(projectConfig, "install");
|
||
if (!string.IsNullOrEmpty(installConfigJson))
|
||
{
|
||
windowTitle = ConfigManager.ExtractJsonValue(installConfigJson, "window_title");
|
||
if (string.IsNullOrEmpty(windowTitle)) windowTitle = "安装程序";
|
||
}
|
||
|
||
string downloadConfig = ConfigManager.ExtractJsonObject(projectConfig, "download_config");
|
||
if (!string.IsNullOrEmpty(downloadConfig))
|
||
{
|
||
maxThreads = ConfigManager.ExtractJsonInt(downloadConfig, "max_threads", 8);
|
||
userAgent = ConfigManager.ExtractJsonValue(downloadConfig, "user_agent");
|
||
if (string.IsNullOrEmpty(userAgent)) userAgent = "r0_installer";
|
||
}
|
||
|
||
string stepsArray = ConfigManager.ExtractJsonArray(installConfigJson, "steps");
|
||
List<string> stepConfigs = new List<string>();
|
||
if (!string.IsNullOrEmpty(stepsArray))
|
||
stepConfigs = ConfigManager.ParseJsonObjectArray(stepsArray);
|
||
|
||
enabledSteps.Clear();
|
||
foreach (string step in stepConfigs)
|
||
{
|
||
bool enabled = ConfigManager.ExtractJsonBool(step, "enabled");
|
||
if (!enabled) continue;
|
||
|
||
string stepType = ConfigManager.ExtractJsonValue(step, "type");
|
||
string displayName = ConfigManager.ExtractJsonValue(step, "display_name");
|
||
string stepId = ConfigManager.ExtractJsonValue(step, "step_id");
|
||
|
||
bool shouldExecute = true;
|
||
if (installMode == InstallMode.ClientOnly && stepType == "runtime_check")
|
||
shouldExecute = false;
|
||
if (installMode == InstallMode.ClientOnly && stepId != "main" && stepType == "download_and_run")
|
||
shouldExecute = false;
|
||
if (installMode == InstallMode.AcOnly && stepId == "main" && stepType == "download_and_run")
|
||
shouldExecute = false;
|
||
|
||
if (!shouldExecute) continue;
|
||
|
||
enabledSteps.Add(new StepInfo
|
||
{
|
||
ConfigJson = step,
|
||
DisplayName = displayName,
|
||
StepType = stepType,
|
||
ShouldExecute = true,
|
||
});
|
||
}
|
||
|
||
string runtimeArray = ConfigManager.ExtractJsonArray(projectConfig, "runtime_requirements");
|
||
if (!string.IsNullOrEmpty(runtimeArray))
|
||
runtimeConfigs = ConfigManager.ParseJsonObjectArray(runtimeArray);
|
||
|
||
BuildStepIndicators();
|
||
|
||
UpdateStepStatus("正在关闭相关程序...");
|
||
CloseProjectProcesses();
|
||
await Task.Delay(500);
|
||
|
||
UpdateStepStatus("正在获取版本信息...");
|
||
versionData = await ConfigManager.GetVersionIndex(projectId);
|
||
if (versionData == null)
|
||
{
|
||
MessageBox.Show("无法获取版本信息,请检查网络连接", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
Application.Exit();
|
||
return;
|
||
}
|
||
|
||
mainVersion = ConfigManager.ExtractJsonValue(versionData, "version");
|
||
string threadsStr = ConfigManager.ExtractJsonValue(versionData, "threads");
|
||
if (!string.IsNullOrEmpty(threadsStr)) { int t; if (int.TryParse(threadsStr, out t) && t > 0) maxThreads = t; }
|
||
|
||
this.Invoke(new Action(() => { versionLabel.Text = String.Format("v{0}", mainVersion); }));
|
||
|
||
await ExecuteSteps();
|
||
|
||
UpdateTotalProgress(100);
|
||
await Task.Delay(2000);
|
||
Application.Exit();
|
||
}
|
||
catch (Exception ex) { installError = ex; }
|
||
|
||
if (installError != null)
|
||
{
|
||
MessageBox.Show(String.Format("安装过程中出错: {0}", installError.Message), "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
Application.Exit();
|
||
}
|
||
}
|
||
|
||
private async Task ExecuteSteps()
|
||
{
|
||
for (int i = 0; i < enabledSteps.Count; i++)
|
||
{
|
||
StepInfo si = enabledSteps[i];
|
||
SetCurrentStep(i);
|
||
|
||
if (!si.ShouldExecute)
|
||
{
|
||
UpdateStepProgress(100, String.Format("跳过{0}...", si.DisplayName));
|
||
await Task.Delay(300);
|
||
}
|
||
else
|
||
{
|
||
switch (si.StepType)
|
||
{
|
||
case "runtime_check":
|
||
await CheckAndInstallRuntimeEnvironments();
|
||
break;
|
||
case "download_and_run":
|
||
await ExecuteDownloadAndRunStep(si.ConfigJson);
|
||
break;
|
||
default:
|
||
UpdateStepProgress(100, String.Format("未知步骤类型: {0}", si.StepType));
|
||
await Task.Delay(1000);
|
||
break;
|
||
}
|
||
}
|
||
|
||
currentStepSubProgress = 1.0;
|
||
RecalcTotalProgress();
|
||
}
|
||
}
|
||
|
||
private async Task ExecuteDownloadAndRunStep(string stepJson)
|
||
{
|
||
string displayName = ConfigManager.ExtractJsonValue(stepJson, "display_name");
|
||
string downloadUrlField = ConfigManager.ExtractJsonValue(stepJson, "download_url_field");
|
||
string tempFilename = ConfigManager.ExtractJsonValue(stepJson, "temp_filename");
|
||
string tempFilenameTemplate = ConfigManager.ExtractJsonValue(stepJson, "temp_filename_template");
|
||
string installerArgs = ConfigManager.ExtractJsonValue(stepJson, "installer_args");
|
||
bool waitForExit = ConfigManager.ExtractJsonBool(stepJson, "wait_for_exit");
|
||
|
||
string downloadUrl = "";
|
||
if (!string.IsNullOrEmpty(downloadUrlField) && versionData != null)
|
||
{
|
||
string raw = ConfigManager.ExtractJsonValue(versionData, downloadUrlField);
|
||
if (!string.IsNullOrEmpty(raw))
|
||
downloadUrl = raw.Replace("\\/", "/");
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(downloadUrl))
|
||
{
|
||
UpdateStepProgress(100, String.Format("{0}: 下载地址为空,跳过", displayName));
|
||
await Task.Delay(1000);
|
||
return;
|
||
}
|
||
|
||
string saveName;
|
||
if (!string.IsNullOrEmpty(tempFilenameTemplate))
|
||
saveName = tempFilenameTemplate.Replace("{version}", mainVersion);
|
||
else if (!string.IsNullOrEmpty(tempFilename))
|
||
{
|
||
if (!string.IsNullOrEmpty(mainVersion) && tempFilename.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string baseName = Path.GetFileNameWithoutExtension(tempFilename);
|
||
saveName = baseName + "_v" + mainVersion + ".exe";
|
||
}
|
||
else
|
||
saveName = tempFilename;
|
||
}
|
||
else
|
||
saveName = "download_" + Guid.NewGuid().ToString("N") + ".exe";
|
||
|
||
string savePath = ConfigManager.ResolveWritableDownloadPath(
|
||
Path.Combine(ConfigManager.GetWorkDirectory(), saveName));
|
||
|
||
CloseProjectProcesses();
|
||
await Task.Delay(300);
|
||
|
||
UpdateStepProgress(0, String.Format("正在下载{0}...", displayName));
|
||
string actualPath = await DownloadWithRetry(downloadUrl, savePath, displayName);
|
||
|
||
if (!ConfigManager.IsDownloadedFileValid(actualPath))
|
||
{
|
||
UpdateStepProgress(100, String.Format("{0}: 下载文件无效,跳过", displayName));
|
||
await Task.Delay(1500);
|
||
return;
|
||
}
|
||
|
||
UpdateStepProgress(100, String.Format("正在安装{0}...", displayName));
|
||
ProcessStartInfo psi = new ProcessStartInfo();
|
||
psi.FileName = actualPath;
|
||
if (!string.IsNullOrEmpty(installerArgs)) psi.Arguments = installerArgs;
|
||
psi.UseShellExecute = true;
|
||
psi.WorkingDirectory = Path.GetDirectoryName(actualPath);
|
||
using (Process process = Process.Start(psi))
|
||
{
|
||
if (process != null && waitForExit)
|
||
await Task.Run(new Action(process.WaitForExit));
|
||
}
|
||
UpdateStepProgress(100, String.Format("{0}完成", displayName));
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Runtime environment
|
||
|
||
private async Task CheckAndInstallRuntimeEnvironments()
|
||
{
|
||
try
|
||
{
|
||
UpdateStepProgress(5, "正在检测运行环境...");
|
||
bool installationNeeded = false;
|
||
int progressPerRuntime = runtimeConfigs.Count > 0 ? 90 / runtimeConfigs.Count : 90;
|
||
int currentProgress = 5;
|
||
|
||
foreach (string rtConfig in runtimeConfigs)
|
||
{
|
||
bool enabled = ConfigManager.ExtractJsonBool(rtConfig, "enabled");
|
||
if (!enabled) { currentProgress += progressPerRuntime; continue; }
|
||
|
||
string displayName = ConfigManager.ExtractJsonValue(rtConfig, "display_name");
|
||
bool alwaysInstall = ConfigManager.ExtractJsonBool(rtConfig, "always_install");
|
||
bool isInstalled = alwaysInstall ? false : CheckRuntimeInstalled(rtConfig);
|
||
|
||
if (alwaysInstall || !isInstalled)
|
||
{
|
||
UpdateStepProgress(currentProgress, String.Format("正在安装{0}...", displayName));
|
||
bool success = await InstallRuntimeFromConfig(rtConfig);
|
||
if (!success)
|
||
{
|
||
UpdateStepProgress(currentProgress + progressPerRuntime / 2, String.Format("{0}安装失败,但程序将继续运行...", displayName));
|
||
await Task.Delay(2000);
|
||
}
|
||
installationNeeded = true;
|
||
}
|
||
|
||
currentProgress += progressPerRuntime;
|
||
UpdateStepProgress(currentProgress, String.Format("{0}完成", displayName));
|
||
}
|
||
|
||
UpdateStepProgress(100, installationNeeded ? "运行环境安装完成" : "运行环境检测完成");
|
||
await Task.Delay(installationNeeded ? 1000 : 500);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine("运行环境检测失败: " + ex.Message);
|
||
UpdateStepProgress(100, "运行环境检测失败,程序将继续运行...");
|
||
Thread.Sleep(2000);
|
||
}
|
||
}
|
||
|
||
private bool CheckRuntimeInstalled(string rtConfig)
|
||
{
|
||
string detectionJson = ConfigManager.ExtractJsonObject(rtConfig, "detection");
|
||
if (string.IsNullOrEmpty(detectionJson)) return false;
|
||
string detectionType = ConfigManager.ExtractJsonValue(detectionJson, "type");
|
||
switch (detectionType)
|
||
{
|
||
case "registry": return CheckRegistryDetection(detectionJson);
|
||
case "dotnet_runtime": return CheckDotNetRuntimeDetection(detectionJson);
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
private bool CheckRegistryDetection(string detectionJson)
|
||
{
|
||
string keysArray = ConfigManager.ExtractJsonArray(detectionJson, "keys");
|
||
string valueName = ConfigManager.ExtractJsonValue(detectionJson, "value_name");
|
||
int expectedValue = ConfigManager.ExtractJsonInt(detectionJson, "expected_value", 1);
|
||
if (string.IsNullOrEmpty(keysArray)) return false;
|
||
List<string> keys = ConfigManager.ParseJsonStringArray(keysArray);
|
||
foreach (string keyPath in keys)
|
||
{
|
||
try
|
||
{
|
||
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(keyPath))
|
||
{
|
||
if (key != null)
|
||
{
|
||
object installed = key.GetValue(valueName);
|
||
if (installed != null && (int)installed == expectedValue) return true;
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private bool CheckDotNetRuntimeDetection(string detectionJson)
|
||
{
|
||
string runtimeName = ConfigManager.ExtractJsonValue(detectionJson, "runtime_name");
|
||
string version = ConfigManager.ExtractJsonValue(detectionJson, "version");
|
||
try
|
||
{
|
||
ProcessStartInfo psi = new ProcessStartInfo();
|
||
psi.FileName = "dotnet"; psi.Arguments = "--list-runtimes";
|
||
psi.RedirectStandardOutput = true; psi.RedirectStandardError = true;
|
||
psi.UseShellExecute = false; psi.CreateNoWindow = true;
|
||
using (Process process = Process.Start(psi))
|
||
{
|
||
if (process == null) return false;
|
||
string output = process.StandardOutput.ReadToEnd();
|
||
process.WaitForExit();
|
||
string[] lines = output.Split('\n');
|
||
foreach (string line in lines)
|
||
if (line.Contains(runtimeName) && line.Contains(version)) return true;
|
||
}
|
||
}
|
||
catch { }
|
||
return false;
|
||
}
|
||
|
||
private async Task<bool> InstallRuntimeFromConfig(string rtConfig)
|
||
{
|
||
string displayName = ConfigManager.ExtractJsonValue(rtConfig, "display_name");
|
||
bool is64 = Environment.Is64BitOperatingSystem;
|
||
string url = is64 ? ConfigManager.ExtractJsonValue(rtConfig, "download_url_x64") : ConfigManager.ExtractJsonValue(rtConfig, "download_url_x86");
|
||
string arch = is64 ? "x64" : "x86";
|
||
string filenameTemplate = ConfigManager.ExtractJsonValue(rtConfig, "temp_filename_template");
|
||
string installArgs = ConfigManager.ExtractJsonValue(rtConfig, "install_args");
|
||
string fileName = filenameTemplate.Replace("{arch}", arch).Replace("{version}", ConfigManager.ExtractJsonValue(ConfigManager.ExtractJsonObject(rtConfig, "detection"), "version"));
|
||
string filePath = ConfigManager.ResolveWritableDownloadPath(
|
||
Path.Combine(ConfigManager.GetWorkDirectory(), fileName));
|
||
|
||
try
|
||
{
|
||
string actualPath = await DownloadWithRetry(url, filePath, displayName);
|
||
|
||
if (!ConfigManager.IsDownloadedFileValid(actualPath))
|
||
throw new Exception(String.Format("{0}下载文件无效", displayName));
|
||
|
||
UpdateStepStatus(String.Format("正在安装{0}...", displayName));
|
||
ProcessStartInfo psi = new ProcessStartInfo();
|
||
psi.FileName = actualPath;
|
||
if (!string.IsNullOrEmpty(installArgs)) psi.Arguments = installArgs;
|
||
// 本程序的 manifest 已要求管理员权限,子进程会继承提权;
|
||
// 这里不再设置 Verb="runas",避免冗余的二次提权(杀软会将其当作可疑提权行为)。
|
||
psi.UseShellExecute = true;
|
||
psi.WorkingDirectory = Path.GetDirectoryName(actualPath);
|
||
using (Process process = Process.Start(psi))
|
||
{
|
||
if (process != null)
|
||
{
|
||
await Task.Run(new Action(process.WaitForExit));
|
||
int exitCode = process.ExitCode;
|
||
if (exitCode != 0 && exitCode != 3010)
|
||
throw new Exception(String.Format("{0}安装失败,退出代码: {1}", displayName, exitCode));
|
||
}
|
||
}
|
||
if (File.Exists(actualPath)) { try { await Task.Delay(500); File.Delete(actualPath); } catch { } }
|
||
return true;
|
||
}
|
||
catch (Exception ex) { Debug.WriteLine(String.Format("{0}安装失败: {1}", displayName, ex.Message)); return false; }
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Download with retry and resume
|
||
|
||
private const int DOWNLOAD_MAX_RETRIES = 10;
|
||
private const int RETRY_DELAY_BASE_MS = 2000;
|
||
|
||
private long speedCalcLastBytes = 0;
|
||
private DateTime speedCalcLastTime = DateTime.Now;
|
||
private double currentSpeed = 0;
|
||
|
||
private class ChunkInfo
|
||
{
|
||
public int Index;
|
||
public long StartPos;
|
||
public long EndPos;
|
||
public long Downloaded;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Top-level download with full retry logic. Never throws - retries until success.
|
||
/// </summary>
|
||
private async Task<string> DownloadWithRetry(string url, string savePath, string displayName)
|
||
{
|
||
string downloadPath = ConfigManager.ResolveWritableDownloadPath(savePath);
|
||
|
||
for (int attempt = 1; ; attempt++)
|
||
{
|
||
Exception caught = null;
|
||
try
|
||
{
|
||
ConfigManager.PrepareDownloadFile(downloadPath);
|
||
await DownloadFile(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);
|
||
UpdateStepProgress(0, 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 DownloadFile(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)
|
||
{
|
||
int maxRetries = DOWNLOAD_MAX_RETRIES;
|
||
|
||
for (int attempt = 0; attempt < maxRetries; 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);
|
||
string rightLabel = string.IsNullOrEmpty(speedInfo) ? String.Format("{0}%", percent) : String.Format("{0}% {1}", percent, speedInfo);
|
||
UpdateStepProgress(percent, String.Format("正在下载{0} {1}", displayName, sizeInfo), rightLabel);
|
||
}
|
||
}
|
||
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 < maxRetries)
|
||
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);
|
||
string rightLabel = string.IsNullOrEmpty(speedInfo) ? String.Format("{0}%", percent) : String.Format("{0}% {1}", percent, speedInfo);
|
||
UpdateStepProgress(percent, String.Format("正在下载{0} {1}", displayName, sizeInfo), rightLabel);
|
||
}
|
||
|
||
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);
|
||
UpdateStepProgress(0, 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
|
||
}
|
||
}
|