Initial commit: R0Installer source, patch generator, API backend and build scripts
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace R0Installer
|
||||
{
|
||||
public class UpdateForm : Form
|
||||
{
|
||||
private Panel progressBar;
|
||||
private Panel progressFill;
|
||||
private Label titleLabel;
|
||||
private Label statusLabel;
|
||||
private Label percentLabel;
|
||||
private Label versionLabel;
|
||||
|
||||
private string currentVersion;
|
||||
private UpdateManager updateManager;
|
||||
|
||||
private readonly Color primaryGreen = Color.FromArgb(76, 175, 80);
|
||||
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 string installPath;
|
||||
private bool updateConfirmed;
|
||||
|
||||
private string projectId;
|
||||
private string updateTarget;
|
||||
private string projectConfig;
|
||||
private string updateConfig;
|
||||
|
||||
private string windowTitle;
|
||||
private string[] processesToClose;
|
||||
private string launchAfterUpdate;
|
||||
private string holdFile;
|
||||
|
||||
public UpdateForm(string version, bool updateConfirmed, string projectId, string updateTarget)
|
||||
{
|
||||
this.currentVersion = version;
|
||||
this.updateConfirmed = updateConfirmed;
|
||||
this.projectId = projectId;
|
||||
this.updateTarget = updateTarget;
|
||||
|
||||
this.installPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
LoadConfig();
|
||||
|
||||
InitializeComponent();
|
||||
SetupUI();
|
||||
|
||||
this.Load += UpdateForm_Load;
|
||||
}
|
||||
|
||||
private void LoadConfig()
|
||||
{
|
||||
projectConfig = ConfigManager.GetProjectConfigSync(projectId);
|
||||
|
||||
if (projectConfig != null)
|
||||
{
|
||||
updateConfig = ConfigManager.GetUpdateConfig(projectConfig, updateTarget);
|
||||
}
|
||||
|
||||
if (updateConfig != null)
|
||||
{
|
||||
windowTitle = ConfigManager.ExtractJsonValue(updateConfig, "window_title");
|
||||
launchAfterUpdate = ConfigManager.ExtractJsonValue(updateConfig, "launch_after_update");
|
||||
holdFile = ConfigManager.ExtractJsonValue(updateConfig, "hold_file");
|
||||
|
||||
string procArray = ConfigManager.ExtractJsonArray(updateConfig, "processes_to_close");
|
||||
if (!string.IsNullOrEmpty(procArray))
|
||||
{
|
||||
List<string> procs = ConfigManager.ParseJsonStringArray(procArray);
|
||||
processesToClose = procs.ToArray();
|
||||
}
|
||||
|
||||
this.updateManager = new UpdateManager(updateConfig, projectConfig);
|
||||
}
|
||||
else
|
||||
{
|
||||
windowTitle = "更新程序";
|
||||
processesToClose = new string[0];
|
||||
launchAfterUpdate = "";
|
||||
holdFile = "";
|
||||
|
||||
string fallbackUpdateJson = "{\"check_api\":\"/v3/update/check?project=" + projectId + "&v={0}\",\"temp_filename_template\":\"update_v{version}.exe\"}";
|
||||
string fallbackProjectJson = "{}";
|
||||
this.updateManager = new UpdateManager(fallbackUpdateJson, fallbackProjectJson);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(windowTitle)) windowTitle = "更新程序";
|
||||
if (processesToClose == null) processesToClose = new string[0];
|
||||
|
||||
this.updateManager.OnProgress = OnProgressUpdate;
|
||||
this.updateManager.OnStatus = OnStatusUpdate;
|
||||
}
|
||||
|
||||
private async void UpdateForm_Load(object sender, EventArgs e)
|
||||
{
|
||||
await StartUpdateProcess();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.SuspendLayout();
|
||||
|
||||
this.Text = windowTitle;
|
||||
this.Size = new Size(500, 200);
|
||||
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 += CloseBtn_Click;
|
||||
closeBtn.MouseEnter += (s, e) => ((Label)s).ForeColor = Color.White;
|
||||
closeBtn.MouseLeave += (s, e) => ((Label)s).ForeColor = textGray;
|
||||
this.Controls.Add(closeBtn);
|
||||
|
||||
titleLabel = new Label();
|
||||
titleLabel.Text = windowTitle.Replace("程序", "");
|
||||
titleLabel.Font = new Font("Microsoft YaHei", 16, FontStyle.Bold);
|
||||
titleLabel.ForeColor = primaryGreen;
|
||||
titleLabel.AutoSize = true;
|
||||
titleLabel.BackColor = Color.Transparent;
|
||||
titleLabel.Location = new Point((this.Width - 180) / 2, 30);
|
||||
this.Controls.Add(titleLabel);
|
||||
|
||||
versionLabel = new Label();
|
||||
versionLabel.Text = "当前版本: " + currentVersion;
|
||||
versionLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Regular);
|
||||
versionLabel.ForeColor = textGray;
|
||||
versionLabel.AutoSize = true;
|
||||
versionLabel.Location = new Point(40, 75);
|
||||
this.Controls.Add(versionLabel);
|
||||
|
||||
statusLabel = new Label();
|
||||
statusLabel.Text = "正在检查更新...";
|
||||
statusLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Regular);
|
||||
statusLabel.ForeColor = textGray;
|
||||
statusLabel.AutoSize = false;
|
||||
statusLabel.Size = new Size(300, 25);
|
||||
statusLabel.Location = new Point(40, 110);
|
||||
this.Controls.Add(statusLabel);
|
||||
|
||||
percentLabel = new Label();
|
||||
percentLabel.Text = "";
|
||||
percentLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Bold);
|
||||
percentLabel.ForeColor = primaryGreen;
|
||||
percentLabel.AutoSize = false;
|
||||
percentLabel.Size = new Size(160, 25);
|
||||
percentLabel.TextAlign = ContentAlignment.MiddleRight;
|
||||
percentLabel.Location = new Point(this.Width - 200, 110);
|
||||
this.Controls.Add(percentLabel);
|
||||
|
||||
progressBar = new Panel();
|
||||
progressBar.Size = new Size(this.Width - 80, 8);
|
||||
progressBar.Location = new Point(40, 140);
|
||||
progressBar.BackColor = panelBg;
|
||||
this.Controls.Add(progressBar);
|
||||
|
||||
progressFill = new Panel();
|
||||
progressFill.Size = new Size(0, 8);
|
||||
progressFill.Location = new Point(0, 0);
|
||||
progressFill.BackColor = primaryGreen;
|
||||
progressBar.Controls.Add(progressFill);
|
||||
|
||||
this.Paint += UpdateForm_Paint;
|
||||
}
|
||||
|
||||
private void UpdateForm_Paint(object sender, PaintEventArgs e)
|
||||
{
|
||||
Graphics g = e.Graphics;
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
|
||||
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 events
|
||||
|
||||
private void CloseBtn_Click(object sender, EventArgs e) { Application.Exit(); }
|
||||
|
||||
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)
|
||||
{
|
||||
Point diff = new Point(e.X - dragStart.X, e.Y - dragStart.Y);
|
||||
this.Location = new Point(this.Location.X + diff.X, this.Location.Y + diff.Y);
|
||||
}
|
||||
}
|
||||
|
||||
private void Form_MouseUp(object sender, MouseEventArgs e) { isDragging = false; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Progress
|
||||
|
||||
private void OnProgressUpdate(int percent, string message, string speedText)
|
||||
{
|
||||
if (this.InvokeRequired) { this.Invoke(new Action<int, string, string>(OnProgressUpdate), percent, message, speedText); return; }
|
||||
|
||||
int targetWidth = (int)(progressBar.Width * percent / 100.0);
|
||||
progressFill.Width = targetWidth;
|
||||
|
||||
if (message != null)
|
||||
statusLabel.Text = message;
|
||||
|
||||
if (!string.IsNullOrEmpty(speedText))
|
||||
percentLabel.Text = String.Format("{0}% {1}", percent, speedText);
|
||||
else
|
||||
percentLabel.Text = String.Format("{0}%", percent);
|
||||
}
|
||||
|
||||
private void OnStatusUpdate(string message)
|
||||
{
|
||||
if (this.InvokeRequired) { this.Invoke(new Action<string>(OnStatusUpdate), message); return; }
|
||||
statusLabel.Text = message;
|
||||
}
|
||||
|
||||
private void UpdateVersionLabel(string newVersion)
|
||||
{
|
||||
if (this.InvokeRequired) { this.Invoke(new Action<string>(UpdateVersionLabel), newVersion); return; }
|
||||
versionLabel.Text = String.Format("更新: {0} → {1}", currentVersion, newVersion);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update process
|
||||
|
||||
private async Task StartUpdateProcess()
|
||||
{
|
||||
try
|
||||
{
|
||||
OnStatusUpdate("正在获取更新信息...");
|
||||
UpdateCheckResult result = await updateManager.CheckForUpdate(currentVersion);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
OnStatusUpdate("获取更新信息失败,请检查网络连接");
|
||||
Console.WriteLine("ERROR");
|
||||
await Task.Delay(3000);
|
||||
Application.Exit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updateConfirmed && !result.has_update)
|
||||
{
|
||||
OnStatusUpdate("当前已是最新版本");
|
||||
Console.WriteLine("OK");
|
||||
Application.Exit();
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasHoldFile = !string.IsNullOrEmpty(holdFile) && holdFile != "null";
|
||||
if (hasHoldFile)
|
||||
{
|
||||
CreateHoldFile();
|
||||
}
|
||||
|
||||
OnStatusUpdate("正在关闭相关程序...");
|
||||
CloseProcesses();
|
||||
await Task.Delay(500);
|
||||
|
||||
UpdateVersionLabel(result.latest_version);
|
||||
|
||||
bool success = false;
|
||||
bool isIncrementalUpdate = false;
|
||||
|
||||
switch (result.update_type)
|
||||
{
|
||||
case "full":
|
||||
OnStatusUpdate("正在准备全量更新...");
|
||||
success = await updateManager.PerformFullUpdate(result);
|
||||
if (success)
|
||||
{
|
||||
OnProgressUpdate(100, "更新完成,正在启动安装程序...", "");
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
break;
|
||||
|
||||
case "incremental":
|
||||
isIncrementalUpdate = true;
|
||||
OnStatusUpdate("正在准备增量更新...");
|
||||
success = await updateManager.PerformIncrementalUpdate(result.download_url, installPath);
|
||||
if (success)
|
||||
{
|
||||
OnProgressUpdate(100, "更新完成!", "");
|
||||
await Task.Delay(1000);
|
||||
if (hasHoldFile) DeleteHoldFile();
|
||||
LaunchMainProgram();
|
||||
}
|
||||
break;
|
||||
|
||||
case "multi_incremental":
|
||||
isIncrementalUpdate = true;
|
||||
OnStatusUpdate("正在准备增量更新...");
|
||||
success = await updateManager.PerformMultiIncrementalUpdate(result.incremental_updates, installPath);
|
||||
if (success)
|
||||
{
|
||||
OnProgressUpdate(100, "更新完成!", "");
|
||||
await Task.Delay(1000);
|
||||
if (hasHoldFile) DeleteHoldFile();
|
||||
LaunchMainProgram();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
OnStatusUpdate("未知的更新类型: " + result.update_type);
|
||||
await Task.Delay(3000);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
OnStatusUpdate("更新失败,请稍后重试");
|
||||
if (hasHoldFile && isIncrementalUpdate) DeleteHoldFile();
|
||||
await Task.Delay(3000);
|
||||
}
|
||||
|
||||
Application.Exit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnStatusUpdate("更新出错: " + ex.Message);
|
||||
Debug.WriteLine("更新错误: " + ex.ToString());
|
||||
Console.WriteLine("ERROR: " + ex.Message);
|
||||
System.Threading.Thread.Sleep(5000);
|
||||
Application.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseProcesses()
|
||||
{
|
||||
foreach (string name in processesToClose)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process[] processes = Process.GetProcessesByName(name);
|
||||
foreach (Process proc in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
proc.Kill();
|
||||
proc.WaitForExit(5000);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("关闭进程失败: " + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
proc.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("获取进程失败: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetHoldFilePath()
|
||||
{
|
||||
return Path.Combine(Path.GetTempPath(), holdFile);
|
||||
}
|
||||
|
||||
private void CreateHoldFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
string holdPath = GetHoldFilePath();
|
||||
File.WriteAllText(holdPath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("创建 hold 文件失败: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteHoldFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
string holdPath = GetHoldFilePath();
|
||||
if (File.Exists(holdPath))
|
||||
{
|
||||
File.Delete(holdPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("删除 hold 文件失败: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchMainProgram()
|
||||
{
|
||||
if (string.IsNullOrEmpty(launchAfterUpdate)) return;
|
||||
|
||||
try
|
||||
{
|
||||
string mainExe = Path.Combine(installPath, launchAfterUpdate);
|
||||
|
||||
if (File.Exists(mainExe))
|
||||
{
|
||||
ProcessStartInfo psi = new ProcessStartInfo();
|
||||
psi.FileName = mainExe;
|
||||
psi.UseShellExecute = true;
|
||||
psi.WorkingDirectory = installPath;
|
||||
Process.Start(psi);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("程序未找到: " + mainExe);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("启动程序失败: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user