From f2936ac6adf4e694511f32805994b2911a047163 Mon Sep 17 00:00:00 2001 From: TaurusXin Date: Thu, 21 May 2026 21:13:14 +0800 Subject: [PATCH] refactor: project structure --- .github/workflows/release.yml | 31 +++++ .goreleaser.yml | 36 ++++++ Makefile | 39 ------- go.mod | 2 +- internal/cli/cli.go | 168 +++++++++++++++++++++++++++ internal/filter/filter.go | 25 ++++ internal/stats/stats.go | 35 ++++++ main.go | 209 +--------------------------------- 8 files changed, 298 insertions(+), 247 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yml delete mode 100644 Makefile create mode 100644 internal/cli/cli.go create mode 100644 internal/filter/filter.go create mode 100644 internal/stats/stats.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c37ac49 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..9cf9414 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,36 @@ +version: 2 + +builds: + - binary: tcping + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + - "386" + ignore: + - goos: darwin + goarch: "386" + ldflags: + - -w -s -buildid= + +archives: + - format: tar.gz + name_template: "tcping-{{ .Os }}-{{ .Arch }}" + format_overrides: + - goos: windows + format: zip + +checksum: + name_template: checksums.txt + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" diff --git a/Makefile b/Makefile deleted file mode 100644 index 86ec666..0000000 --- a/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -NAME=tcping -BINDIR=bin -GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-w -s -buildid=' -VERSION=1.2.0 -# The -w and -s flags reduce binary sizes by excluding unnecessary symbols and debug info -# The -buildid= flag makes builds reproducible - -all: releases - -linux-amd64: - GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-arm64: - GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -darwin-amd64: - GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -darwin-arm64: - GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -windows-amd64: - GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe - -windows-386: - GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe - -releases: linux-amd64 linux-arm64 darwin-amd64 darwin-arm64 windows-amd64 windows-386 - chmod +x $(BINDIR)/$(NAME)-* - tar zcf $(BINDIR)/$(NAME)-linux-amd64-$(VERSION).tar.gz -C $(BINDIR) $(NAME)-linux-amd64 - tar zcf $(BINDIR)/$(NAME)-linux-arm64-$(VERSION).tar.gz -C $(BINDIR) $(NAME)-linux-arm64 - tar zcf $(BINDIR)/$(NAME)-darwin-amd64-$(VERSION).tar.gz -C $(BINDIR) $(NAME)-darwin-amd64 - tar zcf $(BINDIR)/$(NAME)-darwin-arm64-$(VERSION).tar.gz -C $(BINDIR) $(NAME)-darwin-arm64 - zip -j $(BINDIR)/$(NAME)-windows-386-$(VERSION).zip $(BINDIR)/$(NAME)-windows-386.exe - zip -j $(BINDIR)/$(NAME)-windows-amd64-$(VERSION).zip $(BINDIR)/$(NAME)-windows-amd64.exe - rm -f $(BINDIR)/$(NAME)-darwin-amd64 $(BINDIR)/$(NAME)-darwin-arm64 $(BINDIR)/$(NAME)-linux-amd64 $(BINDIR)/$(NAME)-linux-arm64 $(BINDIR)/$(NAME)-windows-386.exe $(BINDIR)/$(NAME)-windows-amd64.exe - -clean: - rm $(BINDIR)/*-$(VERSION).tar.gz $(BINDIR)/*-$(VERSION).zip diff --git a/go.mod b/go.mod index eb4e2f9..b7f044f 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module tcping +module github.com/taurusxin/tcping-go go 1.22.1 diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..9f0f6de --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,168 @@ +package cli + +import ( + "fmt" + "net" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + flag "github.com/spf13/pflag" + "github.com/taurusxin/tcping-go/internal/filter" + "github.com/taurusxin/tcping-go/internal/stats" +) + +const version = "1.3.0" + +// Run executes the tcping CLI. +func Run() { + var ( + port int + count int + timeoutDuration time.Duration + infinite bool + ipv6 bool + fast bool + + successCount int + successDelay []time.Duration + attemptCount int + stopped bool + ) + + showHelp := flag.BoolP("help", "h", false, "Show help") + showVersion := flag.BoolP("version", "v", false, "Show version") + flag.IntVarP(&count, "count", "c", 4, "Number of probes") + flag.DurationVarP(&timeoutDuration, "timeout", "s", 2*time.Second, "Timeout") + flag.BoolVarP(&infinite, "infinite", "t", false, "Infinite probes") + flag.BoolVarP(&ipv6, "ipv6", "6", false, "Use IPv6; requires domain name") + flag.BoolVarP(&fast, "fast", "f", false, "Fast mode; reduce delay between successful probes") + + flag.Parse() + + if *showHelp { + flag.Usage() + os.Exit(0) + } + + if *showVersion { + fmt.Printf("tcping v%s\n", version) + os.Exit(0) + } + + args := flag.Args() + if len(args) < 1 { + flag.Usage() + os.Exit(1) + } + + hostname := args[0] + port = 80 + if len(args) >= 2 { + p, err := strconv.Atoi(args[1]) + if err != nil { + fmt.Println("Port must be an integer") + os.Exit(1) + } + port = p + } + if port < 1 || port > 65535 { + fmt.Println("Port must be between 1 and 65535") + os.Exit(1) + } + + stopped = false + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + ip := "" + + if net.ParseIP(hostname) == nil { + ips, err := net.LookupIP(hostname) + if err != nil { + fmt.Printf("Failed to resolve %s: %s\n", hostname, err) + os.Exit(1) + } + record := "A" + if ipv6 { + record = "AAAA" + } + ip, err = filter.IP(ips, ipv6) + if err != nil { + fmt.Printf("No %s record found for %s\n", record, hostname) + os.Exit(1) + } + fmt.Printf("Using %s %s record: %s\n", hostname, record, ip) + } else { + ip = hostname + } + + address := net.JoinHostPort(ip, fmt.Sprintf("%d", port)) + + if infinite { + count = -1 + } + + done := make(chan bool, 1) + go func() { + for i := 0; infinite || i < count; i++ { + start := time.Now() + conn, err := net.DialTimeout("tcp", address, timeoutDuration) + duration := time.Since(start) + attemptCount++ + + fmt.Printf("[%d] ", i+1) + if err != nil { + fmt.Printf("Connection to %s failed: %s\n", address, "timeout") + } else { + successCount++ + successDelay = append(successDelay, duration) + fmt.Printf("Reply from %s: time=%s\n", address, fmt.Sprintf("%.3fms", float64(duration)/float64(time.Millisecond))) + conn.Close() + } + + if !infinite && attemptCount >= count { + break + } + if fast { + time.Sleep(150 * time.Millisecond) + } else { + time.Sleep(1 * time.Second) + } + } + done <- true + }() + + select { + case <-sigChan: + fmt.Println("\nTest interrupted by user") + stopped = true + case <-done: + } + + if !stopped { + fmt.Println() + } + + successDelayMs := make([]float64, len(successDelay)) + for i, delay := range successDelay { + successDelayMs[i] = float64(delay) / float64(time.Millisecond) + } + + minDelay := 0.0 + maxDelay := 0.0 + avgDelay := 0.0 + successRate := 0.0 + + if successCount > 0 { + minDelay = stats.Min(successDelayMs) + maxDelay = stats.Max(successDelayMs) + avgDelay = stats.Avg(successDelayMs) + } + if attemptCount > 0 { + successRate = float64(successCount) / float64(attemptCount) * 100 + } + fmt.Printf("Test finished, success %d/%d (%.2f%%)\nmin = %.3fms, max = %.3fms, avg = %.3fms\n", successCount, attemptCount, successRate, minDelay, maxDelay, avgDelay) +} diff --git a/internal/filter/filter.go b/internal/filter/filter.go new file mode 100644 index 0000000..aa79d7c --- /dev/null +++ b/internal/filter/filter.go @@ -0,0 +1,25 @@ +package filter + +import ( + "fmt" + "net" +) + +// IP filters a list of resolved IPs and returns the first match +// for the requested address family (IPv4 or IPv6). +func IP(ips []net.IP, ipv6 bool) (string, error) { + if ipv6 { + for _, ip := range ips { + if ip.To16() != nil && ip.To4() == nil { + return ip.String(), nil + } + } + } else { + for _, ip := range ips { + if ip.To4() != nil && ip.To16() != nil { + return ip.String(), nil + } + } + } + return "", fmt.Errorf("no suitable IP address found") +} diff --git a/internal/stats/stats.go b/internal/stats/stats.go new file mode 100644 index 0000000..99cf970 --- /dev/null +++ b/internal/stats/stats.go @@ -0,0 +1,35 @@ +package stats + +// Min returns the minimum value in a float64 slice. +// The slice must not be empty. +func Min(array []float64) float64 { + min := array[0] + for _, value := range array { + if value < min { + min = value + } + } + return min +} + +// Max returns the maximum value in a float64 slice. +// The slice must not be empty. +func Max(array []float64) float64 { + max := array[0] + for _, value := range array { + if value > max { + max = value + } + } + return max +} + +// Avg returns the arithmetic mean of a float64 slice. +// The slice must not be empty. +func Avg(array []float64) float64 { + sum := 0.0 + for _, value := range array { + sum += value + } + return sum / float64(len(array)) +} diff --git a/main.go b/main.go index 3ef3ff2..591a02d 100644 --- a/main.go +++ b/main.go @@ -1,212 +1,7 @@ package main -import ( - "fmt" - "net" - "os" - "os/signal" - "strconv" - "syscall" - "time" - - flag "github.com/spf13/pflag" -) +import "github.com/taurusxin/tcping-go/internal/cli" func main() { - var ( - port int - count int - timeoutDuration time.Duration - infinite bool - ipv6 bool - fast bool - - successCount int - successDelay []time.Duration - attemptCount int - stopped bool - ) - - version := "1.2.0" - - showHelp := flag.BoolP("help", "h", false, "Show help") - showVersion := flag.BoolP("version", "v", false, "Show version") - flag.IntVarP(&count, "count", "c", 4, "Number of probes, default 4") - flag.DurationVarP(&timeoutDuration, "timeout", "s", 2*time.Second, "Timeout, default 2s") - flag.BoolVarP(&infinite, "infinite", "t", false, "Infinite probes") - flag.BoolVarP(&ipv6, "ipv6", "6", false, "Use IPv6; requires domain name") - flag.BoolVarP(&fast, "fast", "f", false, "Fast mode; reduce delay between successful probes") - - flag.Parse() - - if *showHelp { - flag.Usage() - os.Exit(0) - } - - if *showVersion { - fmt.Printf("tcping v%s\n", version) - os.Exit(0) - } - - args := flag.Args() - if len(args) < 1 { - flag.Usage() - os.Exit(1) - } - - hostname := args[0] - port = 80 - if len(args) >= 2 { - p, err := strconv.Atoi(args[1]) - if err != nil { - fmt.Println("Port must be an integer") - os.Exit(1) - } - port = p - } - if port < 1 || port > 65535 { - fmt.Println("Port must be between 1 and 65535") - os.Exit(1) - } - - // 设置信号捕获 - stopped = false - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - ip := "" - - if net.ParseIP(hostname) == nil { - // 解析域名 - ips, err := net.LookupIP(hostname) - if err != nil { - fmt.Printf("Failed to resolve %s: %s\n", hostname, err) - os.Exit(1) - } - record := "A" - if ipv6 { - record = "AAAA" - } - ip, err = filterIP(ips, ipv6) - if err != nil { - fmt.Printf("No %s record found for %s\n", record, hostname) - os.Exit(1) - } - fmt.Printf("Using %s %s record: %s\n", hostname, record, ip) - } else { - ip = hostname - } - - address := net.JoinHostPort(ip, fmt.Sprintf("%d", port)) - - if infinite { - count = -1 - } - - done := make(chan bool, 1) - go func() { - for i := 0; infinite || i < count; i++ { - start := time.Now() - conn, err := net.DialTimeout("tcp", address, timeoutDuration) - duration := time.Since(start) - attemptCount++ - - fmt.Printf("[%d] ", i+1) - if err != nil { - fmt.Printf("Connection to %s failed: %s\n", address, "timeout") - } else { - successCount++ - successDelay = append(successDelay, duration) - fmt.Printf("Reply from %s: time=%s\n", address, fmt.Sprintf("%.3fms", float64(duration)/float64(time.Millisecond))) - conn.Close() - } - - if !infinite && attemptCount >= count { - break - } - if fast { - time.Sleep(150 * time.Millisecond) - } else { - time.Sleep(1 * time.Second) - } - } - done <- true - }() - - select { - case <-sigChan: - fmt.Println("\nTest interrupted by user") - stopped = true - case <-done: - } - - if !stopped { - fmt.Println() - } - - successDelayMs := make([]float64, len(successDelay)) - for i, delay := range successDelay { - successDelayMs[i] = float64(delay) / float64(time.Millisecond) - } - - minDelay := 0.0 - maxDelay := 0.0 - avgDelay := 0.0 - successRate := 0.0 - - if successCount > 0 { - minDelay = float64_min(successDelayMs) - maxDelay = float64_max(successDelayMs) - avgDelay = float64_avg(successDelayMs) - } - if attemptCount > 0 { - successRate = float64(successCount) / float64(attemptCount) * 100 - } - fmt.Printf("Test finished, success %d/%d (%.2f%%)\nmin = %.3fms, max = %.3fms, avg = %.3fms\n", successCount, attemptCount, successRate, minDelay, maxDelay, avgDelay) -} - -func filterIP(ips []net.IP, ipv6 bool) (string, error) { - if ipv6 { - for _, ip := range ips { - if ip.To16() != nil && ip.To4() == nil { - return ip.String(), nil - } - } - } else { - for _, ip := range ips { - if ip.To4() != nil && ip.To16() != nil { - return ip.String(), nil - } - } - } - return "", fmt.Errorf("no suitable IP address found") -} - -func float64_min(array []float64) float64 { - min := array[0] - for _, value := range array { - if value < min { - min = value - } - } - return min -} - -func float64_max(array []float64) float64 { - max := array[0] - for _, value := range array { - if value > max { - max = value - } - } - return max -} - -func float64_avg(array []float64) float64 { - sum := 0.0 - for _, value := range array { - sum += value - } - return sum / float64(len(array)) + cli.Run() }