12 Commits

Author SHA1 Message Date
218e1456f2 将临时目录移动到程序执行位置 2025-08-22 22:55:26 +08:00
734d3bb348 修正文件名称 2025-08-07 04:43:49 +08:00
b294c96687 移除临时文件备份 2025-07-20 00:07:09 +08:00
9084f35b8f 文件移动问题 2025-07-19 18:24:19 +08:00
6d12a89c9c 自身更新 2025-07-19 17:58:38 +08:00
848de6f3fa 修改了结束exe程序的位置 2025-07-18 17:22:23 +08:00
9b790fe9e0 将移动临时文件修改为复制临时文件 2025-07-18 17:10:55 +08:00
f950b05b62 移除清理临时文件的方法 2025-07-18 16:10:22 +08:00
a3bfee2755 7zdll目录修改 2025-07-03 12:46:26 +08:00
qinsi_travel
e3b32ed453 删除配置 2025-07-02 21:27:28 +08:00
qinsi_travel
0891d7534a 恢复密钥 2025-07-02 21:23:36 +08:00
ef9aa7bc5e 优化 2025-07-02 15:50:05 +08:00
6 changed files with 607 additions and 488 deletions

View File

View File

@@ -137,16 +137,16 @@
<Reference Include="System.Configuration" />
</ItemGroup>
<ItemGroup>
<Compile Include="Form1.cs">
<Compile Include="Update.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
<Compile Include="Update.Designer.cs">
<DependentUpon>Update.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="Form1.resx">
<DependentUpon>Form1.cs</DependentUpon>
<EmbeddedResource Include="Update.resx">
<DependentUpon>Update.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
@@ -184,22 +184,23 @@
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="7z-x86.dll" />
<EmbeddedResource Include="7z-x64.dll" />
<Content Include="7z.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>这台计算机上缺少此项目引用的 NuGet 程序包。使用"NuGet 程序包还原"可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('packages\Fody.6.9.2\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Fody.6.9.2\build\Fody.targets'))" />
<Error Condition="!Exists('packages\Costura.Fody.6.0.0\build\Costura.Fody.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Costura.Fody.6.0.0\build\Costura.Fody.props'))" />
<Error Condition="!Exists('packages\Costura.Fody.6.0.0\build\Costura.Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Costura.Fody.6.0.0\build\Costura.Fody.targets'))" />
<Error Condition="!Exists('packages\Fody.6.9.2\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Fody.6.9.2\build\Fody.targets'))" />
<Error Condition="!Exists('packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets'))" />
<Error Condition="!Exists('packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets'))" />
<!-- <Error Condition="!Exists('packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets'))" /> -->
<!-- <Error Condition="!Exists('packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets'))" /> -->
</Target>
<Import Project="packages\Costura.Fody.6.0.0\build\Costura.Fody.targets" Condition="Exists('packages\Costura.Fody.6.0.0\build\Costura.Fody.targets')" />
<Import Project="packages\Fody.6.9.2\build\Fody.targets" Condition="Exists('packages\Fody.6.9.2\build\Fody.targets')" />
<Import Project="packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets" Condition="Exists('packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets')" />
<Import Project="packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets" Condition="Exists('packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets')" />
<Import Project="packages\Costura.Fody.6.0.0\build\Costura.Fody.targets" Condition="Exists('packages\Costura.Fody.6.0.0\build\Costura.Fody.targets')" />
<!-- <Import Project="packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets" Condition="Exists('packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets')" /> -->
<!-- <Import Project="packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets" Condition="Exists('packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets')" /> -->
</Project>

131
README.md
View File

@@ -32,8 +32,8 @@
## 用户界面
- **实时进度显示**:通过进度条、已完成数量/总数以及 **已下载总量(实时速度)** 的形式(两者均保留一位小数,每 0.5 秒刷新),清晰地展示更新进度。
- **简洁的状态反馈**:界面只显示当前正在处理的文件名等核心信息,避免被冗长的日志刷屏。
- **实时进度显示**:通过进度条、已完成数量/总数以及 **已下载总量(实时速度)** 的形式清晰地展示更新进度。已下载总量会包含已存在于临时目录的有效文件,速度则只计算运行时下载的部分
- **简洁的状态反馈**:界面会以 `下载:文件名...` 的格式显示当前任务,过长的文件名会被自动截断,避免信息刷屏。
- **友好的错误提示**:当发生严重错误时,会弹出简明扼要的错误信息窗口,而不是难以理解的完整堆栈跟踪。
- **自动定位与退出**:窗体启动时会自动停靠在屏幕右下角,更新完成后会自动关闭,对用户干扰极小。
@@ -53,45 +53,43 @@
## 🚀 主要功能
### 📥 多源下载支持
- **123盘云存储**: 支持主备域名自动切换,提供高可用性下载服务
- **阿里云OSS**: 备用下载源,确保文件下载的可靠性
- **智能DNS解析**: 使用多个DNS服务器提高域名解析成功率
- **123盘云存储**: (UID: `1826795402`, Path: `/1826795402/KeyAuth`) 支持主备域名自动切换,提供高可用性下载服务
- **阿里云OSS**: 备用下载源,确保文件下载的可靠性
- **智能DNS解析**: 使用多个DNS服务器提高域名解析成功率
### 🔄 智能更新机制
- **MD5完整性验证**: 下载前后进行MD5校验确保文件完整性
- **增量更新**: 仅下载已变更的文件,节省带宽和时间
- **断点续传**: 支持网络中断后继续下载,提高下载成功率
- **并发下载**: 可配置的多线程并发下载,显著提升下载速度
- **MD5完整性验证**: 下载前后进行MD5校验确保文件完整性
- **增量更新**: 仅下载已变更的文件,节省带宽和时间
- **断点续传支持**: 通过检查本地临时文件实现若文件已存在且MD5匹配则跳过下载。
- **并发下载**: 可配置的多线程并发下载,显著提升下载速度
### 🛡️ 文件处理与安全
- **文件占用处理**: 智能检测并处理被占用的文件
- **批处理脚本**: 为被占用文件创建延迟替换脚本
- **临时文件管理**: 自动清理临时文件,保持系统整洁
- **路径智能识别**: 基于MD5数据自动识别项目基准目录
- **文件占用处理**: 智能检测并处理被占用的文件,并创建延迟替换脚本。
- **临时文件管理**: 自动清理临时文件,保持系统整洁。
- **路径智能识别**: 基于MD5数据自动识别项目基准目录。
### 📦 自动解压功能
- **7z格式支持**: 内置7z解压引擎支持多种压缩格式
- **自动权限设置**: 解压后自动为exe文件设置管理员运行权限
- **覆盖解压**: 支持覆盖模式解压,确保文件更新
- **多架构兼容**: 自动选择32位/64位解压库适配不同运行环境
- **7z格式支持**: 内置7z解压引擎支持多种压缩格式
- **自动权限设置**: 解压后自动为exe文件设置管理员运行权限
- **多架构兼容**: 自动选择32位/64位解压库适配不同运行环境。
### 🔧 错误处理与重试
- **多次重试机制**: 下载失败自动重试,可配置重试次数
- **异常处理**: 完善的异常捕获和处理机制
- **状态实时显示**: 实时显示下载进度、状态和错误信息
- **多次重试机制**: 下载失败自动重试,可配置重试次数
- **异常处理**: 完善的异常捕获和处理机制
- **状态实时显示**: 实时显示下载进度、状态和错误信息
## 🏗️ 技术架构
### 核心组件
- **.NET Framework 4.7.2**: 基于稳定的.NET Framework构建
- **异步编程**: 全面采用async/await模式确保UI响应性
- **多线程下载**: 使用SemaphoreSlim控制并发数量
- **资源嵌入**: 将依赖库嵌入程序,实现单文件部署
- **.NET Framework**: 基于稳定的.NET Framework构建
- **异步编程**: 全面采用async/await模式确保UI响应性
- **多线程下载**: 使用SemaphoreSlim控制并发数量
- **资源嵌入**: 将依赖库嵌入程序,实现单文件部署
### 依赖库
- **Aliyun.OSS.SDK**: 阿里云对象存储服务支持
- **Newtonsoft.Json**: JSON数据处理
- **SevenZipExtractor**: 7z压缩文件解压支持
- **Aliyun.OSS.SDK**: 阿里云对象存储服务支持
- **Newtonsoft.Json**: JSON数据处理
- **SevenZipExtractor**: 7z压缩文件解压支持
## 📋 配置说明
@@ -115,80 +113,29 @@
"version": "1.0.0",
"data": {
"program.exe": "5d41402abc4b2a76b9719d911017c592",
"lib/library.dll": "098f6bcd4621d373cade4e832627b4f6",
"config/settings.ini": "5e40d4c123456789abcdef1234567890"
"lib/library.dll": "098f6bcd4621d373cade4e832627b4f6"
}
}
```
## 🔄 工作流程
1. **启动检查**: 程序启动时清理旧的临时文件
2. **下载MD5**: 从云存储下载最新的MD5文件
3. **文件比较**: 对比本地文件与在线MD5识别需要更新的文件
4. **并发下载**: 使用多线程下载需要更新的文件
5. **完整性验证**: 验证下载文件的MD5值
6. **文件替换**: 将新文件移动到目标位置
7. **7z解压**: 自动检测并解压tim.7z文件
8. **权限设置**: 为解压的exe文件设置管理员运行权限
9. **清理完成**: 清理临时文件,显示完成状态
1. **启动检查**: 程序启动时清理旧的更新文件 (`.new` 后缀)。
2. **下载MD5**: 从云存储下载最新的MD5文件
3. **文件比较**: 对比本地文件与在线MD5识别需要更新的文件
4. **并发下载**: 使用多线程下载需要更新的文件到临时目录。
5. **完整性验证**: 验证下载文件的MD5值
6. **文件替换**: 将新文件移动到目标位置,对被占用文件创建替换脚本。
7. **7z解压**: 自动检测并解压`tim.7z`文件(如果存在)。
8. **权限设置**: 为解压的exe文件设置管理员运行权限
9. **清理完成**: 清理临时目录,显示完成状态后退出。
## 🎯 使用场景
- **软件自动更新**: 为桌面应用程序提供自动更新功能
- **文件同步**: 在不同设备间同步文件和配置
- **批量部署**: 企业环境下的软件批量部署和更新
- **游戏更新**: 游戏客户端的增量更新和补丁分发
## 🛠️ 开发特性
### 网络优化
- **静态HttpClient**: 避免套接字耗尽问题
- **连接池复用**: 提高网络请求效率
- **超时控制**: 合理的超时设置,避免长时间等待
### 内存管理
- **流式处理**: 大文件下载使用流式处理,控制内存占用
- **及时释放**: 及时释放不再使用的资源
- **临时文件**: 合理使用临时文件,避免内存溢出
### 用户体验
- **进度显示**: 实时显示下载进度和文件信息
- **状态反馈**: 详细的状态信息和错误提示
- **窗口定位**: 智能定位到屏幕右下角,不影响用户操作
## 📈 性能特点
- **高效下载**: 多线程并发下载,充分利用网络带宽
- **智能重试**: 失败文件智能重试,提高成功率
- **资源节约**: 仅下载变更文件,节省网络流量
- **快速启动**: 优化的启动流程,快速进入工作状态
## 🔒 安全性
- **MD5验证**: 确保文件在传输过程中没有被篡改
- **用户权限**: 在当前用户权限下操作,避免权限滥用
- **临时目录**: 使用用户临时目录,避免权限问题
- **异常处理**: 完善的异常处理,避免程序崩溃
## 📝 版本历史
### 最新版本特性
- ✅ 新增"已下载总量 + 实时下载速度"显示,速度与大小均保留一位小数,避免大文件下载时 UI 停滞误判。
- ✅ 添加7z自动解压功能
- ✅ 支持解压后程序自动设置管理员权限
- ✅ 优化多线程下载性能
- ✅ 增强错误处理和重试机制
- ✅ 改进用户界面和状态显示
- ✅ 过滤 .db / .db3 数据库文件,避免占用导致失败
- ✅ 更新列表或压缩包中出现 tim.dll 时,自动结束 tim.exe 后替换
- ✅ 所有文件匹配与进程检测均采用大小写不敏感逻辑,避免大小写差异造成的更新失败
- ✅ 精简 Status_Box 文本,并在失败时弹窗展示核心错误信息
## 🤝 技术支持
如需技术支持或报告问题,请联系开发团队。
- **软件自动更新**: 为桌面应用程序提供自动更新功能
- **文件同步**: 在不同设备间同步文件和配置
- **游戏更新**: 游戏客户端的增量更新和补丁分发。
---
**注意**: 本程序需要网络连接以下载更新文件。首次运行时可能需要较长时间来下载必要的文件。
**注意**: 本程序需要网络连接以下载更新文件。首次运行时可能需要较长时间来下载所有必要的文件。

View File

@@ -75,6 +75,15 @@ namespace CheckDownload
// 基准目录路径(用于文件更新的目标目录)
private string _baseDirectory;
// 7z.dll 库文件路径
private readonly string _sevenZipDllPath;
// 应用程序名称
private readonly string _appName;
// 当前进程ID
private readonly int _currentProcessId;
// === 新增: 用于显示整体下载大小与速度 ===
private long _totalDownloadedBytes = 0; // 已下载总字节数
private DateTime _downloadStartTime; // 下载开始时间
@@ -82,15 +91,24 @@ namespace CheckDownload
private readonly object _speedLock = new object(); // 锁,用于多线程更新 UI
private long _bytesSinceLastSpeedCalc = 0; // 距离上次速度计算新增的字节数
// 更新锁文件路径
private string _updateLockFilePath;
/// <summary>
/// 初始化窗体
/// </summary>
public Update()
{
InitializeComponent();
// 设置 7z.dll 的路径为程序运行目录
_sevenZipDllPath = Path.Combine(Application.StartupPath, "7z.dll");
_appName = Assembly.GetExecutingAssembly().GetName().Name;
_currentProcessId = Process.GetCurrentProcess().Id;
_baseDirectory = Application.StartupPath;
_updateLockFilePath = Path.Combine(_baseDirectory, "update.lock");
ConfigureProgressBar();
InitializeTempDirectory();
}
/// <summary>
@@ -188,6 +206,26 @@ namespace CheckDownload
}
finally
{
// 删除更新锁文件,让批处理脚本知道程序已退出
CleanupLockFile();
}
}
/// <summary>
/// 清理更新锁文件
/// </summary>
private void CleanupLockFile()
{
try
{
if (!string.IsNullOrEmpty(_updateLockFilePath) && File.Exists(_updateLockFilePath))
{
File.Delete(_updateLockFilePath);
}
}
catch
{
// 忽略清理锁文件时的错误
}
}
@@ -207,6 +245,7 @@ namespace CheckDownload
{
try
{
InitializeTempDirectory();
CleanupNewFiles();
UpdateStatus("下载在线MD5文件并读取...");
UpdateCount("");
@@ -246,7 +285,7 @@ namespace CheckDownload
UpdateProgressValue(100);
// 无需更新时清理临时文件夹
CleanupTempDirectory();
//CleanupTempDirectory();
// 显示更新完成并等待2秒
UpdateStatus("更新完成");
@@ -296,7 +335,7 @@ namespace CheckDownload
if (_completedCount == 0 && orderedFileList.Count > 0)
{
throw new Exception("所有文件下载失败。");
throw new Exception("UpdateFile: 所有文件下载失败。");
}
await VerifyAndSaveAllFiles();
@@ -304,7 +343,7 @@ namespace CheckDownload
await DecompressTim7zAsync();
// 校验和保存成功后清理临时目录
CleanupTempDirectory();
//CleanupTempDirectory();
// 显示完成状态并退出
UpdateStatus("更新完成");
@@ -314,8 +353,7 @@ namespace CheckDownload
}
catch (Exception ex)
{
UpdateStatus("更新失败");
MessageBox.Show($"更新失败:\n{ex.ToString()}", "Update Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
HandleError("文件更新主流程 (UpdateFile)", ex);
await Task.Delay(3000);
this.Close();
}
@@ -370,8 +408,7 @@ namespace CheckDownload
}
catch (Exception ex) when (ex is OssException || ex is JsonException)
{
UpdateStatus($"读取在线MD5文件失败: {ex.Message}");
return (null, null, null);
throw new Exception($"ReadOnlineMd5File: 读取或解析在线MD5文件 '{filePath}' 失败。", ex);
}
}
@@ -453,37 +490,6 @@ namespace CheckDownload
return differences;
}
/// <summary>
/// 批量下载文件列表并进行MD5验证支持重试机制
/// </summary>
/// <param name="fileList">包含文件路径和期望MD5值的字典</param>
private async Task DownloadAndVerifyFiles(Dictionary<string, string> fileList)
{
_totalCount = fileList.Count;
_completedCount = 0;
_downloadedFiles.Clear();
var failedFiles = new ConcurrentDictionary<string, string>();
await PerformDownloads(fileList, failedFiles);
if (!failedFiles.IsEmpty)
{
UpdateStatus($"有 {failedFiles.Count} 个文件下载失败,开始重试...");
var stillFailing = await RetryFailedFilesAsync(new Dictionary<string, string>(failedFiles));
if (stillFailing.Any())
{
UpdateStatus($"重试后仍有 {stillFailing.Count} 个文件下载失败。");
}
}
if (_completedCount == 0 && fileList.Count > 0)
{
throw new Exception("所有文件下载失败。");
}
await VerifyAndSaveAllFiles();
}
/// <summary>
/// 使用信号量控制并发数量,执行多个文件的并发下载任务
/// </summary>
@@ -509,7 +515,12 @@ namespace CheckDownload
int progress = (int)((double)_completedCount / _totalCount * 95);
UpdateProgressValue(progress);
UpdateStatus($"{Path.GetFileName(file.Key)}");
string fileName = Path.GetFileName(file.Key);
string truncatedFileName = TruncateString(fileName, 10);
if (!string.IsNullOrWhiteSpace(truncatedFileName))
{
UpdateStatus($"下载:{truncatedFileName}");
}
UpdateCount($"{_completedCount}/{_totalCount}");
}
else
@@ -533,14 +544,9 @@ namespace CheckDownload
/// <returns>下载成功返回true否则返回false</returns>
private async Task<bool> AttemptDownloadAsync(string filePath, string expectedMd5)
{
// kill exe if running
if (Path.GetExtension(filePath).Equals(".exe", StringComparison.OrdinalIgnoreCase))
{
KillProcessIfRunning(filePath);
}
string tempFilePath = Path.Combine(_tempDirectory, filePath);
string fileName = Path.GetFileName(filePath);
string truncatedFileName = TruncateString(fileName, 10);
try
{
@@ -549,9 +555,9 @@ namespace CheckDownload
return true;
}
if (!string.IsNullOrWhiteSpace(fileName))
if (!string.IsNullOrWhiteSpace(truncatedFileName))
{
UpdateStatus($"下载:{fileName}");
UpdateStatus($"下载:{truncatedFileName}");
}
UpdateCount($"{_completedCount + 1}/{_totalCount}");
if (await DownloadFileFromOneDrive(filePath, expectedMd5, tempFilePath))
@@ -559,9 +565,9 @@ namespace CheckDownload
return true;
}
if (!string.IsNullOrWhiteSpace(fileName))
if (!string.IsNullOrWhiteSpace(truncatedFileName))
{
UpdateStatus($"下载:{fileName}");
UpdateStatus($"下载:{truncatedFileName}");
}
UpdateCount($"{_completedCount + 1}/{_totalCount}");
string ossKey = $"File/{expectedMd5}";
@@ -582,9 +588,9 @@ namespace CheckDownload
}
catch (Exception ex) when (ex is OssException || ex is WebException)
{
if (!string.IsNullOrWhiteSpace(fileName))
if (!string.IsNullOrWhiteSpace(truncatedFileName))
{
UpdateStatus($"下载:{fileName}");
UpdateStatus($"下载:{truncatedFileName}");
}
UpdateCount($"{_completedCount + 1}/{_totalCount}");
UpdateSize("");
@@ -593,9 +599,9 @@ namespace CheckDownload
}
catch (Exception ex)
{
if (!string.IsNullOrWhiteSpace(fileName))
if (!string.IsNullOrWhiteSpace(truncatedFileName))
{
UpdateStatus($"下载异常: {fileName} - {ex.Message}");
UpdateStatus($"下载异常: {truncatedFileName} - {ex.Message}");
}
return false;
}
@@ -614,6 +620,7 @@ namespace CheckDownload
{
if (!File.Exists(tempFilePath))
{
//UpdateStatus($"[调试] 临时文件不存在: {fileName}");
return false;
}
@@ -635,6 +642,7 @@ namespace CheckDownload
{
var fileInfo = new FileInfo(tempFilePath);
Interlocked.Add(ref _totalDownloadedBytes, fileInfo.Length);
//UpdateStatus($"[调试] 临时文件验证通过,大小: {fileInfo.Length} bytes - {fileName}");
}
catch
{
@@ -644,10 +652,6 @@ namespace CheckDownload
}
else
{
if (!string.IsNullOrWhiteSpace(fileName))
{
UpdateStatus($"临时文件不完整,重新下载: {fileName}");
}
File.Delete(tempFilePath);
return false;
}
@@ -656,7 +660,7 @@ namespace CheckDownload
{
if (!string.IsNullOrWhiteSpace(fileName))
{
UpdateStatus($"检查临时文件时出错,将重新下载: {fileName} - {ex.Message}");
UpdateStatus($"临时文件异常,删除文件: {fileName} - {ex.Message}");
}
try
{
@@ -694,7 +698,6 @@ namespace CheckDownload
UpdateSize("");
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
@@ -737,7 +740,6 @@ namespace CheckDownload
UpdateSize("");
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
@@ -783,7 +785,6 @@ namespace CheckDownload
string authUrl = GenerateAuthUrl($"http://{OneDriveMainDomain}{OneDrivePath}", fileName);
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
@@ -812,7 +813,6 @@ namespace CheckDownload
string authUrl = GenerateAuthUrl($"http://{OneDriveBackupDomain}{OneDrivePath}", fileName);
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
@@ -883,7 +883,6 @@ namespace CheckDownload
{
var requestUri = $"http://{dnsServer}/resolve?name={domain}&type=1&short=1";
var request = new HttpRequestMessage(HttpMethod.Get, requestUri) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
request.Headers.Host = dnsServer;
var response = await _httpClient.SendAsync(request);
@@ -916,6 +915,10 @@ namespace CheckDownload
UpdateStatus("正在校验文件...");
UpdateCount("");
UpdateSize("");
// 新增:在开始校验前创建临时文件备份
//await CreateTempFileBackup();
var failedFiles = new ConcurrentBag<string>();
var filesForScripting = new ConcurrentBag<(string original, string newFile)>();
@@ -932,9 +935,18 @@ namespace CheckDownload
string expectedMd5 = item.Value;
string tempFilePath = Path.Combine(_tempDirectory, relativePath);
// 添加调试信息:检查临时文件是否存在
if (!File.Exists(tempFilePath))
{
//UpdateStatus($"[调试] 临时文件在校验前消失: {relativePath}");
failedFiles.Add(relativePath);
return;
}
string actualMd5 = CalculateMD5FromFile(tempFilePath);
if (actualMd5 != expectedMd5.ToLower())
{
//UpdateStatus($"[调试] 文件MD5校验失败: {relativePath}");
failedFiles.Add(relativePath);
return;
}
@@ -946,13 +958,24 @@ namespace CheckDownload
Directory.CreateDirectory(localDir);
}
if (!await TryMoveFileAsync(tempFilePath, localPath))
//UpdateStatus($"[调试] 开始复制文件: {relativePath}");
if (!await TryCopyFileAsync(tempFilePath, localPath)) // 改为使用复制方法
{
//UpdateStatus($"[调试] 直接复制失败,创建.new文件: {relativePath}");
string backupPath = localPath + ".new";
File.Move(tempFilePath, backupPath);
File.Copy(tempFilePath, backupPath, true); // 复制而非移动
filesForScripting.Add((localPath, backupPath));
}
else
{
//UpdateStatus($"[调试] 文件复制成功: {relativePath}");
}
// 复制后再次检查临时文件是否还存在
if (!File.Exists(tempFilePath))
{
//UpdateStatus($"[调试] 警告:文件复制后临时文件消失: {relativePath}");
}
}
finally
{
@@ -977,7 +1000,9 @@ namespace CheckDownload
if (failedFiles.Count > 0)
{
throw new Exception($"{failedFiles.Count}个文件校验失败");
string failedFileList = string.Join(", ", failedFiles.Take(3));
if (failedFiles.Count > 3) failedFileList += "...";
throw new Exception($"VerifyAndSaveAllFiles: {failedFiles.Count}个文件校验失败: {failedFileList}");
}
else
{
@@ -1038,42 +1063,126 @@ namespace CheckDownload
{
string batchFilePath = Path.Combine(_baseDirectory, "update_files.bat");
string processId = Process.GetCurrentProcess().Id.ToString();
string processName = Process.GetCurrentProcess().ProcessName;
// 创建锁文件,程序退出前会删除它
File.WriteAllText(_updateLockFilePath, processId);
var batchContent = new StringBuilder();
batchContent.AppendLine("@echo off");
batchContent.AppendLine("chcp 65001 > nul");
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("title Update Process");
// 简化的等待逻辑,避免复杂的计数器
batchContent.AppendLine("echo 等待程序退出...");
batchContent.AppendLine(":wait_loop");
// 优先检查锁文件
batchContent.AppendLine($"if not exist \"{_updateLockFilePath}\" goto process_check");
// 等待0.5秒
batchContent.AppendLine("ping 127.0.0.1 -n 1 -w 500 >nul");
batchContent.AppendLine("goto wait_loop");
batchContent.AppendLine(":process_check");
// 再次确认进程已退出
batchContent.AppendLine($"tasklist /FI \"PID eq {processId}\" 2>nul | find /I \"{processId}\" >nul");
batchContent.AppendLine("if %ERRORLEVEL% equ 0 (");
batchContent.AppendLine(" ping 127.0.0.1 -n 1 -w 500 >nul");
batchContent.AppendLine(" goto process_check");
batchContent.AppendLine(")");
// 额外等待确保完全退出
batchContent.AppendLine("ping 127.0.0.1 -n 1 -w 1000 >nul");
batchContent.AppendLine("echo 开始更新文件...");
// 简化的文件处理逻辑,避免复杂的标签
int fileIndex = 0;
foreach (var file in files)
{
batchContent.AppendLine($"del \"{file.original}\" /f /q");
batchContent.AppendLine($"move \"{file.newFile}\" \"{file.original}\"");
string fileName = Path.GetFileName(file.original);
batchContent.AppendLine($"echo 处理文件: {fileName}");
// 检查新文件是否存在
batchContent.AppendLine($"if not exist \"{file.newFile}\" (");
batchContent.AppendLine($" echo 错误: 新文件不存在 - {fileName}");
batchContent.AppendLine($" goto file_{fileIndex}_end");
batchContent.AppendLine($")");
// 删除原文件(如果存在)
batchContent.AppendLine($"if exist \"{file.original}\" (");
batchContent.AppendLine($" echo 删除原文件: {fileName}");
batchContent.AppendLine($" del \"{file.original}\" /f /q");
batchContent.AppendLine($")");
// 复制新文件
batchContent.AppendLine($"echo 复制新文件: {fileName}");
batchContent.AppendLine($"copy /Y \"{file.newFile}\" \"{file.original}\"");
// 验证复制是否成功
batchContent.AppendLine($"if exist \"{file.original}\" (");
batchContent.AppendLine($" echo 成功更新: {fileName}");
batchContent.AppendLine($" del \"{file.newFile}\" /f /q");
batchContent.AppendLine($") else (");
batchContent.AppendLine($" echo 失败: 无法创建 {fileName}");
batchContent.AppendLine($")");
batchContent.AppendLine($":file_{fileIndex}_end");
fileIndex++;
}
// 清理工作
batchContent.AppendLine("echo 清理临时文件...");
// 删除锁文件
batchContent.AppendLine($"if exist \"{_updateLockFilePath}\" del \"{_updateLockFilePath}\" /f /q");
// 简化的清理逻辑
batchContent.AppendLine($"cd /d \"{_baseDirectory}\"");
batchContent.AppendLine("for %%f in (*.new) do del \"%%f\" /f /q");
batchContent.AppendLine("for %%f in (*.bak) do del \"%%f\" /f /q");
batchContent.AppendLine("echo 更新完成");
batchContent.AppendLine("ping 127.0.0.1 -n 1 -w 2000 >nul");
// 删除批处理脚本自身
batchContent.AppendLine("del \"%~f0\" /f /q");
batchContent.AppendLine("exit");
File.WriteAllText(batchFilePath, batchContent.ToString(), new UTF8Encoding(false));
// 使用Win7兼容的启动方式
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c \"{batchFilePath}\"",
CreateNoWindow = true,
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden
FileName = batchFilePath,
CreateNoWindow = false, // Win7下设为false更稳定
UseShellExecute = true, // Win7下必须使用Shell执行
WindowStyle = ProcessWindowStyle.Minimized,
WorkingDirectory = _baseDirectory
};
Process.Start(startInfo);
try
{
Process.Start(startInfo);
}
catch (Exception)
{
// 如果上面的方式失败尝试直接用cmd执行
var fallbackStartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c \"{batchFilePath}\"",
CreateNoWindow = true,
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = _baseDirectory
};
Process.Start(fallbackStartInfo);
}
UpdateStatus("已创建更新脚本,程序退出后将自动完成更新");
}
catch (Exception ex)
{
UpdateStatus($"创建替换脚本时出错: {ex.Message}");
HandleError("创建文件替换脚本 (CreateReplaceScriptForAll)", ex);
}
}
@@ -1084,11 +1193,14 @@ namespace CheckDownload
{
try
{
_tempDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Temp",
"CheckDownload"
);
string exeDir = AppDomain.CurrentDomain.BaseDirectory;
_tempDirectory = Path.Combine(exeDir, "Temp");
if (File.Exists(_tempDirectory))
{
throw new IOException($"无法创建目录,因为已存在同名文件: {_tempDirectory}");
}
if (!Directory.Exists(_tempDirectory))
{
@@ -1097,7 +1209,7 @@ namespace CheckDownload
}
catch (Exception ex)
{
UpdateStatus($"初始化临时目录失败: {ex.Message}");
throw new Exception("初始化临时目录 (InitializeTempDirectory)", ex);
}
}
@@ -1116,20 +1228,6 @@ namespace CheckDownload
}
}
/// <summary>
/// 计算字节数组的MD5哈希值返回32位小写十六进制字符串
/// </summary>
/// <param name="data">要计算哈希值的字节数组</param>
/// <returns>32位小写十六进制MD5哈希值</returns>
private string CalculateMD5(byte[] data)
{
using (var md5 = MD5.Create())
{
byte[] hashBytes = md5.ComputeHash(data);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
/// <summary>
/// 从阿里云OSS下载文件包含SDK直连和IP直连两种备用方案
/// </summary>
@@ -1354,7 +1452,7 @@ namespace CheckDownload
string tempPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Temp",
"CheckDownload"
_appName
);
if (Directory.Exists(tempPath))
@@ -1591,12 +1689,6 @@ namespace CheckDownload
await Task.Run(() => {
try
{
string sevenZipDllPath = Extract7zDll();
if (string.IsNullOrEmpty(sevenZipDllPath))
{
throw new Exception("无法提取7z.dll解压中止。");
}
string extractionPath = Path.GetDirectoryName(sevenZipFile);
string tempExtractionDir = Path.Combine(_tempDirectory, Path.GetFileNameWithoutExtension(sevenZipFile) + "_temp");
@@ -1606,7 +1698,8 @@ namespace CheckDownload
}
Directory.CreateDirectory(tempExtractionDir);
using (var archiveFile = new ArchiveFile(sevenZipFile, sevenZipDllPath))
// 使用 SevenZipExtractor 解压文件到临时目录
using (var archiveFile = new ArchiveFile(sevenZipFile, _sevenZipDllPath))
{
archiveFile.Extract(tempExtractionDir, true);
}
@@ -1630,17 +1723,10 @@ namespace CheckDownload
}
Directory.Delete(tempExtractionDir, true);
UpdateStatus("为解压的程序设置管理员权限...");
var exeFiles = Directory.GetFiles(extractionPath, "*.exe", SearchOption.AllDirectories);
foreach (var exeFile in exeFiles)
{
SetRunAsAdminCompatibility(exeFile);
}
}
catch (Exception ex)
{
throw new Exception($"解压失败: {ex.Message}");
throw new Exception("DecompressTim7zAsync: 解压失败。", ex);
}
});
@@ -1650,8 +1736,7 @@ namespace CheckDownload
}
catch (Exception ex)
{
UpdateStatus($"处理 tim.7z 时出错: {ex.Message}");
await Task.Delay(3000);
throw new Exception("DecompressTim7zAsync: 处理 tim.7z 时出错。", ex);
}
}
@@ -1659,69 +1744,6 @@ namespace CheckDownload
/// 为指定程序路径在注册表中设置"以管理员身份运行"的兼容性标志。
/// </summary>
/// <param name="exePath">要设置的.exe文件的完整路径。</param>
private void SetRunAsAdminCompatibility(string exePath)
{
const string keyPath = @"Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers";
try
{
using (RegistryKey key = Registry.CurrentUser.CreateSubKey(keyPath))
{
if (key != null)
{
key.SetValue(exePath, "~ RUNASADMIN");
}
else
{
UpdateStatus($"无法打开或创建注册表项: {keyPath}");
}
}
}
catch (Exception ex)
{
UpdateStatus($"设置管理员权限失败: {exePath} - {ex.Message}");
}
}
/// <summary>
/// 从嵌入的资源中提取与当前进程体系结构匹配的7z.dll到临时目录。
/// </summary>
/// <returns>提取的7z.dll的路径如果失败则返回null。</returns>
private string Extract7zDll()
{
try
{
string dllName = Environment.Is64BitProcess ? "7z-x64.dll" : "7z-x86.dll";
string resourceName = $"CheckDownload.{dllName}";
string dllPath = Path.Combine(_tempDirectory, "7z.dll");
if (File.Exists(dllPath))
{
// 可以选择在这里添加对现有DLL版本的校验但为简化我们先直接返回
return dllPath;
}
using (var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
{
if (resourceStream == null)
{
UpdateStatus($"在嵌入资源中未找到 {dllName}。");
return null;
}
using (var fileStream = new FileStream(dllPath, FileMode.Create, FileAccess.Write))
{
resourceStream.CopyTo(fileStream);
}
}
return dllPath;
}
catch (Exception ex)
{
UpdateStatus($"提取7z.dll失败: {ex.Message}");
return null;
}
}
// add helper to check db extension
private bool IsDatabaseFile(string relativePath)
{
@@ -1729,17 +1751,34 @@ namespace CheckDownload
return ext == ".db" || ext == ".db3";
}
private void KillProcessIfRunning(string exeRelativePath)
private void KillProcessIfRunning(string exePath)
{
try
{
string exeName = Path.GetFileNameWithoutExtension(exeRelativePath);
string exeName = Path.GetFileNameWithoutExtension(exePath);
string targetExeDir;
// 判断是否为完整路径
if (Path.IsPathRooted(exePath))
{
targetExeDir = Path.GetDirectoryName(exePath);
}
else
{
targetExeDir = Path.GetDirectoryName(Path.Combine(_baseDirectory, exePath));
}
foreach (var proc in Process.GetProcessesByName(exeName))
{
if (proc.Id == _currentProcessId) continue;
try
{
proc.Kill();
proc.WaitForExit(5000);
string runningProcessDir = Path.GetDirectoryName(proc.MainModule.FileName);
if (string.Equals(runningProcessDir, targetExeDir, StringComparison.OrdinalIgnoreCase))
{
proc.Kill();
proc.WaitForExit(5000);
}
}
catch { }
}
@@ -1751,22 +1790,35 @@ namespace CheckDownload
{
try
{
foreach (var proc in Process.GetProcesses())
foreach (var proc in Process.GetProcessesByName(baseName))
{
if (string.Equals(proc.ProcessName, baseName, StringComparison.OrdinalIgnoreCase))
if (proc.Id == _currentProcessId) continue;
try
{
try
if (proc.MainModule.FileName.StartsWith(_baseDirectory, StringComparison.OrdinalIgnoreCase))
{
proc.Kill();
proc.WaitForExit(5000);
}
catch { }
}
catch { }
}
}
catch { }
}
/// <summary>
/// Truncates a string to a maximum length and appends an ellipsis.
/// </summary>
/// <param name="value">The string to truncate.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <returns>The truncated string.</returns>
private string TruncateString(string value, int maxLength)
{
if (string.IsNullOrEmpty(value)) return value;
return value.Length <= maxLength ? value : value.Substring(0, maxLength) + "...";
}
// === 新增: 更新整体下载大小与速度 ===
private void UpdateOverallSize()
{
@@ -1787,5 +1839,124 @@ namespace CheckDownload
_lastSpeedUpdateTime = now;
UpdateSize(sizeText);
}
/// <summary>
/// 统一处理并显示错误信息
/// </summary>
/// <param name="location">要显示给用户的基本错误信息</param>
/// <param name="ex">可选的异常对象,其消息将被附加</param>
private void HandleError(string location, Exception ex = null)
{
UpdateStatus("更新失败");
// 递归查找最深层的 InnerException 以获取最根本的错误信息
var innermostException = ex;
while (innermostException?.InnerException != null)
{
innermostException = innermostException.InnerException;
}
string errorMessage = innermostException?.Message ?? ex?.Message ?? "发生未知错误。";
MessageBox.Show(errorMessage, "更新错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
/// <summary>
/// 尝试将文件从源位置复制到目标位置
/// </summary>
private async Task<bool> TryCopyFileAsync(string sourcePath, string targetPath)
{
try
{
// 如果目标文件是.exe文件先检查并kill掉可能正在运行的进程
if (Path.GetExtension(targetPath).Equals(".exe", StringComparison.OrdinalIgnoreCase))
{
KillProcessIfRunning(targetPath);
}
// 确保目标目录存在
string targetDir = Path.GetDirectoryName(targetPath);
if (!Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
// 执行复制操作
File.Copy(sourcePath, targetPath, true);
return true;
}
catch (Exception ex)
{
// 文件可能被占用,稍后重试
try
{
await Task.Delay(1000);
File.Copy(sourcePath, targetPath, true);
return true;
}
catch
{
UpdateStatus($"文件复制失败: {Path.GetFileName(targetPath)}");
return false;
}
}
}
/// <summary>
/// 创建临时文件的备份,防止在程序退出后被删除
/// </summary>
private async Task CreateTempFileBackup()
{
try
{
string tempPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Temp",
_appName
);
if (Directory.Exists(tempPath))
{
string backupDir = Path.Combine(_baseDirectory, "TempBackup");
if (!Directory.Exists(backupDir))
{
Directory.CreateDirectory(backupDir);
}
string timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
string backupPath = Path.Combine(backupDir, $"Temp_{timestamp}");
// 简单复制整个临时目录
await Task.Run(() => CopyDirectory(tempPath, backupPath));
//UpdateStatus($"[调试] 临时文件已备份到: {backupPath}");
}
}
catch (Exception ex)
{
UpdateStatus($"创建临时文件备份失败: {ex.Message}");
}
}
/// <summary>
/// 递归复制目录
/// </summary>
private void CopyDirectory(string sourceDir, string targetDir)
{
Directory.CreateDirectory(targetDir);
foreach (var file in Directory.GetFiles(sourceDir))
{
string fileName = Path.GetFileName(file);
string targetFile = Path.Combine(targetDir, fileName);
File.Copy(file, targetFile, true);
}
foreach (var subDir in Directory.GetDirectories(sourceDir))
{
string dirName = Path.GetFileName(subDir);
string targetSubDir = Path.Combine(targetDir, dirName);
CopyDirectory(subDir, targetSubDir);
}
}
}
}