Files
fast_install/R0Installer/MainForm.cs
T

1162 lines
51 KiB
C#
Raw 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.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
}
}