From ada599325993f0ede1cceaed988a6e2b2976e8d0 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:07:32 +0800 Subject: [PATCH 01/19] =?UTF-8?q?fix:=20=E9=99=90=E5=88=B6=E9=87=87?= =?UTF-8?q?=E6=A0=B7=E7=AA=97=E5=8F=A3=E5=9C=A8=E7=A8=B3=E5=AE=9A=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E5=B9=B6=E5=9B=9E=E6=94=B6Npcap=E6=8D=95=E8=8E=B7?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/src/winload/cli.py | 16 +++++++++- python/src/winload/config.py | 8 +++++ python/src/winload/stats.py | 6 ++-- python/tests/test_cli.py | 6 ++++ readme.jp.md | 22 +++++++------- readme.ko.md | 22 +++++++------- readme.lzh.md | 22 +++++++------- readme.md | 22 +++++++------- readme.zh-cn.md | 22 +++++++------- readme.zh-tw.md | 22 +++++++------- rust/src/app.rs | 4 ++- rust/src/cli.rs | 16 ++++++++++ rust/src/config.rs | 11 +++++++ rust/src/loopback.rs | 57 +++++++++++++++++++++++++++++------- rust/src/runtime.rs | 3 +- rust/src/stats.rs | 15 +++++++++- 16 files changed, 192 insertions(+), 82 deletions(-) diff --git a/python/src/winload/cli.py b/python/src/winload/cli.py index c1dc313..95d873e 100644 --- a/python/src/winload/cli.py +++ b/python/src/winload/cli.py @@ -5,7 +5,16 @@ import sys from typing import Optional, Sequence -from .config import BarStyle, MaxMode, RgbColor, RunConfig, TitleAlign, Unit +from .config import ( + MAX_SAMPLE_CAPACITY, + BarStyle, + MaxMode, + RgbColor, + RunConfig, + TitleAlign, + Unit, + requested_sample_capacity, +) from .diagnostics import format_build_info, get_help_system_info, get_system_info, get_version from .emoji import decorate from .i18n import set_lang, t @@ -173,6 +182,11 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> RunConfig: parser.error("--interval must be greater than 0") if args.average <= 0: parser.error("--average must be greater than 0") + if requested_sample_capacity(args.interval, args.average) > MAX_SAMPLE_CAPACITY: + parser.error( + "--interval and --average retain more than " + f"{MAX_SAMPLE_CAPACITY} samples per interface" + ) if args.max_half_life <= 0: parser.error("--max-half-life must be greater than 0") has_half_life = any( diff --git a/python/src/winload/config.py b/python/src/winload/config.py index 3c5cb21..bb21eee 100644 --- a/python/src/winload/config.py +++ b/python/src/winload/config.py @@ -38,6 +38,14 @@ class MaxMode(StringEnum): RgbColor = Tuple[int, int, int] +MIN_SAMPLE_CAPACITY = 600 +MAX_SAMPLE_CAPACITY = 60_000 + + +def requested_sample_capacity(interval_ms: int, average_window_sec: int) -> int: + """Return the number of snapshots required by a requested average window.""" + return max(1000 * average_window_sec // interval_ms, MIN_SAMPLE_CAPACITY) + @dataclass(frozen=True) class RunConfig: diff --git a/python/src/winload/stats.py b/python/src/winload/stats.py index 446a447..991e865 100644 --- a/python/src/winload/stats.py +++ b/python/src/winload/stats.py @@ -9,6 +9,7 @@ from typing import Optional from .collector import Snapshot +from .config import MAX_SAMPLE_CAPACITY, requested_sample_capacity @dataclass @@ -37,8 +38,9 @@ def __init__( self._avg_window_sec = average_window_sec # 滑动窗口 - max_samples = max( - int(1000 / refresh_interval_ms * average_window_sec), 600 + max_samples = min( + requested_sample_capacity(refresh_interval_ms, average_window_sec), + MAX_SAMPLE_CAPACITY, ) self._samples: deque[Snapshot] = deque(maxlen=max_samples) diff --git a/python/tests/test_cli.py b/python/tests/test_cli.py index 112b3bc..01a4f09 100644 --- a/python/tests/test_cli.py +++ b/python/tests/test_cli.py @@ -37,6 +37,12 @@ def test_invalid_zero_interval_exits(self): parse_args(["--interval", "0"]) self.assertEqual(caught.exception.code, 2) + def test_rejects_an_oversized_sample_window(self): + with contextlib.redirect_stderr(io.StringIO()): + with self.assertRaises(SystemExit) as caught: + parse_args(["--interval", "1", "--average", "61"]) + self.assertEqual(caught.exception.code, 2) + def test_fixed_requires_value(self): with contextlib.redirect_stderr(io.StringIO()): with self.assertRaises(SystemExit): diff --git a/readme.jp.md b/readme.jp.md index 4bf765e..3baa468 100644 --- a/readme.jp.md +++ b/readme.jp.md @@ -364,16 +364,16 @@ python/ │ ├── __init__.py // 2 lines | 📦 Marks the Python package and exposes package-level metadata. │ ├── __main__.py // 6 lines | ▶️ Runs the Python CLI entry point when invoked with python -m winload. │ ├── app.py // 135 lines | Owns mutable application state, traffic updates, and device navigation. -│ ├── cli.py // 205 lines | Builds, localizes, parses, and validates the Python command-line interface. +│ ├── cli.py // 219 lines | Builds, localizes, parses, and validates the Python command-line interface. │ ├── collector.py // 121 lines | 📡 Collects network interface counters with psutil or the optional Netlink backend. -│ ├── config.py // 65 lines | Defines immutable, strongly typed runtime configuration for the Python application. +│ ├── config.py // 73 lines | Defines immutable, strongly typed runtime configuration for the Python application. │ ├── diagnostics.py // 162 lines | Reports version, build, system, and network-interface diagnostic information. -│ ├── emoji.py // 41 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.py // 48 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.py // 108 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. │ ├── main.py // 46 lines | Wires the Python CLI to diagnostics or the interactive terminal runtime. │ ├── netlink.py // 120 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.py // 62 lines | Manages the curses lifecycle, input mapping, refresh cadence, and UI rendering loop. -│ └── stats.py // 206 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ └── stats.py // 208 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.py // 73 lines | 🧾 Resolves source or packaged Git metadata for Python version output. rust/ @@ -387,18 +387,18 @@ rust/ │ │ ├── debug.rs // 105 lines | Draws the F3 runtime diagnostics overlay. │ │ ├── mod.rs // 261 lines | Coordinates the ratatui layout, header, help bar, panels, and debug overlay. │ │ └── panels.rs // 488 lines | Draws traffic histories in classic, line, scatter, and bar styles with optional axes. -│ ├── app.rs // 190 lines | Owns mutable application state, traffic collection, and device navigation. -│ ├── cli.rs // 375 lines | Parses localized command-line arguments and produces a validated RunConfig. +│ ├── app.rs // 192 lines | Owns mutable application state, traffic collection, and device navigation. +│ ├── cli.rs // 417 lines | Parses localized command-line arguments and produces a validated RunConfig. │ ├── collector.rs // 238 lines | 📡 Collects network interface counters and prepares traffic snapshots for the TUI. -│ ├── config.rs // 183 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. +│ ├── config.rs // 194 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. -│ ├── emoji.rs // 40 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 227 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. -│ ├── runtime.rs // 99 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. -│ └── stats.rs // 231 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. +│ └── stats.rs // 244 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.rs // 51 lines | 🧾 Injects Git metadata and configures platform-specific linker behavior. ``` diff --git a/readme.ko.md b/readme.ko.md index 28a210a..cb739ea 100644 --- a/readme.ko.md +++ b/readme.ko.md @@ -364,16 +364,16 @@ python/ │ ├── __init__.py // 2 lines | 📦 Marks the Python package and exposes package-level metadata. │ ├── __main__.py // 6 lines | ▶️ Runs the Python CLI entry point when invoked with python -m winload. │ ├── app.py // 135 lines | Owns mutable application state, traffic updates, and device navigation. -│ ├── cli.py // 205 lines | Builds, localizes, parses, and validates the Python command-line interface. +│ ├── cli.py // 219 lines | Builds, localizes, parses, and validates the Python command-line interface. │ ├── collector.py // 121 lines | 📡 Collects network interface counters with psutil or the optional Netlink backend. -│ ├── config.py // 65 lines | Defines immutable, strongly typed runtime configuration for the Python application. +│ ├── config.py // 73 lines | Defines immutable, strongly typed runtime configuration for the Python application. │ ├── diagnostics.py // 162 lines | Reports version, build, system, and network-interface diagnostic information. -│ ├── emoji.py // 41 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.py // 48 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.py // 108 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. │ ├── main.py // 46 lines | Wires the Python CLI to diagnostics or the interactive terminal runtime. │ ├── netlink.py // 120 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.py // 62 lines | Manages the curses lifecycle, input mapping, refresh cadence, and UI rendering loop. -│ └── stats.py // 206 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ └── stats.py // 208 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.py // 73 lines | 🧾 Resolves source or packaged Git metadata for Python version output. rust/ @@ -387,18 +387,18 @@ rust/ │ │ ├── debug.rs // 105 lines | Draws the F3 runtime diagnostics overlay. │ │ ├── mod.rs // 261 lines | Coordinates the ratatui layout, header, help bar, panels, and debug overlay. │ │ └── panels.rs // 488 lines | Draws traffic histories in classic, line, scatter, and bar styles with optional axes. -│ ├── app.rs // 190 lines | Owns mutable application state, traffic collection, and device navigation. -│ ├── cli.rs // 375 lines | Parses localized command-line arguments and produces a validated RunConfig. +│ ├── app.rs // 192 lines | Owns mutable application state, traffic collection, and device navigation. +│ ├── cli.rs // 417 lines | Parses localized command-line arguments and produces a validated RunConfig. │ ├── collector.rs // 238 lines | 📡 Collects network interface counters and prepares traffic snapshots for the TUI. -│ ├── config.rs // 183 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. +│ ├── config.rs // 194 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. -│ ├── emoji.rs // 40 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 227 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. -│ ├── runtime.rs // 99 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. -│ └── stats.rs // 231 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. +│ └── stats.rs // 244 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.rs // 51 lines | 🧾 Injects Git metadata and configures platform-specific linker behavior. ``` diff --git a/readme.lzh.md b/readme.lzh.md index 363eedd..8780b2b 100644 --- a/readme.lzh.md +++ b/readme.lzh.md @@ -364,16 +364,16 @@ python/ │ ├── __init__.py // 2 lines | 📦 Marks the Python package and exposes package-level metadata. │ ├── __main__.py // 6 lines | ▶️ Runs the Python CLI entry point when invoked with python -m winload. │ ├── app.py // 135 lines | Owns mutable application state, traffic updates, and device navigation. -│ ├── cli.py // 205 lines | Builds, localizes, parses, and validates the Python command-line interface. +│ ├── cli.py // 219 lines | Builds, localizes, parses, and validates the Python command-line interface. │ ├── collector.py // 121 lines | 📡 Collects network interface counters with psutil or the optional Netlink backend. -│ ├── config.py // 65 lines | Defines immutable, strongly typed runtime configuration for the Python application. +│ ├── config.py // 73 lines | Defines immutable, strongly typed runtime configuration for the Python application. │ ├── diagnostics.py // 162 lines | Reports version, build, system, and network-interface diagnostic information. -│ ├── emoji.py // 41 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.py // 48 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.py // 108 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. │ ├── main.py // 46 lines | Wires the Python CLI to diagnostics or the interactive terminal runtime. │ ├── netlink.py // 120 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.py // 62 lines | Manages the curses lifecycle, input mapping, refresh cadence, and UI rendering loop. -│ └── stats.py // 206 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ └── stats.py // 208 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.py // 73 lines | 🧾 Resolves source or packaged Git metadata for Python version output. rust/ @@ -387,18 +387,18 @@ rust/ │ │ ├── debug.rs // 105 lines | Draws the F3 runtime diagnostics overlay. │ │ ├── mod.rs // 261 lines | Coordinates the ratatui layout, header, help bar, panels, and debug overlay. │ │ └── panels.rs // 488 lines | Draws traffic histories in classic, line, scatter, and bar styles with optional axes. -│ ├── app.rs // 190 lines | Owns mutable application state, traffic collection, and device navigation. -│ ├── cli.rs // 375 lines | Parses localized command-line arguments and produces a validated RunConfig. +│ ├── app.rs // 192 lines | Owns mutable application state, traffic collection, and device navigation. +│ ├── cli.rs // 417 lines | Parses localized command-line arguments and produces a validated RunConfig. │ ├── collector.rs // 238 lines | 📡 Collects network interface counters and prepares traffic snapshots for the TUI. -│ ├── config.rs // 183 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. +│ ├── config.rs // 194 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. -│ ├── emoji.rs // 40 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 227 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. -│ ├── runtime.rs // 99 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. -│ └── stats.rs // 231 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. +│ └── stats.rs // 244 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.rs // 51 lines | 🧾 Injects Git metadata and configures platform-specific linker behavior. ``` diff --git a/readme.md b/readme.md index 82b6d5a..744442d 100644 --- a/readme.md +++ b/readme.md @@ -364,16 +364,16 @@ python/ │ ├── __init__.py // 2 lines | 📦 Marks the Python package and exposes package-level metadata. │ ├── __main__.py // 6 lines | ▶️ Runs the Python CLI entry point when invoked with python -m winload. │ ├── app.py // 135 lines | Owns mutable application state, traffic updates, and device navigation. -│ ├── cli.py // 205 lines | Builds, localizes, parses, and validates the Python command-line interface. +│ ├── cli.py // 219 lines | Builds, localizes, parses, and validates the Python command-line interface. │ ├── collector.py // 121 lines | 📡 Collects network interface counters with psutil or the optional Netlink backend. -│ ├── config.py // 65 lines | Defines immutable, strongly typed runtime configuration for the Python application. +│ ├── config.py // 73 lines | Defines immutable, strongly typed runtime configuration for the Python application. │ ├── diagnostics.py // 162 lines | Reports version, build, system, and network-interface diagnostic information. -│ ├── emoji.py // 41 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.py // 48 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.py // 108 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. │ ├── main.py // 46 lines | Wires the Python CLI to diagnostics or the interactive terminal runtime. │ ├── netlink.py // 120 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.py // 62 lines | Manages the curses lifecycle, input mapping, refresh cadence, and UI rendering loop. -│ └── stats.py // 206 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ └── stats.py // 208 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.py // 73 lines | 🧾 Resolves source or packaged Git metadata for Python version output. rust/ @@ -387,18 +387,18 @@ rust/ │ │ ├── debug.rs // 105 lines | Draws the F3 runtime diagnostics overlay. │ │ ├── mod.rs // 261 lines | Coordinates the ratatui layout, header, help bar, panels, and debug overlay. │ │ └── panels.rs // 488 lines | Draws traffic histories in classic, line, scatter, and bar styles with optional axes. -│ ├── app.rs // 190 lines | Owns mutable application state, traffic collection, and device navigation. -│ ├── cli.rs // 375 lines | Parses localized command-line arguments and produces a validated RunConfig. +│ ├── app.rs // 192 lines | Owns mutable application state, traffic collection, and device navigation. +│ ├── cli.rs // 417 lines | Parses localized command-line arguments and produces a validated RunConfig. │ ├── collector.rs // 238 lines | 📡 Collects network interface counters and prepares traffic snapshots for the TUI. -│ ├── config.rs // 183 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. +│ ├── config.rs // 194 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. -│ ├── emoji.rs // 40 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 227 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. -│ ├── runtime.rs // 99 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. -│ └── stats.rs // 231 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. +│ └── stats.rs // 244 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.rs // 51 lines | 🧾 Injects Git metadata and configures platform-specific linker behavior. ``` diff --git a/readme.zh-cn.md b/readme.zh-cn.md index 54e5316..5840a2a 100644 --- a/readme.zh-cn.md +++ b/readme.zh-cn.md @@ -364,16 +364,16 @@ python/ │ ├── __init__.py // 2 lines | 📦 Marks the Python package and exposes package-level metadata. │ ├── __main__.py // 6 lines | ▶️ Runs the Python CLI entry point when invoked with python -m winload. │ ├── app.py // 135 lines | Owns mutable application state, traffic updates, and device navigation. -│ ├── cli.py // 205 lines | Builds, localizes, parses, and validates the Python command-line interface. +│ ├── cli.py // 219 lines | Builds, localizes, parses, and validates the Python command-line interface. │ ├── collector.py // 121 lines | 📡 Collects network interface counters with psutil or the optional Netlink backend. -│ ├── config.py // 65 lines | Defines immutable, strongly typed runtime configuration for the Python application. +│ ├── config.py // 73 lines | Defines immutable, strongly typed runtime configuration for the Python application. │ ├── diagnostics.py // 162 lines | Reports version, build, system, and network-interface diagnostic information. -│ ├── emoji.py // 41 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.py // 48 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.py // 108 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. │ ├── main.py // 46 lines | Wires the Python CLI to diagnostics or the interactive terminal runtime. │ ├── netlink.py // 120 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.py // 62 lines | Manages the curses lifecycle, input mapping, refresh cadence, and UI rendering loop. -│ └── stats.py // 206 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ └── stats.py // 208 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.py // 73 lines | 🧾 Resolves source or packaged Git metadata for Python version output. rust/ @@ -387,18 +387,18 @@ rust/ │ │ ├── debug.rs // 105 lines | Draws the F3 runtime diagnostics overlay. │ │ ├── mod.rs // 261 lines | Coordinates the ratatui layout, header, help bar, panels, and debug overlay. │ │ └── panels.rs // 488 lines | Draws traffic histories in classic, line, scatter, and bar styles with optional axes. -│ ├── app.rs // 190 lines | Owns mutable application state, traffic collection, and device navigation. -│ ├── cli.rs // 375 lines | Parses localized command-line arguments and produces a validated RunConfig. +│ ├── app.rs // 192 lines | Owns mutable application state, traffic collection, and device navigation. +│ ├── cli.rs // 417 lines | Parses localized command-line arguments and produces a validated RunConfig. │ ├── collector.rs // 238 lines | 📡 Collects network interface counters and prepares traffic snapshots for the TUI. -│ ├── config.rs // 183 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. +│ ├── config.rs // 194 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. -│ ├── emoji.rs // 40 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 227 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. -│ ├── runtime.rs // 99 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. -│ └── stats.rs // 231 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. +│ └── stats.rs // 244 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.rs // 51 lines | 🧾 Injects Git metadata and configures platform-specific linker behavior. ``` diff --git a/readme.zh-tw.md b/readme.zh-tw.md index 9c7503c..209ddea 100644 --- a/readme.zh-tw.md +++ b/readme.zh-tw.md @@ -364,16 +364,16 @@ python/ │ ├── __init__.py // 2 lines | 📦 Marks the Python package and exposes package-level metadata. │ ├── __main__.py // 6 lines | ▶️ Runs the Python CLI entry point when invoked with python -m winload. │ ├── app.py // 135 lines | Owns mutable application state, traffic updates, and device navigation. -│ ├── cli.py // 205 lines | Builds, localizes, parses, and validates the Python command-line interface. +│ ├── cli.py // 219 lines | Builds, localizes, parses, and validates the Python command-line interface. │ ├── collector.py // 121 lines | 📡 Collects network interface counters with psutil or the optional Netlink backend. -│ ├── config.py // 65 lines | Defines immutable, strongly typed runtime configuration for the Python application. +│ ├── config.py // 73 lines | Defines immutable, strongly typed runtime configuration for the Python application. │ ├── diagnostics.py // 162 lines | Reports version, build, system, and network-interface diagnostic information. -│ ├── emoji.py // 41 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.py // 48 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.py // 108 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. │ ├── main.py // 46 lines | Wires the Python CLI to diagnostics or the interactive terminal runtime. │ ├── netlink.py // 120 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.py // 62 lines | Manages the curses lifecycle, input mapping, refresh cadence, and UI rendering loop. -│ └── stats.py // 206 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ └── stats.py // 208 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.py // 73 lines | 🧾 Resolves source or packaged Git metadata for Python version output. rust/ @@ -387,18 +387,18 @@ rust/ │ │ ├── debug.rs // 105 lines | Draws the F3 runtime diagnostics overlay. │ │ ├── mod.rs // 261 lines | Coordinates the ratatui layout, header, help bar, panels, and debug overlay. │ │ └── panels.rs // 488 lines | Draws traffic histories in classic, line, scatter, and bar styles with optional axes. -│ ├── app.rs // 190 lines | Owns mutable application state, traffic collection, and device navigation. -│ ├── cli.rs // 375 lines | Parses localized command-line arguments and produces a validated RunConfig. +│ ├── app.rs // 192 lines | Owns mutable application state, traffic collection, and device navigation. +│ ├── cli.rs // 417 lines | Parses localized command-line arguments and produces a validated RunConfig. │ ├── collector.rs // 238 lines | 📡 Collects network interface counters and prepares traffic snapshots for the TUI. -│ ├── config.rs // 183 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. +│ ├── config.rs // 194 lines | Defines validated, strongly typed runtime configuration shared by the Rust modules. │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. -│ ├── emoji.rs // 40 lines | ✨ Decorates CLI-facing labels with optional emoji icons. +│ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 227 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. -│ ├── runtime.rs // 99 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. -│ └── stats.rs // 231 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. +│ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. +│ └── stats.rs // 244 lines | 📈 Calculates rolling traffic rates, totals, and adaptive graph scale values. └── _build_info.rs // 51 lines | 🧾 Injects Git metadata and configures platform-specific linker behavior. ``` diff --git a/rust/src/app.rs b/rust/src/app.rs index b97c957..c258316 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -3,7 +3,7 @@ use crate::collector::{Collector, DeviceInfo}; use crate::config::{BarStyle, GraphStyle, MaxMode, RunConfig, TitleAlign, Unit, XAxis, YAxis}; use crate::i18n::t; -use crate::loopback::{LoopbackCounters, LoopbackMode}; +use crate::loopback::{LoopbackCapture, LoopbackCounters, LoopbackMode}; use crate::stats::StatisticsEngine; pub struct DeviceView { @@ -38,6 +38,7 @@ pub struct App { pub loopback_mode: LoopbackMode, pub loopback_info: Option, pub loopback_counters: Option, + pub loopback_capture: Option, collector: Collector, } @@ -96,6 +97,7 @@ impl App { loopback_mode, loopback_info: None, loopback_counters: None, + loopback_capture: None, collector, } } diff --git a/rust/src/cli.rs b/rust/src/cli.rs index 7302689..960f18a 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -193,6 +193,14 @@ impl Args { if self.average == 0 { return Err("--average must be greater than 0".into()); } + if crate::config::requested_sample_capacity(self.interval, self.average) + > crate::config::MAX_SAMPLE_CAPACITY + { + return Err(format!( + "--interval and --average retain more than {} samples per interface", + crate::config::MAX_SAMPLE_CAPACITY + )); + } if !self.max_half_life.is_finite() || self.max_half_life <= 0.0 { return Err("--max-half-life must be greater than 0".into()); } @@ -314,6 +322,14 @@ mod tests { ); } + #[test] + fn oversized_sample_window_is_rejected() { + let error = parsed(&["winload", "--interval", "1", "--average", "61"]) + .validate() + .unwrap_err(); + assert!(error.contains("retain more than 60000 samples")); + } + #[test] fn long_help_localizes_shortcuts_between_options_and_system_info() { for (lang, title, previous, graph, system) in [ diff --git a/rust/src/config.rs b/rust/src/config.rs index 057b1e4..200ae0a 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -2,6 +2,17 @@ use ratatui::style::Color; +pub const MIN_SAMPLE_CAPACITY: u64 = 600; +pub const MAX_SAMPLE_CAPACITY: u64 = 60_000; + +pub fn requested_sample_capacity(interval_ms: u64, average_window_sec: u64) -> u64 { + 1000_u64 + .saturating_mul(average_window_sec) + .checked_div(interval_ms) + .unwrap_or(u64::MAX) + .max(MIN_SAMPLE_CAPACITY) +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)] pub enum Unit { Bit, diff --git a/rust/src/loopback.rs b/rust/src/loopback.rs index 4ef72ba..3fda922 100644 --- a/rust/src/loopback.rs +++ b/rust/src/loopback.rs @@ -5,7 +5,7 @@ //! //! 此模块仅在 Windows 平台编译。非 Windows 平台下提供空实现。 -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; /// Npcap 下载地址 (仅 Windows) @@ -36,6 +36,37 @@ impl LoopbackCounters { } } +/// Owns the Npcap capture thread and releases it when the application exits. +pub struct LoopbackCapture { + stop: Arc, + #[cfg(target_os = "windows")] + worker: Option>, +} + +impl LoopbackCapture { + #[cfg(target_os = "windows")] + fn new(stop: Arc, worker: std::thread::JoinHandle<()>) -> Self { + Self { + stop, + worker: Some(worker), + } + } + + pub fn stop(&self) { + self.stop.store(true, Ordering::Relaxed); + } +} + +impl Drop for LoopbackCapture { + fn drop(&mut self) { + self.stop(); + #[cfg(target_os = "windows")] + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + /// 回环捕获模式 #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LoopbackMode { @@ -80,7 +111,7 @@ pub mod platform { /// 返回 Ok(info_msg) 成功时,后台线程会持续累加计数器。 /// 返回 Err(msg) 如果 Npcap 不可用或打开设备失败。 #[cfg(feature = "npcap")] - pub fn start_npcap(counters: LoopbackCounters) -> Result { + pub fn start_npcap(counters: LoopbackCounters) -> Result<(String, LoopbackCapture), String> { // Pre-flight: verify wpcap.dll is loadable before calling any pcap functions. // The binary uses /DELAYLOAD:wpcap.dll, so pcap functions are not resolved // until first call. If the DLL is missing, that first call would crash. @@ -140,21 +171,27 @@ pub mod platform { let dev_name = loopback_dev.name.clone(); let info_msg = format!("[npcap] Found loopback device: {dev_name}"); - // 在后台线程中持续捕获 - thread::Builder::new() + // 在后台线程中持续捕获,并在应用退出时停止并回收线程。 + let stop = Arc::new(AtomicBool::new(false)); + let thread_stop = Arc::clone(&stop); + let worker = thread::Builder::new() .name("npcap-loopback".to_string()) .spawn(move || { - if let Err(e) = npcap_capture_loop(&dev_name, &counters) { + if let Err(e) = npcap_capture_loop(&dev_name, &counters, &thread_stop) { eprintln!("[npcap] Capture error: {e}"); } }) .map_err(|e| format!("Failed to spawn npcap thread: {e}"))?; - Ok(info_msg) + Ok((info_msg, LoopbackCapture::new(stop, worker))) } #[cfg(feature = "npcap")] - fn npcap_capture_loop(device_name: &str, counters: &LoopbackCounters) -> Result<(), String> { + fn npcap_capture_loop( + device_name: &str, + counters: &LoopbackCounters, + stop: &AtomicBool, + ) -> Result<(), String> { let mut cap = pcap::Capture::from_device(device_name) .map_err(|e| format!("Cannot open device: {e}"))? .promisc(false) @@ -163,7 +200,7 @@ pub mod platform { .open() .map_err(|e| format!("Cannot start capture: {e}"))?; - loop { + while !stop.load(Ordering::Relaxed) { match cap.next_packet() { Ok(packet) => { // Npcap loopback 使用 DLT_NULL 格式: @@ -201,7 +238,7 @@ pub mod platform { } #[cfg(not(feature = "npcap"))] - pub fn start_npcap(_counters: LoopbackCounters) -> Result { + pub fn start_npcap(_counters: LoopbackCounters) -> Result<(String, LoopbackCapture), String> { Err(format!( "winload was compiled without Npcap support (feature 'npcap' disabled).\n\ Recompile with: cargo build --features npcap\n\n\ @@ -219,7 +256,7 @@ pub mod platform { pub mod platform { use super::*; - pub fn start_npcap(_counters: LoopbackCounters) -> Result { + pub fn start_npcap(_counters: LoopbackCounters) -> Result<(String, LoopbackCapture), String> { Err("--npcap is only supported on Windows. \ On Linux/macOS, loopback traffic is natively available." .to_string()) diff --git a/rust/src/runtime.rs b/rust/src/runtime.rs index 6da0ab8..69601e9 100644 --- a/rust/src/runtime.rs +++ b/rust/src/runtime.rs @@ -47,9 +47,10 @@ fn start_loopback(app: &mut App) -> io::Result<()> { LoopbackMode::None => unreachable!(), }; match result { - Ok(info) => { + Ok((info, capture)) => { app.loopback_info = Some(info); app.loopback_counters = Some(counters); + app.loopback_capture = Some(capture); Ok(()) } Err(error) => Err(io::Error::other(format!( diff --git a/rust/src/stats.rs b/rust/src/stats.rs index e4a23e8..37ff21f 100644 --- a/rust/src/stats.rs +++ b/rust/src/stats.rs @@ -5,6 +5,7 @@ use std::collections::VecDeque; use crate::collector::Snapshot; +use crate::config::{requested_sample_capacity, MAX_SAMPLE_CAPACITY}; /// 某一方向(收/发)的统计结果 #[derive(Clone, Debug)] @@ -69,7 +70,8 @@ impl StatisticsEngine { smart_max_half_life: Option, ) -> Self { let second_window = (1000u64 / refresh_interval_ms).max(1) as usize; - let max_samples = ((1000u64 / refresh_interval_ms) * average_window_sec).max(600) as usize; + let max_samples = requested_sample_capacity(refresh_interval_ms, average_window_sec) + .min(MAX_SAMPLE_CAPACITY) as usize; let decay_factor = match smart_max_half_life { Some(half_life) if half_life > 0.0 => { @@ -229,3 +231,14 @@ pub fn format_bytes(total_bytes: u64) -> String { format!("{:.2} Byte", b) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sample_capacity_stays_bounded_for_direct_callers() { + let engine = StatisticsEngine::new(1, u64::MAX, None); + assert_eq!(engine.max_samples, MAX_SAMPLE_CAPACITY as usize); + } +} From ca840b7d64d072c75fb9b99a315760b6de1f6c00 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:12:41 +0800 Subject: [PATCH 02/19] =?UTF-8?q?fix:=20=E8=A1=A5=E5=85=A8Npcap=E6=8D=95?= =?UTF-8?q?=E8=8E=B7=E7=BA=BF=E7=A8=8B=E9=80=80=E5=87=BA=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.jp.md | 2 +- readme.ko.md | 2 +- readme.lzh.md | 2 +- readme.md | 2 +- readme.zh-cn.md | 2 +- readme.zh-tw.md | 2 +- rust/src/loopback.rs | 1 + 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/readme.jp.md b/readme.jp.md index 3baa468..3f9b3d9 100644 --- a/readme.jp.md +++ b/readme.jp.md @@ -394,7 +394,7 @@ rust/ │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. │ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 265 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. diff --git a/readme.ko.md b/readme.ko.md index cb739ea..a369209 100644 --- a/readme.ko.md +++ b/readme.ko.md @@ -394,7 +394,7 @@ rust/ │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. │ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 265 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. diff --git a/readme.lzh.md b/readme.lzh.md index 8780b2b..adeec97 100644 --- a/readme.lzh.md +++ b/readme.lzh.md @@ -394,7 +394,7 @@ rust/ │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. │ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 265 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. diff --git a/readme.md b/readme.md index 744442d..d807981 100644 --- a/readme.md +++ b/readme.md @@ -394,7 +394,7 @@ rust/ │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. │ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 265 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. diff --git a/readme.zh-cn.md b/readme.zh-cn.md index 5840a2a..37e4233 100644 --- a/readme.zh-cn.md +++ b/readme.zh-cn.md @@ -394,7 +394,7 @@ rust/ │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. │ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 265 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. diff --git a/readme.zh-tw.md b/readme.zh-tw.md index 209ddea..7709830 100644 --- a/readme.zh-tw.md +++ b/readme.zh-tw.md @@ -394,7 +394,7 @@ rust/ │ ├── diagnostics.rs // 45 lines | Prints build, platform, and network-interface diagnostics outside the TUI. │ ├── emoji.rs // 50 lines | ✨ Decorates CLI-facing labels with optional emoji icons. │ ├── graph.rs // 279 lines | 📊 Renders incoming and outgoing traffic graphs for terminal display. -│ ├── loopback.rs // 264 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. +│ ├── loopback.rs // 265 lines | 🪟 Captures and counts Windows loopback traffic through Npcap when enabled. │ ├── main.rs // 42 lines | Boots the Rust application and dispatches CLI actions to focused modules. │ ├── netlink.rs // 175 lines | 🔗 Reads Linux and Android network counters directly through RTNETLINK. │ ├── runtime.rs // 100 lines | Runs the terminal lifecycle, refresh loop, and semantic keyboard controls. diff --git a/rust/src/loopback.rs b/rust/src/loopback.rs index 3fda922..36b437e 100644 --- a/rust/src/loopback.rs +++ b/rust/src/loopback.rs @@ -235,6 +235,7 @@ pub mod platform { } } } + Ok(()) } #[cfg(not(feature = "npcap"))] From da64366a7a7765a0cf2ecccaad92c53012881f07 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:26:36 +0800 Subject: [PATCH 03/19] =?UTF-8?q?fix(ci):=20=E9=98=BB=E6=AD=A2npm=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E5=8C=85=E5=8F=91=E5=B8=83=E5=A4=B1=E8=B4=A5=E8=A2=AB?= =?UTF-8?q?=E5=BF=BD=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33f0c6e..701b158 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1287,7 +1287,11 @@ jobs: PKGJSON cd "${PKG_DIR}" - npm publish --provenance --access public --tag "${NPM_TAG}" 2>&1 || echo "⚠️ Failed to publish ${PKG_NAME} (may already exist)" + if npm view "${PKG_NAME}@${NPM_VERSION}" version --registry https://registry.npmjs.org > /dev/null 2>&1; then + echo "${PKG_NAME}@${NPM_VERSION} already exists; skipping" + else + npm publish --provenance --access public --tag "${NPM_TAG}" + fi cd "$GITHUB_WORKSPACE" done @@ -1335,7 +1339,7 @@ jobs: publish-npm-unscope: name: Publish to npm (unscoped) needs: [check, release, publish-npm-scope] - if: always() && needs.check.outputs.should_publish_npm == 'true' && (needs.release.result == 'success' || needs.release.result == 'skipped') + if: always() && needs.check.outputs.should_publish_npm == 'true' && needs.publish-npm-scope.result == 'success' && (needs.release.result == 'success' || needs.release.result == 'skipped') permissions: id-token: write contents: read @@ -1374,7 +1378,7 @@ jobs: publish-npm-github-packages: name: Publish to GitHub Packages for npm needs: [check, release, publish-npm-scope] - if: always() && needs.check.outputs.should_publish_npm == 'true' && (needs.release.result == 'success' || needs.release.result == 'skipped') + if: always() && needs.check.outputs.should_publish_npm == 'true' && needs.publish-npm-scope.result == 'success' && (needs.release.result == 'success' || needs.release.result == 'skipped') permissions: contents: read packages: write @@ -1404,7 +1408,12 @@ jobs: echo "📦 Publishing $(basename $dir) to GitHub Packages..." cd "$dir" echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc - npm publish --registry https://npm.pkg.github.com --tag "${NPM_TAG}" 2>&1 || echo "⚠️ Failed to publish $(basename $dir) to GitHub Packages (may already exist)" + PKG_NAME=$(node -p "require('./package.json').name") + if npm view "${PKG_NAME}@${NPM_VERSION}" version --registry https://npm.pkg.github.com > /dev/null 2>&1; then + echo "${PKG_NAME}@${NPM_VERSION} already exists; skipping" + else + npm publish --registry https://npm.pkg.github.com --tag "${NPM_TAG}" + fi cd "$GITHUB_WORKSPACE" done From a0d4bd9599b600a696f5a6ef580b0b51d14c0eb0 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:26:50 +0800 Subject: [PATCH 04/19] =?UTF-8?q?fix(ci):=20=E8=AE=A9Gitee=E9=99=84?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=A4=B1=E8=B4=A5=E9=98=BB=E6=96=AD?= =?UTF-8?q?=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 701b158..4d75846 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1704,3 +1704,7 @@ jobs: echo "$(ts) ════════════════════════════════════════" echo "📊 Upload complete: $SUCCESS succeeded, $FAIL failed" + if [ "$FAIL" -ne 0 ]; then + echo "::error::Gitee release asset upload failed for $FAIL file(s)." + exit 1 + fi From 8836cb698a3e0a2f14367c151923d12a35c28237 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:27:32 +0800 Subject: [PATCH 05/19] =?UTF-8?q?ci:=20=E4=B8=BAPR=E5=92=8Cmain=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=9F=BA=E7=A1=80=E6=B5=8B=E8=AF=95=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d75846..8240588 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -160,6 +160,36 @@ jobs: echo "📋 Flags: build=$BUILD release=$RELEASE scoop=$PUBLISH_SCOOP homebrew=$PUBLISH_HOMEBREW aur=$PUBLISH_AUR npm=$PUBLISH_NPM pypi=$PUBLISH_PYPI crates=$PUBLISH_CRATES benchmark=$BENCHMARK" + # ── 基础测试 ─────────────────────────────────────────── + quality: + name: Run Tests + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Python package + run: python3 -m pip install --disable-pip-version-check -e ./python + + - name: Test Python implementation + env: + PYTHONPATH: python/src + run: python3 -m unittest discover -s python/tests + + - name: Test repository scripts + env: + PYTHONPATH: python/src + run: python3 -m unittest discover -s test + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + + - name: Test Rust implementation + working-directory: rust + run: cargo test --locked --no-default-features + # ── 同步代码到 Gitee ───────────────────────────────────── # 每次 push 都同步,与 check 并行运行 sync-gitee-code: From 57c17edd83507989e0642f6feca8edbc383ac496 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:28:04 +0800 Subject: [PATCH 06/19] =?UTF-8?q?fix(ci):=20=E6=A0=A1=E9=AA=8C=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E4=BA=A7=E7=89=A9=E5=B9=B6=E5=A4=84=E7=90=86i686?= =?UTF-8?q?=E8=B7=B3=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8240588..0b865f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -525,6 +525,7 @@ jobs: path: rust/renamed/*.rpm - name: Prepare artifact + if: matrix.target != 'i686-unknown-linux-musl' || !env.I686_MUSL_SKIP shell: bash run: | VERSION="${{ needs.check.outputs.version }}" @@ -548,6 +549,7 @@ jobs: echo "asset_name=$ASSET_WITH_VERSION" >> "$GITHUB_ENV" - name: Upload build artifact + if: matrix.target != 'i686-unknown-linux-musl' || !env.I686_MUSL_SKIP uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: ${{ env.asset_name }} @@ -577,6 +579,36 @@ jobs: echo "Downloaded artifacts:" ls -lh + - name: Verify required release artifacts + shell: bash + run: | + VERSION="${{ needs.check.outputs.version }}" + REQUIRED_ASSETS=( + "winload-windows-x86_64-msvc-npcap-${VERSION}.exe" + "winload-windows-x86_64-msvc-no-npcap-${VERSION}.exe" + "winload-windows-aarch64-msvc-npcap-${VERSION}.exe" + "winload-windows-aarch64-msvc-no-npcap-${VERSION}.exe" + "winload-linux-x86_64-${VERSION}" + "winload-linux-i686-${VERSION}" + "winload-linux-aarch64-${VERSION}" + "winload-macos-x86_64-${VERSION}" + "winload-macos-aarch64-${VERSION}" + "winload-android-aarch64-${VERSION}" + "winload-android-x86_64-${VERSION}" + ) + + MISSING=0 + for asset in "${REQUIRED_ASSETS[@]}"; do + if [ ! -f "$asset" ]; then + echo "::error::Missing required release artifact: $asset" + MISSING=1 + fi + done + + if [ "$MISSING" -ne 0 ]; then + exit 1 + fi + - name: Delete existing release & tag (force fresh release) env: GH_TOKEN: ${{ github.token }} From 1b4cc98ae80e9fdd70209351e6435e4cacfac778 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:28:19 +0800 Subject: [PATCH 07/19] =?UTF-8?q?ci:=20=E9=81=BF=E5=85=8D=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E5=8F=91=E5=B8=83=E6=B5=81=E7=A8=8B=E4=BA=92=E7=9B=B8?= =?UTF-8?q?=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b865f1..1738838 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [main] +concurrency: + group: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('push-{0}', github.ref_name) }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: # ── 判断是否需要构建 / 发布 ────────────────────────────── check: From e35603923a77b05e08c41a195c743783e0362217 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:31:22 +0800 Subject: [PATCH 08/19] =?UTF-8?q?ci:=20=E5=BF=BD=E7=95=A5npm=E4=B8=8EGitee?= =?UTF-8?q?=E5=8D=95=E9=A1=B9=E5=8F=91=E5=B8=83=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1738838..ec5be17 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1356,7 +1356,7 @@ jobs: if npm view "${PKG_NAME}@${NPM_VERSION}" version --registry https://registry.npmjs.org > /dev/null 2>&1; then echo "${PKG_NAME}@${NPM_VERSION} already exists; skipping" else - npm publish --provenance --access public --tag "${NPM_TAG}" + npm publish --provenance --access public --tag "${NPM_TAG}" 2>&1 || echo "::warning::Failed to publish ${PKG_NAME}; continuing." fi cd "$GITHUB_WORKSPACE" done @@ -1478,7 +1478,7 @@ jobs: if npm view "${PKG_NAME}@${NPM_VERSION}" version --registry https://npm.pkg.github.com > /dev/null 2>&1; then echo "${PKG_NAME}@${NPM_VERSION} already exists; skipping" else - npm publish --registry https://npm.pkg.github.com --tag "${NPM_TAG}" + npm publish --registry https://npm.pkg.github.com --tag "${NPM_TAG}" 2>&1 || echo "::warning::Failed to publish ${PKG_NAME} to GitHub Packages; continuing." fi cd "$GITHUB_WORKSPACE" done @@ -1771,6 +1771,5 @@ jobs: echo "📊 Upload complete: $SUCCESS succeeded, $FAIL failed" if [ "$FAIL" -ne 0 ]; then - echo "::error::Gitee release asset upload failed for $FAIL file(s)." - exit 1 + echo "::warning::Gitee release asset upload failed for $FAIL file(s); continuing." fi From e46b66ca59d49f1e1dc1c9927fc3246a29f892d2 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:39:17 +0800 Subject: [PATCH 09/19] =?UTF-8?q?fix(netlink):=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E6=9C=AA=E5=AF=B9=E9=BD=90=E6=8A=A5=E6=96=87=E5=A4=B4=E5=BC=95?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust/src/netlink.rs | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/rust/src/netlink.rs b/rust/src/netlink.rs index 0c21915..ca104b4 100644 --- a/rust/src/netlink.rs +++ b/rust/src/netlink.rs @@ -20,15 +20,8 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { const NLMSG_DONE: u16 = 3; const IFLA_IFNAME: u16 = 3; const IFLA_STATS64: u16 = 23; - - #[repr(C)] - struct Nlmsghdr { - len: u32, - typ: u16, - flags: u16, - seq: u32, - pid: u32, - } + const NLMSG_HDR_LEN: usize = 16; + const IFINFOMSG_LEN: usize = 16; #[repr(C, packed)] struct Sockaddrne { family: u16, @@ -44,12 +37,11 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { return result; } - let mut req = [0u8; 32]; - let hdr = &mut *(req.as_mut_ptr() as *mut Nlmsghdr); - hdr.len = 32; - hdr.typ = RTM_GETLINK; - hdr.flags = NLM_F_REQUEST | NLM_F_DUMP; - hdr.seq = 1; + let mut req = [0u8; NLMSG_HDR_LEN + IFINFOMSG_LEN]; + req[0..4].copy_from_slice(&(req.len() as u32).to_ne_bytes()); + req[4..6].copy_from_slice(&RTM_GETLINK.to_ne_bytes()); + req[6..8].copy_from_slice(&(NLM_F_REQUEST | NLM_F_DUMP).to_ne_bytes()); + req[8..12].copy_from_slice(&1_u32.to_ne_bytes()); let sa = Sockaddrne { family: libc::AF_NETLINK as u16, pad: 0, @@ -60,7 +52,7 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { let sent = libc::sendto( fd, req.as_ptr() as *const libc::c_void, - 32, + req.len(), 0, &sa as *const _ as *const libc::sockaddr, std::mem::size_of::() as libc::socklen_t, @@ -78,16 +70,16 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { } let n = n as usize; let mut off = 0usize; - while off + 16 <= n { - let hdr = &*(buf.as_ptr().add(off) as *const Nlmsghdr); - let msg_len = hdr.len as usize; - if msg_len < 16 || off + msg_len > n { + while off + NLMSG_HDR_LEN <= n { + let msg_len = u32::from_ne_bytes(buf[off..off + 4].try_into().unwrap()) as usize; + let msg_type = u16::from_ne_bytes(buf[off + 4..off + 6].try_into().unwrap()); + if msg_len < NLMSG_HDR_LEN || off + msg_len > n { break; } - match hdr.typ { + match msg_type { NLMSG_DONE => break 'outer, RTM_NEWLINK => { - let mut rta = off + 32; + let mut rta = off + NLMSG_HDR_LEN + IFINFOMSG_LEN; let end = off + msg_len; let mut iface: Option = None; let mut rx = 0u64; @@ -106,7 +98,7 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { iface = Some(String::from_utf8_lossy(&s[..nul]).into_owned()); } IFLA_STATS64 => { - let d = &buf[rta + 4..]; + let d = &buf[rta + 4..rta + rlen]; if d.len() >= 32 { rx = u64::from_ne_bytes(d[16..24].try_into().unwrap()); tx = u64::from_ne_bytes(d[24..32].try_into().unwrap()); From b5d54f05eec267bc858d2ffeeaa2342d666957c3 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:40:13 +0800 Subject: [PATCH 10/19] =?UTF-8?q?fix(release):=20=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E5=8C=85=E7=9A=84SHA-256=E6=91=98=E8=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 9 +++++++ docs/scripts/install/install.sh | 34 +++++++++++++++++++++------ docs/scripts/install/install_gitee.sh | 34 +++++++++++++++++++++------ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec5be17..ea1fc04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -613,6 +613,15 @@ jobs: exit 1 fi + - name: Generate release checksums + shell: bash + run: | + VERSION="${{ needs.check.outputs.version }}" + CHECKSUM_FILE="winload-checksums-${VERSION}.txt" + sha256sum winload-*-${VERSION}* > "$CHECKSUM_FILE" + echo "Generated $CHECKSUM_FILE:" + cat "$CHECKSUM_FILE" + - name: Delete existing release & tag (force fresh release) env: GH_TOKEN: ${{ github.token }} diff --git a/docs/scripts/install/install.sh b/docs/scripts/install/install.sh index 15a11e1..641f008 100644 --- a/docs/scripts/install/install.sh +++ b/docs/scripts/install/install.sh @@ -77,25 +77,45 @@ fi BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" TMP_DIR=$(mktemp -d) trap 'rm -rf "$TMP_DIR"' EXIT +CHECKSUM_FILE="winload-checksums-${VERSION}.txt" + +echo "📥 Downloading ${CHECKSUM_FILE}..." +curl -fSL -o "${TMP_DIR}/${CHECKSUM_FILE}" "${BASE_URL}/${CHECKSUM_FILE}" + +download_and_verify() { + local asset="$1" + local target="${TMP_DIR}/${asset}" + local checksum + + echo "📥 Downloading ${asset}..." + curl -fSL -o "$target" "${BASE_URL}/${asset}" + checksum=$(awk -v asset="$asset" '$2 == asset { print $1; exit }' "${TMP_DIR}/${CHECKSUM_FILE}") + if [ -z "$checksum" ]; then + echo "❌ No SHA-256 checksum found for ${asset}." + exit 1 + fi + if ! printf '%s %s\n' "$checksum" "$target" | sha256sum --check --status; then + echo "❌ SHA-256 verification failed for ${asset}." + exit 1 + fi + echo "✅ SHA-256 verified: ${asset}" +} if [ "$PKG_MGR" = "termux" ]; then ANDROID_ASSET="winload-android-${ARCH_NAME}-${VERSION}" - echo "📥 Downloading ${ANDROID_ASSET}..." - curl -fSL -o "${TMP_DIR}/winload" "${BASE_URL}/${ANDROID_ASSET}" + download_and_verify "$ANDROID_ASSET" echo "📦 Installing to ${PREFIX}/bin/..." - install -Dm755 "${TMP_DIR}/winload" "${PREFIX}/bin/winload" + install -Dm755 "${TMP_DIR}/${ANDROID_ASSET}" "${PREFIX}/bin/winload" elif [ "$PKG_MGR" = "apt" ]; then PLATFORM="linux-${ARCH_NAME}" PKG_FILE="winload-${PLATFORM}-${VERSION}.deb" - echo "📥 Downloading ${PKG_FILE}..." - curl -fSL -o "${TMP_DIR}/${PKG_FILE}" "${BASE_URL}/${PKG_FILE}" + download_and_verify "$PKG_FILE" echo "📦 Installing via apt..." sudo dpkg -i "${TMP_DIR}/${PKG_FILE}" || sudo apt-get install -f -y elif [ "$PKG_MGR" = "dnf" ]; then PLATFORM="linux-${ARCH_NAME}" PKG_FILE="winload-${PLATFORM}-${VERSION}.rpm" - echo "📥 Downloading ${PKG_FILE}..." - curl -fSL -o "${TMP_DIR}/${PKG_FILE}" "${BASE_URL}/${PKG_FILE}" + download_and_verify "$PKG_FILE" echo "📦 Installing via dnf..." sudo dnf install -y "${TMP_DIR}/${PKG_FILE}" fi diff --git a/docs/scripts/install/install_gitee.sh b/docs/scripts/install/install_gitee.sh index 940dc0b..a757464 100644 --- a/docs/scripts/install/install_gitee.sh +++ b/docs/scripts/install/install_gitee.sh @@ -84,25 +84,45 @@ fi BASE_URL="https://gitee.com/${OWNER}/${REPO}/releases/download/${VERSION}" TMP_DIR=$(mktemp -d) trap 'rm -rf "$TMP_DIR"' EXIT +CHECKSUM_FILE="winload-checksums-${VERSION}.txt" + +echo "📥 正在下载 ${CHECKSUM_FILE}..." +curl -fSL -o "${TMP_DIR}/${CHECKSUM_FILE}" "${BASE_URL}/${CHECKSUM_FILE}" + +download_and_verify() { + local asset="$1" + local target="${TMP_DIR}/${asset}" + local checksum + + echo "📥 正在下载 ${asset}..." + curl -fSL -o "$target" "${BASE_URL}/${asset}" + checksum=$(awk -v asset="$asset" '$2 == asset { print $1; exit }' "${TMP_DIR}/${CHECKSUM_FILE}") + if [ -z "$checksum" ]; then + echo "❌ 未找到 ${asset} 的 SHA-256 校验值。" + exit 1 + fi + if ! printf '%s %s\n' "$checksum" "$target" | sha256sum --check --status; then + echo "❌ ${asset} 的 SHA-256 校验失败。" + exit 1 + fi + echo "✅ SHA-256 校验通过: ${asset}" +} if [ "$PKG_MGR" = "termux" ]; then ANDROID_ASSET="winload-android-${ARCH_NAME}-${VERSION}" - echo "📥 正在从 Gitee 下载 ${ANDROID_ASSET}..." - curl -fSL -o "${TMP_DIR}/winload" "${BASE_URL}/${ANDROID_ASSET}" + download_and_verify "$ANDROID_ASSET" echo "📦 安装到 ${PREFIX}/bin/ ..." - install -Dm755 "${TMP_DIR}/winload" "${PREFIX}/bin/winload" + install -Dm755 "${TMP_DIR}/${ANDROID_ASSET}" "${PREFIX}/bin/winload" elif [ "$PKG_MGR" = "apt" ]; then PLATFORM="linux-${ARCH_NAME}" PKG_FILE="winload-${PLATFORM}-${VERSION}.deb" - echo "📥 正在从 Gitee 下载 ${PKG_FILE}..." - curl -fSL -o "${TMP_DIR}/${PKG_FILE}" "${BASE_URL}/${PKG_FILE}" + download_and_verify "$PKG_FILE" echo "📦 通过 apt 安装中..." sudo dpkg -i "${TMP_DIR}/${PKG_FILE}" || sudo apt-get install -f -y elif [ "$PKG_MGR" = "dnf" ]; then PLATFORM="linux-${ARCH_NAME}" PKG_FILE="winload-${PLATFORM}-${VERSION}.rpm" - echo "📥 正在从 Gitee 下载 ${PKG_FILE}..." - curl -fSL -o "${TMP_DIR}/${PKG_FILE}" "${BASE_URL}/${PKG_FILE}" + download_and_verify "$PKG_FILE" echo "📦 通过 dnf 安装中..." sudo dnf install -y "${TMP_DIR}/${PKG_FILE}" fi From c67715a73d5454e21d4b73d2fb2ce2f9d251cc64 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:41:13 +0800 Subject: [PATCH 11/19] =?UTF-8?q?fix(netlink):=20=E9=99=90=E5=88=B6?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E8=B6=85=E6=97=B6=E5=B9=B6=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust/src/netlink.rs | 87 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/rust/src/netlink.rs b/rust/src/netlink.rs index ca104b4..562a092 100644 --- a/rust/src/netlink.rs +++ b/rust/src/netlink.rs @@ -17,16 +17,19 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { const RTM_NEWLINK: u16 = 16; const NLM_F_REQUEST: u16 = 1; const NLM_F_DUMP: u16 = 0x300; + const NLMSG_ERROR: u16 = 2; const NLMSG_DONE: u16 = 3; const IFLA_IFNAME: u16 = 3; const IFLA_STATS64: u16 = 23; const NLMSG_HDR_LEN: usize = 16; const IFINFOMSG_LEN: usize = 16; - #[repr(C, packed)] - struct Sockaddrne { + const REQUEST_SEQUENCE: u32 = 1; + + #[repr(C)] + struct SockaddrNl { family: u16, pad: u16, - pid: i32, + pid: u32, groups: u32, } @@ -37,12 +40,28 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { return result; } + let timeout = libc::timeval { + tv_sec: 1, + tv_usec: 0, + }; + if libc::setsockopt( + fd, + libc::SOL_SOCKET, + libc::SO_RCVTIMEO, + &timeout as *const _ as *const libc::c_void, + std::mem::size_of::() as libc::socklen_t, + ) < 0 + { + libc::close(fd); + return result; + } + let mut req = [0u8; NLMSG_HDR_LEN + IFINFOMSG_LEN]; req[0..4].copy_from_slice(&(req.len() as u32).to_ne_bytes()); req[4..6].copy_from_slice(&RTM_GETLINK.to_ne_bytes()); req[6..8].copy_from_slice(&(NLM_F_REQUEST | NLM_F_DUMP).to_ne_bytes()); - req[8..12].copy_from_slice(&1_u32.to_ne_bytes()); - let sa = Sockaddrne { + req[8..12].copy_from_slice(&REQUEST_SEQUENCE.to_ne_bytes()); + let sa = SockaddrNl { family: libc::AF_NETLINK as u16, pad: 0, pid: 0, @@ -55,7 +74,7 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { req.len(), 0, &sa as *const _ as *const libc::sockaddr, - std::mem::size_of::() as libc::socklen_t, + std::mem::size_of::() as libc::socklen_t, ); if sent < 0 { libc::close(fd); @@ -63,24 +82,64 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { } let mut buf = vec![0u8; 32768]; + let mut completed = false; 'outer: loop { - let n = libc::recv(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0); + let mut peer = SockaddrNl { + family: 0, + pad: 0, + pid: 0, + groups: 0, + }; + let mut peer_len = std::mem::size_of::() as libc::socklen_t; + let n = libc::recvfrom( + fd, + buf.as_mut_ptr() as *mut libc::c_void, + buf.len(), + 0, + &mut peer as *mut _ as *mut libc::sockaddr, + &mut peer_len, + ); if n <= 0 { break; } + if peer_len != std::mem::size_of::() as libc::socklen_t + || peer.family != libc::AF_NETLINK as u16 + || peer.pid != 0 + || peer.groups != 0 + { + continue; + } let n = n as usize; let mut off = 0usize; while off + NLMSG_HDR_LEN <= n { let msg_len = u32::from_ne_bytes(buf[off..off + 4].try_into().unwrap()) as usize; let msg_type = u16::from_ne_bytes(buf[off + 4..off + 6].try_into().unwrap()); - if msg_len < NLMSG_HDR_LEN || off + msg_len > n { + let msg_seq = u32::from_ne_bytes(buf[off + 8..off + 12].try_into().unwrap()); + let msg_pid = u32::from_ne_bytes(buf[off + 12..off + 16].try_into().unwrap()); + let Some(end) = off.checked_add(msg_len) else { + break; + }; + if msg_len < NLMSG_HDR_LEN || end > n { break; } + let Some(aligned_len) = msg_len.checked_add(3).map(|len| len & !3) else { + break; + }; + let Some(next_off) = off.checked_add(aligned_len) else { + break; + }; + if msg_seq != REQUEST_SEQUENCE || msg_pid != 0 { + off = next_off; + continue; + } match msg_type { - NLMSG_DONE => break 'outer, + NLMSG_DONE => { + completed = true; + break 'outer; + } + NLMSG_ERROR => break 'outer, RTM_NEWLINK => { let mut rta = off + NLMSG_HDR_LEN + IFINFOMSG_LEN; - let end = off + msg_len; let mut iface: Option = None; let mut rx = 0u64; let mut tx = 0u64; @@ -122,12 +181,16 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { } _ => {} } - off += (msg_len + 3) & !3; + off = next_off; } } libc::close(fd); } - result + if completed { + result + } else { + HashMap::new() + } } pub(crate) fn netlink_devices() -> Vec { From 9ceea4d0c8f2998f2c918b1fc9ede24b717e5573 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:45:16 +0800 Subject: [PATCH 12/19] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8Dnetlink?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E4=B8=8E=E8=84=9A=E6=9C=AC=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=8F=91=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 2 +- rust/src/netlink.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea1fc04..8b25c40 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -185,7 +185,7 @@ jobs: - name: Test repository scripts env: PYTHONPATH: python/src - run: python3 -m unittest discover -s test + run: python3 -m unittest discover -s test/readme - name: Install Rust toolchain uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable diff --git a/rust/src/netlink.rs b/rust/src/netlink.rs index 562a092..da5524c 100644 --- a/rust/src/netlink.rs +++ b/rust/src/netlink.rs @@ -34,6 +34,7 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { } let mut result = HashMap::new(); + let mut completed = false; unsafe { let fd = libc::socket(libc::AF_NETLINK, libc::SOCK_RAW, 0); if fd < 0 { @@ -82,7 +83,6 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { } let mut buf = vec![0u8; 32768]; - let mut completed = false; 'outer: loop { let mut peer = SockaddrNl { family: 0, From d0797af2b739777cc98b9be7e8c41b72063db405 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Wed, 26 Aug 2026 09:47:55 +0800 Subject: [PATCH 13/19] =?UTF-8?q?fix(netlink):=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E6=9E=84=E9=80=A0=E8=AF=B7=E6=B1=82=E6=97=B6=E9=87=8D=E5=8F=A0?= =?UTF-8?q?=E5=80=9F=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust/src/netlink.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/src/netlink.rs b/rust/src/netlink.rs index da5524c..7f42068 100644 --- a/rust/src/netlink.rs +++ b/rust/src/netlink.rs @@ -57,8 +57,9 @@ pub(crate) fn netlink_collect(elapsed: f64) -> HashMap { return result; } + let request_len = (NLMSG_HDR_LEN + IFINFOMSG_LEN) as u32; let mut req = [0u8; NLMSG_HDR_LEN + IFINFOMSG_LEN]; - req[0..4].copy_from_slice(&(req.len() as u32).to_ne_bytes()); + req[0..4].copy_from_slice(&request_len.to_ne_bytes()); req[4..6].copy_from_slice(&RTM_GETLINK.to_ne_bytes()); req[6..8].copy_from_slice(&(NLM_F_REQUEST | NLM_F_DUMP).to_ne_bytes()); req[8..12].copy_from_slice(&REQUEST_SEQUENCE.to_ne_bytes()); From 91eaef4cc75343d99aa6a97fcdc3c0ad308f8c06 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Thu, 27 Aug 2026 18:47:05 +0800 Subject: [PATCH 14/19] =?UTF-8?q?ci:=20=E5=8F=91=E5=B8=83=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E5=90=8E=E9=87=8D=E8=AF=95=E5=B9=B6=E7=BB=88=E6=AD=A2?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 290 +++++++++++++++++++++++++++++++----- 1 file changed, 255 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b25c40..c1454a1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -835,13 +835,13 @@ jobs: echo "📦 Target version: ${VERSION}" echo "📥 Downloading Windows binaries from GitHub release..." - curl -fSL -o winload-windows-x86_64.exe \ - "${BASE_URL}/winload-windows-x86_64-msvc-npcap-${VERSION}.exe" \ - || { echo "❌ Failed to download x64 binary"; exit 1; } + download_release_asset() { + curl -fSL --retry 4 --retry-delay 5 --retry-all-errors --retry-connrefused \ + --connect-timeout 30 --max-time 300 -o "$1" "$2" + } - curl -fSL -o winload-windows-aarch64.exe \ - "${BASE_URL}/winload-windows-aarch64-msvc-npcap-${VERSION}.exe" \ - || { echo "❌ Failed to download arm64 binary"; exit 1; } + download_release_asset winload-windows-x86_64.exe "${BASE_URL}/winload-windows-x86_64-msvc-npcap-${VERSION}.exe" + download_release_asset winload-windows-aarch64.exe "${BASE_URL}/winload-windows-aarch64-msvc-npcap-${VERSION}.exe" echo "✅ Downloaded:" ls -lh winload-windows-*.exe @@ -903,15 +903,47 @@ jobs: run: | VERSION="${{ needs.check.outputs.version }}" - git clone https://vincent-zyu:${GITEE_TOKEN}@gitee.com/vincent-zyu/scoop-bucket.git scoop-repo + retry_gitee_git() { + local operation="$1" + shift + local max_attempts=4 + local delay=5 + local attempt + + for attempt in $(seq 1 "$max_attempts"); do + echo "🔁 Gitee ${operation}: attempt ${attempt}/${max_attempts}" + if "$@"; then + return 0 + fi + if [ "$attempt" -lt "$max_attempts" ]; then + echo "::warning::Gitee ${operation} failed; retrying in ${delay}s." + sleep "$delay" + delay=$((delay * 2)) + fi + done + + echo "::error::Gitee ${operation} failed after ${max_attempts} attempts." + return 1 + } + + clone_gitee_bucket() { + rm -rf scoop-repo + git clone https://vincent-zyu:${GITEE_TOKEN}@gitee.com/vincent-zyu/scoop-bucket.git scoop-repo + } + + retry_gitee_git "Scoop bucket clone" clone_gitee_bucket cp winload.json scoop-repo/bucket/winload.json cd scoop-repo git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add bucket/winload.json - git commit -m "ci: 🍨 Update winload to ${VERSION}" || echo "No changes to commit" - git push + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "ci: 🍨 Update winload to ${VERSION}" + retry_gitee_git "Scoop bucket push" git push + fi echo "✅ Gitee Scoop bucket updated!" @@ -1051,7 +1083,8 @@ jobs: NAME="${entry%%:*}" URL="${entry##*:}" echo "📥 Downloading $NAME from Gitee..." - curl -fSL -o "$NAME" "$URL" || echo "⚠️ Failed to download $NAME (may not exist)" + curl -fSL --retry 4 --retry-delay 5 --retry-all-errors --retry-connrefused \ + --connect-timeout 30 --max-time 300 -o "$NAME" "$URL" done echo "✅ Downloaded:" @@ -1114,15 +1147,47 @@ jobs: run: | VERSION="${{ needs.check.outputs.version }}" - git clone https://vincent-zyu:${GITEE_TOKEN}@gitee.com/vincent-zyu/homebrew-tap.git brew-repo + retry_gitee_git() { + local operation="$1" + shift + local max_attempts=4 + local delay=5 + local attempt + + for attempt in $(seq 1 "$max_attempts"); do + echo "🔁 Gitee ${operation}: attempt ${attempt}/${max_attempts}" + if "$@"; then + return 0 + fi + if [ "$attempt" -lt "$max_attempts" ]; then + echo "::warning::Gitee ${operation} failed; retrying in ${delay}s." + sleep "$delay" + delay=$((delay * 2)) + fi + done + + echo "::error::Gitee ${operation} failed after ${max_attempts} attempts." + return 1 + } + + clone_gitee_tap() { + rm -rf brew-repo + git clone https://vincent-zyu:${GITEE_TOKEN}@gitee.com/vincent-zyu/homebrew-tap.git brew-repo + } + + retry_gitee_git "Homebrew tap clone" clone_gitee_tap cp Formula/winload.rb brew-repo/Formula/winload.rb cd brew-repo git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add Formula/winload.rb - git commit -m "ci: 🍺 Update winload to ${VERSION}" || echo "No changes to commit" - git push + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "ci: 🍺 Update winload to ${VERSION}" + retry_gitee_git "Homebrew tap push" git push + fi echo "✅ Gitee Homebrew tap updated!" @@ -1303,12 +1368,17 @@ jobs: mkdir -p artifacts echo "📥 Downloading binaries for npm publish..." - curl -fSL -o artifacts/winload-windows-x86_64.exe "${BASE_URL}/winload-windows-x86_64-msvc-npcap-${VERSION}.exe" - curl -fSL -o artifacts/winload-windows-aarch64.exe "${BASE_URL}/winload-windows-aarch64-msvc-npcap-${VERSION}.exe" - curl -fSL -o artifacts/winload-linux-x86_64 "${BASE_URL}/winload-linux-x86_64-${VERSION}" - curl -fSL -o artifacts/winload-linux-aarch64 "${BASE_URL}/winload-linux-aarch64-${VERSION}" - curl -fSL -o artifacts/winload-macos-x86_64 "${BASE_URL}/winload-macos-x86_64-${VERSION}" - curl -fSL -o artifacts/winload-macos-aarch64 "${BASE_URL}/winload-macos-aarch64-${VERSION}" + download_release_asset() { + curl -fSL --retry 4 --retry-delay 5 --retry-all-errors --retry-connrefused \ + --connect-timeout 30 --max-time 300 -o "$1" "$2" + } + + download_release_asset artifacts/winload-windows-x86_64.exe "${BASE_URL}/winload-windows-x86_64-msvc-npcap-${VERSION}.exe" + download_release_asset artifacts/winload-windows-aarch64.exe "${BASE_URL}/winload-windows-aarch64-msvc-npcap-${VERSION}.exe" + download_release_asset artifacts/winload-linux-x86_64 "${BASE_URL}/winload-linux-x86_64-${VERSION}" + download_release_asset artifacts/winload-linux-aarch64 "${BASE_URL}/winload-linux-aarch64-${VERSION}" + download_release_asset artifacts/winload-macos-x86_64 "${BASE_URL}/winload-macos-x86_64-${VERSION}" + download_release_asset artifacts/winload-macos-aarch64 "${BASE_URL}/winload-macos-aarch64-${VERSION}" echo "✅ Downloaded:" ls -lh artifacts/ @@ -1325,6 +1395,37 @@ jobs: fi echo "📦 npm version: ${NPM_VERSION} (tag: ${NPM_TAG})" + publish_with_retry() { + local package_name="$1" + local registry="$2" + shift 2 + local max_attempts=4 + local delay=5 + local attempt + + for attempt in $(seq 1 "$max_attempts"); do + echo "🔁 Publishing ${package_name}: attempt ${attempt}/${max_attempts}" + if npm publish "$@"; then + return 0 + fi + + # A connection may drop after npm accepts the immutable version. + if npm view "${package_name}@${NPM_VERSION}" version --registry "$registry" > /dev/null 2>&1; then + echo "${package_name}@${NPM_VERSION} is available after the failed request; treating it as published." + return 0 + fi + + if [ "$attempt" -lt "$max_attempts" ]; then + echo "::warning::Publish failed; retrying in ${delay}s." + sleep "$delay" + delay=$((delay * 2)) + fi + done + + echo "::error::Failed to publish ${package_name} after ${max_attempts} attempts." + return 1 + } + PLATFORMS=( "@vincentzyuapps/winload-win32-x64|win32|x64|artifacts/winload-windows-x86_64.exe|winload.exe" "@vincentzyuapps/winload-win32-arm64|win32|arm64|artifacts/winload-windows-aarch64.exe|winload.exe" @@ -1365,7 +1466,7 @@ jobs: if npm view "${PKG_NAME}@${NPM_VERSION}" version --registry https://registry.npmjs.org > /dev/null 2>&1; then echo "${PKG_NAME}@${NPM_VERSION} already exists; skipping" else - npm publish --provenance --access public --tag "${NPM_TAG}" 2>&1 || echo "::warning::Failed to publish ${PKG_NAME}; continuing." + publish_with_retry "$PKG_NAME" "https://registry.npmjs.org" --provenance --access public --tag "${NPM_TAG}" fi cd "$GITHUB_WORKSPACE" done @@ -1397,7 +1498,32 @@ jobs: echo "📦 Publishing @vincentzyuapps/winload@${NPM_VERSION} (tag: ${NPM_TAG})..." cat package.json - npm publish --provenance --access public --tag "${NPM_TAG}" + publish_with_retry() { + local package_name="$1" + local max_attempts=4 + local delay=5 + local attempt + + for attempt in $(seq 1 "$max_attempts"); do + echo "🔁 Publishing ${package_name}: attempt ${attempt}/${max_attempts}" + if npm publish --provenance --access public --tag "${NPM_TAG}"; then + return 0 + fi + if npm view "${package_name}@${NPM_VERSION}" version --registry https://registry.npmjs.org > /dev/null 2>&1; then + echo "${package_name}@${NPM_VERSION} is available after the failed request; treating it as published." + return 0 + fi + if [ "$attempt" -lt "$max_attempts" ]; then + echo "::warning::Publish failed; retrying in ${delay}s." + sleep "$delay" + delay=$((delay * 2)) + fi + done + + echo "::error::Failed to publish ${package_name} after ${max_attempts} attempts." + return 1 + } + publish_with_retry "@vincentzyuapps/winload" echo "✅ Scoped package published!" @@ -1445,7 +1571,32 @@ jobs: require('fs').writeFileSync('./package.json', JSON.stringify(p, null, 2) + '\n'); " echo "📦 Publishing winload-rust-bin@${NPM_VERSION} (tag: ${NPM_TAG})..." - npm publish --provenance --access public --tag "${NPM_TAG}" + publish_with_retry() { + local package_name="winload-rust-bin" + local max_attempts=4 + local delay=5 + local attempt + + for attempt in $(seq 1 "$max_attempts"); do + echo "🔁 Publishing ${package_name}: attempt ${attempt}/${max_attempts}" + if npm publish --provenance --access public --tag "${NPM_TAG}"; then + return 0 + fi + if npm view "${package_name}@${NPM_VERSION}" version --registry https://registry.npmjs.org > /dev/null 2>&1; then + echo "${package_name}@${NPM_VERSION} is available after the failed request; treating it as published." + return 0 + fi + if [ "$attempt" -lt "$max_attempts" ]; then + echo "::warning::Publish failed; retrying in ${delay}s." + sleep "$delay" + delay=$((delay * 2)) + fi + done + + echo "::error::Failed to publish ${package_name} after ${max_attempts} attempts." + return 1 + } + publish_with_retry env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -1478,6 +1629,32 @@ jobs: NPM_TAG="alpha" fi + publish_with_retry() { + local package_name="$1" + local max_attempts=4 + local delay=5 + local attempt + + for attempt in $(seq 1 "$max_attempts"); do + echo "🔁 Publishing ${package_name}: attempt ${attempt}/${max_attempts}" + if npm publish --registry https://npm.pkg.github.com --tag "${NPM_TAG}"; then + return 0 + fi + if npm view "${package_name}@${NPM_VERSION}" version --registry https://npm.pkg.github.com > /dev/null 2>&1; then + echo "${package_name}@${NPM_VERSION} is available after the failed request; treating it as published." + return 0 + fi + if [ "$attempt" -lt "$max_attempts" ]; then + echo "::warning::Publish failed; retrying in ${delay}s." + sleep "$delay" + delay=$((delay * 2)) + fi + done + + echo "::error::Failed to publish ${package_name} after ${max_attempts} attempts." + return 1 + } + for dir in npm-platforms/@vincentzyuapps/*/; do [ -d "$dir" ] || continue echo "📦 Publishing $(basename $dir) to GitHub Packages..." @@ -1487,7 +1664,7 @@ jobs: if npm view "${PKG_NAME}@${NPM_VERSION}" version --registry https://npm.pkg.github.com > /dev/null 2>&1; then echo "${PKG_NAME}@${NPM_VERSION} already exists; skipping" else - npm publish --registry https://npm.pkg.github.com --tag "${NPM_TAG}" 2>&1 || echo "::warning::Failed to publish ${PKG_NAME} to GitHub Packages; continuing." + publish_with_retry "$PKG_NAME" fi cd "$GITHUB_WORKSPACE" done @@ -1506,7 +1683,32 @@ jobs: cd npm/winload-rust-bin echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc echo "📦 Publishing @vincentzyuapps/winload@${NPM_VERSION} to GitHub Packages (tag: ${NPM_TAG})..." - npm publish --registry https://npm.pkg.github.com --tag "${NPM_TAG}" + publish_with_retry() { + local package_name="@vincentzyuapps/winload" + local max_attempts=4 + local delay=5 + local attempt + + for attempt in $(seq 1 "$max_attempts"); do + echo "🔁 Publishing ${package_name}: attempt ${attempt}/${max_attempts}" + if npm publish --registry https://npm.pkg.github.com --tag "${NPM_TAG}"; then + return 0 + fi + if npm view "${package_name}@${NPM_VERSION}" version --registry https://npm.pkg.github.com > /dev/null 2>&1; then + echo "${package_name}@${NPM_VERSION} is available after the failed request; treating it as published." + return 0 + fi + if [ "$attempt" -lt "$max_attempts" ]; then + echo "::warning::Publish failed; retrying in ${delay}s." + sleep "$delay" + delay=$((delay * 2)) + fi + done + + echo "::error::Failed to publish ${package_name} after ${max_attempts} attempts." + return 1 + } + publish_with_retry echo "✅ GitHub Packages published!" @@ -1616,7 +1818,21 @@ jobs: TAG="${{ steps.release_info.outputs.tag }}" mkdir -p assets cd assets - gh release download "$TAG" --pattern "*" + max_attempts=4 + delay=5 + for attempt in $(seq 1 "$max_attempts"); do + echo "🔁 Downloading release assets: attempt ${attempt}/${max_attempts}" + if gh release download "$TAG" --pattern "*" --clobber; then + break + fi + if [ "$attempt" -eq "$max_attempts" ]; then + echo "::error::Failed to download release assets after ${max_attempts} attempts." + exit 1 + fi + echo "::warning::Download failed; retrying in ${delay}s." + sleep "$delay" + delay=$((delay * 2)) + done echo "📥 Downloaded files:" ls -la @@ -1634,20 +1850,21 @@ jobs: # JSON 安全转义 release body(处理换行、引号、反斜杠等) JSON_BODY=$(jq -R -s '.' < release_body.md) - CURL="curl -s -k --retry 3 --retry-delay 5 --retry-all-errors" + CURL=(curl -sS -k --retry 4 --retry-delay 5 --retry-all-errors --retry-connrefused --connect-timeout 30 --max-time 120) + CURL_FAIL=(curl -fsS -k --retry 4 --retry-delay 5 --retry-all-errors --retry-connrefused --connect-timeout 30 --max-time 120) # 1. 确保 tag 存在于 Gitee - if $CURL --fail "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/tags/${TAG}?access_token=${GITEE_TOKEN}" > /dev/null 2>&1; then + if "${CURL_FAIL[@]}" "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/tags/${TAG}?access_token=${GITEE_TOKEN}" > /dev/null 2>&1; then echo "🏷️ Tag $TAG already exists on Gitee" else echo "🏷️ Creating tag $TAG on Gitee..." - $CURL -X POST --header 'Content-Type: application/json;charset=UTF-8' \ + "${CURL_FAIL[@]}" -X POST --header 'Content-Type: application/json;charset=UTF-8' \ "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/tags" \ -d "{\"access_token\":\"${GITEE_TOKEN}\",\"tag_name\":\"${TAG}\",\"refs\":\"main\",\"tag_message\":\"Release ${TAG}\"}" fi # 2. 检查是否已有对应 tag 的 Release - EXISTING=$($CURL "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/releases/tags/${TAG}?access_token=${GITEE_TOKEN}") + EXISTING=$("${CURL[@]}" "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/releases/tags/${TAG}?access_token=${GITEE_TOKEN}") RELEASE_ID=$(echo "$EXISTING" | jq -r '.id') if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ]; then @@ -1672,7 +1889,7 @@ jobs: prerelease: $prerelease }') - CREATE_RESPONSE=$($CURL -X POST --header 'Content-Type: application/json;charset=UTF-8' \ + CREATE_RESPONSE=$("${CURL_FAIL[@]}" -X POST --header 'Content-Type: application/json;charset=UTF-8' \ "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/releases" \ -d "$RELEASE_PAYLOAD") @@ -1724,15 +1941,17 @@ jobs: echo "$(ts) 🔄 Attempt $ATTEMPT/3 — starting curl (max-time=1200s, connect-timeout=30s)" START_TIME=$(date +%s) - HTTP_CODE_FILE=$(mktemp) - RESPONSE=$(curl -s -k --retry 3 --retry-delay 10 --retry-all-errors \ + if RESPONSE=$(curl -fsS -k --retry 3 --retry-delay 10 --retry-all-errors --retry-connrefused \ --max-time 1200 --connect-timeout 30 \ -w "\n%{http_code}" \ -X POST --header "Content-Type: multipart/form-data" \ -F "access_token=${GITEE_TOKEN}" \ -F "file=@${FILE}" \ - "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/releases/${RELEASE_ID}/attach_files") - CURL_EXIT=$? + "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/releases/${RELEASE_ID}/attach_files"); then + CURL_EXIT=0 + else + CURL_EXIT=$? + fi END_TIME=$(date +%s) ELAPSED=$((END_TIME - START_TIME)) @@ -1780,5 +1999,6 @@ jobs: echo "📊 Upload complete: $SUCCESS succeeded, $FAIL failed" if [ "$FAIL" -ne 0 ]; then - echo "::warning::Gitee release asset upload failed for $FAIL file(s); continuing." + echo "::error::Gitee release asset upload failed for $FAIL file(s)." + exit 1 fi From d5518c7f9f3706663210cb5e56dada6a76e110c9 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Thu, 27 Aug 2026 20:37:51 +0800 Subject: [PATCH 15/19] =?UTF-8?q?ci:=20=E5=90=AF=E7=94=A8Gitee=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E7=9A=84TLS=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1454a1..005a355 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1850,8 +1850,8 @@ jobs: # JSON 安全转义 release body(处理换行、引号、反斜杠等) JSON_BODY=$(jq -R -s '.' < release_body.md) - CURL=(curl -sS -k --retry 4 --retry-delay 5 --retry-all-errors --retry-connrefused --connect-timeout 30 --max-time 120) - CURL_FAIL=(curl -fsS -k --retry 4 --retry-delay 5 --retry-all-errors --retry-connrefused --connect-timeout 30 --max-time 120) + CURL=(curl -sS --retry 4 --retry-delay 5 --retry-all-errors --retry-connrefused --connect-timeout 30 --max-time 120) + CURL_FAIL=(curl -fsS --retry 4 --retry-delay 5 --retry-all-errors --retry-connrefused --connect-timeout 30 --max-time 120) # 1. 确保 tag 存在于 Gitee if "${CURL_FAIL[@]}" "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/tags/${TAG}?access_token=${GITEE_TOKEN}" > /dev/null 2>&1; then @@ -1941,7 +1941,7 @@ jobs: echo "$(ts) 🔄 Attempt $ATTEMPT/3 — starting curl (max-time=1200s, connect-timeout=30s)" START_TIME=$(date +%s) - if RESPONSE=$(curl -fsS -k --retry 3 --retry-delay 10 --retry-all-errors --retry-connrefused \ + if RESPONSE=$(curl -fsS --retry 3 --retry-delay 10 --retry-all-errors --retry-connrefused \ --max-time 1200 --connect-timeout 30 \ -w "\n%{http_code}" \ -X POST --header "Content-Type: multipart/form-data" \ From 8f74b5e9b4e09cf4a4e3ed4b2d3a327567581ed5 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Thu, 27 Aug 2026 20:45:16 +0800 Subject: [PATCH 16/19] =?UTF-8?q?fix(release):=20=E4=BF=AE=E6=AD=A3GitHub?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E8=84=9A=E6=9C=AC=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/release_template.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/release_template.md b/.github/release_template.md index 225e291..c5cd796 100644 --- a/.github/release_template.md +++ b/.github/release_template.md @@ -65,11 +65,11 @@ paru -S winload-rust-bin **One-liner install For Linux (Debian/Ubuntu/RHEL/Fedora and derivatives):** ```bash -curl -fsSL https://raw.githubusercontent.com/__REPO__/main/docs/install_scripts/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/__REPO__/main/docs/scripts/install/install.sh | bash # or install this specific version: -WINLOAD_VERSION=__VERSION__ bash -c "$(curl -fsSL https://raw.githubusercontent.com/__REPO__/main/docs/install_scripts/install.sh)" +WINLOAD_VERSION=__VERSION__ bash -c "$(curl -fsSL https://raw.githubusercontent.com/__REPO__/main/docs/scripts/install/install.sh)" ``` -> 📄 [View install script source](https://github.com/__REPO__/blob/main/docs/install_scripts/install.sh) +> 📄 [View install script source](https://github.com/__REPO__/blob/main/docs/scripts/install/install.sh) **🇨🇳 一键安装脚本在码云的镜像 One-liner install for Linux On Gitee mirror (中国大陆地区更快捏,faster in China):** ```bash From e97a35c7b0f4ecc0b85ab81170547fe44a2da22f Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Thu, 27 Aug 2026 20:45:32 +0800 Subject: [PATCH 17/19] =?UTF-8?q?fix(release):=20=E4=BF=AE=E6=AD=A3Gitee?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E8=84=9A=E6=9C=AC=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/release_template.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/release_template.md b/.github/release_template.md index c5cd796..65d0c0b 100644 --- a/.github/release_template.md +++ b/.github/release_template.md @@ -73,11 +73,11 @@ WINLOAD_VERSION=__VERSION__ bash -c "$(curl -fsSL https://raw.githubusercontent. **🇨🇳 一键安装脚本在码云的镜像 One-liner install for Linux On Gitee mirror (中国大陆地区更快捏,faster in China):** ```bash -curl -fsSL https://gitee.com/vincent-zyu/winload/raw/main/docs/install_scripts/install_gitee.sh | bash +curl -fsSL https://gitee.com/vincent-zyu/winload/raw/main/docs/scripts/install/install_gitee.sh | bash # or install this specific version: -WINLOAD_VERSION=__VERSION__ bash -c "$(curl -fsSL https://gitee.com/vincent-zyu/winload/raw/main/docs/install_scripts/install_gitee.sh)" +WINLOAD_VERSION=__VERSION__ bash -c "$(curl -fsSL https://gitee.com/vincent-zyu/winload/raw/main/docs/scripts/install/install_gitee.sh)" ``` -> 📄 [View Gitee install script](https://gitee.com/vincent-zyu/winload/blob/main/docs/install_scripts/install_gitee.sh) +> 📄 [View Gitee install script](https://gitee.com/vincent-zyu/winload/blob/main/docs/scripts/install/install_gitee.sh) > ⚠️ These two `.sh` install scripts only support systems with **apt or dnf** on **x86_64 / aarch64**. For other platforms, use **npm** or **Cargo**. > From 1a5a9c5f535b053b4167aff5ea8919e1e7780c4d Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Thu, 27 Aug 2026 20:46:05 +0800 Subject: [PATCH 18/19] =?UTF-8?q?fix(ci):=20=E9=98=BB=E6=AD=A2Homebrew?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E7=A9=BA=E6=A0=A1=E9=AA=8C=E5=92=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 005a355..2c6fb99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -966,6 +966,11 @@ jobs: echo "📦 Target version: ${VERSION}" # 下载所有平台的二进制用于计算 SHA256 + download_release_asset() { + curl -fSL --retry 4 --retry-delay 5 --retry-all-errors --retry-connrefused \ + --connect-timeout 30 --max-time 300 -o "$1" "$2" + } + PLATFORMS=( "winload-linux-x86_64:${BASE_URL}/winload-linux-x86_64-${VERSION}" "winload-linux-aarch64:${BASE_URL}/winload-linux-aarch64-${VERSION}" @@ -977,7 +982,7 @@ jobs: NAME="${entry%%:*}" URL="${entry##*:}" echo "📥 Downloading $NAME..." - curl -fSL -o "$NAME" "$URL" || echo "⚠️ Failed to download $NAME (may not exist)" + download_release_asset "$NAME" "$URL" done echo "✅ Downloaded:" @@ -991,11 +996,18 @@ jobs: BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" mkdir -p Formula + for asset in winload-linux-x86_64 winload-linux-aarch64 winload-macos-x86_64 winload-macos-aarch64; do + if [ ! -s "$asset" ]; then + echo "::error::Missing or empty release asset: $asset" + exit 1 + fi + done + # 计算各平台 SHA256 - SHA_LINUX_X64=$(sha256sum winload-linux-x86_64 2>/dev/null | awk '{print $1}' || echo "") - SHA_LINUX_ARM64=$(sha256sum winload-linux-aarch64 2>/dev/null | awk '{print $1}' || echo "") - SHA_MACOS_X64=$(sha256sum winload-macos-x86_64 2>/dev/null | awk '{print $1}' || echo "") - SHA_MACOS_ARM64=$(sha256sum winload-macos-aarch64 2>/dev/null | awk '{print $1}' || echo "") + SHA_LINUX_X64=$(sha256sum winload-linux-x86_64 | awk '{print $1}') + SHA_LINUX_ARM64=$(sha256sum winload-linux-aarch64 | awk '{print $1}') + SHA_MACOS_X64=$(sha256sum winload-macos-x86_64 | awk '{print $1}') + SHA_MACOS_ARM64=$(sha256sum winload-macos-aarch64 | awk '{print $1}') cat > Formula/winload.rb << HEREDOC class Winload < Formula From 274b7d7cb06d2f06cdc7d4a317c88641ddc3c583 Mon Sep 17 00:00:00 2001 From: ra1nyxin Date: Thu, 27 Aug 2026 21:01:25 +0800 Subject: [PATCH 19/19] =?UTF-8?q?ci:=20=E4=BF=AE=E5=A4=8DGitee=20Formula?= =?UTF-8?q?=E7=9A=84=E5=8F=91=E5=B8=83=E9=99=84=E4=BB=B6=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c6fb99..1b54021 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1109,11 +1109,18 @@ jobs: GITEE_BASE_URL="https://gitee.com/vincent-zyu/winload/releases/download/${VERSION}" mkdir -p Formula + for asset in winload-linux-x86_64 winload-linux-aarch64 winload-macos-x86_64 winload-macos-aarch64; do + if [ ! -s "$asset" ]; then + echo "::error::Missing or empty release asset: $asset" + exit 1 + fi + done + # 计算各平台 SHA256 - SHA_LINUX_X64=$(sha256sum winload-linux-x86_64 2>/dev/null | awk '{print $1}' || echo "") - SHA_LINUX_ARM64=$(sha256sum winload-linux-aarch64 2>/dev/null | awk '{print $1}' || echo "") - SHA_MACOS_X64=$(sha256sum winload-macos-x86_64 2>/dev/null | awk '{print $1}' || echo "") - SHA_MACOS_ARM64=$(sha256sum winload-macos-aarch64 2>/dev/null | awk '{print $1}' || echo "") + SHA_LINUX_X64=$(sha256sum winload-linux-x86_64 | awk '{print $1}') + SHA_LINUX_ARM64=$(sha256sum winload-linux-aarch64 | awk '{print $1}') + SHA_MACOS_X64=$(sha256sum winload-macos-x86_64 | awk '{print $1}') + SHA_MACOS_ARM64=$(sha256sum winload-macos-aarch64 | awk '{print $1}') cat > Formula/winload.rb << HEREDOC class Winload < Formula