refactor: project structure
This commit is contained in:
31
.github/workflows/release.yml
vendored
Normal file
31
.github/workflows/release.yml
vendored
Normal file
@@ -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 }}
|
||||
36
.goreleaser.yml
Normal file
36
.goreleaser.yml
Normal file
@@ -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:"
|
||||
39
Makefile
39
Makefile
@@ -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
|
||||
168
internal/cli/cli.go
Normal file
168
internal/cli/cli.go
Normal file
@@ -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)
|
||||
}
|
||||
25
internal/filter/filter.go
Normal file
25
internal/filter/filter.go
Normal file
@@ -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")
|
||||
}
|
||||
35
internal/stats/stats.go
Normal file
35
internal/stats/stats.go
Normal file
@@ -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))
|
||||
}
|
||||
209
main.go
209
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user