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 enabledSteps = new List(); private List runtimeConfigs = new List(); 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(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(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(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(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 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 stepConfigs = new List(); 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 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 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); } /// /// Top-level download with full retry logic. Never throws - retries until success. /// private async Task 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 ProbeUrl(string url) { ProbeResult result = new ProbeResult(); try { HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); req.Method = "HEAD"; req.Timeout = 15000; req.UserAgent = userAgent; using (HttpWebResponse resp = (HttpWebResponse)await Task.Factory.FromAsync(req.BeginGetResponse, req.EndGetResponse, null)) { result.FileSize = resp.ContentLength; string acceptRanges = resp.Headers["Accept-Ranges"]; result.SupportsRange = !string.IsNullOrEmpty(acceptRanges) && acceptRanges.IndexOf("none", StringComparison.OrdinalIgnoreCase) < 0; } } catch { } return result; } private async Task DownloadChunkWithRetry(string url, string savePath, ChunkInfo chunk, ChunkInfo[] allChunks, object progressLock, long totalSize, string displayName) { 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(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(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 ReadStreamAsync(Stream stream, byte[] buffer, int offset, int count) { return Task.Factory.FromAsync( (callback, state) => stream.BeginRead(buffer, offset, count, callback, state), stream.EndRead, null); } private Task WriteStreamAsync(Stream stream, byte[] buffer, int offset, int count) { return Task.Factory.FromAsync( (callback, state) => stream.BeginWrite(buffer, offset, count, callback, state), stream.EndWrite, null); } #endregion } }