├── .devcontainer └── devcontainer.json ├── protocol.go ├── tests ├── extract │ └── main.go ├── server │ └── server.go ├── controller │ └── main.go └── client │ └── client.go ├── .idea ├── vcs.xml ├── modules.xml ├── go-iperf.iml └── workspace.xml ├── .gitignore ├── go.mod ├── shared.go ├── api ├── Dockerfile ├── build.sh ├── proto │ └── control.proto └── go │ └── control.pb.go ├── reporter.go ├── cmd └── main.go ├── LICENSE ├── README.md ├── reporter_windows.go ├── iperf.go ├── execute.go ├── server.go ├── controller.go ├── reporter_darwin.go ├── reporter_linux.go ├── sample.json ├── go.sum ├── report.go └── client.go /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/go:1" 3 | } 4 | -------------------------------------------------------------------------------- /protocol.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | type Protocol string 4 | 5 | const ( 6 | PROTO_TCP = "tcp" 7 | PROTO_UDP = "udp" 8 | ) 9 | -------------------------------------------------------------------------------- /tests/extract/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/BGrewell/go-iperf" 4 | 5 | func main() { 6 | iperf.ExtractBinaries() 7 | } 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Ignore the embedded directory 18 | embedded/ -------------------------------------------------------------------------------- /tests/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/BGrewell/go-iperf" 6 | "os" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | s := iperf.NewServer() 12 | err := s.Start() 13 | if err != nil { 14 | fmt.Println("failed to start server") 15 | os.Exit(-1) 16 | } 17 | 18 | for s.Running { 19 | time.Sleep(1 * time.Second) 20 | } 21 | 22 | fmt.Println("server has exited") 23 | } 24 | -------------------------------------------------------------------------------- /.idea/go-iperf.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/BGrewell/go-iperf 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/BGrewell/go-conversions v0.0.0-20201203155646-5e189e4ca087 7 | github.com/BGrewell/tail v1.0.0 8 | github.com/fsnotify/fsnotify v1.4.9 // indirect 9 | github.com/golang/protobuf v1.5.2 10 | github.com/google/uuid v1.1.2 11 | github.com/hpcloud/tail v1.0.0 // indirect 12 | github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect 13 | google.golang.org/grpc v1.36.1 14 | google.golang.org/protobuf v1.26.0 15 | gopkg.in/fsnotify.v1 v1.4.7 // indirect 16 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /shared.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | type TestMode string 10 | 11 | const ( 12 | MODE_JSON TestMode = "json" 13 | MODE_LIVE TestMode = "live" 14 | ) 15 | 16 | type DebugScanner struct { 17 | Silent bool 18 | } 19 | 20 | func (ds *DebugScanner) Scan(buff io.ReadCloser) { 21 | if buff == nil { 22 | fmt.Println("unable to read, ReadCloser is nil") 23 | return 24 | } 25 | scanner := bufio.NewScanner(buff) 26 | scanner.Split(bufio.ScanWords) 27 | for scanner.Scan() { 28 | text := scanner.Text() 29 | if !ds.Silent { 30 | fmt.Println(text) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile.protogen 2 | FROM golang:latest 3 | 4 | LABEL maintainer="Benjamin Grewell " 5 | 6 | ENV PROTOC_VERSION 3.6.1 7 | ENV PROTOC_GEN_GO_VERSION v1.2.0 8 | 9 | WORKDIR /go/src/github.com/BGrewell/go-iperf/api 10 | 11 | RUN apt update 12 | RUN apt install -y protobuf-compiler python3 python3-pip 13 | RUN go get -u github.com/golang/protobuf/protoc-gen-go 14 | RUN pip3 install grpcio-tools 15 | RUN export PATH=$PATH:$GOPATH/bin 16 | RUN echo $PATH 17 | 18 | COPY proto/control.proto proto/control.proto 19 | 20 | RUN mkdir go 21 | RUN protoc -I proto/. --go_out=plugins=grpc:go control.proto 22 | -------------------------------------------------------------------------------- /reporter.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "github.com/BGrewell/tail" 5 | "time" 6 | ) 7 | 8 | type Reporter struct { 9 | ReportingChannel chan *StreamIntervalReport 10 | LogFile string 11 | running bool 12 | tailer *tail.Tail 13 | } 14 | 15 | func (r *Reporter) Start() { 16 | r.running = true 17 | go r.runLogProcessor() 18 | } 19 | 20 | func (r *Reporter) Stop() { 21 | r.running = false 22 | r.tailer.Stop() 23 | r.tailer.Cleanup() 24 | for { 25 | if len(r.ReportingChannel) == 0 { 26 | break 27 | } 28 | time.Sleep(1 * time.Millisecond) 29 | } 30 | close(r.ReportingChannel) 31 | } 32 | 33 | // runLogProcessor is OS specific because of differences in iperf on Windows and Linux 34 | -------------------------------------------------------------------------------- /api/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! -d go ]; then 4 | echo "[!] Creating go output directory" 5 | mkdir go; 6 | fi 7 | 8 | echo "[+] Building docker container" 9 | docker image build -t go-iperf-builder:1.0 . 10 | docker container run --detach --name builder go-iperf-builder:1.0 11 | docker cp builder:/go/src/github.com/BGrewell/go-iperf/api/go/github.com/BGrewell/go-iperf/api/control.pb.go go/. 12 | echo "[+] Updating of go library complete" 13 | 14 | echo "[+] Removing docker container" 15 | docker rm builder 16 | 17 | echo "[+] Adding new files to source control" 18 | git add go/control.pb.go 19 | git commit -m "regenerated grpc libraries" 20 | git push 21 | 22 | echo "[+] Done. Everything has been rebuilt and the repository has been updated and pushed" -------------------------------------------------------------------------------- /api/proto/control.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/empty.proto"; 4 | 5 | option go_package = "github.com/BGrewell/go-iperf/api"; 6 | 7 | // [START java_declaration] 8 | option java_multiple_files = true; 9 | option java_package = "com.bengrewell.go-iperf.control"; 10 | option java_outer_classname = "Control"; 11 | // [END java_declaration] 12 | 13 | // [START csharp_declaration] 14 | option csharp_namespace = "BenGrewell.GoIperf.Control"; 15 | // [END csharp_declaration] 16 | 17 | package api; 18 | 19 | service Command { 20 | rpc GrpcRequestServer(StartServerRequest) returns (StartServerResponse) {} 21 | } 22 | 23 | message StartServerRequest { 24 | } 25 | 26 | message StartServerResponse { 27 | string id = 1; 28 | int32 listen_port = 2; 29 | } -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"fmt" 5 | "github.com/BGrewell/go-conversions" 6 | //"github.com/BGrewell/go-iperf" 7 | //"time" 8 | "fmt" 9 | "github.com/BGrewell/go-iperf" 10 | "time" 11 | ) 12 | 13 | func main() { 14 | 15 | s := iperf.NewServer() 16 | c := iperf.NewClient("127.0.0.1") 17 | c.SetIncludeServer(true) 18 | fmt.Println(s.Id) 19 | fmt.Println(c.Id) 20 | 21 | err := s.Start() 22 | if err != nil { 23 | fmt.Println(err) 24 | } 25 | 26 | err = c.Start() 27 | if err != nil { 28 | fmt.Println(err) 29 | } 30 | 31 | for c.Running { 32 | time.Sleep(1 * time.Second) 33 | } 34 | 35 | fmt.Println("stopping server") 36 | s.Stop() 37 | 38 | fmt.Printf("Client exit code: %d\n", *c.ExitCode()) 39 | fmt.Printf("Server exit code: %d\n", *s.ExitCode) 40 | iperf.Cleanup() 41 | if c.Report().Error != "" { 42 | fmt.Println(c.Report().Error) 43 | } else { 44 | fmt.Printf("Recv Rate: %s\n", conversions.IntBitRateToString(int64(c.Report().End.SumReceived.BitsPerSecond))) 45 | fmt.Printf("Send Rate: %s\n", conversions.IntBitRateToString(int64(c.Report().End.SumSent.BitsPerSecond))) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/controller/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/BGrewell/go-iperf" 6 | "log" 7 | ) 8 | 9 | func main() { 10 | 11 | // First create a controller for the "client" side (would be on a different computer) 12 | cc, err := iperf.NewController(6802) 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | cc.Port = 6801 //Note: this is just a hack because we start a listener on the port when we get a new controller and in this case since we are on the same pc we would have a port conflict so we start our listener on the port+1 and then fix that after the listener has started since we won't use it anyway 17 | 18 | // Second create a controller for the "server" side (again would normally be on a different computer) 19 | sc, err := iperf.NewController(6801) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | fmt.Printf("[+] Server side controller listening on %d\n", sc.Port) 24 | 25 | // Test iperf 26 | iperfCli, err := cc.NewClient("127.0.0.1") 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | err = iperfCli.Start() 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | <- iperfCli.Done 37 | 38 | fmt.Println(iperfCli.Report().String()) 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Ben Grewell 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /tests/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/BGrewell/go-iperf" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | 11 | includeServer := true 12 | proto := "tcp" 13 | runTime := 10 14 | omitSec := 0 15 | length := "65500" 16 | 17 | c := iperf.NewClient("10.254.100.100") 18 | c.SetIncludeServer(includeServer) 19 | c.SetTimeSec(runTime) 20 | c.SetOmitSec(omitSec) 21 | c.SetProto((iperf.Protocol)(proto)) 22 | c.SetLength(length) 23 | c.SetJSON(false) 24 | c.SetIncludeServer(false) 25 | c.SetStreams(2) 26 | reports := c.SetModeLive() 27 | 28 | go func() { 29 | for report := range reports { 30 | fmt.Println(report.String()) 31 | } 32 | }() 33 | 34 | err := c.Start() 35 | if err != nil { 36 | fmt.Println("failed to start client") 37 | os.Exit(-1) 38 | } 39 | 40 | // Method 1: Wait for the test to finish by pulling from the 'Done' channel which will block until something is put in or it's closed 41 | <-c.Done 42 | 43 | // Method 2: Poll the c.Running state and wait for it to be 'false' 44 | //for c.Running { 45 | // time.Sleep(100 * time.Millisecond) 46 | //} 47 | 48 | //if c.Report().Error != "" { 49 | // fmt.Println(c.Report().Error) 50 | //} else { 51 | // for _, entry := range c.Report().End.Streams { 52 | // fmt.Println(entry.String()) 53 | // } 54 | // for _, entry := range c.Report().ServerOutputJson.End.Streams { 55 | // fmt.Println(entry.String()) 56 | // } 57 | // fmt.Printf("DL Rate: %s\n", conversions.IntBitRateToString(int64(c.Report().End.SumReceived.BitsPerSecond))) 58 | // fmt.Printf("UL Rate: %s\n", conversions.IntBitRateToString(int64(c.Report().End.SumSent.BitsPerSecond))) 59 | //} 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-iperf 2 | A Go based wrapper around iperf3 3 | 4 | ## Basic Usage 5 | 6 | basic client setup 7 | ```go 8 | func main() { 9 | 10 | c := iperf.NewClient("192.168.0.10") 11 | c.SetJSON(true) 12 | c.SetIncludeServer(true) 13 | c.SetStreams(4) 14 | c.SetTimeSec(30) 15 | c.SetInterval(1) 16 | 17 | err := c.Start() 18 | if err != nil { 19 | fmt.Printf("failed to start client: %v\n", err) 20 | os.Exit(-1) 21 | } 22 | 23 | <- c.Done 24 | 25 | fmt.Println(c.Report().String()) 26 | } 27 | ``` 28 | 29 | basic server setup 30 | ```go 31 | func main() { 32 | 33 | s := iperf.NewServer() 34 | err := s.Start() 35 | if err != nil { 36 | fmt.Printf("failed to start server: %v\n", err) 37 | os.Exit(-1) 38 | } 39 | 40 | for s.Running { 41 | time.Sleep(100 * time.Millisecond) 42 | } 43 | 44 | fmt.Println("server finished") 45 | } 46 | ``` 47 | 48 | client with live results printing 49 | ```go 50 | func main() { 51 | 52 | c := iperf.NewClient("192.168.0.10") 53 | c.SetJSON(true) 54 | c.SetIncludeServer(true) 55 | c.SetStreams(4) 56 | c.SetTimeSec(30) 57 | c.SetInterval(1) 58 | liveReports := c.SetModeLive() 59 | 60 | go func() { 61 | for report := range liveReports { 62 | fmt.Println(report.String()) 63 | } 64 | } 65 | 66 | err := c.Start() 67 | if err != nil { 68 | fmt.Printf("failed to start client: %v\n", err) 69 | os.Exit(-1) 70 | } 71 | 72 | <- c.Done 73 | 74 | fmt.Println(c.Report().String()) 75 | } 76 | ``` 77 | 78 | building binary data package with iperf binaries 79 | ``` 80 | go-bindata -pkg iperf -prefix "embedded/" embedded/ 81 | ``` 82 | -------------------------------------------------------------------------------- /reporter_windows.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "fmt" 5 | "github.com/BGrewell/go-conversions" 6 | "github.com/BGrewell/tail" 7 | "log" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func (r *Reporter) runLogProcessor() { 14 | var err error 15 | r.tailer, err = tail.TailFile(r.LogFile, tail.Config{ 16 | Follow: true, 17 | ReOpen: true, 18 | Poll: true, 19 | MustExist: true, 20 | }) 21 | if err != nil { 22 | log.Fatalf("failed to tail log file: %v", err) 23 | } 24 | 25 | for { 26 | select { 27 | case line := <-r.tailer.Lines: 28 | if line == nil { 29 | continue 30 | } 31 | if len(line.Text) > 5 { 32 | id := line.Text[1:4] 33 | stream, err := strconv.Atoi(strings.TrimSpace(id)) 34 | if err != nil { 35 | continue 36 | } 37 | fields := strings.Fields(line.Text[5:]) 38 | if len(fields) >= 6 { 39 | if fields[0] == "local" { 40 | continue 41 | } 42 | timeFields := strings.Split(fields[0], "-") 43 | start, err := strconv.ParseFloat(timeFields[0], 32) 44 | if err != nil { 45 | log.Printf("failed to convert start time: %s\n", err) 46 | } 47 | end, err := strconv.ParseFloat(timeFields[1], 32) 48 | transferedStr := fmt.Sprintf("%s%s", fields[2], fields[3]) 49 | transferedBytes, err := conversions.StringBitRateToInt(transferedStr) 50 | if err != nil { 51 | log.Printf("failed to convert units: %s\n", err) 52 | } 53 | transferedBytes = transferedBytes / 8 54 | rateStr := fmt.Sprintf("%s%s", fields[4], fields[5]) 55 | rate, err := conversions.StringBitRateToInt(rateStr) 56 | if err != nil { 57 | log.Printf("failed to convert units: %s\n", err) 58 | } 59 | omitted := false 60 | if len(fields) >= 7 && fields[6] == "(omitted)" { 61 | omitted = true 62 | } 63 | report := &StreamIntervalReport{ 64 | Socket: stream, 65 | StartInterval: float32(start), 66 | EndInterval: float32(end), 67 | Seconds: float32(end - start), 68 | Bytes: int(transferedBytes), 69 | BitsPerSecond: float64(rate), 70 | Omitted: omitted, 71 | } 72 | r.ReportingChannel <- report 73 | } 74 | } 75 | case <-time.After(100 * time.Millisecond): 76 | if !r.running { 77 | return 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /iperf.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path" 9 | "runtime" 10 | ) 11 | 12 | var ( 13 | Debug = false 14 | binaryDir = "" 15 | binaryLocation = "" 16 | ) 17 | 18 | func init() { 19 | // Extract the binaries 20 | if runtime.GOOS == "windows" { 21 | err := extractWindowsEmbeddedBinaries() 22 | if err != nil { 23 | log.Fatalf("error initializing iperf: %v", err) 24 | } 25 | } else if runtime.GOOS == "darwin" { 26 | err := extractMacEmbeddedBinaries() 27 | if err != nil { 28 | log.Fatalf("error initializing iperf: %v\n", err) 29 | } 30 | } else { 31 | err := extractLinuxEmbeddedBinaries() 32 | if err != nil { 33 | log.Fatalf("error initializing iperf: %v", err) 34 | } 35 | } 36 | } 37 | 38 | func Cleanup() { 39 | os.RemoveAll(binaryDir) 40 | } 41 | 42 | func ExtractBinaries() (err error) { 43 | files := []string{"cygwin1.dll", "iperf3.exe", "iperf3", "iperf3.app"} 44 | err = extractEmbeddedBinaries(files) 45 | fmt.Printf("files extracted to %s\n", binaryDir) 46 | return err 47 | } 48 | 49 | func extractWindowsEmbeddedBinaries() (err error) { 50 | files := []string{"cygwin1.dll", "iperf3.exe"} 51 | err = extractEmbeddedBinaries(files) 52 | binaryLocation = path.Join(binaryDir, "iperf3.exe") 53 | return err 54 | } 55 | 56 | func extractLinuxEmbeddedBinaries() (err error) { 57 | files := []string{"iperf3"} 58 | err = extractEmbeddedBinaries(files) 59 | binaryLocation = path.Join(binaryDir, "iperf3") 60 | return err 61 | } 62 | 63 | func extractMacEmbeddedBinaries() (err error) { 64 | files := []string{"iperf3.app"} 65 | err = extractEmbeddedBinaries(files) 66 | binaryLocation = path.Join(binaryDir, "iperf3.app") 67 | return err 68 | } 69 | 70 | func extractEmbeddedBinaries(files []string) (err error) { 71 | binaryDir, err = ioutil.TempDir("", "goiperf") 72 | if err != nil { 73 | return fmt.Errorf("failed to create temporary iperf directory: %v", err) 74 | } 75 | for _, file := range files { 76 | data, err := Asset(file) 77 | if err != nil { 78 | return fmt.Errorf("failed to extract embedded iperf: %v", err) 79 | } 80 | err = ioutil.WriteFile(path.Join(binaryDir, file), data, 0755) 81 | if err != nil { 82 | return fmt.Errorf("failed to save embedded iperf: %v", err) 83 | } 84 | if Debug { 85 | log.Printf("extracted file: %s\n", path.Join(binaryDir, file)) 86 | } 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /execute.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os/exec" 7 | "strings" 8 | "syscall" 9 | ) 10 | 11 | func ExecuteAsync(cmd string) (outPipe io.ReadCloser, errPipe io.ReadCloser, exitCode chan int, err error) { 12 | exitCode = make(chan int) 13 | cmdParts := strings.Fields(cmd) 14 | binary, err := exec.LookPath(cmdParts[0]) 15 | if err != nil { 16 | return nil, nil, nil, err 17 | } 18 | exe := exec.Command(binary, cmdParts[1:]...) 19 | outPipe, err = exe.StdoutPipe() 20 | if err != nil { 21 | return nil, nil, nil, err 22 | } 23 | errPipe, err = exe.StderrPipe() 24 | if err != nil { 25 | return nil, nil, nil, err 26 | } 27 | err = exe.Start() 28 | if err != nil { 29 | return nil, nil, nil, err 30 | } 31 | go func() { 32 | if err := exe.Wait(); err != nil { 33 | if exiterr, ok := err.(*exec.ExitError); ok { 34 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 35 | exitCode <- status.ExitStatus() 36 | } 37 | } 38 | } else { 39 | exitCode <- 0 40 | } 41 | }() 42 | return outPipe, errPipe, exitCode, nil 43 | } 44 | 45 | func ExecuteAsyncWithCancel(cmd string) (stdOut io.ReadCloser, stdErr io.ReadCloser, exitCode chan int, cancelToken context.CancelFunc, pid int, err error) { 46 | return ExecuteAsyncWithCancelReadIndicator(cmd, nil) 47 | } 48 | 49 | func ExecuteAsyncWithCancelReadIndicator(cmd string, readIndicator chan interface{}) (stdOut io.ReadCloser, stdErr io.ReadCloser, exitCode chan int, cancelToken context.CancelFunc, pid int, err error) { 50 | return executeAsyncWithCancel(cmd, readIndicator) 51 | } 52 | 53 | func executeAsyncWithCancel(cmd string, readIndicator chan interface{}) (stdOut io.ReadCloser, stdErr io.ReadCloser, exitCode chan int, cancelToken context.CancelFunc, pid int, err error) { 54 | exitCode = make(chan int) 55 | ctx, cancel := context.WithCancel(context.Background()) 56 | cmdParts := strings.Fields(cmd) 57 | binary, err := exec.LookPath(cmdParts[0]) 58 | if err != nil { 59 | defer cancel() 60 | return nil, nil, nil, nil, -1, err 61 | } 62 | exe := exec.CommandContext(ctx, binary, cmdParts[1:]...) 63 | stdOut, err = exe.StdoutPipe() 64 | if err != nil { 65 | defer cancel() 66 | return nil, nil, nil, nil, -1, err 67 | } 68 | stdErr, err = exe.StderrPipe() 69 | if err != nil { 70 | defer cancel() 71 | return nil, nil, nil, nil, -1, err 72 | } 73 | err = exe.Start() 74 | if err != nil { 75 | defer cancel() 76 | return nil, nil, nil, nil, -1, err 77 | } 78 | go func() { 79 | // Note: Wait() will close the Stdout/Stderr and in some cases can do it before we read. In order to prevent 80 | // this we need to actually wait until the caller has finished reading. 81 | if readIndicator != nil { 82 | <- readIndicator 83 | } 84 | if err := exe.Wait(); err != nil { 85 | if exiterr, ok := err.(*exec.ExitError); ok { 86 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 87 | exitCode <- status.ExitStatus() 88 | } 89 | } 90 | } else { 91 | exitCode <- 0 92 | } 93 | }() 94 | return stdOut, stdErr, exitCode, cancel, exe.Process.Pid, nil 95 | } -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/google/uuid" 8 | "io" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var ( 14 | defaultPort = 5201 15 | defaultInterval = 1 16 | defaultJSON = true 17 | ) 18 | 19 | func NewServer() *Server { 20 | s := &Server{ 21 | Options: &ServerOptions{ 22 | Port: &defaultPort, 23 | Interval: &defaultInterval, 24 | JSON: &defaultJSON, 25 | }, 26 | } 27 | s.Id = uuid.New().String() 28 | return s 29 | } 30 | 31 | type ServerOptions struct { 32 | OneOff *bool `json:"one_off" yaml:"one_off" xml:"one_off"` 33 | Port *int `json:"port" yaml:"port" xml:"port"` 34 | Format *rune `json:"format" yaml:"format" xml:"format"` 35 | Interval *int `json:"interval" yaml:"interval" xml:"interval"` 36 | JSON *bool `json:"json" yaml:"json" xml:"json"` 37 | LogFile *string `json:"log_file" yaml:"log_file" xml:"log_file"` 38 | } 39 | 40 | type Server struct { 41 | Id string `json:"id" yaml:"id" xml:"id"` 42 | Running bool `json:"running" yaml:"running" xml:"running"` 43 | Options *ServerOptions `json:"-" yaml:"-" xml:"-"` 44 | ExitCode *int `json:"exit_code" yaml:"exit_code" xml:"exit_code"` 45 | Debug bool `json:"-" yaml:"-" xml:"-"` 46 | StdOut bool `json:"-" yaml:"-" xml:"-"` 47 | outputStream io.ReadCloser `json:"output_stream" yaml:"output_stream" xml:"output_stream"` 48 | errorStream io.ReadCloser `json:"error_stream" yaml:"error_stream" xml:"error_stream"` 49 | cancel context.CancelFunc `json:"cancel" yaml:"cancel" xml:"cancel"` 50 | } 51 | 52 | func (s *Server) LoadOptionsJSON(jsonStr string) (err error) { 53 | return json.Unmarshal([]byte(jsonStr), s.Options) 54 | } 55 | 56 | func (s *Server) LoadOptions(options *ServerOptions) { 57 | s.Options = options 58 | } 59 | 60 | func (s *Server) commandString() (cmd string, err error) { 61 | builder := strings.Builder{} 62 | fmt.Fprintf(&builder, "%s -s", binaryLocation) 63 | 64 | if s.Options.OneOff != nil && s.OneOff() == true { 65 | builder.WriteString(" --one-off") 66 | } 67 | 68 | if s.Options.Port != nil { 69 | fmt.Fprintf(&builder, " --port %d", s.Port()) 70 | } 71 | 72 | if s.Options.Format != nil { 73 | fmt.Fprintf(&builder, " --format %c", s.Format()) 74 | } 75 | 76 | if s.Options.Interval != nil { 77 | fmt.Fprintf(&builder, " --interval %d", s.Interval()) 78 | } 79 | 80 | if s.Options.JSON != nil && s.JSON() == true { 81 | builder.WriteString(" --json") 82 | } 83 | 84 | if s.Options.LogFile != nil && s.LogFile() != "" { 85 | fmt.Fprintf(&builder, " --logfile %s --forceflush", s.LogFile()) 86 | } 87 | 88 | return builder.String(), nil 89 | } 90 | 91 | func (s *Server) OneOff() bool { 92 | if s.Options.OneOff == nil { 93 | return false 94 | } 95 | return *s.Options.OneOff 96 | } 97 | 98 | func (s *Server) SetOneOff(oneOff bool) { 99 | s.Options.OneOff = &oneOff 100 | } 101 | 102 | func (s *Server) Port() int { 103 | if s.Options.Port == nil { 104 | return defaultPort 105 | } 106 | return *s.Options.Port 107 | } 108 | 109 | func (s *Server) SetPort(port int) { 110 | s.Options.Port = &port 111 | } 112 | 113 | func (s *Server) Format() rune { 114 | if s.Options.Format == nil { 115 | return ' ' 116 | } 117 | return *s.Options.Format 118 | } 119 | 120 | func (s *Server) SetFormat(format rune) { 121 | s.Options.Format = &format 122 | } 123 | 124 | func (s *Server) Interval() int { 125 | if s.Options.Interval == nil { 126 | return defaultInterval 127 | } 128 | return *s.Options.Interval 129 | } 130 | 131 | func (s *Server) JSON() bool { 132 | if s.Options.JSON == nil { 133 | return false 134 | } 135 | return *s.Options.JSON 136 | } 137 | 138 | func (s *Server) SetJSON(json bool) { 139 | s.Options.JSON = &json 140 | } 141 | 142 | func (s *Server) LogFile() string { 143 | if s.Options.LogFile == nil { 144 | return "" 145 | } 146 | return *s.Options.LogFile 147 | } 148 | 149 | func (s *Server) SetLogFile(filename string) { 150 | s.Options.LogFile = &filename 151 | } 152 | 153 | func (s *Server) Start() (err error) { 154 | _, err = s.start() 155 | return err 156 | } 157 | 158 | func (s *Server) StartEx() (pid int, err error) { 159 | return s.start() 160 | } 161 | 162 | func (s *Server) start() (pid int, err error) { 163 | cmd, err := s.commandString() 164 | if err != nil { 165 | return -1, err 166 | } 167 | var exit chan int 168 | 169 | if s.Debug { 170 | fmt.Printf("executing command: %s\n", cmd) 171 | } 172 | s.outputStream, s.errorStream, exit, s.cancel, pid, err = ExecuteAsyncWithCancel(cmd) 173 | 174 | if err != nil { 175 | return -1, err 176 | } 177 | s.Running = true 178 | 179 | go func() { 180 | ds := DebugScanner{Silent: !s.StdOut} 181 | ds.Scan(s.outputStream) 182 | }() 183 | go func() { 184 | ds := DebugScanner{Silent: !s.Debug} 185 | ds.Scan(s.errorStream) 186 | }() 187 | 188 | go func() { 189 | exitCode := <-exit 190 | s.ExitCode = &exitCode 191 | s.Running = false 192 | }() 193 | return pid,nil 194 | } 195 | 196 | func (s *Server) Stop() { 197 | if s.Running && s.cancel != nil { 198 | s.cancel() 199 | time.Sleep(100 * time.Millisecond) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /controller.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | "sync" 9 | "time" 10 | 11 | api "github.com/BGrewell/go-iperf/api/go" 12 | "google.golang.org/grpc" 13 | ) 14 | 15 | func NewController(port int) (controller *Controller, err error) { 16 | c := &Controller{ 17 | Port: port, 18 | clients: make(map[string]*Client), 19 | servers: make(map[string]*Server), 20 | clientLock: sync.Mutex{}, 21 | serverLock: sync.Mutex{}, 22 | } 23 | err = c.startListener() 24 | 25 | return c, err 26 | } 27 | 28 | // Controller is a helper in the go-iperf package that is designed to run on both the client and the server side. On the 29 | // server side it listens for new gRPC connections, when a connection is made by a client the client can tell it to 30 | // start a new iperf server instance. It will start a instance on an unused port and return the port number to the 31 | // client. This allows the entire iperf setup and session to be performed from the client side. 32 | type Controller struct { 33 | api.UnimplementedCommandServer 34 | Port int 35 | cmdClient api.CommandClient 36 | clientLock sync.Mutex 37 | serverLock sync.Mutex 38 | clients map[string]*Client 39 | servers map[string]*Server 40 | } 41 | 42 | // StartServer is the handler for the gRPC function StartServer() 43 | func (c *Controller) GrpcRequestServer(context.Context, *api.StartServerRequest) (*api.StartServerResponse, error) { 44 | srv, err := c.NewServer() 45 | srv.SetOneOff(true) 46 | if err != nil { 47 | return nil, err 48 | } 49 | err = srv.Start() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | c.serverLock.Lock() 55 | c.servers[srv.Id] = srv 56 | c.serverLock.Unlock() 57 | 58 | reply := &api.StartServerResponse{ 59 | Id: srv.Id, 60 | ListenPort: int32(srv.Port()), 61 | } 62 | 63 | return reply, nil 64 | } 65 | 66 | // StartListener starts a command listener which is used to accept gRPC connections from another go-iperf controller 67 | func (c *Controller) startListener() (err error) { 68 | 69 | listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", "0.0.0.0", c.Port)) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | gs := grpc.NewServer() 75 | api.RegisterCommandServer(gs, c) 76 | 77 | go func() { 78 | err := gs.Serve(listener) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | }() 83 | 84 | time.Sleep(250 * time.Millisecond) 85 | return nil 86 | } 87 | 88 | // NewServer gets a new instance of an iperf server on a free port 89 | func (c *Controller) NewServer() (server *Server, err error) { 90 | freePort, err := GetUnusedTcpPort() 91 | s := NewServer() 92 | s.SetPort(freePort) 93 | c.serverLock.Lock() 94 | c.servers[s.Id] = s 95 | c.serverLock.Unlock() 96 | return s, nil 97 | } 98 | 99 | // StopServer shuts down an iperf server and frees any actively used resources 100 | func (c *Controller) StopServer(id string) (err error) { 101 | c.serverLock.Lock() 102 | delete(c.servers, id) 103 | c.serverLock.Unlock() 104 | return nil 105 | } 106 | 107 | // NewClient gets a new instance of an iperf client and also starts up a matched iperf server instance on the specified 108 | // serverAddr. If it fails to connect to the gRPC interface of the controller on the remote side it will return an error 109 | func (c *Controller) NewClient(serverAddr string) (client *Client, err error) { 110 | grpc, err := GetConnectedClient(serverAddr, c.Port) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) 116 | defer cancel() 117 | reply, err := grpc.GrpcRequestServer(ctx, &api.StartServerRequest{}) 118 | srvPort := int(reply.ListenPort) 119 | fmt.Printf("[!] server is listening on port %d\n", srvPort) 120 | 121 | cli := NewClient(serverAddr) 122 | cli.SetPort(srvPort) 123 | c.clientLock.Lock() 124 | c.clients[cli.Id] = cli 125 | c.clientLock.Unlock() 126 | 127 | return cli, nil 128 | } 129 | 130 | // StopClient will clean up the server side connection and shut down any actively used resources 131 | func (c *Controller) StopClient(id string) (err error) { 132 | c.clientLock.Lock() 133 | delete(c.clients, id) 134 | c.clientLock.Unlock() 135 | return nil 136 | } 137 | 138 | func GetConnectedClient(addr string, port int) (client api.CommandClient, err error) { 139 | conn, err := grpc.Dial(fmt.Sprintf("%s:%d", addr, port), grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(time.Second*2)) 140 | if err != nil { 141 | return nil, err 142 | } 143 | client = api.NewCommandClient(conn) 144 | return client, nil 145 | } 146 | 147 | func GetUnusedTcpPort() (int, error) { 148 | addr, err := net.ResolveTCPAddr("tcp", "localhost:0") 149 | if err != nil { 150 | return 0, err 151 | } 152 | 153 | l, err := net.ListenTCP("tcp", addr) 154 | if err != nil { 155 | return 0, err 156 | } 157 | 158 | defer l.Close() 159 | return l.Addr().(*net.TCPAddr).Port, nil 160 | 161 | } 162 | 163 | //func GetUnusedUdpPort() (int, error) { 164 | // addr, err := net.ResolveUDPAddr("udp", "localhost:0") 165 | // if err != nil { 166 | // return 0, err 167 | // } 168 | // 169 | // l, err := net.ListenUDP("udp", addr) 170 | // if err != nil { 171 | // return 0, err 172 | // } 173 | // 174 | // defer l.Close() 175 | // return l.LocalAddr().(*net.UDPAddr).Port, nil 176 | //} -------------------------------------------------------------------------------- /reporter_darwin.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "fmt" 5 | "github.com/BGrewell/go-conversions" 6 | "github.com/BGrewell/tail" 7 | "log" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | /* 14 | Connecting to host 10.254.100.100, port 5201 15 | [ 4] local 192.168.3.182 port 54104 connected to 10.254.100.100 port 5201 16 | [ ID] Interval Transfer Bandwidth Retr Cwnd 17 | [ 4] 0.00-1.00 sec 109 MBytes 913 Mbits/sec 13 634 KBytes (omitted) 18 | [ 4] 1.00-2.00 sec 110 MBytes 927 Mbits/sec 7 550 KBytes (omitted) 19 | [ 4] 2.00-3.00 sec 109 MBytes 918 Mbits/sec 6 559 KBytes (omitted) 20 | [ 4] 3.00-4.00 sec 111 MBytes 930 Mbits/sec 6 690 KBytes (omitted) 21 | [ 4] 4.00-5.00 sec 111 MBytes 933 Mbits/sec 0 803 KBytes (omitted) 22 | [ 4] 5.00-6.00 sec 111 MBytes 933 Mbits/sec 6 673 KBytes (omitted) 23 | [ 4] 6.00-7.00 sec 111 MBytes 932 Mbits/sec 6 605 KBytes (omitted) 24 | [ 4] 7.00-8.00 sec 110 MBytes 925 Mbits/sec 0 732 KBytes (omitted) 25 | [ 4] 8.00-9.00 sec 111 MBytes 932 Mbits/sec 0 840 KBytes (omitted) 26 | [ 4] 9.00-10.00 sec 110 MBytes 923 Mbits/sec 6 690 KBytes (omitted) 27 | [ 4] 0.00-1.00 sec 111 MBytes 928 Mbits/sec 6 618 KBytes 28 | [ 4] 1.00-2.00 sec 111 MBytes 931 Mbits/sec 0 745 KBytes 29 | [ 4] 2.00-3.00 sec 111 MBytes 929 Mbits/sec 11 614 KBytes 30 | [ 4] 3.00-4.00 sec 110 MBytes 922 Mbits/sec 6 551 KBytes 31 | [ 4] 4.00-5.00 sec 111 MBytes 933 Mbits/sec 6 519 KBytes 32 | [ 4] 5.00-6.00 sec 111 MBytes 928 Mbits/sec 0 663 KBytes 33 | [ 4] 6.00-7.00 sec 111 MBytes 932 Mbits/sec 0 783 KBytes 34 | [ 4] 7.00-8.00 sec 111 MBytes 933 Mbits/sec 6 656 KBytes 35 | [ 4] 8.00-9.00 sec 111 MBytes 933 Mbits/sec 6 598 KBytes 36 | [ 4] 9.00-10.00 sec 110 MBytes 925 Mbits/sec 0 728 KBytes 37 | [ 4] 10.00-11.00 sec 111 MBytes 933 Mbits/sec 0 839 KBytes 38 | [ 4] 11.00-12.00 sec 109 MBytes 918 Mbits/sec 6 680 KBytes 39 | [ 4] 12.00-12.24 sec 25.0 MBytes 888 Mbits/sec 0 711 KBytes 40 | - - - - - - - - - - - - - - - - - - - - - - - - - 41 | [ ID] Interval Transfer Bandwidth Retr 42 | [ 4] 0.00-12.24 sec 1.32 GBytes 928 Mbits/sec 47 sender 43 | [ 4] 0.00-12.24 sec 0.00 Bytes 0.00 bits/sec receiver 44 | 45 | iperf Done. 46 | 47 | */ 48 | func (r *Reporter) runLogProcessor() { 49 | var err error 50 | r.tailer, err = tail.TailFile(r.LogFile, tail.Config{ 51 | Follow: true, 52 | ReOpen: true, 53 | Poll: false, // on linux we don't need to poll as the fsnotify works properly 54 | MustExist: true, 55 | }) 56 | if err != nil { 57 | log.Fatalf("failed to tail log file: %v", err) 58 | } 59 | 60 | for { 61 | select { 62 | case line := <-r.tailer.Lines: 63 | if line == nil { 64 | continue 65 | } 66 | if len(line.Text) > 5 { 67 | id := line.Text[1:4] 68 | stream, err := strconv.Atoi(strings.TrimSpace(id)) 69 | if err != nil { 70 | continue 71 | } 72 | fields := strings.Fields(line.Text[5:]) 73 | if len(fields) >= 9 { 74 | if fields[0] == "local" { 75 | continue 76 | } 77 | timeFields := strings.Split(fields[0], "-") 78 | start, err := strconv.ParseFloat(timeFields[0], 32) 79 | if err != nil { 80 | log.Printf("failed to convert start time: %s\n", err) 81 | } 82 | end, err := strconv.ParseFloat(timeFields[1], 32) 83 | transferedStr := fmt.Sprintf("%s%s", fields[2], fields[3]) 84 | transferedBytes, err := conversions.StringBitRateToInt(transferedStr) 85 | if err != nil { 86 | log.Printf("failed to convert units: %s\n", err) 87 | } 88 | transferedBytes = transferedBytes / 8 89 | rateStr := fmt.Sprintf("%s%s", fields[4], fields[5]) 90 | rate, err := conversions.StringBitRateToInt(rateStr) 91 | if err != nil { 92 | log.Printf("failed to convert units: %s\n", err) 93 | } 94 | retrans, err := strconv.Atoi(fields[6]) 95 | if err != nil { 96 | log.Printf("failed to convert units: %s\n", err) 97 | } 98 | cwndStr := fmt.Sprintf("%s%s", fields[7], fields[8]) 99 | cwnd, err := conversions.StringBitRateToInt(cwndStr) 100 | if err != nil { 101 | log.Printf("failed to convert units: %s\n", err) 102 | } 103 | cwnd = cwnd / 8 104 | omitted := false 105 | if len(fields) >= 10 && fields[9] == "(omitted)" { 106 | omitted = true 107 | } 108 | report := &StreamIntervalReport{ 109 | Socket: stream, 110 | StartInterval: float32(start), 111 | EndInterval: float32(end), 112 | Seconds: float32(end - start), 113 | Bytes: int(transferedBytes), 114 | BitsPerSecond: float64(rate), 115 | Retransmissions: retrans, 116 | CongestionWindow: int(cwnd), 117 | Omitted: omitted, 118 | } 119 | r.ReportingChannel <- report 120 | } 121 | } 122 | case <-time.After(100 * time.Millisecond): 123 | if !r.running { 124 | return 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /reporter_linux.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "fmt" 5 | "github.com/BGrewell/go-conversions" 6 | "github.com/BGrewell/tail" 7 | "log" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | /* 14 | Connecting to host 10.254.100.100, port 5201 15 | [ 4] local 192.168.3.182 port 54104 connected to 10.254.100.100 port 5201 16 | [ ID] Interval Transfer Bandwidth Retr Cwnd 17 | [ 4] 0.00-1.00 sec 109 MBytes 913 Mbits/sec 13 634 KBytes (omitted) 18 | [ 4] 1.00-2.00 sec 110 MBytes 927 Mbits/sec 7 550 KBytes (omitted) 19 | [ 4] 2.00-3.00 sec 109 MBytes 918 Mbits/sec 6 559 KBytes (omitted) 20 | [ 4] 3.00-4.00 sec 111 MBytes 930 Mbits/sec 6 690 KBytes (omitted) 21 | [ 4] 4.00-5.00 sec 111 MBytes 933 Mbits/sec 0 803 KBytes (omitted) 22 | [ 4] 5.00-6.00 sec 111 MBytes 933 Mbits/sec 6 673 KBytes (omitted) 23 | [ 4] 6.00-7.00 sec 111 MBytes 932 Mbits/sec 6 605 KBytes (omitted) 24 | [ 4] 7.00-8.00 sec 110 MBytes 925 Mbits/sec 0 732 KBytes (omitted) 25 | [ 4] 8.00-9.00 sec 111 MBytes 932 Mbits/sec 0 840 KBytes (omitted) 26 | [ 4] 9.00-10.00 sec 110 MBytes 923 Mbits/sec 6 690 KBytes (omitted) 27 | [ 4] 0.00-1.00 sec 111 MBytes 928 Mbits/sec 6 618 KBytes 28 | [ 4] 1.00-2.00 sec 111 MBytes 931 Mbits/sec 0 745 KBytes 29 | [ 4] 2.00-3.00 sec 111 MBytes 929 Mbits/sec 11 614 KBytes 30 | [ 4] 3.00-4.00 sec 110 MBytes 922 Mbits/sec 6 551 KBytes 31 | [ 4] 4.00-5.00 sec 111 MBytes 933 Mbits/sec 6 519 KBytes 32 | [ 4] 5.00-6.00 sec 111 MBytes 928 Mbits/sec 0 663 KBytes 33 | [ 4] 6.00-7.00 sec 111 MBytes 932 Mbits/sec 0 783 KBytes 34 | [ 4] 7.00-8.00 sec 111 MBytes 933 Mbits/sec 6 656 KBytes 35 | [ 4] 8.00-9.00 sec 111 MBytes 933 Mbits/sec 6 598 KBytes 36 | [ 4] 9.00-10.00 sec 110 MBytes 925 Mbits/sec 0 728 KBytes 37 | [ 4] 10.00-11.00 sec 111 MBytes 933 Mbits/sec 0 839 KBytes 38 | [ 4] 11.00-12.00 sec 109 MBytes 918 Mbits/sec 6 680 KBytes 39 | [ 4] 12.00-12.24 sec 25.0 MBytes 888 Mbits/sec 0 711 KBytes 40 | - - - - - - - - - - - - - - - - - - - - - - - - - 41 | [ ID] Interval Transfer Bandwidth Retr 42 | [ 4] 0.00-12.24 sec 1.32 GBytes 928 Mbits/sec 47 sender 43 | [ 4] 0.00-12.24 sec 0.00 Bytes 0.00 bits/sec receiver 44 | 45 | iperf Done. 46 | 47 | */ 48 | func (r *Reporter) runLogProcessor() { 49 | var err error 50 | r.tailer, err = tail.TailFile(r.LogFile, tail.Config{ 51 | Follow: true, 52 | ReOpen: true, 53 | Poll: false, // on linux we don't need to poll as the fsnotify works properly 54 | MustExist: true, 55 | }) 56 | if err != nil { 57 | log.Fatalf("failed to tail log file: %v", err) 58 | } 59 | 60 | for { 61 | select { 62 | case line := <-r.tailer.Lines: 63 | if line == nil { 64 | continue 65 | } 66 | if len(line.Text) > 5 { 67 | id := line.Text[1:4] 68 | stream, err := strconv.Atoi(strings.TrimSpace(id)) 69 | if err != nil { 70 | continue 71 | } 72 | fields := strings.Fields(line.Text[5:]) 73 | if len(fields) >= 9 { 74 | if fields[0] == "local" { 75 | continue 76 | } 77 | timeFields := strings.Split(fields[0], "-") 78 | start, err := strconv.ParseFloat(timeFields[0], 32) 79 | if err != nil { 80 | log.Printf("failed to convert start time: %s\n", err) 81 | } 82 | end, err := strconv.ParseFloat(timeFields[1], 32) 83 | transferedStr := fmt.Sprintf("%s%s", fields[2], fields[3]) 84 | transferedBytes, err := conversions.StringBitRateToInt(transferedStr) 85 | if err != nil { 86 | log.Printf("failed to convert units: %s\n", err) 87 | } 88 | transferedBytes = transferedBytes / 8 89 | rateStr := fmt.Sprintf("%s%s", fields[4], fields[5]) 90 | rate, err := conversions.StringBitRateToInt(rateStr) 91 | if err != nil { 92 | log.Printf("failed to convert units: %s\n", err) 93 | } 94 | retrans, err := strconv.Atoi(fields[6]) 95 | if err != nil { 96 | log.Printf("failed to convert units: %s\n", err) 97 | } 98 | cwndStr := fmt.Sprintf("%s%s", fields[7], fields[8]) 99 | cwnd, err := conversions.StringBitRateToInt(cwndStr) 100 | if err != nil { 101 | log.Printf("failed to convert units: %s\n", err) 102 | } 103 | cwnd = cwnd / 8 104 | omitted := false 105 | if len(fields) >= 10 && fields[9] == "(omitted)" { 106 | omitted = true 107 | } 108 | report := &StreamIntervalReport{ 109 | Socket: stream, 110 | StartInterval: float32(start), 111 | EndInterval: float32(end), 112 | Seconds: float32(end - start), 113 | Bytes: int(transferedBytes), 114 | BitsPerSecond: float64(rate), 115 | Retransmissions: retrans, 116 | CongestionWindow: int(cwnd), 117 | Omitted: omitted, 118 | } 119 | r.ReportingChannel <- report 120 | } 121 | } 122 | case <-time.After(100 * time.Millisecond): 123 | if !r.running { 124 | return 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 25 | 26 | 27 | 28 | 30 | 31 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 1614708953320 121 | 125 | 126 | 127 | 128 | 130 | 131 | true 132 | 133 | -------------------------------------------------------------------------------- /sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "start":{ 3 | "connected":[ 4 | { 5 | "socket":4, 6 | "local_host":"127.0.0.1", 7 | "local_port":64200, 8 | "remote_host":"127.0.0.1", 9 | "remote_port":5201 10 | } 11 | ], 12 | "version":"iperf 3.1.3", 13 | "system_info":"CYGWIN_NT-10.0 BGrewell-MOBL3 2.5.1(0.297/5/3) 2016-04-21 22:14 x86_64", 14 | "timestamp":{ 15 | "time":"Tue, 02 Mar 2021 18:28:14 GMT", 16 | "timesecs":1614709694 17 | }, 18 | "connecting_to":{ 19 | "host":"127.0.0.1", 20 | "port":5201 21 | }, 22 | "cookie":"BGrewell-MOBL3.1614709694.715824.0bf", 23 | "tcp_mss_default":0, 24 | "test_start":{ 25 | "protocol":"TCP", 26 | "num_streams":1, 27 | "blksize":131072, 28 | "omit":0, 29 | "duration":10, 30 | "bytes":0, 31 | "blocks":0, 32 | "reverse":0 33 | } 34 | }, 35 | "intervals":[ 36 | { 37 | "streams":[ 38 | { 39 | "socket":4, 40 | "start":0, 41 | "end":1.000554, 42 | "seconds":1.000554, 43 | "bytes":1635647488, 44 | "bits_per_second":1.307794e+10, 45 | "omitted":false 46 | } 47 | ], 48 | "sum":{ 49 | "start":0, 50 | "end":1.000554, 51 | "seconds":1.000554, 52 | "bytes":1635647488, 53 | "bits_per_second":1.307794e+10, 54 | "omitted":false 55 | } 56 | }, 57 | { 58 | "streams":[ 59 | { 60 | "socket":4, 61 | "start":1.000554, 62 | "end":2.000056, 63 | "seconds":0.999502, 64 | "bytes":1527250944, 65 | "bits_per_second":1.222409e+10, 66 | "omitted":false 67 | } 68 | ], 69 | "sum":{ 70 | "start":1.000554, 71 | "end":2.000056, 72 | "seconds":0.999502, 73 | "bytes":1527250944, 74 | "bits_per_second":1.222409e+10, 75 | "omitted":false 76 | } 77 | }, 78 | { 79 | "streams":[ 80 | { 81 | "socket":4, 82 | "start":2.000056, 83 | "end":3.000353, 84 | "seconds":1.000297, 85 | "bytes":1712062464, 86 | "bits_per_second":1.369244e+10, 87 | "omitted":false 88 | } 89 | ], 90 | "sum":{ 91 | "start":2.000056, 92 | "end":3.000353, 93 | "seconds":1.000297, 94 | "bytes":1712062464, 95 | "bits_per_second":1.369244e+10, 96 | "omitted":false 97 | } 98 | }, 99 | { 100 | "streams":[ 101 | { 102 | "socket":4, 103 | "start":3.000353, 104 | "end":4.000669, 105 | "seconds":1.000316, 106 | "bytes":1730412544, 107 | "bits_per_second":1.383893e+10, 108 | "omitted":false 109 | } 110 | ], 111 | "sum":{ 112 | "start":3.000353, 113 | "end":4.000669, 114 | "seconds":1.000316, 115 | "bytes":1730412544, 116 | "bits_per_second":1.383893e+10, 117 | "omitted":false 118 | } 119 | }, 120 | { 121 | "streams":[ 122 | { 123 | "socket":4, 124 | "start":4.000669, 125 | "end":5.000021, 126 | "seconds":0.999352, 127 | "bytes":1777467392, 128 | "bits_per_second":1.422896e+10, 129 | "omitted":false 130 | } 131 | ], 132 | "sum":{ 133 | "start":4.000669, 134 | "end":5.000021, 135 | "seconds":0.999352, 136 | "bytes":1777467392, 137 | "bits_per_second":1.422896e+10, 138 | "omitted":false 139 | } 140 | }, 141 | { 142 | "streams":[ 143 | { 144 | "socket":4, 145 | "start":5.000021, 146 | "end":6.000122, 147 | "seconds":1.000101, 148 | "bytes":1673920512, 149 | "bits_per_second":1.339001e+10, 150 | "omitted":false 151 | } 152 | ], 153 | "sum":{ 154 | "start":5.000021, 155 | "end":6.000122, 156 | "seconds":1.000101, 157 | "bytes":1673920512, 158 | "bits_per_second":1.339001e+10, 159 | "omitted":false 160 | } 161 | }, 162 | { 163 | "streams":[ 164 | { 165 | "socket":4, 166 | "start":6.000122, 167 | "end":7.000517, 168 | "seconds":1.000395, 169 | "bytes":1585446912, 170 | "bits_per_second":1.267857e+10, 171 | "omitted":false 172 | } 173 | ], 174 | "sum":{ 175 | "start":6.000122, 176 | "end":7.000517, 177 | "seconds":1.000395, 178 | "bytes":1585446912, 179 | "bits_per_second":1.267857e+10, 180 | "omitted":false 181 | } 182 | }, 183 | { 184 | "streams":[ 185 | { 186 | "socket":4, 187 | "start":7.000517, 188 | "end":8.000053, 189 | "seconds":0.999536, 190 | "bytes":1642987520, 191 | "bits_per_second":1.315000e+10, 192 | "omitted":false 193 | } 194 | ], 195 | "sum":{ 196 | "start":7.000517, 197 | "end":8.000053, 198 | "seconds":0.999536, 199 | "bytes":1642987520, 200 | "bits_per_second":1.315000e+10, 201 | "omitted":false 202 | } 203 | }, 204 | { 205 | "streams":[ 206 | { 207 | "socket":4, 208 | "start":8.000053, 209 | "end":9.000105, 210 | "seconds":1.000052, 211 | "bytes":1547173888, 212 | "bits_per_second":1.237675e+10, 213 | "omitted":false 214 | } 215 | ], 216 | "sum":{ 217 | "start":8.000053, 218 | "end":9.000105, 219 | "seconds":1.000052, 220 | "bytes":1547173888, 221 | "bits_per_second":1.237675e+10, 222 | "omitted":false 223 | } 224 | }, 225 | { 226 | "streams":[ 227 | { 228 | "socket":4, 229 | "start":9.000105, 230 | "end":10.000386, 231 | "seconds":1.000281, 232 | "bytes":1773142016, 233 | "bits_per_second":1.418115e+10, 234 | "omitted":false 235 | } 236 | ], 237 | "sum":{ 238 | "start":9.000105, 239 | "end":10.000386, 240 | "seconds":1.000281, 241 | "bytes":1773142016, 242 | "bits_per_second":1.418115e+10, 243 | "omitted":false 244 | } 245 | } 246 | ], 247 | "end":{ 248 | "streams":[ 249 | { 250 | "sender":{ 251 | "socket":4, 252 | "start":0, 253 | "end":10.000386, 254 | "seconds":10.000386, 255 | "bytes":16605511680, 256 | "bits_per_second":1.328390e+10 257 | }, 258 | "receiver":{ 259 | "socket":4, 260 | "start":0, 261 | "end":10.000386, 262 | "seconds":10.000386, 263 | "bytes":16605511680, 264 | "bits_per_second":1.328390e+10 265 | } 266 | } 267 | ], 268 | "sum_sent":{ 269 | "start":0, 270 | "end":10.000386, 271 | "seconds":10.000386, 272 | "bytes":16605511680, 273 | "bits_per_second":1.328390e+10 274 | }, 275 | "sum_received":{ 276 | "start":0, 277 | "end":10.000386, 278 | "seconds":10.000386, 279 | "bytes":16605511680, 280 | "bits_per_second":1.328390e+10 281 | }, 282 | "cpu_utilization_percent":{ 283 | "host_total":83.983525, 284 | "host_user":5.155306, 285 | "host_system":78.828219, 286 | "remote_total":34.722681, 287 | "remote_user":7.224950, 288 | "remote_system":27.497731 289 | } 290 | } 291 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BGrewell/go-conversions v0.0.0-20201203155646-5e189e4ca087 h1:/ZR3IHtTSPqwzYQSfZYwGjrmDI5c1u2jEDDtT75Fmqw= 3 | github.com/BGrewell/go-conversions v0.0.0-20201203155646-5e189e4ca087/go.mod h1:XtXcz/MP04vhr6c6R/5gZPJZVQpbXlFsKTHr5yy/5sU= 4 | github.com/BGrewell/tail v1.0.0 h1:sG+Uvv+UApHtj5z+AWWB9i5m2SCH0RLfxYqXujYQo+Q= 5 | github.com/BGrewell/tail v1.0.0/go.mod h1:0PFYWAobUZKZLEYIxxmjFgnfvCLA600LkFbGO9KFIRA= 6 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 7 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 8 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 9 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 12 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 13 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 14 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 15 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 16 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 17 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 18 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 19 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 22 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 23 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 24 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 25 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 26 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 27 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 28 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 29 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 30 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 31 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 32 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 33 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 34 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 35 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 36 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 37 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 39 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 40 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 41 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 42 | github.com/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts= 43 | github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs= 44 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 45 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 48 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 49 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 50 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 51 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 52 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 53 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 54 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 55 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 56 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= 57 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 58 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 59 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 62 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 63 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= 65 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 67 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 68 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 70 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 71 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 72 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 73 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 75 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 76 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 77 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 78 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= 79 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 80 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 81 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 82 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 83 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 84 | google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY= 85 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 86 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 87 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 88 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 89 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 90 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 91 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 92 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 93 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 94 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 95 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 96 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 97 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 98 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 99 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 100 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 101 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 102 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 103 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 104 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 105 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 106 | -------------------------------------------------------------------------------- /report.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | ) 8 | 9 | type StreamInterval struct { 10 | Streams []*StreamIntervalReport `json:"streams"` 11 | Sum *StreamIntervalSumReport `json:"sum"` 12 | } 13 | 14 | func (si *StreamInterval) String() string { 15 | b, err := json.Marshal(si) 16 | if err != nil { 17 | return "error converting to json" 18 | } 19 | 20 | var pretty bytes.Buffer 21 | err = json.Indent(&pretty, b, "", " ") 22 | if err != nil { 23 | return "error converting json to indented format" 24 | } 25 | 26 | return string(pretty.Bytes()) 27 | } 28 | 29 | type StreamIntervalReport struct { 30 | Socket int `json:"socket"` 31 | StartInterval float32 `json:"start"` 32 | EndInterval float32 `json:"end"` 33 | Seconds float32 `json:"seconds"` 34 | Bytes int `json:"bytes"` 35 | BitsPerSecond float64 `json:"bits_per_second"` 36 | Retransmissions int `json:"retransmissions"` 37 | CongestionWindow int `json:"congestion_window"` 38 | Omitted bool `json:"omitted"` 39 | } 40 | 41 | func (sir *StreamIntervalReport) String() string { 42 | b, err := json.Marshal(sir) 43 | if err != nil { 44 | return "error converting to json" 45 | } 46 | 47 | var pretty bytes.Buffer 48 | err = json.Indent(&pretty, b, "", " ") 49 | if err != nil { 50 | return "error converting json to indented format" 51 | } 52 | 53 | return string(pretty.Bytes()) 54 | } 55 | 56 | type StreamIntervalSumReport struct { 57 | StartInterval float32 `json:"start"` 58 | EndInterval float32 `json:"end"` 59 | Seconds float32 `json:"seconds"` 60 | Bytes int `json:"bytes"` 61 | BitsPerSecond float64 `json:"bits_per_second"` 62 | Omitted bool `json:"omitted"` 63 | } 64 | 65 | func (sisr *StreamIntervalSumReport) String() string { 66 | b, err := json.Marshal(sisr) 67 | if err != nil { 68 | return "error converting to json" 69 | } 70 | 71 | var pretty bytes.Buffer 72 | err = json.Indent(&pretty, b, "", " ") 73 | if err != nil { 74 | return "error converting json to indented format" 75 | } 76 | 77 | return string(pretty.Bytes()) 78 | } 79 | 80 | type StreamEndReport struct { 81 | Sender TcpStreamEndReport `json:"sender"` 82 | Receiver TcpStreamEndReport `json:"receiver"` 83 | Udp UdpStreamEndReport `json:"udp"` 84 | } 85 | 86 | func (ser *StreamEndReport) String() string { 87 | b, err := json.Marshal(ser) 88 | if err != nil { 89 | return "error converting to json" 90 | } 91 | 92 | var pretty bytes.Buffer 93 | err = json.Indent(&pretty, b, "", " ") 94 | if err != nil { 95 | return "error converting json to indented format" 96 | } 97 | 98 | return string(pretty.Bytes()) 99 | } 100 | 101 | type UdpStreamEndReport struct { 102 | Socket int `json:"socket"` 103 | Start float32 `json:"start"` 104 | End float32 `json:"end"` 105 | Seconds float32 `json:"seconds"` 106 | Bytes int `json:"bytes"` 107 | BitsPerSecond float64 `json:"bits_per_second"` 108 | JitterMs float32 `json:"jitter_ms"` 109 | LostPackets int `json:"lost_packets"` 110 | Packets int `json:"packets"` 111 | LostPercent float32 `json:"lost_percent"` 112 | OutOfOrder int `json:"out_of_order"` 113 | } 114 | 115 | func (user *UdpStreamEndReport) String() string { 116 | b, err := json.Marshal(user) 117 | if err != nil { 118 | return "error converting to json" 119 | } 120 | 121 | var pretty bytes.Buffer 122 | err = json.Indent(&pretty, b, "", " ") 123 | if err != nil { 124 | return "error converting json to indented format" 125 | } 126 | 127 | return string(pretty.Bytes()) 128 | } 129 | 130 | type TcpStreamEndReport struct { 131 | Socket int `json:"socket"` 132 | Start float32 `json:"start"` 133 | End float32 `json:"end"` 134 | Seconds float32 `json:"seconds"` 135 | Bytes int `json:"bytes"` 136 | BitsPerSecond float64 `json:"bits_per_second"` 137 | } 138 | 139 | func (tser *TcpStreamEndReport) String() string { 140 | b, err := json.Marshal(tser) 141 | if err != nil { 142 | return "error converting to json" 143 | } 144 | 145 | var pretty bytes.Buffer 146 | err = json.Indent(&pretty, b, "", " ") 147 | if err != nil { 148 | return "error converting json to indented format" 149 | } 150 | 151 | return string(pretty.Bytes()) 152 | } 153 | 154 | type StreamEndSumReport struct { 155 | Start float32 `json:"start"` 156 | End float32 `json:"end"` 157 | Seconds float32 `json:"seconds"` 158 | Bytes int `json:"bytes"` 159 | BitsPerSecond float64 `json:"bits_per_second"` 160 | } 161 | 162 | func (sesr *StreamEndSumReport) String() string { 163 | b, err := json.Marshal(sesr) 164 | if err != nil { 165 | return "error converting to json" 166 | } 167 | 168 | var pretty bytes.Buffer 169 | err = json.Indent(&pretty, b, "", " ") 170 | if err != nil { 171 | return "error converting json to indented format" 172 | } 173 | 174 | return string(pretty.Bytes()) 175 | } 176 | 177 | type CpuUtilizationReport struct { 178 | HostTotal float32 `json:"host_total"` 179 | HostUser float32 `json:"host_user"` 180 | HostSystem float32 `json:"host_system"` 181 | RemoteTotal float32 `json:"remote_total"` 182 | RemoteUser float32 `json:"remote_user"` 183 | RemoteSystem float32 `json:"remote_system"` 184 | } 185 | 186 | func (cur *CpuUtilizationReport) String() string { 187 | b, err := json.Marshal(cur) 188 | if err != nil { 189 | return "error converting to json" 190 | } 191 | 192 | var pretty bytes.Buffer 193 | err = json.Indent(&pretty, b, "", " ") 194 | if err != nil { 195 | return "error converting json to indented format" 196 | } 197 | 198 | return string(pretty.Bytes()) 199 | } 200 | 201 | type ConnectionInfo struct { 202 | Socket int `json:"socket"` 203 | LocalHost string `json:"local_host"` 204 | LocalPort int `json:"local_port"` 205 | RemoteHost string `json:"remote_host"` 206 | RemotePort int `json:"remote_port"` 207 | } 208 | 209 | func (ci *ConnectionInfo) String() string { 210 | b, err := json.Marshal(ci) 211 | if err != nil { 212 | return "error converting to json" 213 | } 214 | 215 | var pretty bytes.Buffer 216 | err = json.Indent(&pretty, b, "", " ") 217 | if err != nil { 218 | return "error converting json to indented format" 219 | } 220 | 221 | return string(pretty.Bytes()) 222 | } 223 | 224 | type TimestampInfo struct { 225 | Time string `json:"time"` 226 | TimeSecs int `json:"timesecs"` 227 | } 228 | 229 | func (tsi *TimestampInfo) String() string { 230 | b, err := json.Marshal(tsi) 231 | if err != nil { 232 | return "error converting to json" 233 | } 234 | 235 | var pretty bytes.Buffer 236 | err = json.Indent(&pretty, b, "", " ") 237 | if err != nil { 238 | return "error converting json to indented format" 239 | } 240 | 241 | return string(pretty.Bytes()) 242 | } 243 | 244 | type ConnectingToInfo struct { 245 | Host string `json:"host"` 246 | Port int `json:"port"` 247 | } 248 | 249 | func (cti *ConnectingToInfo) String() string { 250 | b, err := json.Marshal(cti) 251 | if err != nil { 252 | return "error converting to json" 253 | } 254 | 255 | var pretty bytes.Buffer 256 | err = json.Indent(&pretty, b, "", " ") 257 | if err != nil { 258 | return "error converting json to indented format" 259 | } 260 | 261 | return string(pretty.Bytes()) 262 | } 263 | 264 | type TestStartInfo struct { 265 | Protocol string `json:"protocol"` 266 | NumStreams int `json:"num_streams"` 267 | BlkSize int `json:"blksize"` 268 | Omit int `json:"omit"` 269 | Duration int `json:"duration"` 270 | Bytes int `json:"bytes"` 271 | Blocks int `json:"blocks"` 272 | Reverse int `json:"reverse"` 273 | } 274 | 275 | func (tsi *TestStartInfo) String() string { 276 | b, err := json.Marshal(tsi) 277 | if err != nil { 278 | return "error converting to json" 279 | } 280 | 281 | var pretty bytes.Buffer 282 | err = json.Indent(&pretty, b, "", " ") 283 | if err != nil { 284 | return "error converting json to indented format" 285 | } 286 | 287 | return string(pretty.Bytes()) 288 | } 289 | 290 | type StartInfo struct { 291 | Connected []*ConnectionInfo `json:"connected"` 292 | Version string `json:"version"` 293 | SystemInfo string `json:"system_info"` 294 | Timestamp TimestampInfo `json:"timestamp"` 295 | ConnectingTo ConnectingToInfo `json:"connecting_to"` 296 | Cookie string `json:"cookie"` 297 | TcpMssDefault int `json:"tcp_mss_default"` 298 | TestStart TestStartInfo `json:"test_start"` 299 | } 300 | 301 | func (si *StartInfo) String() string { 302 | b, err := json.Marshal(si) 303 | if err != nil { 304 | return "error converting to json" 305 | } 306 | 307 | var pretty bytes.Buffer 308 | err = json.Indent(&pretty, b, "", " ") 309 | if err != nil { 310 | return "error converting json to indented format" 311 | } 312 | 313 | return string(pretty.Bytes()) 314 | } 315 | 316 | type EndInfo struct { 317 | Streams []*StreamEndReport `json:"streams"` 318 | SumSent StreamEndSumReport `json:"sum_sent"` 319 | SumReceived StreamEndSumReport `json:"sum_received"` 320 | CpuReport CpuUtilizationReport `json:"cpu_utilization_percent"` 321 | } 322 | 323 | func (ei *EndInfo) String() string { 324 | b, err := json.Marshal(ei) 325 | if err != nil { 326 | return "error converting to json" 327 | } 328 | 329 | var pretty bytes.Buffer 330 | err = json.Indent(&pretty, b, "", " ") 331 | if err != nil { 332 | return "error converting json to indented format" 333 | } 334 | 335 | return string(pretty.Bytes()) 336 | } 337 | 338 | type ServerReport struct { 339 | Start StartInfo `json:"start"` 340 | Intervals []*StreamInterval `json:"intervals"` 341 | End EndInfo `json:"end"` 342 | Error string `json:"error"` 343 | } 344 | 345 | func (sr *ServerReport) String() string { 346 | b, err := json.Marshal(sr) 347 | if err != nil { 348 | return "error converting to json" 349 | } 350 | 351 | var pretty bytes.Buffer 352 | err = json.Indent(&pretty, b, "", " ") 353 | if err != nil { 354 | return "error converting json to indented format" 355 | } 356 | 357 | return string(pretty.Bytes()) 358 | } 359 | 360 | type TestReport struct { 361 | Start StartInfo `json:"start"` 362 | Intervals []*StreamInterval `json:"intervals"` 363 | End EndInfo `json:"end"` 364 | Error string `json:"error"` 365 | ServerOutputJson ServerReport `json:"server_output_json"` 366 | } 367 | 368 | func (tr *TestReport) String() string { 369 | b, err := json.Marshal(tr) 370 | if err != nil { 371 | return "error converting to json" 372 | } 373 | 374 | var pretty bytes.Buffer 375 | err = json.Indent(&pretty, b, "", " ") 376 | if err != nil { 377 | return "error converting json to indented format" 378 | } 379 | 380 | return string(pretty.Bytes()) 381 | } 382 | 383 | func Loads(jsonStr string) (report *TestReport, err error) { 384 | r := &TestReport{} 385 | err = json.Unmarshal([]byte(jsonStr), r) 386 | return r, err 387 | } 388 | 389 | func Load(filename string) (report *TestReport, err error) { 390 | contents, err := ioutil.ReadFile(filename) 391 | if err != nil { 392 | return nil, err 393 | } 394 | return Loads(string(contents)) 395 | } 396 | -------------------------------------------------------------------------------- /api/go/control.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0-devel 4 | // protoc v3.6.1 5 | // source: control.proto 6 | 7 | package api 8 | 9 | import ( 10 | context "context" 11 | _ "github.com/golang/protobuf/ptypes/empty" 12 | grpc "google.golang.org/grpc" 13 | codes "google.golang.org/grpc/codes" 14 | status "google.golang.org/grpc/status" 15 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 16 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 17 | reflect "reflect" 18 | sync "sync" 19 | ) 20 | 21 | const ( 22 | // Verify that this generated code is sufficiently up-to-date. 23 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 24 | // Verify that runtime/protoimpl is sufficiently up-to-date. 25 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 26 | ) 27 | 28 | type StartServerRequest struct { 29 | state protoimpl.MessageState 30 | sizeCache protoimpl.SizeCache 31 | unknownFields protoimpl.UnknownFields 32 | } 33 | 34 | func (x *StartServerRequest) Reset() { 35 | *x = StartServerRequest{} 36 | if protoimpl.UnsafeEnabled { 37 | mi := &file_control_proto_msgTypes[0] 38 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 39 | ms.StoreMessageInfo(mi) 40 | } 41 | } 42 | 43 | func (x *StartServerRequest) String() string { 44 | return protoimpl.X.MessageStringOf(x) 45 | } 46 | 47 | func (*StartServerRequest) ProtoMessage() {} 48 | 49 | func (x *StartServerRequest) ProtoReflect() protoreflect.Message { 50 | mi := &file_control_proto_msgTypes[0] 51 | if protoimpl.UnsafeEnabled && x != nil { 52 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 53 | if ms.LoadMessageInfo() == nil { 54 | ms.StoreMessageInfo(mi) 55 | } 56 | return ms 57 | } 58 | return mi.MessageOf(x) 59 | } 60 | 61 | // Deprecated: Use StartServerRequest.ProtoReflect.Descriptor instead. 62 | func (*StartServerRequest) Descriptor() ([]byte, []int) { 63 | return file_control_proto_rawDescGZIP(), []int{0} 64 | } 65 | 66 | type StartServerResponse struct { 67 | state protoimpl.MessageState 68 | sizeCache protoimpl.SizeCache 69 | unknownFields protoimpl.UnknownFields 70 | 71 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 72 | ListenPort int32 `protobuf:"varint,2,opt,name=listen_port,json=listenPort,proto3" json:"listen_port,omitempty"` 73 | } 74 | 75 | func (x *StartServerResponse) Reset() { 76 | *x = StartServerResponse{} 77 | if protoimpl.UnsafeEnabled { 78 | mi := &file_control_proto_msgTypes[1] 79 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 80 | ms.StoreMessageInfo(mi) 81 | } 82 | } 83 | 84 | func (x *StartServerResponse) String() string { 85 | return protoimpl.X.MessageStringOf(x) 86 | } 87 | 88 | func (*StartServerResponse) ProtoMessage() {} 89 | 90 | func (x *StartServerResponse) ProtoReflect() protoreflect.Message { 91 | mi := &file_control_proto_msgTypes[1] 92 | if protoimpl.UnsafeEnabled && x != nil { 93 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 94 | if ms.LoadMessageInfo() == nil { 95 | ms.StoreMessageInfo(mi) 96 | } 97 | return ms 98 | } 99 | return mi.MessageOf(x) 100 | } 101 | 102 | // Deprecated: Use StartServerResponse.ProtoReflect.Descriptor instead. 103 | func (*StartServerResponse) Descriptor() ([]byte, []int) { 104 | return file_control_proto_rawDescGZIP(), []int{1} 105 | } 106 | 107 | func (x *StartServerResponse) GetId() string { 108 | if x != nil { 109 | return x.Id 110 | } 111 | return "" 112 | } 113 | 114 | func (x *StartServerResponse) GetListenPort() int32 { 115 | if x != nil { 116 | return x.ListenPort 117 | } 118 | return 0 119 | } 120 | 121 | var File_control_proto protoreflect.FileDescriptor 122 | 123 | var file_control_proto_rawDesc = []byte{ 124 | 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 125 | 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 126 | 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 127 | 0x6f, 0x22, 0x14, 0x0a, 0x12, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 128 | 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x46, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x72, 0x74, 129 | 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 130 | 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 131 | 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 132 | 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x32, 133 | 0x53, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x48, 0x0a, 0x11, 0x47, 0x72, 134 | 0x70, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 135 | 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 136 | 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 137 | 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 138 | 0x73, 0x65, 0x22, 0x00, 0x42, 0x6b, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x62, 0x65, 0x6e, 0x67, 139 | 0x72, 0x65, 0x77, 0x65, 0x6c, 0x6c, 0x2e, 0x67, 0x6f, 0x2d, 0x69, 0x70, 0x65, 0x72, 0x66, 0x2e, 140 | 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x42, 0x07, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 141 | 0x50, 0x01, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x42, 142 | 0x47, 0x72, 0x65, 0x77, 0x65, 0x6c, 0x6c, 0x2f, 0x67, 0x6f, 0x2d, 0x69, 0x70, 0x65, 0x72, 0x66, 143 | 0x2f, 0x61, 0x70, 0x69, 0xaa, 0x02, 0x1a, 0x42, 0x65, 0x6e, 0x47, 0x72, 0x65, 0x77, 0x65, 0x6c, 144 | 0x6c, 0x2e, 0x47, 0x6f, 0x49, 0x70, 0x65, 0x72, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 145 | 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 146 | } 147 | 148 | var ( 149 | file_control_proto_rawDescOnce sync.Once 150 | file_control_proto_rawDescData = file_control_proto_rawDesc 151 | ) 152 | 153 | func file_control_proto_rawDescGZIP() []byte { 154 | file_control_proto_rawDescOnce.Do(func() { 155 | file_control_proto_rawDescData = protoimpl.X.CompressGZIP(file_control_proto_rawDescData) 156 | }) 157 | return file_control_proto_rawDescData 158 | } 159 | 160 | var file_control_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 161 | var file_control_proto_goTypes = []interface{}{ 162 | (*StartServerRequest)(nil), // 0: api.StartServerRequest 163 | (*StartServerResponse)(nil), // 1: api.StartServerResponse 164 | } 165 | var file_control_proto_depIdxs = []int32{ 166 | 0, // 0: api.Command.GrpcRequestServer:input_type -> api.StartServerRequest 167 | 1, // 1: api.Command.GrpcRequestServer:output_type -> api.StartServerResponse 168 | 1, // [1:2] is the sub-list for method output_type 169 | 0, // [0:1] is the sub-list for method input_type 170 | 0, // [0:0] is the sub-list for extension type_name 171 | 0, // [0:0] is the sub-list for extension extendee 172 | 0, // [0:0] is the sub-list for field type_name 173 | } 174 | 175 | func init() { file_control_proto_init() } 176 | func file_control_proto_init() { 177 | if File_control_proto != nil { 178 | return 179 | } 180 | if !protoimpl.UnsafeEnabled { 181 | file_control_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 182 | switch v := v.(*StartServerRequest); i { 183 | case 0: 184 | return &v.state 185 | case 1: 186 | return &v.sizeCache 187 | case 2: 188 | return &v.unknownFields 189 | default: 190 | return nil 191 | } 192 | } 193 | file_control_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 194 | switch v := v.(*StartServerResponse); i { 195 | case 0: 196 | return &v.state 197 | case 1: 198 | return &v.sizeCache 199 | case 2: 200 | return &v.unknownFields 201 | default: 202 | return nil 203 | } 204 | } 205 | } 206 | type x struct{} 207 | out := protoimpl.TypeBuilder{ 208 | File: protoimpl.DescBuilder{ 209 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 210 | RawDescriptor: file_control_proto_rawDesc, 211 | NumEnums: 0, 212 | NumMessages: 2, 213 | NumExtensions: 0, 214 | NumServices: 1, 215 | }, 216 | GoTypes: file_control_proto_goTypes, 217 | DependencyIndexes: file_control_proto_depIdxs, 218 | MessageInfos: file_control_proto_msgTypes, 219 | }.Build() 220 | File_control_proto = out.File 221 | file_control_proto_rawDesc = nil 222 | file_control_proto_goTypes = nil 223 | file_control_proto_depIdxs = nil 224 | } 225 | 226 | // Reference imports to suppress errors if they are not otherwise used. 227 | var _ context.Context 228 | var _ grpc.ClientConnInterface 229 | 230 | // This is a compile-time assertion to ensure that this generated file 231 | // is compatible with the grpc package it is being compiled against. 232 | const _ = grpc.SupportPackageIsVersion6 233 | 234 | // CommandClient is the client API for Command service. 235 | // 236 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 237 | type CommandClient interface { 238 | GrpcRequestServer(ctx context.Context, in *StartServerRequest, opts ...grpc.CallOption) (*StartServerResponse, error) 239 | } 240 | 241 | type commandClient struct { 242 | cc grpc.ClientConnInterface 243 | } 244 | 245 | func NewCommandClient(cc grpc.ClientConnInterface) CommandClient { 246 | return &commandClient{cc} 247 | } 248 | 249 | func (c *commandClient) GrpcRequestServer(ctx context.Context, in *StartServerRequest, opts ...grpc.CallOption) (*StartServerResponse, error) { 250 | out := new(StartServerResponse) 251 | err := c.cc.Invoke(ctx, "/api.Command/GrpcRequestServer", in, out, opts...) 252 | if err != nil { 253 | return nil, err 254 | } 255 | return out, nil 256 | } 257 | 258 | // CommandServer is the server API for Command service. 259 | type CommandServer interface { 260 | GrpcRequestServer(context.Context, *StartServerRequest) (*StartServerResponse, error) 261 | } 262 | 263 | // UnimplementedCommandServer can be embedded to have forward compatible implementations. 264 | type UnimplementedCommandServer struct { 265 | } 266 | 267 | func (*UnimplementedCommandServer) GrpcRequestServer(context.Context, *StartServerRequest) (*StartServerResponse, error) { 268 | return nil, status.Errorf(codes.Unimplemented, "method GrpcRequestServer not implemented") 269 | } 270 | 271 | func RegisterCommandServer(s *grpc.Server, srv CommandServer) { 272 | s.RegisterService(&_Command_serviceDesc, srv) 273 | } 274 | 275 | func _Command_GrpcRequestServer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 276 | in := new(StartServerRequest) 277 | if err := dec(in); err != nil { 278 | return nil, err 279 | } 280 | if interceptor == nil { 281 | return srv.(CommandServer).GrpcRequestServer(ctx, in) 282 | } 283 | info := &grpc.UnaryServerInfo{ 284 | Server: srv, 285 | FullMethod: "/api.Command/GrpcRequestServer", 286 | } 287 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 288 | return srv.(CommandServer).GrpcRequestServer(ctx, req.(*StartServerRequest)) 289 | } 290 | return interceptor(ctx, in, info, handler) 291 | } 292 | 293 | var _Command_serviceDesc = grpc.ServiceDesc{ 294 | ServiceName: "api.Command", 295 | HandlerType: (*CommandServer)(nil), 296 | Methods: []grpc.MethodDesc{ 297 | { 298 | MethodName: "GrpcRequestServer", 299 | Handler: _Command_GrpcRequestServer_Handler, 300 | }, 301 | }, 302 | Streams: []grpc.StreamDesc{}, 303 | Metadata: "control.proto", 304 | } 305 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package iperf 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/google/uuid" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | func NewClient(host string) *Client { 17 | json := true 18 | proto := Protocol(PROTO_TCP) 19 | time := 10 20 | length := "128KB" 21 | streams := 1 22 | c := &Client{ 23 | Options: &ClientOptions{ 24 | JSON: &json, 25 | Proto: &proto, 26 | TimeSec: &time, 27 | Length: &length, 28 | Streams: &streams, 29 | Host: &host, 30 | }, 31 | } 32 | c.Id = uuid.New().String() 33 | c.Done = make(chan bool) 34 | return c 35 | } 36 | 37 | type ClientOptions struct { 38 | Port *int `json:"port" yaml:"port" xml:"port"` 39 | Format *rune `json:"format" yaml:"format" xml:"format"` 40 | Interval *int `json:"interval" yaml:"interval" xml:"interval"` 41 | JSON *bool `json:"json" yaml:"json" xml:"json"` 42 | LogFile *string `json:"log_file" yaml:"log_file" xml:"log_file"` 43 | Host *string `json:"host" yaml:"host" xml:"host"` 44 | Proto *Protocol `json:"proto" yaml:"proto" xml:"proto"` 45 | Bandwidth *string `json:"bandwidth" yaml:"bandwidth" xml:"bandwidth"` 46 | TimeSec *int `json:"time_sec" yaml:"time_sec" xml:"time_sec"` 47 | Bytes *string `json:"bytes" yaml:"bytes" xml:"bytes"` 48 | BlockCount *string `json:"block_count" yaml:"block_count" xml:"block_count"` 49 | Length *string `json:"length" yaml:"length" xml:"length"` 50 | Streams *int `json:"streams" yaml:"streams" xml:"streams"` 51 | Reverse *bool `json:"reverse" yaml:"reverse" xml:"reverse"` 52 | Window *string `json:"window" yaml:"window" xml:"window"` 53 | MSS *int `json:"mss" yaml:"mss" xml:"mss"` 54 | NoDelay *bool `json:"no_delay" yaml:"no_delay" xml:"no_delay"` 55 | Version4 *bool `json:"version_4" yaml:"version_4" xml:"version_4"` 56 | Version6 *bool `json:"version_6" yaml:"version_6" xml:"version_6"` 57 | TOS *int `json:"tos" yaml:"tos" xml:"tos"` 58 | ZeroCopy *bool `json:"zero_copy" yaml:"zero_copy" xml:"zero_copy"` 59 | OmitSec *int `json:"omit_sec" yaml:"omit_sec" xml:"omit_sec"` 60 | Prefix *string `json:"prefix" yaml:"prefix" xml:"prefix"` 61 | IncludeServer *bool `json:"include_server" yaml:"include_server" xml:"include_server"` 62 | } 63 | 64 | type Client struct { 65 | Id string `json:"id" yaml:"id" xml:"id"` 66 | Running bool `json:"running" yaml:"running" xml:"running"` 67 | Done chan bool `json:"-" yaml:"-" xml:"-"` 68 | Options *ClientOptions `json:"options" yaml:"options" xml:"options"` 69 | Debug bool `json:"-" yaml:"-" xml:"-"` 70 | StdOut bool `json:"-" yaml:"-" xml:"-"` 71 | exitCode *int 72 | report *TestReport 73 | outputStream io.ReadCloser 74 | errorStream io.ReadCloser 75 | cancel context.CancelFunc 76 | mode TestMode 77 | live bool 78 | reportingChan chan *StreamIntervalReport 79 | reportingFile string 80 | } 81 | 82 | func (c *Client) LoadOptionsJSON(jsonStr string) (err error) { 83 | return json.Unmarshal([]byte(jsonStr), c.Options) 84 | } 85 | 86 | func (c *Client) LoadOptions(options *ClientOptions) { 87 | c.Options = options 88 | } 89 | 90 | func (c *Client) commandString() (cmd string, err error) { 91 | builder := strings.Builder{} 92 | if c.Options.Host == nil || *c.Options.Host == "" { 93 | return "", errors.New("unable to execute client. The field 'host' is required") 94 | } 95 | fmt.Fprintf(&builder, "%s -c %s", binaryLocation, c.Host()) 96 | 97 | if c.Options.Port != nil { 98 | fmt.Fprintf(&builder, " -p %d", c.Port()) 99 | } 100 | 101 | if c.Options.Format != nil && *c.Options.Format != ' ' { 102 | fmt.Fprintf(&builder, " -f %c", c.Format()) 103 | } 104 | 105 | if c.Options.Interval != nil { 106 | fmt.Fprintf(&builder, " -i %d", c.Interval()) 107 | } 108 | 109 | if c.Options.Proto != nil && *c.Options.Proto == PROTO_UDP { 110 | fmt.Fprintf(&builder, " -u") 111 | } 112 | 113 | if c.Options.Bandwidth != nil { 114 | fmt.Fprintf(&builder, " -b %s", c.Bandwidth()) 115 | } 116 | 117 | if c.Options.TimeSec != nil { 118 | fmt.Fprintf(&builder, " -t %d", c.TimeSec()) 119 | } 120 | 121 | if c.Options.Bytes != nil { 122 | fmt.Fprintf(&builder, " -n %s", c.Bytes()) 123 | } 124 | 125 | if c.Options.BlockCount != nil { 126 | fmt.Fprintf(&builder, " -k %s", c.BlockCount()) 127 | } 128 | 129 | if c.Options.Length != nil { 130 | fmt.Fprintf(&builder, " -l %s", c.Length()) 131 | } 132 | 133 | if c.Options.Streams != nil { 134 | fmt.Fprintf(&builder, " -P %d", c.Streams()) 135 | } 136 | 137 | if c.Options.Reverse != nil && *c.Options.Reverse { 138 | builder.WriteString(" -R") 139 | } 140 | 141 | if c.Options.Window != nil { 142 | fmt.Fprintf(&builder, " -w %s", c.Window()) 143 | } 144 | 145 | if c.Options.MSS != nil { 146 | fmt.Fprintf(&builder, " -M %d", c.MSS()) 147 | } 148 | 149 | if c.Options.NoDelay != nil && *c.Options.NoDelay { 150 | builder.WriteString(" -N") 151 | } 152 | 153 | if c.Options.Version6 != nil && *c.Options.Version6 { 154 | builder.WriteString(" -6") 155 | } 156 | 157 | if c.Options.TOS != nil { 158 | fmt.Fprintf(&builder, " -S %d", c.TOS()) 159 | } 160 | 161 | if c.Options.ZeroCopy != nil && *c.Options.ZeroCopy { 162 | builder.WriteString(" -Z") 163 | } 164 | 165 | if c.Options.OmitSec != nil { 166 | fmt.Fprintf(&builder, " -O %d", c.OmitSec()) 167 | } 168 | 169 | if c.Options.Prefix != nil { 170 | fmt.Fprintf(&builder, " -T %s", c.Prefix()) 171 | } 172 | 173 | if c.Options.LogFile != nil && *c.Options.LogFile != "" { 174 | fmt.Fprintf(&builder, " --logfile %s", c.LogFile()) 175 | } 176 | 177 | if c.Options.JSON != nil && *c.Options.JSON { 178 | builder.WriteString(" -J") 179 | } 180 | 181 | if c.Options.IncludeServer != nil && *c.Options.IncludeServer { 182 | builder.WriteString(" --get-server-output") 183 | } 184 | 185 | return builder.String(), nil 186 | } 187 | 188 | func (c *Client) Host() string { 189 | if c.Options.Host == nil { 190 | return "" 191 | } 192 | return *c.Options.Host 193 | } 194 | 195 | func (c *Client) SetHost(host string) { 196 | c.Options.Host = &host 197 | } 198 | 199 | func (c *Client) Port() int { 200 | if c.Options.Port == nil { 201 | return 5201 202 | } 203 | return *c.Options.Port 204 | } 205 | 206 | func (c *Client) SetPort(port int) { 207 | c.Options.Port = &port 208 | } 209 | 210 | func (c *Client) Format() rune { 211 | if c.Options.Format == nil { 212 | return ' ' 213 | } 214 | return *c.Options.Format 215 | } 216 | 217 | func (c *Client) SetFormat(format rune) { 218 | c.Options.Format = &format 219 | } 220 | 221 | func (c *Client) Interval() int { 222 | if c.Options.Interval == nil { 223 | return 1 224 | } 225 | return *c.Options.Interval 226 | } 227 | 228 | func (c *Client) SetInterval(interval int) { 229 | c.Options.Interval = &interval 230 | } 231 | 232 | func (c *Client) Proto() Protocol { 233 | if c.Options.Proto == nil { 234 | return PROTO_TCP 235 | } 236 | return *c.Options.Proto 237 | } 238 | 239 | func (c *Client) SetProto(proto Protocol) { 240 | c.Options.Proto = &proto 241 | } 242 | 243 | func (c *Client) Bandwidth() string { 244 | if c.Options.Bandwidth == nil && c.Proto() == PROTO_TCP { 245 | return "0" 246 | } else if c.Options.Bandwidth == nil && c.Proto() == PROTO_UDP { 247 | return "1M" 248 | } 249 | return *c.Options.Bandwidth 250 | } 251 | 252 | func (c *Client) SetBandwidth(bandwidth string) { 253 | c.Options.Bandwidth = &bandwidth 254 | } 255 | 256 | func (c *Client) TimeSec() int { 257 | if c.Options.TimeSec == nil { 258 | return 10 259 | } 260 | return *c.Options.TimeSec 261 | } 262 | 263 | func (c *Client) SetTimeSec(timeSec int) { 264 | c.Options.TimeSec = &timeSec 265 | } 266 | 267 | func (c *Client) Bytes() string { 268 | if c.Options.Bytes == nil { 269 | return "" 270 | } 271 | return *c.Options.Bytes 272 | } 273 | 274 | func (c *Client) SetBytes(bytes string) { 275 | c.Options.Bytes = &bytes 276 | } 277 | 278 | func (c *Client) BlockCount() string { 279 | if c.Options.BlockCount == nil { 280 | return "" 281 | } 282 | return *c.Options.BlockCount 283 | } 284 | 285 | func (c *Client) SetBlockCount(blockCount string) { 286 | c.Options.BlockCount = &blockCount 287 | } 288 | 289 | func (c *Client) Length() string { 290 | if c.Options.Length == nil { 291 | if c.Proto() == PROTO_UDP { 292 | return "1460" 293 | } else { 294 | return "128K" 295 | } 296 | } 297 | return *c.Options.Length 298 | } 299 | 300 | func (c *Client) SetLength(length string) { 301 | c.Options.Length = &length 302 | } 303 | 304 | func (c *Client) Streams() int { 305 | if c.Options.Streams == nil { 306 | return 1 307 | } 308 | return *c.Options.Streams 309 | } 310 | 311 | func (c *Client) SetStreams(streamCount int) { 312 | c.Options.Streams = &streamCount 313 | } 314 | 315 | func (c *Client) Reverse() bool { 316 | if c.Options.Reverse == nil { 317 | return false 318 | } 319 | return *c.Options.Reverse 320 | } 321 | 322 | func (c *Client) SetReverse(reverse bool) { 323 | c.Options.Reverse = &reverse 324 | } 325 | 326 | func (c *Client) Window() string { 327 | if c.Options.Window == nil { 328 | return "" 329 | } 330 | return *c.Options.Window 331 | } 332 | 333 | func (c *Client) SetWindow(window string) { 334 | c.Options.Window = &window 335 | } 336 | 337 | func (c *Client) MSS() int { 338 | if c.Options.MSS == nil { 339 | return 1460 340 | } 341 | return *c.Options.MSS 342 | } 343 | 344 | func (c *Client) SetMSS(mss int) { 345 | c.Options.MSS = &mss 346 | } 347 | 348 | func (c *Client) NoDelay() bool { 349 | if c.Options.NoDelay == nil { 350 | return false 351 | } 352 | return *c.Options.NoDelay 353 | } 354 | 355 | func (c *Client) SetNoDelay(noDelay bool) { 356 | c.Options.NoDelay = &noDelay 357 | } 358 | 359 | func (c *Client) Version4() bool { 360 | if c.Options.Version6 == nil && c.Options.Version4 == nil { 361 | return true 362 | } else if c.Options.Version6 != nil && *c.Options.Version6 == true { 363 | return false 364 | } 365 | return *c.Options.Version4 366 | } 367 | 368 | func (c *Client) SetVersion4(set bool) { 369 | c.Options.Version4 = &set 370 | } 371 | 372 | func (c *Client) Version6() bool { 373 | if c.Options.Version6 == nil { 374 | return false 375 | } 376 | return *c.Options.Version6 377 | } 378 | 379 | func (c *Client) SetVersion6(set bool) { 380 | c.Options.Version6 = &set 381 | } 382 | 383 | func (c *Client) TOS() int { 384 | if c.Options.TOS == nil { 385 | return 0 386 | } 387 | return *c.Options.TOS 388 | } 389 | 390 | func (c *Client) SetTOS(value int) { 391 | c.Options.TOS = &value 392 | } 393 | 394 | func (c *Client) ZeroCopy() bool { 395 | if c.Options.ZeroCopy == nil { 396 | return false 397 | } 398 | return *c.Options.ZeroCopy 399 | } 400 | 401 | func (c *Client) SetZeroCopy(set bool) { 402 | c.Options.ZeroCopy = &set 403 | } 404 | 405 | func (c *Client) OmitSec() int { 406 | if c.Options.OmitSec == nil { 407 | return 0 408 | } 409 | return *c.Options.OmitSec 410 | } 411 | 412 | func (c *Client) SetOmitSec(value int) { 413 | c.Options.OmitSec = &value 414 | } 415 | 416 | func (c *Client) Prefix() string { 417 | if c.Options.Prefix == nil { 418 | return "" 419 | } 420 | return *c.Options.Prefix 421 | } 422 | 423 | func (c *Client) SetPrefix(prefix string) { 424 | c.Options.Prefix = &prefix 425 | } 426 | 427 | func (c *Client) LogFile() string { 428 | if c.Options.LogFile == nil { 429 | return "" 430 | } 431 | return *c.Options.LogFile 432 | } 433 | 434 | func (c *Client) SetLogFile(logfile string) { 435 | c.Options.LogFile = &logfile 436 | } 437 | 438 | func (c *Client) JSON() bool { 439 | if c.Options.JSON == nil { 440 | return false 441 | } 442 | return *c.Options.JSON 443 | } 444 | 445 | func (c *Client) SetJSON(set bool) { 446 | c.Options.JSON = &set 447 | } 448 | 449 | func (c *Client) IncludeServer() bool { 450 | if c.Options.IncludeServer == nil { 451 | return false 452 | } 453 | return *c.Options.IncludeServer 454 | } 455 | 456 | func (c *Client) SetIncludeServer(set bool) { 457 | c.Options.IncludeServer = &set 458 | } 459 | 460 | func (c *Client) ExitCode() *int { 461 | return c.exitCode 462 | } 463 | 464 | func (c *Client) Report() *TestReport { 465 | return c.report 466 | } 467 | 468 | func (c *Client) Mode() TestMode { 469 | return c.mode 470 | } 471 | 472 | func (c *Client) SetModeJson() { 473 | c.SetJSON(true) 474 | c.reportingChan = nil 475 | c.reportingFile = "" 476 | } 477 | 478 | func (c *Client) SetModeLive() <-chan *StreamIntervalReport { 479 | c.SetJSON(false) // having JSON == true will cause reporting to fail 480 | c.live = true 481 | c.reportingChan = make(chan *StreamIntervalReport, 10000) 482 | f, err := ioutil.TempFile("", "iperf_") 483 | if err != nil { 484 | log.Fatalf("failed to create logfile: %v", err) 485 | } 486 | c.reportingFile = f.Name() 487 | c.SetLogFile(c.reportingFile) 488 | return c.reportingChan 489 | } 490 | 491 | func (c *Client) Start() (err error) { 492 | _, err = c.start() 493 | return err 494 | } 495 | 496 | func (c *Client) StartEx() (pid int, err error) { 497 | return c.start() 498 | } 499 | 500 | func (c *Client) start() (pid int, err error) { 501 | read := make(chan interface{}) 502 | cmd, err := c.commandString() 503 | if err != nil { 504 | return -1, err 505 | } 506 | var exit chan int 507 | 508 | if c.Debug { 509 | fmt.Printf("executing command: %s\n", cmd) 510 | } 511 | c.outputStream, c.errorStream, exit, c.cancel, pid, err = ExecuteAsyncWithCancelReadIndicator(cmd, read) 512 | 513 | if err != nil { 514 | return -1, err 515 | } 516 | c.Running = true 517 | 518 | //go func() { 519 | // ds := DebugScanner{Silent: !c.StdOut} 520 | // ds.Scan(c.outputStream) 521 | //}() 522 | go func() { 523 | ds := DebugScanner{Silent: !c.Debug} 524 | ds.Scan(c.errorStream) 525 | }() 526 | 527 | go func() { 528 | var reporter *Reporter 529 | if c.live { 530 | reporter = &Reporter{ 531 | ReportingChannel: c.reportingChan, 532 | LogFile: c.reportingFile, 533 | } 534 | reporter.Start() 535 | } else { 536 | if c.Debug { 537 | fmt.Println("reading output") 538 | } 539 | testOutput, err := ioutil.ReadAll(c.outputStream) 540 | read <- true 541 | if err != nil { 542 | if c.Debug { 543 | fmt.Println(err.Error()) 544 | } 545 | } 546 | if c.Debug { 547 | fmt.Println("parsing output") 548 | } 549 | c.report, err = Loads(string(testOutput)) 550 | if err != nil && c.Debug { 551 | fmt.Println(err.Error()) 552 | } 553 | } 554 | if c.Debug { 555 | fmt.Println("complete") 556 | } 557 | exitCode := <-exit 558 | c.exitCode = &exitCode 559 | c.Running = false 560 | c.Done <- true 561 | if reporter != nil { 562 | reporter.Stop() 563 | } 564 | }() 565 | return pid, nil 566 | } 567 | 568 | func (c *Client) Stop() { 569 | if c.Running && c.cancel != nil { 570 | c.cancel() 571 | os.Remove(c.reportingFile) 572 | } 573 | } 574 | --------------------------------------------------------------------------------