Skip to content

Commit a864fb6

Browse files
Fix reload race and tiering bugs, time-window ring, --listen, --help, tests
store: - in-memory ring is a 24h time window instead of a 1440-round cap: pace=fast held only 6h and interval=5 only 2h, so last_24h stats and the detector's 4h baseline silently shrank. Trimming is a reslice; out-of-order rounds from a reload overlap are inserted in order. - Downsample no longer judges a whole day by its first round: a lossy first round (<=4 replies) left the day un-downsampled forever. - Tier remembers files it already downsampled instead of re-reading every cold day of every target each night. - ReadRange walks calendar days from local midnight; stepping from `from` skipped the last day's file when to's time of day was earlier. reload: cfg.Targets is written by the reload loop and read by /api/targets; access now goes through SetTargets/TargetList (data race, seen with -race). web: at most 5 concurrent data requests (others wait up to 30s, then 503); sessions expire 7200s after login; logout revokes the token; the page goes back to /login on 401. cli: - --listen host:port (default 0.0.0.0:8517), configurable again since the config file went away in v2.1; --localhost keeps the --listen port. - --help with copy-ready examples (quick start in /home/pingping, login, --listen, nohup, targets via vim/echo, ICMP permission); also './pingping help'. A flag placed after user=/passwd= says to put it first. docs: README gets mkdir + nohup in the install block and a Parameters section at the bottom; docs/advance.readme.md (systemd, /opt, reverse proxy) removed; targets/*.example no longer say "restart to apply". tests: store, web, probe, reload/config, detector, help (every flag documented, every example accepted by the real parsers); coverage 16% -> 75%. CI runs gofmt, vet and go test -race.
1 parent 3b2d9a0 commit a864fb6

16 files changed

Lines changed: 1353 additions & 94 deletions

.github/workflows/test.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: test
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
6+
jobs:
7+
test:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: actions/setup-go@v5
12+
with: { go-version: '1.22' }
13+
- name: gofmt
14+
run: test -z "$(gofmt -l .)" || (gofmt -l . && exit 1)
15+
- run: go vet ./...
16+
- run: go test -race -count=1 ./...

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ Just scp and run
1212

1313
```bash
1414
#install
15-
cd /home/pingping/
15+
mkdir -p /home/pingping && cd /home/pingping
1616
wget https://github.com/githubflyideas/pingping/releases/download/v2.11.2/pingping-v2.11.2-linux-amd64.tar.gz
1717
tar -zxvf pingping-v2.11.2-linux-amd64.tar.gz
1818

1919
#run
2020
./pingping user=admin passwd=admin
21+
22+
#run in background
23+
nohup ./pingping user=admin passwd=admin > pingping.log 2>&1 &
2124
```
2225
Open http://localhost:8517 and watch your first puff of network smoke.
2326

@@ -50,3 +53,34 @@ Latest [Releases](https://github.com/githubflyideas/pingping/releases)
5053

5154

5255
apache 2.0
56+
57+
58+
## Parameters
59+
60+
`./pingping --help` prints all of this with copy-ready examples.
61+
62+
```
63+
./pingping [flags] [user=NAME[,NAME2...] passwd=PASS[,PASS2...]]
64+
```
65+
66+
| Parameter | Default | Meaning |
67+
|---|---|---|
68+
| `--listen host:port` | `0.0.0.0:8517` | Web UI address. `0.0.0.0` = all interfaces; an IP binds one interface only |
69+
| `--localhost` | off | Bind `127.0.0.1` only (this machine only), keeping the `--listen` port |
70+
| `--version` | | Print version and exit |
71+
| `--help` | | Help with examples (also `./pingping help`) |
72+
| `user=a,b passwd=x,y` | no login | Turn the login page on. Users and passwords pair by position and the counts must match. A login lasts 2 hours; a restart logs everyone out |
73+
74+
Flags come first, `user=` / `passwd=` last:
75+
76+
```
77+
./pingping # 0.0.0.0:8517, no login
78+
./pingping --listen 0.0.0.0:9000 user=admin passwd=admin
79+
./pingping --localhost # 127.0.0.1:8517
80+
nohup ./pingping user=admin passwd=admin > pingping.log 2>&1 & # background; stop with: pkill -x pingping
81+
```
82+
83+
Everything else is fixed: `targets/` and `data/` sit in the directory you start it in (`/home/pingping`), the default pace
84+
probes every 60 s with 20 packets, full samples are kept 30 days and data is deleted after 300 days.
85+
Targets are not parameters — edit `targets/ping.list` / `targets/tcp.list`; changes apply within 3 seconds.
86+

config.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package main
22

33
import (
4-
"fmt"
4+
"fmt"
55
"net"
66
"os"
77
"path/filepath"
88
"regexp"
99
"strconv"
1010
"strings"
11+
"sync"
1112
)
1213

1314
// pingping 2.0: a probe, text files, and a smoke graph. Nothing else.
@@ -21,6 +22,23 @@ type Config struct {
2122
Probe ProbeCfg
2223
HotDays int // full samples kept this long
2324
RetentionDays int // downsampled data kept this long
25+
26+
// Targets is written by the reload loop while web handlers read it. Once the
27+
// program is running, go through SetTargets / TargetList only.
28+
tmu sync.RWMutex
29+
}
30+
31+
func (c *Config) SetTargets(ts []TargetCfg) {
32+
c.tmu.Lock()
33+
c.Targets = ts
34+
c.tmu.Unlock()
35+
}
36+
37+
// TargetList returns a copy that is safe to use after a concurrent reload.
38+
func (c *Config) TargetList() []TargetCfg {
39+
c.tmu.RLock()
40+
defer c.tmu.RUnlock()
41+
return append([]TargetCfg(nil), c.Targets...)
2442
}
2543

2644
func defaultConfig() *Config {
@@ -99,7 +117,6 @@ func validateTargets(cfg *Config) error {
99117
return nil
100118
}
101119

102-
103120
// loadTargetLists reads targets/ping.list and tcp.list.
104121
// line format: host[:port] [name...] [pace=fast|slow] [interval=sec]
105122
func loadTargetLists(dir string) ([]TargetCfg, error) {
@@ -169,4 +186,3 @@ func parseListLine(line, typ string) (TargetCfg, error) {
169186
}
170187
return t, nil
171188
}
172-

docs/advance.readme.md

Lines changed: 0 additions & 44 deletions
This file was deleted.

help_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"flag"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func helpOutput() string {
11+
var b bytes.Buffer
12+
usage(&b)
13+
return b.String()
14+
}
15+
16+
// Every flag the program defines must be documented in --help.
17+
func TestHelpDocumentsEveryFlag(t *testing.T) {
18+
out := helpOutput()
19+
flag.VisitAll(func(f *flag.Flag) {
20+
if !strings.HasPrefix(f.Name, "test.") && !strings.Contains(out, "--"+f.Name) {
21+
t.Errorf("--%s missing from --help", f.Name)
22+
}
23+
})
24+
if strings.Contains(out, "%!") {
25+
t.Fatal("broken format verb in help text")
26+
}
27+
}
28+
29+
// Every ./pingping example in --help must be accepted by the real parsers, so
30+
// the help cannot drift from the code.
31+
func TestHelpExamplesParse(t *testing.T) {
32+
n := 0
33+
for _, line := range strings.Split(helpOutput(), "\n") {
34+
line = strings.TrimPrefix(strings.TrimSpace(line), "nohup ")
35+
if !strings.HasPrefix(line, "./pingping") || strings.HasPrefix(line, "./pingping [") {
36+
continue
37+
}
38+
n++
39+
fs := flag.NewFlagSet("example", flag.ContinueOnError)
40+
listen := fs.String("listen", defaultConfig().Listen, "")
41+
local := fs.Bool("localhost", false, "")
42+
fs.Bool("version", false, "")
43+
args := strings.Fields(strings.SplitN(strings.SplitN(line, "#", 2)[0], ">", 2)[0])[1:]
44+
if err := fs.Parse(args); err != nil {
45+
t.Errorf("%q: %v", line, err)
46+
continue
47+
}
48+
if _, err := parseAuthArgs(fs.Args()); err != nil {
49+
t.Errorf("%q: %v", line, err)
50+
}
51+
if _, err := listenAddr(*listen, *local); err != nil {
52+
t.Errorf("%q: %v", line, err)
53+
}
54+
}
55+
if n < 5 {
56+
t.Fatalf("only %d examples found", n)
57+
}
58+
}
59+
60+
// Every echo line in --help must be a valid target line for its list.
61+
func TestHelpTargetLinesParse(t *testing.T) {
62+
n := 0
63+
for _, line := range strings.Split(helpOutput(), "\n") {
64+
line = strings.TrimSpace(line)
65+
if !strings.HasPrefix(line, "echo \"") {
66+
continue
67+
}
68+
n++
69+
body := line[len(`echo "`) : strings.Index(line[len(`echo "`):], `"`)+len(`echo "`)]
70+
typ := "icmp"
71+
if strings.HasSuffix(line, "tcp.list") {
72+
typ = "tcp"
73+
}
74+
if _, err := parseListLine(body, typ); err != nil {
75+
t.Errorf("%q: %v", line, err)
76+
}
77+
}
78+
if n < 4 {
79+
t.Fatalf("only %d target examples found", n)
80+
}
81+
}
82+
83+
func TestWantsHelp(t *testing.T) {
84+
for _, a := range [][]string{{"--help"}, {"-h"}, {"help"}, {"--listen", "0.0.0.0:1", "-help"}} {
85+
if !wantsHelp(a) {
86+
t.Errorf("%v: want help", a)
87+
}
88+
}
89+
if wantsHelp([]string{"user=help", "passwd=x"}) {
90+
t.Error("a user named help is not a help request")
91+
}
92+
}
93+
94+
func TestFlagAfterAuthArgsIsExplained(t *testing.T) {
95+
_, err := parseAuthArgs([]string{"user=a", "passwd=b", "--listen", "0.0.0.0:9000"})
96+
if err == nil || !strings.Contains(err.Error(), "put flags first") {
97+
t.Fatalf("want a put-flags-first hint, got %v", err)
98+
}
99+
}

0 commit comments

Comments
 (0)