Skip to content

Commit a0a8fa9

Browse files
authored
Merge pull request #368 from nxtrace/main
Update dependencies and add MTR probe history feature
2 parents 21fdd74 + 60a9199 commit a0a8fa9

18 files changed

Lines changed: 1268 additions & 149 deletions

.github/workflows/build.yml

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -348,29 +348,3 @@ jobs:
348348
files: dist_release/*
349349
token: ${{ secrets.GT_TOKEN }}
350350
fail_on_unmatched_files: true
351-
352-
publish-new-formula:
353-
runs-on: ubuntu-latest
354-
needs: [ release ]
355-
if: startsWith(github.ref, 'refs/tags/v')
356-
steps:
357-
- name: config git
358-
run: |
359-
git config --global user.email "${{ secrets.GIT_MAIL }}"
360-
git config --global user.name "${{ secrets.GIT_NAME }}"
361-
- name: Clone repo
362-
run: git clone https://github.com/nxtrace/homebrew-nexttrace.git
363-
- name: Exec script
364-
run: |
365-
set -Eeuo pipefail
366-
cd homebrew-nexttrace
367-
bash genFormula.sh
368-
- name: Git Push
369-
run: |
370-
set -Eeuo pipefail
371-
cd homebrew-nexttrace
372-
git add -A
373-
git commit -m 'Publish a new version with Formula' || true
374-
git remote set-url origin https://${{ secrets.GT_TOKEN }}@github.com/nxtrace/homebrew-nexttrace.git
375-
git push
376-
- run: echo "🍏 This job's status is ${{ job.status }}."
Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,51 @@
11
name: Publish New Formula
22

3+
permissions:
4+
contents: read
5+
6+
defaults:
7+
run:
8+
shell: bash
9+
310
concurrency:
411
group: ${{ github.workflow }}-${{ github.ref }}
512
cancel-in-progress: true
613

7-
# Controls when the action will run. Workflow runs when manually triggered using the UI
8-
# or API.
914
on:
1015
workflow_dispatch:
16+
schedule:
17+
# Sunday 20:00 UTC = Monday 04:00 Asia/Shanghai
18+
- cron: "0 20 * * 0"
1119

12-
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
1320
jobs:
14-
# This workflow contains a single job called "greet"
1521
publish-new-formula:
16-
# The type of runner that the job will run on
1722
runs-on: ubuntu-latest
18-
19-
# Steps represent a sequence of tasks that will be executed as part of the job
2023
steps:
21-
# Runs a single command using the runners shell
22-
- name: config git
24+
- name: Configure git
2325
run: |
2426
git config --global user.email "${{ secrets.GIT_MAIL }}"
2527
git config --global user.name "${{ secrets.GIT_NAME }}"
26-
- name: Clone repo
27-
run: |
28-
git clone https://github.com/nxtrace/homebrew-nexttrace.git
29-
- name: Exec script
28+
29+
- name: Clone Homebrew tap
30+
run: git clone https://github.com/nxtrace/homebrew-nexttrace.git
31+
32+
- name: Generate formulae from NTrace-core latest release
3033
run: |
3134
set -Eeuo pipefail
3235
cd homebrew-nexttrace
33-
bash genFormula.sh
34-
# - name: setup SSH keys and known_hosts
35-
# run: |
36-
# mkdir -p ~/.ssh
37-
# ssh-keyscan github.com >> ~/.ssh/known_hosts
38-
# ssh-agent -a $SSH_AUTH_SOCK > /dev/null
39-
# ssh-add - <<< "${{ secrets.ID_RSA }}"
40-
# env:
41-
# SSH_AUTH_SOCK: /tmp/ssh_agent.sock
42-
- name: Git Push
36+
RELEASE_REPO=nxtrace/NTrace-core bash genFormula.sh
37+
38+
- name: Commit and push if changed
4339
run: |
4440
set -Eeuo pipefail
4541
cd homebrew-nexttrace
46-
git add -A
47-
git commit -m 'Publish a new version with Formula' || true
42+
git add Formula README.md genFormula.sh
43+
if git diff --cached --quiet; then
44+
echo "No Homebrew formula update needed."
45+
exit 0
46+
fi
47+
git commit -m 'Publish a new version with Formula'
4848
git remote set-url origin https://${{ secrets.GT_TOKEN }}@github.com/nxtrace/homebrew-nexttrace.git
4949
git push
50-
# env:
51-
# SSH_AUTH_SOCK: /tmp/ssh_agent.sock
52-
- run: echo "🍏 This job's status is ${{ job.status }}."
50+
51+
- run: echo "This job's status is ${{ job.status }}."

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Please note, there are exceptions to this synchronization. If a version of NTrac
109109

110110
- Linuxbrew's installation command
111111
112-
Same as the macOS Homebrew's installation method (homebrew-core version only supports amd64)
112+
Same as the macOS Homebrew installation method. The homebrew-core formula provides the Full flavor (`nexttrace`); the `nxtrace/nexttrace` tap provides all three flavors.
113113
114114
- deepin installation command
115115
```shell
@@ -137,9 +137,12 @@ Please note, there are exceptions to this synchronization. If a version of NTrac
137137
```shell
138138
brew install nexttrace
139139
```
140-
- This repository's ACTIONS automatically built version (updates faster)
140+
- `nxtrace/nexttrace` tap version (periodically synced from the latest NTrace-core release)
141141
```shell
142-
brew tap nxtrace/nexttrace && brew install nxtrace/nexttrace/nexttrace
142+
brew tap nxtrace/nexttrace
143+
brew install nxtrace/nexttrace/nexttrace
144+
brew install nxtrace/nexttrace/nexttrace-tiny
145+
brew install nxtrace/nexttrace/ntr
143146
```
144147
- The homebrew-core build is maintained by chenrui333, please note that this version's updates may lag behind the repository Action automatically version
145148
@@ -191,7 +194,7 @@ Starting from this release, NextTrace is published in **three flavors** under th
191194
| Default mode | traceroute | traceroute | MTR TUI |
192195
| Binary name | `nexttrace` | `nexttrace-tiny` | `ntr` |
193196
194-
> **Note:** `APT (nexttrace-debs)` provides all three flavors: **Full** (`nexttrace`), **Tiny** (`nexttrace-tiny`), and **NTR** (`ntr`). Other package managers (Homebrew, AUR, Scoop, etc.) currently install the **Full** (`nexttrace`) version only.
197+
> **Note:** `APT (nexttrace-debs)` and the `Homebrew tap (nxtrace/nexttrace)` provide all three flavors: **Full** (`nexttrace`), **Tiny** (`nexttrace-tiny`), and **NTR** (`ntr`). `homebrew-core`, AUR, Scoop, and other package managers currently install the **Full** (`nexttrace`) version only.
195198
196199
### Feature Matrix
197200
@@ -553,11 +556,17 @@ When running in a terminal (TTY), MTR mode uses an **interactive full-screen TUI
553556
- default: PTR (or IP fallback) ↔ IP only
554557
- with `--show-ips`: PTR (IP) ↔ IP only
555558
- **`e`** — toggle MPLS label display on/off
559+
- **`d` / `D`** — toggle the optional history display; the default TUI remains the classic metric table
560+
- **`g` / `G`** — in history display only, cycle History chart mode: heatmap → bars → sparkline
556561
- The TUI header displays **source → destination**, with `--source`/`--dev` information when specified.
557562
- When using LeoMoeAPI, the preferred API IP address is shown in the header.
558563
- Uses the **alternate screen buffer**, so your previous terminal history is preserved on exit.
559564
- When stdin is not a TTY (e.g. piped), it falls back to a simple table refresh.
560565

566+
History display keeps a rolling 3-minute, timestamp-based probe history while the classic table is shown, then renders `Host`, `Last`, `Avg`, `Loss`, and `History` when toggled with `d`. The History column uses a fixed 100ms latency scale. Unicode blocks/sparklines are used by default; with `--no-color`, plain ASCII is used and timeouts are shown as `x`.
567+
568+
Acknowledgement: the optional MTR history display is inspired by [TraceBar](https://github.com/tracebar-app/tracebar), a macOS continuous traceroute monitor licensed under the [MIT License](https://github.com/tracebar-app/tracebar/blob/main/LICENSE).
569+
561570
The **report mode** (`-r`/`--report`) produces a one-shot summary after all probes complete, suitable for scripting:
562571

563572
```text

README_zh_CN.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ Document Language: [English](README.md) | 简体中文
107107

108108
- Linuxbrew 安装命令
109109

110-
同macOS Homebrew安装方法(homebrew-core版仅支持amd64)
110+
同 macOS Homebrew 安装方法;homebrew-core formula 提供 Full flavor(`nexttrace`),`nxtrace/nexttrace` tap 提供三种 flavor。
111111

112112
- deepin 安装命令
113113

@@ -137,9 +137,12 @@ Document Language: [English](README.md) | 简体中文
137137
```shell
138138
brew install nexttrace
139139
```
140-
- 本仓库ACTIONS自动构建版(更新更快)
140+
- `nxtrace/nexttrace` tap 版(按 NTrace-core 最新 release 定期同步)
141141
```shell
142-
brew tap nxtrace/nexttrace && brew install nxtrace/nexttrace/nexttrace
142+
brew tap nxtrace/nexttrace
143+
brew install nxtrace/nexttrace/nexttrace
144+
brew install nxtrace/nexttrace/nexttrace-tiny
145+
brew install nxtrace/nexttrace/ntr
143146
```
144147
- homebrew-core 构建由 chenrui333 维护,请注意该版本更新可能会落后仓库Action自动构建版本
145148

@@ -192,7 +195,7 @@ Document Language: [English](README.md) | 简体中文
192195
| 默认运行模式 | traceroute | traceroute | MTR TUI |
193196
| 二进制名 | `nexttrace` | `nexttrace-tiny` | `ntr` |
194197

195-
> **注意:** `APT (nexttrace-debs)` 目前提供 **Full**`nexttrace`)、**Tiny**`nexttrace-tiny`)和 **NTR**`ntr`)三种包;其它包管理器(Homebrew、AUR、Scoop 等)目前仍仅提供 **完整版**`nexttrace`)。
198+
> **注意:** `APT (nexttrace-debs)` `Homebrew tap (nxtrace/nexttrace)` 目前提供 **Full**`nexttrace`)、**Tiny**`nexttrace-tiny`)和 **NTR**`ntr`)三种包;`homebrew-core`、AUR、Scoop 等其它包管理器目前仍仅提供 **完整版**`nexttrace`)。
196199

197200
### 功能对比
198201

@@ -545,11 +548,17 @@ nexttrace -t --tcp --max-hops 20 --first 3 --no-rdns 8.8.8.8
545548
- 默认:PTR(无 PTR 时回退 IP)↔ 仅 IP
546549
- 启用 `--show-ips`:PTR (IP) ↔ 仅 IP
547550
- **`e`** — 切换 MPLS 标签显示开/关
551+
- **`d` / `D`** — 切换可选历史显示;默认 TUI 仍是经典指标表
552+
- **`g` / `G`** — 仅在历史显示中循环切换 History 图表:heatmap → bars → sparkline
548553
- TUI 标题栏显示**源 → 目标**路由信息,指定 `--source`/`--dev` 时会展示对应信息。
549554
- 使用 LeoMoeAPI 时,标题栏会显示首选 API IP 地址。
550555
- 使用**备用屏幕缓冲区**,退出后恢复之前的终端历史记录。
551556
- 当 stdin 非 TTY(如管道输入)时,降级为简单表格刷新模式。
552557

558+
历史显示会在经典表格显示期间同步收集最近 3 分钟、按探测时间戳归窗的历史样本,按 `d` 后显示 `Host``Last``Avg``Loss``History`。History 列使用固定 100ms 延迟刻度。默认使用 Unicode block/sparkline;启用 `--no-color` 时使用 ASCII,超时样本显示为 `x`
559+
560+
致谢:可选 MTR 历史显示参考了 [TraceBar](https://github.com/tracebar-app/tracebar) 的连续 traceroute 历史可视化体验;TraceBar 使用 [MIT License](https://github.com/tracebar-app/tracebar/blob/main/LICENSE)。
561+
553562
**报告模式**`-r`/`--report`)在所有探测完成后一次性输出统计,适合脚本使用:
554563

555564
```text

cmd/mtr_mode.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,16 @@ func runMTRTUI(method trace.Method, conf trace.Config, hopIntervalMs int, maxPer
8686
roundConf := normalizeMTRTraceConfig(conf)
8787

8888
opts := buildMTRInteractiveOptions(ui, hopIntervalMs, maxPerHop)
89+
history := attachMTRHistoryIfTTY(ui, &opts)
8990

9091
// TTY 模式下使用 TUI 渲染器 + 暂停支持,非 TTY 使用简单表格
9192
var onSnapshot trace.MTROnSnapshot
9293
if ui.IsTTY() {
9394
opts.IsPaused = ui.IsPaused
9495
onSnapshot = printer.MTRTUIPrinter(target, domain, target, config.Version, startTime,
95-
srcHost, srcIP, lang, func() string { return buildAPIInfo(dataOrigin) }, showIPs, ui.IsPaused, ui.CurrentDisplayMode, ui.CurrentNameMode, ui.IsMPLSDisabled)
96+
srcHost, srcIP, lang, func() string { return buildAPIInfo(dataOrigin) }, showIPs, ui.IsPaused,
97+
ui.CurrentDisplayMode, ui.CurrentNameMode, ui.IsMPLSDisabled,
98+
ui.IsHistoryMode, ui.CurrentHistoryChartMode, history.Snapshot)
9699
} else {
97100
onSnapshot = func(iteration int, stats []trace.MTRHopStat) {
98101
printer.MTRTablePrinter(stats, iteration, ui.CurrentDisplayMode(), ui.CurrentNameMode(), lang, showIPs)
@@ -119,6 +122,25 @@ func buildMTRInteractiveOptions(ui *mtrUI, hopIntervalMs int, maxPerHop int) tra
119122
return opts
120123
}
121124

125+
func attachMTRHistoryIfTTY(ui *mtrUI, opts *trace.MTROptions) *printer.MTRHistoryStore {
126+
if ui == nil || opts == nil || !ui.IsTTY() {
127+
return nil
128+
}
129+
history := printer.NewMTRHistoryStore(printer.MTRHistoryWindow)
130+
opts.OnProbe = history.AddProbeEvent
131+
if opts.IsResetRequested != nil {
132+
resetRequested := opts.IsResetRequested
133+
opts.IsResetRequested = func() bool {
134+
if !resetRequested() {
135+
return false
136+
}
137+
history.Reset()
138+
return true
139+
}
140+
}
141+
return history
142+
}
143+
122144
// runMTRReport 执行 MTR 非全屏报告模式(对齐 mtr -rzw 风格)。
123145
// 探测完 maxPerHop 后一次性输出最终统计到 stdout,不进入 alternate screen。
124146
func runMTRReport(method trace.Method, conf trace.Config, hopIntervalMs int, maxPerHop int, domain string, dataOrigin string, wide bool, showIPs bool) {

cmd/mtr_mode_test.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,41 @@ func TestBuildMTRInteractiveOptions_AsyncMetadataFollowsTTY(t *testing.T) {
354354
}
355355
}
356356

357+
func TestAttachMTRHistoryIfTTY(t *testing.T) {
358+
nonTTYUI := &mtrUI{isTTY: false}
359+
nonTTYOpts := buildMTRInteractiveOptions(nonTTYUI, 1000, 0)
360+
if history := attachMTRHistoryIfTTY(nonTTYUI, &nonTTYOpts); history != nil {
361+
t.Fatal("non-TTY MTR should not allocate history store")
362+
}
363+
if nonTTYOpts.OnProbe != nil {
364+
t.Fatal("non-TTY MTR should not attach OnProbe history collection")
365+
}
366+
367+
ttyUI := &mtrUI{isTTY: true}
368+
ttyOpts := buildMTRInteractiveOptions(ttyUI, 1000, 0)
369+
history := attachMTRHistoryIfTTY(ttyUI, &ttyOpts)
370+
if history == nil {
371+
t.Fatal("TTY MTR should allocate history store")
372+
}
373+
if ttyOpts.OnProbe == nil {
374+
t.Fatal("TTY MTR should attach OnProbe history collection")
375+
}
376+
377+
now := time.Now()
378+
ttyOpts.OnProbe(trace.MTRProbeEvent{TTL: 1, Success: true, RTT: 10 * time.Millisecond, Timestamp: now})
379+
if snap := history.Snapshot(now); len(snap) != 1 || len(snap[0].Samples) != 1 {
380+
t.Fatalf("history did not collect TTY probe event: %+v", snap)
381+
}
382+
383+
atomic.StoreInt32(&ttyUI.restartReq, 1)
384+
if ttyOpts.IsResetRequested == nil || !ttyOpts.IsResetRequested() {
385+
t.Fatal("wrapped reset callback should consume reset request")
386+
}
387+
if snap := history.Snapshot(now); len(snap) != 0 {
388+
t.Fatalf("history should be cleared on reset, got %+v", snap)
389+
}
390+
}
391+
357392
func TestDefaultConstants_NormalVsMTR(t *testing.T) {
358393
if defaultPacketIntervalMs != 50 {
359394
t.Fatalf("defaultPacketIntervalMs = %d, want 50", defaultPacketIntervalMs)
@@ -725,6 +760,19 @@ func TestParseMTRKey_NameToggle(t *testing.T) {
725760
}
726761
}
727762

763+
func TestParseMTRKey_HistoryControls(t *testing.T) {
764+
for _, b := range []byte{'d', 'D'} {
765+
if got := ParseMTRKey(b); got != "history_toggle" {
766+
t.Errorf("ParseMTRKey(%q) = %q, want %q", b, got, "history_toggle")
767+
}
768+
}
769+
for _, b := range []byte{'g', 'G'} {
770+
if got := ParseMTRKey(b); got != "history_chart" {
771+
t.Errorf("ParseMTRKey(%q) = %q, want %q", b, got, "history_chart")
772+
}
773+
}
774+
}
775+
728776
func TestMTRUI_NameModeToggle(t *testing.T) {
729777
_, cancel := context.WithCancel(context.Background())
730778
defer cancel()
@@ -769,6 +817,37 @@ func TestMTRUI_NameModeNotResetByRestart(t *testing.T) {
769817
}
770818
}
771819

820+
func TestMTRUI_HistoryModeAndChartCycle(t *testing.T) {
821+
_, cancel := context.WithCancel(context.Background())
822+
defer cancel()
823+
ui := newMTRUI(cancel, 0)
824+
825+
if ui.IsHistoryMode() {
826+
t.Fatal("history mode should default to classic/off")
827+
}
828+
ui.CycleHistoryChartMode()
829+
if got := ui.CurrentHistoryChartMode(); got != 0 {
830+
t.Fatalf("chart mode should not change outside history mode, got %d", got)
831+
}
832+
833+
ui.ToggleHistoryMode()
834+
if !ui.IsHistoryMode() {
835+
t.Fatal("history mode should be enabled after toggle")
836+
}
837+
ui.CycleHistoryChartMode()
838+
if got := ui.CurrentHistoryChartMode(); got != 1 {
839+
t.Fatalf("chart mode after first cycle = %d, want 1", got)
840+
}
841+
ui.CycleHistoryChartMode()
842+
if got := ui.CurrentHistoryChartMode(); got != 2 {
843+
t.Fatalf("chart mode after second cycle = %d, want 2", got)
844+
}
845+
ui.CycleHistoryChartMode()
846+
if got := ui.CurrentHistoryChartMode(); got != 0 {
847+
t.Fatalf("chart mode after wrap = %d, want 0", got)
848+
}
849+
}
850+
772851
// ---------------------------------------------------------------------------
773852
// mtrInputParser 测试
774853
// ---------------------------------------------------------------------------
@@ -851,13 +930,16 @@ func TestMTRInputParser_RecognizesNormalKeysAfterEscapeNoise(t *testing.T) {
851930
}
852931

853932
// 现在喂入正常快捷键序列
854-
keys := []byte{'p', ' ', 'r', 'y', 'n', 'q'}
933+
keys := []byte{'p', ' ', 'r', 'y', 'n', 'e', 'd', 'g', 'q'}
855934
expected := []mtrInputAction{
856935
mtrActionPause,
857936
mtrActionResume,
858937
mtrActionRestart,
859938
mtrActionDisplayMode,
860939
mtrActionNameToggle,
940+
mtrActionMPLSToggle,
941+
mtrActionHistoryToggle,
942+
mtrActionHistoryChart,
861943
mtrActionQuit,
862944
}
863945
actions := feedAll(&p, keys)

0 commit comments

Comments
 (0)