diff --git a/speedtest.go b/speedtest.go index c2cca16..a2f922e 100644 --- a/speedtest.go +++ b/speedtest.go @@ -20,7 +20,8 @@ var ( serverIds = kingpin.Flag("server", "Select server id to run speedtest.").Short('s').Ints() customURL = kingpin.Flag("custom-url", "Specify the url of the server instead of fetching from speedtest.net.").String() savingMode = kingpin.Flag("saving-mode", "Test with few resources, though low accuracy (especially > 30Mbps).").Bool() - jsonOutput = kingpin.Flag("json", "Output results in json format.").Bool() + jsonOutput = kingpin.Flag("json", "Output results in json like format.").Bool() + unixOutput = kingpin.Flag("unix", "Output results in unix like format.").Bool() location = kingpin.Flag("location", "Change the location with a precise coordinate (format: lat,lon).").String() city = kingpin.Flag("city", "Change the location with a predefined city label.").String() showCityList = kingpin.Flag("city-list", "List all predefined city labels.").Bool() @@ -46,6 +47,11 @@ func main() { speedtest.SetUnit(parseUnit(*unit)) + // start unix output for saving mode by default. + if *savingMode && !*jsonOutput && !*unixOutput { + *unixOutput = true + } + // 0. speed test setting var speedtestClient = speedtest.New(speedtest.WithUserConfig( &speedtest.UserConfig{ @@ -60,8 +66,6 @@ func main() { CityFlag: *city, LocationFlag: *location, Keyword: *search, - NoDownload: *noDownload, - NoUpload: *noUpload, })) if *showCityList { @@ -70,7 +74,7 @@ func main() { } // 1. retrieving user information - taskManager := InitTaskManager(!*jsonOutput) + taskManager := InitTaskManager(*jsonOutput, *unixOutput) taskManager.AsyncRun("Retrieving User Information", func(task *Task) { u, err := speedtestClient.FetchUserInfo() task.CheckError(err) @@ -125,7 +129,7 @@ func main() { taskManager.Println("Test Server: " + server.String()) taskManager.Run("Latency: --", func(task *Task) { task.CheckError(server.PingTest(func(latency time.Duration) { - task.Printf("Latency: %v", latency) + task.Updatef("Latency: %v", latency) })) task.Printf("Latency: %v Jitter: %v Min: %v Max: %v", server.Latency, server.Jitter, server.MinLatency, server.MaxLatency) task.Complete() @@ -136,27 +140,31 @@ func main() { analyzer, err = speedtest.NewPacketLossAnalyzer(&speedtest.PacketLossAnalyzerOptions{ SourceInterface: *source, }) - server.PacketLoss = -1.0 // N/A as default + packetLossAnalyzerCtx, packetLossAnalyzerCancel := context.WithTimeout(context.Background(), time.Second*40) - go func() { - err = analyzer.RunWithContext(packetLossAnalyzerCtx, server.Host, func(packetLoss *transport.PLoss) { - server.PacketLoss = packetLoss.Loss() - }) - if errors.Is(err, transport.ErrUnsupported) { - packetLossAnalyzerCancel() // cancel early - } - }() + taskManager.Run("Packet Loss Analyzer", func(task *Task) { + go func() { + err = analyzer.RunWithContext(packetLossAnalyzerCtx, server.Host, func(packetLoss *transport.PLoss) { + server.PacketLoss = *packetLoss + }) + if errors.Is(err, transport.ErrUnsupported) { + packetLossAnalyzerCancel() // cancel early + } + }() + task.Println("Packet Loss Analyzer: Running in background (<= 30 Sec)") + task.Complete() + }) // 3.1 create accompany Echo accEcho := newAccompanyEcho(server, time.Millisecond*500) - taskManager.Run("Download", func(task *Task) { + taskManager.RunWithTrigger(!*noDownload, "Download", func(task *Task) { accEcho.Run() speedtestClient.SetCallbackDownload(func(downRate speedtest.ByteRate) { lc := accEcho.CurrentLatency() if lc == 0 { - task.Printf("Download: %s (Latency: --)", downRate) + task.Updatef("Download: %s (Latency: --)", downRate) } else { - task.Printf("Download: %s (Latency: %dms)", downRate, lc/1000000) + task.Updatef("Download: %s (Latency: %dms)", downRate, lc/1000000) } }) if *multi { @@ -170,14 +178,14 @@ func main() { task.Complete() }) - taskManager.Run("Upload", func(task *Task) { + taskManager.RunWithTrigger(!*noUpload, "Upload", func(task *Task) { accEcho.Run() speedtestClient.SetCallbackUpload(func(upRate speedtest.ByteRate) { lc := accEcho.CurrentLatency() if lc == 0 { - task.Printf("Upload: %s (Latency: --)", upRate) + task.Updatef("Upload: %s (Latency: --)", upRate) } else { - task.Printf("Upload: %s (Latency: %dms)", upRate, lc/1000000) + task.Updatef("Upload: %s (Latency: %dms)", upRate, lc/1000000) } }) if *multi { @@ -190,16 +198,16 @@ func main() { task.Printf("Upload: %s (Used: %.2fMB) (Latency: %dms Jitter: %dms Min: %dms Max: %dms)", server.ULSpeed, float64(server.Context.Manager.GetTotalUpload())/1000/1000, mean/1000000, std/1000000, minL/1000000, maxL/1000000) task.Complete() }) - taskManager.Reset() - speedtestClient.Manager.Reset() + + if *noUpload && *noDownload { + time.Sleep(time.Second * 30) + } packetLossAnalyzerCancel() if !*jsonOutput { - if server.PacketLoss != -1 { - fmt.Printf(" Packet Loss: %.2f%%", server.PacketLoss*100) - } else { - fmt.Printf(" Packet Loss: N/A") - } + taskManager.Println(server.PacketLoss.String()) } + taskManager.Reset() + speedtestClient.Manager.Reset() } taskManager.Stop() diff --git a/speedtest/request.go b/speedtest/request.go index 029b6ea..54acd6b 100644 --- a/speedtest/request.go +++ b/speedtest/request.go @@ -30,10 +30,6 @@ var ( ) func (s *Server) MultiDownloadTestContext(ctx context.Context, servers Servers) error { - if s.Context.config.NoDownload { - dbg.Println("Download test disabled") - return nil - } ss := servers.Available() if ss.Len() == 0 { return errors.New("not found available servers") @@ -69,10 +65,6 @@ func (s *Server) MultiDownloadTestContext(ctx context.Context, servers Servers) } func (s *Server) MultiUploadTestContext(ctx context.Context, servers Servers) error { - if s.Context.config.NoUpload { - dbg.Println("Upload test disabled") - return nil - } ss := servers.Available() if ss.Len() == 0 { return errors.New("not found available servers") @@ -118,10 +110,6 @@ func (s *Server) DownloadTestContext(ctx context.Context) error { } func (s *Server) downloadTestContext(ctx context.Context, downloadRequest downloadFunc) error { - if s.Context.config.NoDownload { - dbg.Println("Download test disabled") - return nil - } var errorTimes int64 = 0 var requestTimes int64 = 0 start := time.Now() @@ -153,10 +141,6 @@ func (s *Server) UploadTestContext(ctx context.Context) error { } func (s *Server) uploadTestContext(ctx context.Context, uploadRequest uploadFunc) error { - if s.Context.config.NoUpload { - dbg.Println("Upload test disabled") - return nil - } var errorTimes int64 = 0 var requestTimes int64 = 0 start := time.Now() diff --git a/speedtest/server.go b/speedtest/server.go index 2265384..2997bcd 100644 --- a/speedtest/server.go +++ b/speedtest/server.go @@ -6,6 +6,7 @@ import ( "encoding/xml" "errors" "fmt" + "github.com/showwin/speedtest-go/speedtest/transport" "math" "net/http" "net/url" @@ -35,23 +36,23 @@ var ( // Server information type Server struct { - URL string `xml:"url,attr" json:"url"` - Lat string `xml:"lat,attr" json:"lat"` - Lon string `xml:"lon,attr" json:"lon"` - Name string `xml:"name,attr" json:"name"` - Country string `xml:"country,attr" json:"country"` - Sponsor string `xml:"sponsor,attr" json:"sponsor"` - ID string `xml:"id,attr" json:"id"` - Host string `xml:"host,attr" json:"host"` - Distance float64 `json:"distance"` - Latency time.Duration `json:"latency"` - MaxLatency time.Duration `json:"max_latency"` - MinLatency time.Duration `json:"min_latency"` - Jitter time.Duration `json:"jitter"` - DLSpeed ByteRate `json:"dl_speed"` - ULSpeed ByteRate `json:"ul_speed"` - TestDuration TestDuration `json:"test_duration"` - PacketLoss float64 `json:"packet_loss"` + URL string `xml:"url,attr" json:"url"` + Lat string `xml:"lat,attr" json:"lat"` + Lon string `xml:"lon,attr" json:"lon"` + Name string `xml:"name,attr" json:"name"` + Country string `xml:"country,attr" json:"country"` + Sponsor string `xml:"sponsor,attr" json:"sponsor"` + ID string `xml:"id,attr" json:"id"` + Host string `xml:"host,attr" json:"host"` + Distance float64 `json:"distance"` + Latency time.Duration `json:"latency"` + MaxLatency time.Duration `json:"max_latency"` + MinLatency time.Duration `json:"min_latency"` + Jitter time.Duration `json:"jitter"` + DLSpeed ByteRate `json:"dl_speed"` + ULSpeed ByteRate `json:"ul_speed"` + TestDuration TestDuration `json:"test_duration"` + PacketLoss transport.PLoss `json:"packet_loss"` Context *Speedtest `json:"-"` } diff --git a/speedtest/speedtest.go b/speedtest/speedtest.go index 32162c4..94a03f6 100644 --- a/speedtest/speedtest.go +++ b/speedtest/speedtest.go @@ -54,9 +54,6 @@ type UserConfig struct { Location *Location Keyword string // Fuzzy search - - NoDownload bool - NoUpload bool } func parseAddr(addr string) (string, string) { diff --git a/speedtest/transport/tcp.go b/speedtest/transport/tcp.go index 2a55ecb..2dcbd27 100644 --- a/speedtest/transport/tcp.go +++ b/speedtest/transport/tcp.go @@ -171,18 +171,27 @@ func (client *Client) InitPacketLoss() error { return client.Write(initPacket) } +// PLoss Packet loss statistics +// The packet loss here generally refers to uplink packet loss. +// We use the following formula to calculate the packet loss: +// packetLoss = [1 - (Sent - Dup) / (Max + 1)] * 100% type PLoss struct { - Sent int - Dup int - MaximumReceived int + Sent int `json:"sent"` // Number of sent packets acknowledged by the remote. + Dup int `json:"dup"` // Number of duplicate packets acknowledged by the remote. + Max int `json:"max"` // The maximum index value received by the remote. } -func (p *PLoss) String() string { - return fmt.Sprintf("Sent: %d, DupPacket: %d, MaximumReceived: %d", p.Sent, p.Dup, p.MaximumReceived) +func (p PLoss) String() string { + if p.Sent == 0 { + // if p.Sent == 0, maybe all data is dropped by the upper gateway. + // we believe this feature is not applicable on this server now. + return "Packet Loss: N/A" + } + return fmt.Sprintf("Packet Loss: %.2f%% (Sent: %d/Dup: %d/Max: %d)", p.Loss()*100, p.Sent, p.Dup, p.Max) } -func (p *PLoss) Loss() float64 { - return 1 - (float64(p.Sent-p.Dup))/float64(p.MaximumReceived+1) +func (p PLoss) Loss() float64 { + return 1 - (float64(p.Sent-p.Dup))/float64(p.Max+1) } func (client *Client) PacketLoss() (*PLoss, error) { @@ -211,9 +220,9 @@ func (client *Client) PacketLoss() (*PLoss, error) { return nil, err } return &PLoss{ - Sent: x0, - Dup: x1, - MaximumReceived: x2, + Sent: x0, + Dup: x1, + Max: x2, }, nil } diff --git a/task.go b/task.go index 948382b..e81fe1b 100644 --- a/task.go +++ b/task.go @@ -8,8 +8,9 @@ import ( ) type TaskManager struct { - sm ysmrr.SpinnerManager - isOut bool + sm ysmrr.SpinnerManager + isOut bool + noProgress bool } type Task struct { @@ -18,16 +19,17 @@ type Task struct { title string } -func InitTaskManager(isOut bool) *TaskManager { - tm := &TaskManager{sm: ysmrr.NewSpinnerManager(), isOut: isOut} - if isOut { +func InitTaskManager(jsonOutput, unixOutput bool) *TaskManager { + isOut := !jsonOutput || unixOutput + tm := &TaskManager{sm: ysmrr.NewSpinnerManager(), isOut: isOut, noProgress: unixOutput} + if isOut && !unixOutput { tm.sm.Start() } return tm } func (tm *TaskManager) Reset() { - if tm.isOut { + if tm.isOut && !tm.noProgress { tm.sm.Stop() tm.sm = ysmrr.NewSpinnerManager() tm.sm.Start() @@ -35,12 +37,16 @@ func (tm *TaskManager) Reset() { } func (tm *TaskManager) Stop() { - if tm.isOut { + if tm.isOut && !tm.noProgress { tm.sm.Stop() } } func (tm *TaskManager) Println(message string) { + if tm.noProgress { + fmt.Println(message) + return + } if tm.isOut { context := &Task{manager: tm} context.spinner = tm.sm.AddSpinner(message) @@ -48,10 +54,20 @@ func (tm *TaskManager) Println(message string) { } } +func (tm *TaskManager) RunWithTrigger(enable bool, title string, callback func(task *Task)) { + if enable { + tm.Run(title, callback) + } +} + func (tm *TaskManager) Run(title string, callback func(task *Task)) { context := &Task{manager: tm, title: title} if tm.isOut { - context.spinner = tm.sm.AddSpinner(title) + if tm.noProgress { + //fmt.Println(title) + } else { + context.spinner = tm.sm.AddSpinner(title) + } } callback(context) } @@ -59,19 +75,44 @@ func (tm *TaskManager) Run(title string, callback func(task *Task)) { func (tm *TaskManager) AsyncRun(title string, callback func(task *Task)) { context := &Task{manager: tm, title: title} if tm.isOut { - context.spinner = tm.sm.AddSpinner(title) + if tm.noProgress { + //fmt.Println(title) + } else { + context.spinner = tm.sm.AddSpinner(title) + } } go callback(context) } func (t *Task) Complete() { + if t.manager.noProgress { + return + } if t.spinner == nil { return } t.spinner.Complete() } +func (t *Task) Updatef(format string, a ...interface{}) { + if t.spinner == nil || t.manager.noProgress { + return + } + t.spinner.UpdateMessagef(format, a...) +} + +func (t *Task) Update(format string) { + if t.spinner == nil || t.manager.noProgress { + return + } + t.spinner.UpdateMessage(format) +} + func (t *Task) Println(message string) { + if t.manager.noProgress { + fmt.Println(message) + return + } if t.spinner == nil { return } @@ -79,6 +120,10 @@ func (t *Task) Println(message string) { } func (t *Task) Printf(format string, a ...interface{}) { + if t.manager.noProgress { + fmt.Printf(format+"\n", a...) + return + } if t.spinner == nil { return } @@ -94,6 +139,6 @@ func (t *Task) CheckError(err error) { } else { fmt.Printf("Fatal: %s, err: %v", strings.ToLower(t.title), err) } - os.Exit(1) + os.Exit(0) } }