Skip to content

refactor(downloader): 重构下载器分片与系统资源分配逻辑 - #3506

Open
Pigeon0v0 wants to merge 17 commits into
devfrom
refactor/downloader-range
Open

refactor(downloader): 重构下载器分片与系统资源分配逻辑#3506
Pigeon0v0 wants to merge 17 commits into
devfrom
refactor/downloader-range

Conversation

@Pigeon0v0

@Pigeon0v0 Pigeon0v0 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

本 PR 对下载器的分片策略与系统资源分配进行了重构。

修改内容:针对文件下载回退到 HTTP/1.1(至少在我这里高峰期 1.1 的速度显著快于 2.0),元数据与 API 请求仍然使用 HTTP/2.0;仅针对大于 4 MiB 的文件尝试分片;批量任务中未知大小的文件直接下载,不进行 Range 探测;大文件下载采用新的动态分片机制,分片大小更加灵活;内存缓存改为全局共享,并及时将内容写入硬盘,避免极端情况下内存过高占用。

测试时进行 Minecraft 26.2 清洁安装,下载高峰阶段相比 PCL 2.13.1.0 的相同阶段内存占用可减少约 2/3(稳定在 180 - 220 MB),同时保持下载速度基本持平甚至略微更快;与 PCL CE 2.15.0 相比内存占用差距不大(此 PR 版本占用略微减少),主要改进了部分情况下下载速度极其缓慢的问题。

同时,close #3356

本 PR 主要使用 AI 完成,因此需要较为细致的测试与检查。同时,回退到 HTTP/1.1 还是保留 HTTP/2.0 可能仍需要进一步调查与讨论。

Generated by GPT-5.6 Sol xHigh & GPT-5.6 Terra xHigh

Summary by Sourcery

重构下载器,使用新的自适应基于范围的分段机制并引入共享的全局资源管理,从而改进内存使用和大文件性能,同时重构配置和 UI,使其以“连接数”而非“线程数”的方式进行交互。

Enhancements:

  • 将每个文件基于多块的 DownloadService 使用方式替换为:内部顺序下载器以及仅在文件足够大时才启用的自适应基于范围的并行下载器。
  • 引入 DownloadResourceManager,在全局范围内协调所有下载的 HTTP 连接、共享缓冲区和带宽限速。
  • 将网络配置从基于线程的限制重构为基于连接的限制,包括每文件连接数限制和主机级约束,并为现有设置提供向后兼容的映射。
  • 调整加载器逻辑,以便在校验和验证通过后更好地重用已有文件,并避免对小文件或大小未知的文件进行不必要的范围探测。
  • 更新 UI、配置和监控界面(例如速度面板、设置滑块),使其显示连接数和新的每文件连接设置,而非线程数。
Original summary in English

Summary by Sourcery

Refactor the downloader to use a new adaptive range-based segmentation mechanism with shared global resource management, improving memory usage and large-file performance while reworking configuration and UI to talk in terms of connections rather than threads.

Enhancements:

  • Replace the per-file multi-chunk DownloadService usage with an internal sequential downloader and an adaptive range-based parallel downloader that only engages for sufficiently large files.
  • Introduce DownloadResourceManager to globally coordinate HTTP connections, shared buffers, and bandwidth throttling across all downloads.
  • Rework network configuration from thread-based limits to connection-based limits, including per-file connection limits and host-level constraints, with backward-compatible mapping for existing settings.
  • Adjust loader logic to better reuse existing files after checksum verification and to avoid unnecessary range probes for small or unknown-size files.
  • Update UI, config, and monitoring surfaces (e.g., speed panel, settings sliders) to reflect connection counts and new per-file connection settings instead of thread counts.

Sourcery 总结

重构下载器,使其能够根据文件大小自适应分段并共享资源,从而提升大文件下载的可靠性和内存使用效率,同时改进连接控制和 HTTP 行为。

新功能:

  • 添加可配置的 HTTP 协议选择和按文件设置的下载连接数限制。
  • 为大文件引入基于范围的自适应并行下载,并支持慢速或失败分段的恢复。
  • 添加对下载连接、内存缓冲区、带宽限制以及总体监控的全局共享管理。

错误修复:

  • 在批量下载中,避免对小文件和大小未知的文件进行不必要的范围探测。
  • 改进对现有已验证文件的复用,并扩展下载重试处理。

增强功能:

  • 将面向线程的下载控制和监控替换为面向连接的限制和状态报告。
  • 通过共享资源配额并将下载数据增量写入磁盘,降低下载器的内存压力。
  • 改进小文件、大文件及批量文件的下载协议选择和性能。
Original summary in English

Sourcery 总结

重构下载器的分片、资源管理与连接控制逻辑,以改善大文件下载性能、稳定性和内存使用。

新功能:

  • 为大文件提供自适应 Range 并行下载,并支持慢速分片恢复。
  • 新增下载 HTTP 协议选择和单文件连接数配置。
  • 引入全局下载资源管理,统一协调连接、缓冲区、带宽和下载监控。

错误修复:

  • 避免批量下载中对小文件或大小未知文件进行不必要的 Range 探测。
  • 改进已校验本地文件复用、下载完整性校验和失败重试处理。

改进:

  • 将下载控制与监控从线程数调整为连接数,并加入全局及主机级连接限制。
  • 通过共享缓冲区配额和增量写盘降低下载过程中的内存占用。
  • 优化不同文件规模和下载场景下的 HTTP 协议选择与下载策略。
Original summary in English

Sourcery 摘要

重构下载器的分片策略与资源管理,以提升大文件下载性能、稳定性和内存使用效率。

新功能:

  • 为下载任务增加 HTTP 协议模式选择和单文件连接数配置。
  • 为大文件引入自适应 Range 并行下载,并支持慢速分片拆分与失败恢复。
  • 引入全局下载资源管理,统一协调连接、缓冲区、带宽限速和下载监控。

错误修复:

  • 避免批量下载中对小文件或大小未知文件进行不必要的 Range 探测。
  • 改进已校验本地文件复用、下载完整性校验以及下载源失败后的重试处理。

增强功能:

  • 将下载限制和监控从线程数调整为连接数,并增加全局及主机级连接控制。
  • 通过共享缓冲区配额和增量写盘降低下载过程中的内存占用。
  • 根据文件规模和下载场景优化 HTTP/1.1 与 HTTP/2 的使用策略。
Original summary in English

Sourcery 摘要

重构下载器的分片、连接管理和资源调度逻辑,以提升大文件下载性能、稳定性及内存使用效率。

新功能:

  • 为大文件引入自适应 Range 并行下载,并支持慢速分片拆分、失败恢复和协议选择。
  • 新增全局下载资源管理,统一协调连接数、缓冲区、带宽限速和下载监控。
  • 增加单文件连接数限制及 HTTP 下载模式配置,并保留旧配置的兼容映射。

错误修复:

  • 避免批量下载中对小文件和大小未知文件进行不必要的 Range 探测。
  • 改进已校验本地文件复用、临时文件校验、下载完整性检查和失败重试处理。

改进:

  • 将下载控制和监控从线程数调整为连接数,并增加全局及主机级连接管理。
  • 通过共享内存缓冲配额和增量写盘降低下载过程中的内存占用,并优化不同文件规模下的下载策略。

日常维护:

  • 移除旧的基于 DownloadService 的分块下载实现及相关接口。
Original summary in English

Summary by Sourcery

重构下载器的分片、连接管理和资源调度逻辑,以提升大文件下载性能、稳定性及内存使用效率。

New Features:

  • 为大文件引入自适应 Range 并行下载,并支持慢速分片拆分、失败恢复和协议选择。
  • 新增全局下载资源管理,统一协调连接数、缓冲区、带宽限速和下载监控。
  • 增加单文件连接数限制及 HTTP 下载模式配置,并保留旧配置的兼容映射。

Bug Fixes:

  • 避免批量下载中对小文件和大小未知文件进行不必要的 Range 探测。
  • 改进已校验本地文件复用、临时文件校验、下载完整性检查和失败重试处理。

Enhancements:

  • 将下载控制和监控从线程数调整为连接数,并增加全局及主机级连接管理。
  • 通过共享内存缓冲配额和增量写盘降低下载过程中的内存占用,并优化不同文件规模下的下载策略。

Chores:

  • 移除旧的基于 DownloadService 的分块下载实现及相关接口。

@pcl-ce-automation pcl-ce-automation Bot added 🚧 正在处理 开发人员正在对该内容进行开发、测试或修复,进展中 size: XXL PR 大小评估:巨型 labels Aug 5, 2026
@sourcery-ai

sourcery-ai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

审查者指南

该 PR 将旧版的逐文件分块下载替换为自适应的、基于 Range 的大文件下载流程,并提供顺序下载回退机制;同时通过全局连接、缓冲区、带宽和监控资源进行协调。该 PR 还新增了 HTTP 协议选择、面向连接的配置、更智能的加载器复用与探测机制,以及相应的 UI 更新。

自适应大文件下载时序图

sequenceDiagram
    participant LoaderDownload
    participant FileDownloader
    participant AdaptiveRangeDownloader
    participant DownloadResourceManager
    participant HTTPServer
    participant Disk

    LoaderDownload->>FileDownloader: DownloadAsync
    FileDownloader->>AdaptiveRangeDownloader: TryDownloadAsync
    AdaptiveRangeDownloader->>DownloadResourceManager: AcquireConnectionAsync
    AdaptiveRangeDownloader->>HTTPServer: Range probe
    HTTPServer-->>AdaptiveRangeDownloader: PartialContent with ContentRange
    AdaptiveRangeDownloader->>DownloadResourceManager: AcquireConnectionAsync
    AdaptiveRangeDownloader->>HTTPServer: Range segment requests
    HTTPServer-->>AdaptiveRangeDownloader: PartialContent
    AdaptiveRangeDownloader->>DownloadResourceManager: ReserveBufferAsync
    AdaptiveRangeDownloader->>DownloadResourceManager: ThrottleAsync
    AdaptiveRangeDownloader->>Disk: RandomAccess.WriteAsync
    AdaptiveRangeDownloader->>DownloadResourceManager: RecordDownloadedBytes
    AdaptiveRangeDownloader-->>FileDownloader: completed
    FileDownloader->>FileDownloader: PromoteTempFile
Loading

下载策略选择流程图

flowchart TD
    Start[DownloadSingleAsync] --> Size{expectedSize known}
    Size -->|less than 4 MiB| Sequential[DownloadSequentiallyAsync]
    Size -->|unknown| Probe[TryDownloadAsync]
    Size -->|at least 4 MiB| Range[TryDownloadAsync]
    Probe -->|Range unsupported or small| Sequential
    Range -->|Range supported| Parallel[Adaptive range workers]
    Range -->|Range unsupported| Sequential
    Sequential --> Promote[PromoteTempFile]
    Parallel --> Promote
    Promote --> Complete[MarkDownloadCompleted]
Loading

文件级变更

变更 详细信息 文件
使用顺序流式下载和自适应 HTTP Range 下载替换旧版的分块下载流程,以支持大文件下载。
  • 对小文件和大小未知的批量下载使用顺序下载。
  • 仅在需要时探测 Range 支持情况,动态拆分大文件,并对速度缓慢或失败的分段进行重试或细分。
  • 使用池化缓冲区将分段直接写入预分配的临时文件,同时支持限速、进度报告和完成后提升文件。
  • 新增请求超时、源重试、响应范围验证,并在不支持 Range 时回退到顺序下载。
Plain Craft Launcher 2/Modules/Network/Downloader/AdaptiveRangeDownloader.cs
Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs
Plain Craft Launcher 2/Modules/Network/Downloader/Requester.cs
引入全局下载资源协调机制,统一管理连接、缓冲区、带宽和聚合监控。
  • 强制执行全局连接数、每个主机的连接数以及单个文件的连接数配额。
  • 共享受限的缓冲区预算,并集中管理带宽限速和已下载字节数统计。
  • 向监控界面提供聚合后的活动连接数和下载速度。
Plain Craft Launcher 2/Modules/Network/Downloader/DownloadResourceManager.cs
Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs
Plain Craft Launcher 2/Modules/Network/Management/NetManager.cs
围绕基于连接的限制,重新设计下载协议选择和网络配置。
  • 新增可配置的 HTTP/1.1、HTTP/2 和自动请求模式。
  • 在自动模式下,对小型请求和批量请求使用 HTTP/2;对 Range 探测和大文件分段使用 HTTP/1.1。
  • 将现有线程设置映射为连接数限制,同时新增单文件连接数设置,并限制最终生效值的范围。
PCL.Core/App/Config.cs
PCL.Core/App/ConfigEnums.cs
PCL.Core/IO/Net/NetworkService.cs
Plain Craft Launcher 2/Modules/Base/ModSetup.cs
Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs
Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs
调整加载器行为,以复用已验证的文件并避免不必要的 Range 探测。
  • 在将现有文件视为已完成之前先进行验证;在适用时,将哈希检查移至后台执行。
  • 将已知的元数据大小传递给下载器,并对已知的小文件或大小未知的批量项目跳过探测。
  • 更新资源索引元数据处理逻辑,以提供预期大小。
Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs
Plain Craft Launcher 2/Modules/Minecraft/ModDownload.cs
将配置、UI、本地化和下载监控中的术语从“线程”调整为“连接”。
  • 新增单文件连接数和 HTTP 模式控制,并兼容旧版配置值。
  • 在设置和监控界面中显示连接数及共享下载速度。
  • 更新本地化标签和相关命名,包括下载临时文件常量。
PCL.Core/App/Localization/Languages/en-US.xaml
PCL.Core/App/Localization/Languages/zh-CN.xaml
Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml
Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml.cs
Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.cs
Plain Craft Launcher 2/Modules/Network/Management/NetManager.cs
Plain Craft Launcher 2/Modules/Minecraft/ModSkin.cs

针对关联 Issue 的评估

Issue 目标 已解决 说明
#3356 修复 C# 版本 11w49a 至 13w12~ 无法正常下载的问题,使相关版本能够完成文件及元数据下载,而不会因下载器或网络请求异常失败。
#3504 修复下载任务接近结束时可能出现的严重速度异常放缓问题。

可能关联的 Issue


提示和命令

与 Sourcery 交互

  • 触发新的审查: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 根据审查评论生成 GitHub issue: 回复审查评论,请 Sourcery 根据该评论创建 issue。你也可以使用 @sourcery-ai issue 回复审查评论,以从中创建 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title,以随时重新生成标题。
  • 生成 pull request 摘要: 在 pull request 正文中任意位置写入 @sourcery-ai summary,即可在指定位置随时生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary,以随时重新生成摘要。
  • 生成审查者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时重新生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,以解决所有 Sourcery 评论。如果你已经处理完所有评论并不想再看到它们,这一功能会很有用。
  • 忽略所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss,以忽略所有现有的 Sourcery 审查。如果你想从新的审查开始,这一功能尤其有用——别忘了评论 @sourcery-ai review 来触发新的审查!

自定义使用体验

访问你的控制面板

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

The PR replaces legacy per-file chunked downloading with an adaptive, range-based large-file pipeline and sequential fallback, coordinated by global connection, buffer, bandwidth, and monitoring resources; it also adds HTTP protocol selection, connection-oriented configuration, smarter loader reuse/probing, and corresponding UI updates.

Sequence diagram for adaptive large-file downloading

sequenceDiagram
    participant LoaderDownload
    participant FileDownloader
    participant AdaptiveRangeDownloader
    participant DownloadResourceManager
    participant HTTPServer
    participant Disk

    LoaderDownload->>FileDownloader: DownloadAsync
    FileDownloader->>AdaptiveRangeDownloader: TryDownloadAsync
    AdaptiveRangeDownloader->>DownloadResourceManager: AcquireConnectionAsync
    AdaptiveRangeDownloader->>HTTPServer: Range probe
    HTTPServer-->>AdaptiveRangeDownloader: PartialContent with ContentRange
    AdaptiveRangeDownloader->>DownloadResourceManager: AcquireConnectionAsync
    AdaptiveRangeDownloader->>HTTPServer: Range segment requests
    HTTPServer-->>AdaptiveRangeDownloader: PartialContent
    AdaptiveRangeDownloader->>DownloadResourceManager: ReserveBufferAsync
    AdaptiveRangeDownloader->>DownloadResourceManager: ThrottleAsync
    AdaptiveRangeDownloader->>Disk: RandomAccess.WriteAsync
    AdaptiveRangeDownloader->>DownloadResourceManager: RecordDownloadedBytes
    AdaptiveRangeDownloader-->>FileDownloader: completed
    FileDownloader->>FileDownloader: PromoteTempFile
Loading

Flow diagram for download strategy selection

flowchart TD
    Start[DownloadSingleAsync] --> Size{expectedSize known}
    Size -->|less than 4 MiB| Sequential[DownloadSequentiallyAsync]
    Size -->|unknown| Probe[TryDownloadAsync]
    Size -->|at least 4 MiB| Range[TryDownloadAsync]
    Probe -->|Range unsupported or small| Sequential
    Range -->|Range supported| Parallel[Adaptive range workers]
    Range -->|Range unsupported| Sequential
    Sequential --> Promote[PromoteTempFile]
    Parallel --> Promote
    Promote --> Complete[MarkDownloadCompleted]
Loading

File-Level Changes

Change Details Files
Replace the legacy chunked download flow with sequential streaming and adaptive HTTP Range downloads for large files.
  • Use sequential downloads for small files and batch downloads with unknown size.
  • Probe Range support only when needed, split large files dynamically, and retry or subdivide slow/failed segments.
  • Write segments directly to a preallocated temporary file with pooled buffers, throttling, progress reporting, and completion promotion.
  • Add request timeouts, source retries, response-range validation, and fallback to sequential downloads when Range is unsupported.
Plain Craft Launcher 2/Modules/Network/Downloader/AdaptiveRangeDownloader.cs
Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs
Plain Craft Launcher 2/Modules/Network/Downloader/Requester.cs
Introduce global download resource coordination for connections, buffers, bandwidth, and aggregate monitoring.
  • Enforce global and per-host connection quotas plus a per-file connection limit.
  • Share a bounded buffer budget and centralize bandwidth throttling and downloaded-byte accounting.
  • Expose aggregate active connection count and download speed to monitoring surfaces.
Plain Craft Launcher 2/Modules/Network/Downloader/DownloadResourceManager.cs
Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs
Plain Craft Launcher 2/Modules/Network/Management/NetManager.cs
Rework download protocol selection and network configuration around connection-based limits.
  • Add configurable HTTP/1.1, HTTP/2, and automatic request modes.
  • Use HTTP/2 for small/batch requests and HTTP/1.1 for Range probes and large-file segments in automatic mode.
  • Map existing thread settings to connection limits while adding a per-file connection setting and clamping effective values.
PCL.Core/App/Config.cs
PCL.Core/App/ConfigEnums.cs
PCL.Core/IO/Net/NetworkService.cs
Plain Craft Launcher 2/Modules/Base/ModSetup.cs
Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs
Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs
Adjust loader behavior to reuse validated files and avoid unnecessary Range discovery.
  • Validate existing files before treating them as completed, offloading hash checks when applicable.
  • Pass known metadata sizes to the downloader and bypass probing for known small files or unknown-size batch items.
  • Update asset-index metadata handling to supply expected sizes.
Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs
Plain Craft Launcher 2/Modules/Minecraft/ModDownload.cs
Update configuration, UI, localization, and download monitoring terminology from threads to connections.
  • Add per-file connection and HTTP mode controls with backward-compatible configuration values.
  • Display connection counts and shared download speed in settings and monitoring views.
  • Update localized labels and related naming, including the download temporary-file constant.
PCL.Core/App/Localization/Languages/en-US.xaml
PCL.Core/App/Localization/Languages/zh-CN.xaml
Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml
Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml.cs
Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.cs
Plain Craft Launcher 2/Modules/Network/Management/NetManager.cs
Plain Craft Launcher 2/Modules/Minecraft/ModSkin.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#3356 修复 C# 版本 11w49a 至 13w12~ 无法正常下载的问题,使相关版本能够完成文件及元数据下载,而不会因下载器或网络请求异常失败。
#3504 修复下载任务接近结束时可能出现的严重速度异常放缓问题。

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

# Conflicts:
#	PCL.Core/App/Localization/Languages/zh-CN.xaml
#	Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml
@Pigeon0v0
Pigeon0v0 marked this pull request as ready for review August 29, 2026 12:59
@pcl-ce-automation pcl-ce-automation Bot added 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 and removed 🚧 正在处理 开发人员正在对该内容进行开发、测试或修复,进展中 labels Aug 29, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

嘿——我发现了 4 个问题

AI Agent 提示词
请处理这次代码审查中的评论:

## 单独评论

### 评论 1
<location path="Plain Craft Launcher 2/Modules/Minecraft/ModSkin.cs" line_range="156-158" />
<code_context>
             if (!File.Exists(fileAddress))
             {
-                FileDownloader.DownloadAsync(address, fileAddress + ModNet.netDownloadEnd).GetAwaiter().GetResult();
+                FileDownloader.DownloadAsync(address, fileAddress + ModNet.NetDownloadEnd).GetAwaiter().GetResult();
                 File.Delete(fileAddress);
-                FileSystem.Rename(fileAddress + ModNet.netDownloadEnd, fileAddress);
+                FileSystem.Rename(fileAddress + ModNet.NetDownloadEnd, fileAddress);
                 ModBase.Log("[Minecraft] 皮肤下载成功:" + fileAddress);
             }
</code_context>
<issue_to_address>
**issue (bug_risk):** `McSkinDownload``fileAddress + ModNet.NetDownloadEnd` 作为 `localPath` 传入,但重构后的下载器始终会写入 `localPath + ModNet.NetDownloadEnd`。因此,下载会创建 `fileAddress + .PCLDownloading + .PCLDownloading`,而调用方尝试重命名 `fileAddress + .PCLDownloading`,由于该路径不存在,重命名会失败。

**触发条件:** 通过 `McSkinDownload` 下载皮肤时。

**建议修复:**`fileAddress` 传递给 `FileDownloader.DownloadAsync`,或者让下载器统一接受已经添加后缀的临时路径。
</issue_to_address>

### 评论 2
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs" line_range="138" />
<code_context>
+        var totalSize = response.Content.Headers.ContentLength ?? -1;
+        // 某些源使用 chunked 传输没有 Content-Length,但清单校验信息仍可能带有文件大小
+        // 这个大小只用于慢速判断,完整性仍按响应头的 totalSize 校验
+        var slowCheckSize = totalSize > 0 ? totalSize : trackedFile?.Check?.actualSize ?? -1;
+        if (trackedFile is not null)
+        {
</code_context>
<issue_to_address>
**issue (broader_impact):** 当响应没有 `Content-Length` 时,顺序下载不会验证清单中的 `trackedFile.Check.actualSize``slowCheckSize` 仅用于慢速判断,而最终大小检查由于 `totalSize``-1` 被跳过,因此,如果分块传输的响应提前结束,文件仍会被当作成功完成的完整文件处理。

**触发条件:** 源使用不带 `Content-Length` 的分块传输,并且在达到清单声明的大小之前终止传输时。

**建议修复:** 将预期大小与响应的 `Content-Length` 分开跟踪,并在已下载字节数与 `trackedFile.Check.actualSize` 不一致时拒绝完成下载。
</issue_to_address>

### 评论 3
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/DownloadResourceManager.cs" line_range="54" />
<code_context>
+        CancellationToken cancellationToken)
+    {
+        var host = Uri.TryCreate(url, UriKind.Absolute, out var uri) ? uri.Host : url;
+        var hostQuota = HostConnectionQuotas.GetOrAdd(host, static _ => new AsyncQuota());
+        var hostLease = await hostQuota.AcquireAsync(1, () => ModNet.NetTaskConnectionsPerHostLimit, cancellationToken)
+            .ConfigureAwait(false);
</code_context>
<issue_to_address>
**issue (bug_risk):** 所有之前见过的主机都会永久保留在 `HostConnectionQuotas` 中,下载完成后也不会移除配额。因此,来自不受限制或由用户控制的主机名的下载会在进程的整个生命周期内为每个主机累积一个 `AsyncQuota` 对象。

**触发条件:** 启动器会话从许多不同的主机下载内容时,例如不断变化的签名 CDN 或镜像 URL。

**建议修复:** 在主机配额的使用量降为零后移除处于空闲状态的配额,或者使用有界的、会驱逐条目的主机配额注册表。
</issue_to_address>

### 评论 4
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs" line_range="259-270" />
<code_context>
+    internal static async Task<HttpResponseMessage> SendDownloadRequestAsync(string url,
+        HttpRequestMessage request, CancellationToken cancellationToken)
+    {
+        using var requestTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+        requestTimeout.CancelAfter(RequestTimeoutMilliseconds);
+        try
+        {
+            return await GetHttpClient(url)
+                .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, requestTimeout.Token)
+                .ConfigureAwait(false);
+        }
+        catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested &&
+                                                    requestTimeout.IsCancellationRequested)
+        {
+            throw new TimeoutException($"等待下载源响应超时(30 秒):{url}", ex);
+        }
+    }
</code_context>
<issue_to_address>
**issue (bug_risk):** 等待响应头时发生的请求超时会被转换为 `TimeoutException`,而 `AdaptiveRangeDownloader.TryRecover` 处理的异常类型中不包括该异常。因此,单个分段的响应头超时会取消整个自适应下载,而不是按照所宣称的慢速连接恢复机制重试该分段。

**触发条件:** 某个范围分段请求接收响应头的时间超过 30 秒,而其他分段仍然可用时。

**建议修复:** 将转换后的 `TimeoutException` 视为可恢复的分段失败,或者为范围分段超时保留一种可重试的异常类型。
</issue_to_address>

Sourcery 对开源项目免费——如果您喜欢我们的审查,请考虑分享它们 ✨
帮助我变得更有用!请在每条评论上点击 👍 或 👎,我会利用这些反馈来改进审查结果。
Original comment in English

Hey - I've found 4 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="Plain Craft Launcher 2/Modules/Minecraft/ModSkin.cs" line_range="156-158" />
<code_context>
             if (!File.Exists(fileAddress))
             {
-                FileDownloader.DownloadAsync(address, fileAddress + ModNet.netDownloadEnd).GetAwaiter().GetResult();
+                FileDownloader.DownloadAsync(address, fileAddress + ModNet.NetDownloadEnd).GetAwaiter().GetResult();
                 File.Delete(fileAddress);
-                FileSystem.Rename(fileAddress + ModNet.netDownloadEnd, fileAddress);
+                FileSystem.Rename(fileAddress + ModNet.NetDownloadEnd, fileAddress);
                 ModBase.Log("[Minecraft] 皮肤下载成功:" + fileAddress);
             }
</code_context>
<issue_to_address>
**issue (bug_risk):** `McSkinDownload` passes `fileAddress + ModNet.NetDownloadEnd` as `localPath`, but the refactored downloader always writes to `localPath + ModNet.NetDownloadEnd`. The download therefore creates `fileAddress + .PCLDownloading + .PCLDownloading`, while the caller renames `fileAddress + .PCLDownloading`, so the rename fails because that path does not exist.

**Triggers:** When downloading a skin through `McSkinDownload`.

**Suggested fix:** Pass `fileAddress` to `FileDownloader.DownloadAsync`, or make the downloader consistently accept an already-suffixed temporary path.
</issue_to_address>

### Comment 2
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs" line_range="138" />
<code_context>
+        var totalSize = response.Content.Headers.ContentLength ?? -1;
+        // 某些源使用 chunked 传输没有 Content-Length,但清单校验信息仍可能带有文件大小
+        // 这个大小只用于慢速判断,完整性仍按响应头的 totalSize 校验
+        var slowCheckSize = totalSize > 0 ? totalSize : trackedFile?.Check?.actualSize ?? -1;
+        if (trackedFile is not null)
+        {
</code_context>
<issue_to_address>
**issue (broader_impact):** Sequential downloads do not validate the manifest's `trackedFile.Check.actualSize` when the response has no `Content-Length`. `slowCheckSize` is used only for the slow-speed heuristic, and the final size check is skipped because `totalSize` is `-1`, so a chunked response that ends early is promoted as a successful complete file.

**Triggers:** When a source uses chunked transfer without `Content-Length` and terminates before the manifest-declared size.

**Suggested fix:** Track the expected size separately from the response `Content-Length` and reject completion when the downloaded byte count differs from `trackedFile.Check.actualSize`.
</issue_to_address>

### Comment 3
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/DownloadResourceManager.cs" line_range="54" />
<code_context>
+        CancellationToken cancellationToken)
+    {
+        var host = Uri.TryCreate(url, UriKind.Absolute, out var uri) ? uri.Host : url;
+        var hostQuota = HostConnectionQuotas.GetOrAdd(host, static _ => new AsyncQuota());
+        var hostLease = await hostQuota.AcquireAsync(1, () => ModNet.NetTaskConnectionsPerHostLimit, cancellationToken)
+            .ConfigureAwait(false);
</code_context>
<issue_to_address>
**issue (bug_risk):** Every previously seen host is retained permanently in `HostConnectionQuotas`, and no quota is removed after its downloads finish. Downloads from unbounded or user-controlled hostnames therefore accumulate one `AsyncQuota` object per host for the lifetime of the process.

**Triggers:** When a launcher session downloads from many distinct hosts, such as changing signed CDN or mirror URLs.

**Suggested fix:** Remove an idle host quota after its usage reaches zero, or use a bounded/evicting host-quota registry.
</issue_to_address>

### Comment 4
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs" line_range="259-270" />
<code_context>
+    internal static async Task<HttpResponseMessage> SendDownloadRequestAsync(string url,
+        HttpRequestMessage request, CancellationToken cancellationToken)
+    {
+        using var requestTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+        requestTimeout.CancelAfter(RequestTimeoutMilliseconds);
+        try
+        {
+            return await GetHttpClient(url)
+                .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, requestTimeout.Token)
+                .ConfigureAwait(false);
+        }
+        catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested &&
+                                                    requestTimeout.IsCancellationRequested)
+        {
+            throw new TimeoutException($"等待下载源响应超时(30 秒):{url}", ex);
+        }
+    }
</code_context>
<issue_to_address>
**issue (bug_risk):** A request timeout while waiting for response headers is converted to `TimeoutException`, which is not one of the exceptions handled by `AdaptiveRangeDownloader.TryRecover`; a single segment header timeout therefore cancels the entire adaptive download instead of retrying that segment under the advertised slow-connection recovery path.

**Triggers:** When a range-segment request takes longer than 30 seconds to receive response headers while other segments are still usable.

**Suggested fix:** Treat the converted `TimeoutException` as a recoverable segment failure, or preserve a retryable exception type for range-segment timeouts.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread Plain Craft Launcher 2/Modules/Minecraft/ModSkin.cs Outdated
Comment thread Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs Outdated
Comment thread Plain Craft Launcher 2/Modules/Network/Downloader/DownloadResourceManager.cs Outdated
Comment thread Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a2a8f061d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +195 to +197
if (speed < SequentialSlowSpeedBytesPerSecond)
throw new SlowSequentialDownloadException(
$"顺序下载速度连续 {SequentialSlowCheckSeconds} 秒低于 {SequentialSlowSpeedBytesPerSecond / 1024} KiB/s");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow slow sequential downloads to finish

On an uncapped connection that remains below 40 KiB/s, every sequential attempt for a sufficiently large file throws here after five seconds. Because the same check is enabled for every source and all four retry rounds, a valid but slow single-source download can never complete; disable this restart heuristic on the final attempt or otherwise permit eventual progress.

Useful? React with 👍 / 👎.

Comment on lines +92 to +95
var availableAt = Math.Max(now, _nextBandwidthTick);
var duration = Math.Max(1L, (long)Math.Ceiling((double)bytes * Stopwatch.Frequency / limit));
_nextBandwidthTick = availableAt + duration;
delayTicks = availableAt - now;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove canceled reads from the bandwidth schedule

When a speed cap is enabled, this permanently advances the shared schedule before the delay completes. If queued reads are then canceled, their unused reservations remain; with 64 default connections at the minimum 0.1 MiB/s cap, aborting a task can leave the next download waiting roughly 40 seconds, and the 256-connection setting can leave about 160 seconds of stale delay. Reclaim canceled reservations or use a limiter that accounts only for consumed permits.

Useful? React with 👍 / 👎.

Comment on lines +169 to +173
catch
{
lock (_lock)
_waiters.Remove(waiter);
throw;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wake another quota waiter after a cancellation race

If Release removes a waiter and that waiter's cancellation wins before TrySetResult is observed, this catch cannot remove it because it was already dequeued, while no subsequent waiter is signaled. When the released quota is otherwise idle, unrelated live waiters can then remain blocked indefinitely; transfer the wake-up to another waiter when this race occurs or use cancellation-safe semaphore semantics.

Useful? React with 👍 / 👎.

<RowDefinition Height="27" />
<RowDefinition Height="27" />
<RowDefinition Height="27" />
<RowDefinition Height="27" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the target-folder row auto-sized

This row corresponds to the target-folder hint, whose localized text spans two lines, but the change replaces its previous Auto height with a fixed 27 pixels. The hint is therefore clipped or overlaps the install-behavior row, especially in English; retain an auto-sized row for Grid.Row="9".

Useful? React with 👍 / 👎.

Generated by GPT-5.6 Terra Max

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 292138af6f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +218 to +219
if (manifestExpectedSize >= 0 && downloaded != manifestExpectedSize)
throw new IOException($"下载不完整:已写入 {downloaded},应为清单声明的 {manifestExpectedSize}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let a matching hash override a stale manifest size

When a DownloadFile supplies both a hash and an inaccurate actualSize, this rejects the downloaded file on every source and retry even if its hash is correct. That conflicts with FileChecker.Check, which intentionally accepts a size mismatch when the hash matches (ModBase.cs lines 1252–1278), so manifests with stale size metadata can no longer be downloaded; validate the completed file with its checker or preserve the hash-over-size behavior.

Useful? React with 👍 / 👎.

Comment on lines 146 to 160
public static DownloadService CreateDownloadService(string url, bool useBrowserUserAgent = false)
{
var chunkCount = Math.Min(Math.Max(1, ModNet.NetTaskThreadLimit), 4);
var chunkCount = Math.Min(Math.Clamp(ModNet.NetTaskConnectionLimit, 1,
ModNet.NetTaskSingleFileConnectionLimitMax), ModNet.NetTaskSingleFileConnectionLimit);
return new DownloadService(new DownloadConfiguration
{
ChunkCount = chunkCount,
ParallelCount = chunkCount,
ParallelDownload = chunkCount > 1,
MaximumBytesPerSecond = ModNet.NetTaskSpeedLimitHigh > 0 ? ModNet.NetTaskSpeedLimitHigh : 0,
DownloadFileExtension = ModNet.netDownloadEnd,
DownloadFileExtension = ModNet.NetDownloadEnd,
EnableAutoResumeDownload = false,
MaximumMemoryBufferBytes = 256L * 1024 * 1024,
MaximumMemoryBufferBytes = 2L * 1024 * 1024,
BufferBlockSize = 64 * 1024,
RequestConfiguration = DownloadRequestFactory.Create(url, useBrowserUserAgent)
});
}

@tangge233 tangge233 Aug 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

为什么 Http 请求组件里会有创建下载服务的函数?HOW?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

我不到哇.jpg

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70d67f1cd3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +300 to +303
using var request = _owner.CreateRequest(HttpMethod.Get, DownloadRequestKind.RangeSegment);
request.Headers.Range = new RangeHeaderValue(segment.CurrentOffset, segment.End);
using var response = await FileDownloader.SendDownloadRequestAsync(_owner._url, request,
cancellationToken).ConfigureAwait(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin range requests to one object version

When a large file at a mutable URL changes during the parallel download—or different CDN nodes serve same-length revisions—these requests send only Range and validate only offsets and total length. A download without a hash checker, such as the custom-download path, can therefore combine bytes from multiple revisions and still be promoted successfully. Retain a strong ETag from the probe/first response and send If-Range on every segment, falling back to a sequential restart if the validator changes.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: XXL PR 大小评估:巨型 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[正在调查]下载器在下载任务的末期可能出现速度异常 [C#]: 11w49a至13w12~的版本无法正常下载

3 participants