523 lines
21 KiB
C#
523 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Data;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Drawing;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using Aliyun.OSS;
|
|
using Aliyun.OSS.Common;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
namespace CheckDownload
|
|
{
|
|
public partial class Update : Form
|
|
{
|
|
// MD5文件名称
|
|
private const string Md5File = "md5.json";
|
|
// 阿里云OSS访问地址
|
|
private const string OssEndpoint = "oss-cn-hongkong.aliyuncs.com";
|
|
// 阿里云OSS存储空间名称
|
|
private const string OssBucketName = "sxho-hk";
|
|
// 阿里云OSS访问密钥ID
|
|
private const string OssAccessKeyId = "LTAI5tBc8BgooVrHrzfEeg1q";
|
|
// 阿里云OSS访问密钥Secret
|
|
private const string OssAccessKeySecret = "nMvSp0UqzodTzKImuAMK1a1bkSks5O";
|
|
// 用于存储下载的文件数据
|
|
private Dictionary<string, (byte[] Data, string ExpectedMd5)> _downloadedFiles = new Dictionary<string, (byte[], string)>();
|
|
// 已完成的下载数量
|
|
private int _completedCount = 0;
|
|
// 总下载数量
|
|
private int _totalCount = 0;
|
|
|
|
// 初始化窗体
|
|
public Update()
|
|
{
|
|
InitializeComponent();
|
|
ConfigureProgressBar();
|
|
}
|
|
|
|
// 配置进度条
|
|
private void ConfigureProgressBar()
|
|
{
|
|
Update_Pro.Minimum = 0;
|
|
Update_Pro.Maximum = 100;
|
|
Update_Pro.Value = 0;
|
|
Update_Pro.Step = 1;
|
|
}
|
|
|
|
private void UpdateProgressValue(int percentage)
|
|
{
|
|
if (percentage < 0) percentage = 0;
|
|
if (percentage > 100) percentage = 100;
|
|
|
|
if (this.InvokeRequired)
|
|
{
|
|
this.Invoke((MethodInvoker)delegate
|
|
{
|
|
Update_Pro.Value = Math.Min(Math.Max(percentage, Update_Pro.Minimum), Update_Pro.Maximum);
|
|
Application.DoEvents();
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Update_Pro.Value = Math.Min(Math.Max(percentage, Update_Pro.Minimum), Update_Pro.Maximum);
|
|
Application.DoEvents();
|
|
}
|
|
}
|
|
|
|
private void UpdateStatus(string message)
|
|
{
|
|
if (this.InvokeRequired)
|
|
{
|
|
this.Invoke((MethodInvoker)delegate { Status_Box.Text = message; });
|
|
}
|
|
else
|
|
{
|
|
Status_Box.Text = message;
|
|
}
|
|
}
|
|
|
|
public async void Update_Load(object sender, EventArgs e)
|
|
{
|
|
PositionFormToBottomRight();
|
|
await UpdateFile();
|
|
}
|
|
|
|
private void PositionFormToBottomRight()
|
|
{
|
|
Rectangle workingArea = Screen.GetWorkingArea(this);
|
|
this.Location = new Point(workingArea.Right - this.Width, workingArea.Bottom - this.Height);
|
|
}
|
|
|
|
private async Task UpdateFile()
|
|
{
|
|
try
|
|
{
|
|
const int totalSteps = 4;
|
|
int currentStep = 0;
|
|
|
|
UpdateStatus("下载在线MD5文件并读取...");
|
|
OssClient client = new OssClient(OssEndpoint, OssAccessKeyId, OssAccessKeySecret);
|
|
var onlineData = ReadOnlineMd5File(client, OssBucketName, Md5File);
|
|
if (!ValidateOnlineData(onlineData.Version, onlineData.Md5, onlineData.Data))
|
|
{
|
|
UpdateStatus("在线MD5文件无效");
|
|
await Task.Delay(3000);
|
|
this.Close();
|
|
return;
|
|
}
|
|
|
|
UpdateStatus("比较本地和在线MD5文件...");
|
|
var compareResult = CompareMd5Data(onlineData.Data);
|
|
if (compareResult.Count == 0)
|
|
{
|
|
UpdateStatus("所有文件都是最新的,无需更新");
|
|
UpdateProgressValue(100);
|
|
await Task.Delay(2000);
|
|
this.Close();
|
|
return;
|
|
}
|
|
|
|
UpdateStatus("下载并验证文件...");
|
|
_totalCount = compareResult.Count;
|
|
await DownloadAndVerifyFiles(compareResult);
|
|
UpdateProgressValue(100);
|
|
|
|
UpdateStatus("更新完成");
|
|
await Task.Delay(1000);
|
|
this.Close();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UpdateStatus($"更新失败: {ex.Message}");
|
|
await Task.Delay(3000);
|
|
this.Close();
|
|
}
|
|
}
|
|
|
|
private (string Version, string Md5, JObject Data) ReadOnlineMd5File(OssClient client, string bucket, string key)
|
|
{
|
|
try
|
|
{
|
|
var obj = client.GetObject(bucket, key);
|
|
using (var reader = new StreamReader(obj.Content))
|
|
{
|
|
string json = reader.ReadToEnd();
|
|
var parsed = JObject.Parse(json);
|
|
string version = parsed["version"]?.ToString();
|
|
var data = (JObject)parsed["data"];
|
|
string jsonMd5 = CalculateMD5(json);
|
|
return (version, jsonMd5, data);
|
|
}
|
|
}
|
|
catch (Exception ex) when (ex is OssException || ex is JsonException)
|
|
{
|
|
UpdateStatus($"读取在线MD5文件失败: {ex.Message}");
|
|
return (null, null, null);
|
|
}
|
|
}
|
|
|
|
private bool ValidateOnlineData(string version, string md5, JObject data)
|
|
{
|
|
if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5) || data == null)
|
|
{
|
|
UpdateStatus("在线MD5文件无效");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private Dictionary<string, string> CompareMd5Data(JObject onlineData, string currentPath = "")
|
|
{
|
|
var differences = new Dictionary<string, string>();
|
|
|
|
foreach (var onlineProperty in onlineData.Properties())
|
|
{
|
|
string key = onlineProperty.Name;
|
|
JToken onlineValue = onlineProperty.Value;
|
|
string fullPath = string.IsNullOrEmpty(currentPath) ? key : $"{currentPath}/{key}";
|
|
|
|
if (onlineValue.Type == JTokenType.String)
|
|
{
|
|
string expectedMd5 = onlineValue.ToString();
|
|
if (!File.Exists(fullPath))
|
|
{
|
|
if (!differences.ContainsKey(fullPath))
|
|
{
|
|
differences[fullPath] = expectedMd5;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string localMd5 = CalculateMD5(File.ReadAllBytes(fullPath));
|
|
if (localMd5 != expectedMd5)
|
|
{
|
|
if (!differences.ContainsKey(fullPath))
|
|
{
|
|
differences[fullPath] = expectedMd5;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (onlineValue.Type == JTokenType.Object)
|
|
{
|
|
var subDifferences = CompareMd5Data((JObject)onlineValue, fullPath);
|
|
foreach (var diff in subDifferences)
|
|
{
|
|
if (!differences.ContainsKey(diff.Key))
|
|
{
|
|
differences[diff.Key] = diff.Value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return differences;
|
|
}
|
|
|
|
private async Task DownloadAndVerifyFiles(Dictionary<string, string> fileList)
|
|
{
|
|
try
|
|
{
|
|
OssClient client = new OssClient(OssEndpoint, OssAccessKeyId, OssAccessKeySecret);
|
|
_totalCount = fileList.Count;
|
|
_completedCount = 0;
|
|
_downloadedFiles.Clear();
|
|
|
|
var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
Parallel.ForEach(fileList, options, (file, state) =>
|
|
{
|
|
string filePath = file.Key;
|
|
string expectedMd5 = file.Value;
|
|
|
|
try
|
|
{
|
|
string ossKey = $"File/{expectedMd5}";
|
|
var obj = client.GetObject(OssBucketName, ossKey);
|
|
using (var memoryStream = new MemoryStream())
|
|
{
|
|
obj.Content.CopyTo(memoryStream);
|
|
byte[] fileData = memoryStream.ToArray();
|
|
|
|
lock (_downloadedFiles)
|
|
{
|
|
_downloadedFiles[filePath] = (fileData, expectedMd5);
|
|
}
|
|
|
|
Interlocked.Increment(ref _completedCount);
|
|
int progress = (int)((double)_completedCount / _totalCount * 95);
|
|
UpdateProgressValue(progress);
|
|
UpdateStatus($"已下载 {_completedCount}/{_totalCount}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UpdateStatus($"下载文件 {filePath} 失败: {ex.Message}");
|
|
}
|
|
});
|
|
});
|
|
|
|
if (_downloadedFiles.Count != fileList.Count)
|
|
{
|
|
throw new Exception($"部分文件下载失败,成功 {_downloadedFiles.Count}/{fileList.Count}");
|
|
}
|
|
|
|
VerifyAndSaveAllFiles();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UpdateStatus($"下载过程出错: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool GetProcessHandleCount(IntPtr hProcess, out uint pdwHandleCount);
|
|
|
|
private void VerifyAndSaveAllFiles()
|
|
{
|
|
UpdateStatus("正在校验文件...");
|
|
var failedFiles = new List<string>();
|
|
int processedCount = 0;
|
|
int totalFiles = _downloadedFiles.Count;
|
|
|
|
foreach (var item in _downloadedFiles)
|
|
{
|
|
string relativePath = item.Key;
|
|
byte[] data = item.Value.Data;
|
|
string expectedMd5 = item.Value.ExpectedMd5;
|
|
|
|
try
|
|
{
|
|
processedCount++;
|
|
UpdateStatus($"正在校验和保存文件 ({processedCount}/{totalFiles}): {relativePath}");
|
|
|
|
string actualMd5 = CalculateMD5(data);
|
|
if (actualMd5 != expectedMd5.ToLower())
|
|
{
|
|
throw new Exception($"MD5校验失败 (期望: {expectedMd5}, 实际: {actualMd5})");
|
|
}
|
|
|
|
string localPath = Path.Combine(Application.StartupPath, relativePath);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(localPath));
|
|
|
|
// 尝试保存文件,如果文件被占用则尝试解锁
|
|
if (!TrySaveFile(localPath, data))
|
|
{
|
|
// 如果无法解锁文件,则使用备用文件名保存
|
|
string backupPath = localPath + ".new";
|
|
File.WriteAllBytes(backupPath, data);
|
|
|
|
// 创建一个批处理文件,在应用程序退出后替换文件
|
|
CreateReplaceScript(localPath, backupPath);
|
|
UpdateStatus($"文件 {relativePath} 正在被占用,将在程序重启后更新");
|
|
}
|
|
|
|
int percentage = 95 + (int)(processedCount * 5.0 / totalFiles);
|
|
UpdateProgressValue(Math.Min(100, percentage));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UpdateStatus($"文件校验失败: {relativePath} - {ex.Message}");
|
|
failedFiles.Add(relativePath);
|
|
}
|
|
}
|
|
|
|
foreach (var failedFile in failedFiles)
|
|
{
|
|
_downloadedFiles.Remove(failedFile);
|
|
}
|
|
|
|
if (failedFiles.Count > 0)
|
|
{
|
|
throw new Exception($"{failedFiles.Count}个文件校验失败");
|
|
}
|
|
else
|
|
{
|
|
UpdateStatus("所有文件校验和保存成功");
|
|
UpdateProgressValue(100);
|
|
}
|
|
}
|
|
|
|
private bool TrySaveFile(string filePath, byte[] data)
|
|
{
|
|
try
|
|
{
|
|
// 直接尝试写入文件
|
|
File.WriteAllBytes(filePath, data);
|
|
return true;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// 文件被占用,尝试解锁
|
|
UpdateStatus($"文件 {Path.GetFileName(filePath)} 被占用,尝试解锁...");
|
|
|
|
try
|
|
{
|
|
// 尝试找出占用文件的进程
|
|
var processes = Process.GetProcesses();
|
|
foreach (var process in processes)
|
|
{
|
|
try
|
|
{
|
|
// 跳过当前进程
|
|
if (process.Id == Process.GetCurrentProcess().Id)
|
|
continue;
|
|
|
|
// 检查进程是否可能占用了文件
|
|
string processName = process.ProcessName.ToLower();
|
|
if (processName.Contains("explorer") ||
|
|
processName.Contains("chrome") ||
|
|
processName.Contains("edge") ||
|
|
processName.Contains("firefox") ||
|
|
processName.Contains("iexplore") ||
|
|
processName.Contains("winword") ||
|
|
processName.Contains("excel") ||
|
|
processName.Contains("powerpnt") ||
|
|
processName.Contains("acrobat") ||
|
|
processName.Contains("reader"))
|
|
{
|
|
// 跳过常见的系统进程
|
|
continue;
|
|
}
|
|
|
|
// 尝试获取进程句柄数量,如果句柄数量较多,可能正在使用文件
|
|
uint handleCount = 0;
|
|
if (GetProcessHandleCount(process.Handle, out handleCount) && handleCount > 0)
|
|
{
|
|
// 尝试关闭进程
|
|
UpdateStatus($"尝试关闭可能占用文件的进程: {process.ProcessName}");
|
|
process.CloseMainWindow();
|
|
|
|
// 等待进程关闭
|
|
if (!process.WaitForExit(1000))
|
|
{
|
|
// 如果进程没有响应,尝试强制结束
|
|
process.Kill();
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 忽略处理单个进程时的错误
|
|
}
|
|
finally
|
|
{
|
|
process.Dispose();
|
|
}
|
|
}
|
|
|
|
// 再次尝试写入文件
|
|
Thread.Sleep(500); // 等待一段时间确保文件已释放
|
|
File.WriteAllBytes(filePath, data);
|
|
UpdateStatus($"文件 {Path.GetFileName(filePath)} 解锁成功并已更新");
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
// 如果仍然无法写入,返回失败
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UpdateStatus($"保存文件时发生错误: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void CreateReplaceScript(string originalFile, string newFile)
|
|
{
|
|
try
|
|
{
|
|
string batchFilePath = Path.Combine(Application.StartupPath, "update_files.bat");
|
|
string processId = Process.GetCurrentProcess().Id.ToString();
|
|
string escapedOriginalFile = originalFile.Replace("\\", "\\\\");
|
|
string escapedNewFile = newFile.Replace("\\", "\\\\");
|
|
|
|
// 创建批处理文件内容
|
|
StringBuilder batchContent = new StringBuilder();
|
|
batchContent.AppendLine("@echo off");
|
|
batchContent.AppendLine(":check_process");
|
|
batchContent.AppendLine($"tasklist /FI \"PID eq {processId}\" 2>NUL | find /I \"{ processId }\" >NUL");
|
|
batchContent.AppendLine("if %ERRORLEVEL% == 0 (");
|
|
batchContent.AppendLine(" timeout /t 1 /nobreak > NUL");
|
|
batchContent.AppendLine(" goto check_process");
|
|
batchContent.AppendLine(")");
|
|
batchContent.AppendLine("timeout /t 2 /nobreak > NUL");
|
|
batchContent.AppendLine($"del \"{escapedOriginalFile}\" /f /q");
|
|
batchContent.AppendLine($"move \"{escapedNewFile}\" \"{escapedOriginalFile}\"");
|
|
batchContent.AppendLine("del \"%~f0\" /f /q");
|
|
|
|
// 写入批处理文件
|
|
bool fileExists = File.Exists(batchFilePath);
|
|
using (StreamWriter writer = new StreamWriter(batchFilePath, fileExists))
|
|
{
|
|
if (!fileExists)
|
|
{
|
|
writer.Write(batchContent.ToString());
|
|
}
|
|
else
|
|
{
|
|
// 如果文件已存在,只添加移动文件的命令
|
|
writer.WriteLine($"del \"{escapedOriginalFile}\" /f /q");
|
|
writer.WriteLine($"move \"{escapedNewFile}\" \"{escapedOriginalFile}\"");
|
|
}
|
|
}
|
|
|
|
// 如果是第一次创建批处理文件,启动它
|
|
if (!fileExists)
|
|
{
|
|
ProcessStartInfo startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = "cmd.exe",
|
|
Arguments = $"/c start \"\" /min \"{batchFilePath}\"",
|
|
CreateNoWindow = true,
|
|
UseShellExecute = true,
|
|
WindowStyle = ProcessWindowStyle.Hidden
|
|
};
|
|
Process.Start(startInfo);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UpdateStatus($"创建替换脚本时出错: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private string CalculateMD5(string input)
|
|
{
|
|
using (var md5 = MD5.Create())
|
|
{
|
|
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
|
|
byte[] hashBytes = md5.ComputeHash(inputBytes);
|
|
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
|
}
|
|
}
|
|
|
|
private string CalculateMD5(byte[] data)
|
|
{
|
|
using (var md5 = MD5.Create())
|
|
{
|
|
byte[] hashBytes = md5.ComputeHash(data);
|
|
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
|
}
|
|
}
|
|
}
|
|
} |