221 lines
5.5 KiB
Go
221 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/Bowery/prompt"
|
|
"github.com/bndr/gotabulate"
|
|
"github.com/joliv/spark"
|
|
stdn "github.com/traetox/speedtest/speedtestdotnet"
|
|
//stdn "./speedtestdotnet" //for testing
|
|
)
|
|
|
|
const (
|
|
tableFormat = "simple"
|
|
maxFailureCount = 3
|
|
initialTestCount = 5
|
|
basePingCount = 5
|
|
fullTestCount = 20
|
|
)
|
|
|
|
var (
|
|
speedtestDuration = flag.Int("t", 3, "Target duration for speedtests (in seconds)")
|
|
search = flag.String("s", "", "Server name substring to search candidate servers")
|
|
)
|
|
|
|
func init() {
|
|
flag.Parse()
|
|
if *speedtestDuration <= 0 {
|
|
fmt.Fprintf(os.Stderr, "Invalid test duration")
|
|
os.Exit(-1)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
cfg, err := stdn.GetConfig()
|
|
if err != nil {
|
|
fmt.Printf("ERROR: %v\n", err)
|
|
return
|
|
}
|
|
if len(cfg.Servers) <= 0 {
|
|
fmt.Printf("No acceptable servers found\n")
|
|
return
|
|
}
|
|
var headers []string
|
|
var data [][]string
|
|
var testServers []stdn.Testserver
|
|
if *search == "" {
|
|
fmt.Printf("Gathering server list and testing...\n")
|
|
if testServers, err = autoGetTestServers(cfg); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
os.Exit(-1)
|
|
}
|
|
fmt.Printf("%d Closest responding servers:\n", len(testServers))
|
|
for i := range testServers {
|
|
data = append(data, []string{fmt.Sprintf("%d", i),
|
|
testServers[i].Name, testServers[i].Sponsor,
|
|
fmt.Sprintf("%.02f", testServers[i].Distance),
|
|
fmt.Sprintf("%s", testServers[i].Latency)})
|
|
}
|
|
headers = []string{"ID", "Name", "Sponsor", "Distance (km)", "Latency (ms)"}
|
|
} else {
|
|
if testServers, err = getSearchServers(cfg, *search); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
os.Exit(-1)
|
|
}
|
|
headers = []string{"ID", "Name", "Sponsor", "Distance (km)"}
|
|
fmt.Printf("%d Matching servers:\n", len(testServers))
|
|
for i := range testServers {
|
|
data = append(data, []string{fmt.Sprintf("%d", i),
|
|
testServers[i].Name, testServers[i].Sponsor,
|
|
fmt.Sprintf("%.02f", testServers[i].Distance)})
|
|
}
|
|
|
|
}
|
|
t := gotabulate.Create(data)
|
|
t.SetHeaders(headers)
|
|
t.SetWrapStrings(false)
|
|
fmt.Printf("%s", t.Render(tableFormat))
|
|
fmt.Printf("Enter server ID for bandwidth test, or \"quit\" to exit\n")
|
|
for {
|
|
s, err := prompt.Basic("ID> ", true)
|
|
if err != nil {
|
|
fmt.Printf("input failure \"%v\"\n", err)
|
|
os.Exit(-1)
|
|
}
|
|
//be REALLY forgiving on exit logic
|
|
if strings.HasPrefix(strings.ToLower(s), "exit") {
|
|
os.Exit(0)
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(s), "quit") {
|
|
os.Exit(0)
|
|
}
|
|
|
|
//try to convert the string to a number
|
|
id, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "\"%s\" is not a valid id\n", s)
|
|
continue
|
|
}
|
|
if id > uint64(len(testServers)) {
|
|
fmt.Fprintf(os.Stderr, "No server with ID \"%d\" available\n", id)
|
|
continue
|
|
}
|
|
if err = fullTest(testServers[id]); err != nil {
|
|
if err == io.EOF {
|
|
fmt.Fprintf(os.Stderr, "Error, the remote server kicked us.\n")
|
|
fmt.Fprintf(os.Stderr, "Maximum request size may have changed\n")
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Test failed with unknown error: %v\n", err)
|
|
}
|
|
os.Exit(-1)
|
|
} else {
|
|
break //we are done
|
|
}
|
|
}
|
|
}
|
|
|
|
func testLatency(server stdn.Testserver) error {
|
|
//perform a full latency test
|
|
durs, err := server.Ping(fullTestCount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var avg, max, min uint64
|
|
var latencies []float64
|
|
for i := range durs {
|
|
ms := uint64(durs[i].Nanoseconds() / 1000000)
|
|
latencies = append(latencies, float64(ms))
|
|
avg += ms
|
|
if ms > max {
|
|
max = ms
|
|
}
|
|
if ms < min || min == 0 {
|
|
min = ms
|
|
}
|
|
}
|
|
avg = avg / uint64(len(durs))
|
|
median := durs[len(durs)/2].Nanoseconds() / 1000000
|
|
sparkline := spark.Line(latencies)
|
|
fmt.Printf("Latency: %s\t%dms avg\t%dms median\t%dms max\t%dms min\n", sparkline, avg, median, max, min)
|
|
return nil
|
|
}
|
|
|
|
func testDownstream(server stdn.Testserver) error {
|
|
bps, err := server.Downstream(*speedtestDuration)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Download: %s\n", stdn.HumanSpeed(bps))
|
|
return nil
|
|
}
|
|
|
|
func testUpstream(server stdn.Testserver) error {
|
|
bps, err := server.Upstream(*speedtestDuration)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Upload: %s\n", stdn.HumanSpeed(bps))
|
|
return nil
|
|
}
|
|
|
|
func fullTest(server stdn.Testserver) error {
|
|
if err := testLatency(server); err != nil {
|
|
return err
|
|
}
|
|
if err := testDownstream(server); err != nil {
|
|
return err
|
|
}
|
|
if err := testUpstream(server); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func autoGetTestServers(cfg *stdn.Config) ([]stdn.Testserver, error) {
|
|
//get the first 5 closest servers
|
|
testServers := []stdn.Testserver{}
|
|
failures := 0
|
|
for i := range cfg.Servers {
|
|
if failures >= maxFailureCount {
|
|
if len(testServers) > 0 {
|
|
return testServers, nil
|
|
}
|
|
return nil, fmt.Errorf("Failed to perform latency test on closest servers\n")
|
|
}
|
|
if len(testServers) >= initialTestCount {
|
|
return testServers, nil
|
|
}
|
|
//get a latency from the server, the last latency will also be store in the
|
|
//server structure
|
|
if _, err := cfg.Servers[i].MedianPing(basePingCount); err != nil {
|
|
failures++
|
|
continue
|
|
}
|
|
testServers = append(testServers, cfg.Servers[i])
|
|
}
|
|
return testServers, nil
|
|
}
|
|
|
|
func getSearchServers(cfg *stdn.Config, query string) ([]stdn.Testserver, error) {
|
|
//get the first 5 closest servers
|
|
testServers := []stdn.Testserver{}
|
|
for i := range cfg.Servers {
|
|
if strings.Contains(strings.ToLower(cfg.Servers[i].Name), strings.ToLower(query)) {
|
|
testServers = append(testServers, cfg.Servers[i])
|
|
}
|
|
}
|
|
if len(testServers) == 0 {
|
|
return nil, errors.New("no servers found")
|
|
}
|
|
return testServers, nil
|
|
}
|
|
|
|
|