Skip to content

Commit c155fb7

Browse files
feat(probe): add Probe Time — long-running single-proxy monitoring
Repeatedly probes one proxy on an interval (pageload/download/upload), logs download speed per tick, and distinguishes a real block (N consecutive proxy failures with a healthy no-proxy baseline) from a local network outage. Adds core-restart on dead SOCKS inbound, jittered tick interval, session crash-resume/interrupted status, and sample retention pruning. Exposed via both the web UI (new Probe Time panel + canvas speed chart) and a new `probe` CLI subcommand. Only one probe session may run at a time, since concurrent probes would compete for bandwidth and invalidate the measurements. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 3fe06bd commit c155fb7

8 files changed

Lines changed: 1306 additions & 7 deletions

File tree

cmd/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ func rootCmd() *cobra.Command {
149149
proxyCmd(),
150150
proxiesCmd(),
151151
subCmd(),
152+
probeCmdCLI(),
152153
reportCmd(),
153154
)
154155
return root

cmd/proxy_cmd.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import (
55
"encoding/json"
66
"fmt"
77
"os"
8+
"os/signal"
89
"strings"
10+
"syscall"
911
"time"
1012

1113
"github.com/spf13/cobra"
1214

15+
"github.com/hiddify/hiddify_config_health/internal/probe"
1316
"github.com/hiddify/hiddify_config_health/internal/score"
1417
"github.com/hiddify/hiddify_config_health/internal/store"
1518
"github.com/hiddify/hiddify_config_health/internal/telemetry"
@@ -136,6 +139,142 @@ func subCmd() *cobra.Command {
136139
return c
137140
}
138141

142+
// probeCmdCLI runs a long-lived probe session against a single proxy from the
143+
// terminal: starts one core, ticks health checks on an interval, and prints
144+
// one line per sample until interrupted (Ctrl-C) or the proxy is detected
145+
// blocked. This is the CLI counterpart to the web UI's "Probe Time" panel —
146+
// same internal/probe.Runner, same single-session semantics.
147+
func probeCmdCLI() *cobra.Command {
148+
var (
149+
probeCore string
150+
probeInterval int
151+
probeActions []string
152+
probeBlockAfterN int
153+
probeDownloadURL string
154+
probeUploadURL string
155+
probePageloadURL string
156+
)
157+
c := &cobra.Command{
158+
Use: "probe <proxy-uri>",
159+
Short: "Repeatedly probe ONE proxy over time to measure speed and detect blocking",
160+
Args: cobra.ExactArgs(1),
161+
RunE: func(cmd *cobra.Command, args []string) error {
162+
cfg := probe.Config{
163+
ExampleDir: "cli-probe",
164+
ProxyURI: args[0],
165+
Core: probeCore,
166+
Interval: time.Duration(probeInterval) * time.Second,
167+
Actions: probeActions,
168+
BlockAfterFailures: probeBlockAfterN,
169+
DownloadURL: probeDownloadURL,
170+
UploadURL: probeUploadURL,
171+
PageloadURL: probePageloadURL,
172+
Seed: time.Now().UnixNano(),
173+
}
174+
r, err := probe.New(cfg)
175+
if err != nil {
176+
return err
177+
}
178+
179+
db, _ := store.Open(flagDBPath)
180+
if db != nil {
181+
defer db.Close()
182+
}
183+
var sessionID int64
184+
if db != nil {
185+
sessionID, _ = db.StartProbeSession(cfg.ExampleDir, "", cfg.Interval, cfg.Actions)
186+
}
187+
188+
ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
189+
defer stop()
190+
191+
if err := r.Start(ctx); err != nil {
192+
return err
193+
}
194+
fmt.Printf("Probing %s every %s — Ctrl-C to stop.\n"+
195+
"Only ONE proxy can be probed at a time; don't route other traffic through it while this runs.\n\n",
196+
args[0], cfg.Interval)
197+
198+
samples, events := r.Samples(), r.Events()
199+
for samples != nil || events != nil {
200+
select {
201+
case s, ok := <-samples:
202+
if !ok {
203+
samples = nil
204+
continue
205+
}
206+
printProbeSample(s)
207+
if db != nil {
208+
_ = db.SaveProbeSample(sessionID, store.ProbeSample{
209+
Timestamp: s.Timestamp, OK: s.OK, DownloadBPS: s.DownloadBPS,
210+
UploadBPS: s.UploadBPS, PageloadMs: s.PageloadMs, Err: s.Err,
211+
ConsecutiveFailures: s.ConsecutiveFailures, BaselineFailed: s.BaselineFailed,
212+
CoreRestarted: s.CoreRestarted,
213+
})
214+
}
215+
case e, ok := <-events:
216+
if !ok {
217+
events = nil
218+
continue
219+
}
220+
if e.Blocked {
221+
fmt.Printf(">>> BLOCKED at %s <<<\n", e.Timestamp.Format(time.RFC3339))
222+
if db != nil {
223+
_ = db.MarkProbeBlocked(sessionID, e.Timestamp)
224+
}
225+
} else {
226+
fmt.Printf(">>> recovered at %s <<<\n", e.Timestamp.Format(time.RFC3339))
227+
}
228+
case <-ctx.Done():
229+
r.Stop()
230+
if db != nil {
231+
_ = db.StopProbeSession(sessionID)
232+
}
233+
fmt.Println("\nstopped.")
234+
return nil
235+
}
236+
}
237+
return nil
238+
},
239+
}
240+
c.Flags().StringVar(&probeCore, "core", "sing-box", "core to run the proxy on (sing-box | xray)")
241+
c.Flags().IntVar(&probeInterval, "interval", 300, "seconds between probes")
242+
c.Flags().StringSliceVar(&probeActions, "actions", []string{"download"}, "comma-separated: pageload,download,upload,mix")
243+
c.Flags().IntVar(&probeBlockAfterN, "block-after", 3, "consecutive proxy failures (with healthy baseline) before reporting blocked")
244+
c.Flags().StringVar(&probeDownloadURL, "download-url", "", "override the download-test URL")
245+
c.Flags().StringVar(&probeUploadURL, "upload-url", "", "override the upload-test URL")
246+
c.Flags().StringVar(&probePageloadURL, "pageload-url", "", "override the pageload-test URL")
247+
return c
248+
}
249+
250+
func printProbeSample(s probe.Sample) {
251+
t := s.Timestamp.Format("15:04:05")
252+
status := "OK"
253+
if !s.OK {
254+
status = "FAIL"
255+
}
256+
extra := ""
257+
if s.OK && s.DownloadBPS > 0 {
258+
extra += fmt.Sprintf(" ↓%s", fmtBPS(s.DownloadBPS))
259+
}
260+
if s.OK && s.UploadBPS > 0 {
261+
extra += fmt.Sprintf(" ↑%s", fmtBPS(s.UploadBPS))
262+
}
263+
if s.CoreRestarted {
264+
extra += " [core restarted]"
265+
}
266+
if s.BaselineFailed {
267+
extra += " [your network is down — not counted toward block]"
268+
}
269+
if !s.OK && s.ConsecutiveFailures > 0 {
270+
extra += fmt.Sprintf(" (failure #%d)", s.ConsecutiveFailures)
271+
}
272+
if !s.OK && s.Err != "" {
273+
extra += " — " + s.Err
274+
}
275+
fmt.Printf("%s %-4s%s\n", t, status, extra)
276+
}
277+
139278
func addProxyFlags(c *cobra.Command) {
140279
c.Flags().BoolVar(&flagJSON, "json", false, "emit machine-readable JSON report (for CI)")
141280
c.Flags().BoolVar(&flagFull, "full", false, "run the full advanced suite (load/entropy/probe/tls-fingerprint)")

0 commit comments

Comments
 (0)