diff --git a/.gitignore b/.gitignore index 17c5158..f2928f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,18 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Ignore the embedded directory +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Ignore the embedded directory embedded/ \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 04bb7f1..58b02fc 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,107 +1,131 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1614708953320 + + + + + + + true + + + + + + file://$PROJECT_DIR$/tests/client/client.go + 57 + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE index c20e27e..51cb224 100644 --- a/LICENSE +++ b/LICENSE @@ -1,25 +1,25 @@ -BSD 2-Clause License - -Copyright (c) 2020, Ben Grewell -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +BSD 2-Clause License + +Copyright (c) 2020, Ben Grewell +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 6868010..41d8d92 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# go-iperf -A Go based wrapper around iperf3 - - -``` -go-bindata -pkg iperf -prefix "embedded/" embedded/``` +# go-iperf +A Go based wrapper around iperf3 + + +``` +go-bindata -pkg iperf -prefix "embedded/" embedded/``` diff --git a/client.go b/client.go index 0341d4b..f38d4bc 100644 --- a/client.go +++ b/client.go @@ -1,43 +1,491 @@ package iperf -import "github.com/google/uuid" +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/google/uuid" + "io" + "io/ioutil" + "log" + "os" + "strings" +) -func NewClient() *Client { +func NewClient(host string) *Client { json := true proto := Protocol(PROTO_TCP) time := 10 length := "128KB" streams := 1 c := &Client{ - JSON: &json, - Proto: &proto, - TimeSec: &time, - Length: &length, - Streams: &streams, + options: &ClientOptions{ + JSON: &json, + Proto: &proto, + TimeSec: &time, + Length: &length, + Streams: &streams, + Host: &host, + }, } c.Id = uuid.New().String() + c.Done = make(chan bool) return c } -type Client struct { +type ClientOptions struct { SharedOptions - Host string - Proto *Protocol - Bandwidth *string - TimeSec *int - Bytes *string - BlockCount *string - Length *string - Streams *int - Reverse *bool - Window *string - MSS *int - NoDelay *bool - Version4 *bool - Version6 *bool - TOS *int - ZeroCopy *bool - OmitSec *int - Prefix *string - JSON *bool + Host *string + Proto *Protocol + Bandwidth *string + TimeSec *int + Bytes *string + BlockCount *string + Length *string + Streams *int + Reverse *bool + Window *string + MSS *int + NoDelay *bool + Version4 *bool + Version6 *bool + TOS *int + ZeroCopy *bool + OmitSec *int + Prefix *string + JSON *bool + LogFile *string + IncludeServer *bool +} + +type Client struct { + Id string + Running bool + Done chan bool + options *ClientOptions + exitCode *int + report *TestReport + outputStream io.ReadCloser + errorStream io.ReadCloser + cancel context.CancelFunc + mode TestMode + live bool + reportingChan chan *StreamIntervalReport + reportingFile string +} + +func (c *Client) LoadOptionsJSON(jsonStr string) (err error) { + return json.Unmarshal([]byte(jsonStr), c.options) +} + +func (c *Client) commandString() (cmd string, err error) { + builder := strings.Builder{} + if c.options.Host == nil || *c.options.Host == "" { + return "", errors.New("unable to execute client. The field 'host' is required") + } + fmt.Fprintf(&builder, "%s -c %s", binaryLocation, c.Host()) + + if c.options.Proto != nil && *c.options.Proto == PROTO_UDP { + fmt.Fprintf(&builder, " -u") + } + + if c.options.Bandwidth != nil { + fmt.Fprintf(&builder, " -b %s", c.Bandwidth()) + } + + if c.options.TimeSec != nil { + fmt.Fprintf(&builder, " -t %d", c.TimeSec()) + } + + if c.options.Bytes != nil { + fmt.Fprintf(&builder, " -n %s", c.Bytes()) + } + + if c.options.BlockCount != nil { + fmt.Fprintf(&builder, " -k %s", c.BlockCount()) + } + + if c.options.Length != nil { + fmt.Fprintf(&builder, " -l %s", c.Length()) + } + + if c.options.Streams != nil { + fmt.Fprintf(&builder, " -P %d", c.Streams()) + } + + if c.options.Reverse != nil && *c.options.Reverse { + builder.WriteString(" -R") + } + + if c.options.Window != nil { + fmt.Fprintf(&builder, " -w %s", c.Window()) + } + + if c.options.MSS != nil { + fmt.Fprintf(&builder, " -M %d", c.MSS()) + } + + if c.options.NoDelay != nil && *c.options.NoDelay { + builder.WriteString(" -N") + } + + if c.options.Version6 != nil && *c.options.Version6 { + builder.WriteString(" -6") + } + + if c.options.TOS != nil { + fmt.Fprintf(&builder, " -S %d", c.TOS()) + } + + if c.options.ZeroCopy != nil && *c.options.ZeroCopy { + builder.WriteString(" -Z") + } + + if c.options.OmitSec != nil { + fmt.Fprintf(&builder, " -O %d", c.OmitSec()) + } + + if c.options.Prefix != nil { + fmt.Fprintf(&builder, " -T %s", c.Prefix()) + } + + if c.options.LogFile != nil && *c.options.LogFile != "" { + fmt.Fprintf(&builder, " --logfile %s", c.LogFile()) + } + + if c.options.JSON != nil && *c.options.JSON { + builder.WriteString(" -J") + } + + if c.options.IncludeServer != nil && *c.options.IncludeServer { + builder.WriteString(" --get-server-output") + } + + return builder.String(), nil +} + +func (c *Client) Host() string { + if c.options.Host == nil { + return "" + } + return *c.options.Host +} + +func (c *Client) SetHost(host string) { + c.options.Host = &host +} + +func (c *Client) Proto() Protocol { + if c.options.Proto == nil { + return PROTO_TCP + } + return *c.options.Proto +} + +func (c *Client) SetProto(proto Protocol) { + c.options.Proto = &proto +} + +func (c *Client) Bandwidth() string { + if c.options.Bandwidth == nil && c.Proto() == PROTO_TCP { + return "0" + } else if c.options.Bandwidth == nil && c.Proto() == PROTO_UDP { + return "1M" + } + return *c.options.Bandwidth +} + +func (c *Client) SetBandwidth(bandwidth string) { + c.options.Bandwidth = &bandwidth +} + +func (c *Client) TimeSec() int { + if c.options.TimeSec == nil { + return 10 + } + return *c.options.TimeSec +} + +func (c *Client) SetTimeSec(timeSec int) { + c.options.TimeSec = &timeSec +} + +func (c *Client) Bytes() string { + if c.options.Bytes == nil { + return "" + } + return *c.options.Bytes +} + +func (c *Client) SetBytes(bytes string) { + c.options.Bytes = &bytes +} + +func (c *Client) BlockCount() string { + if c.options.BlockCount == nil { + return "" + } + return *c.options.BlockCount +} + +func (c *Client) SetBlockCount(blockCount string) { + c.options.BlockCount = &blockCount +} + +func (c *Client) Length() string { + if c.options.Length == nil { + if c.Proto() == PROTO_UDP { + return "1460" + } else { + return "128K" + } + } + return *c.options.Length +} + +func (c *Client) SetLength(length string) { + c.options.Length = &length +} + +func (c *Client) Streams() int { + if c.options.Streams == nil { + return 1 + } + return *c.options.Streams +} + +func (c *Client) SetStreams(streamCount int) { + c.options.Streams = &streamCount +} + +func (c *Client) Reverse() bool { + if c.options.Reverse == nil { + return false + } + return *c.options.Reverse +} + +func (c *Client) SetReverse(reverse bool) { + c.options.Reverse = &reverse +} + +func (c *Client) Window() string { + if c.options.Window == nil { + return "" + } + return *c.options.Window +} + +func (c *Client) SetWindow(window string) { + c.options.Window = &window +} + +func (c *Client) MSS() int { + if c.options.MSS == nil { + return 1460 + } + return *c.options.MSS +} + +func (c *Client) SetMSS(mss int) { + c.options.MSS = &mss +} + +func (c *Client) NoDelay() bool { + if c.options.NoDelay == nil { + return false + } + return *c.options.NoDelay +} + +func (c *Client) SetNoDelay(noDelay bool) { + c.options.NoDelay = &noDelay +} + +func (c *Client) Version4() bool { + if c.options.Version6 == nil && c.options.Version4 == nil { + return true + } else if c.options.Version6 != nil && *c.options.Version6 == true { + return false + } + return *c.options.Version4 +} + +func (c *Client) SetVersion4(set bool) { + c.options.Version4 = &set +} + +func (c *Client) Version6() bool { + if c.options.Version6 == nil { + return false + } + return *c.options.Version6 +} + +func (c *Client) SetVersion6(set bool) { + c.options.Version6 = &set +} + +func (c *Client) TOS() int { + if c.options.TOS == nil { + return 0 + } + return *c.options.TOS +} + +func (c *Client) SetTOS(value int) { + c.options.TOS = &value +} + +func (c *Client) ZeroCopy() bool { + if c.options.ZeroCopy == nil { + return false + } + return *c.options.ZeroCopy +} + +func (c *Client) SetZeroCopy(set bool) { + c.options.ZeroCopy = &set +} + +func (c *Client) OmitSec() int { + if c.options.OmitSec == nil { + return 0 + } + return *c.options.OmitSec +} + +func (c *Client) SetOmitSec(value int) { + c.options.OmitSec = &value +} + +func (c *Client) Prefix() string { + if c.options.Prefix == nil { + return "" + } + return *c.options.Prefix +} + +func (c *Client) SetPrefix(prefix string) { + c.options.Prefix = &prefix +} + +func (c *Client) LogFile() string { + if c.options.LogFile == nil { + return "" + } + return *c.options.LogFile +} + +func (c *Client) SetLogFile(logfile string) { + c.options.LogFile = &logfile +} + +func (c *Client) JSON() bool { + if c.options.JSON == nil { + return false + } + return *c.options.JSON +} + +func (c *Client) SetJSON(set bool) { + c.options.JSON = &set +} + +func (c *Client) IncludeServer() bool { + if c.options.IncludeServer == nil { + return false + } + return *c.options.IncludeServer +} + +func (c *Client) SetIncludeServer(set bool) { + c.options.IncludeServer = &set +} + +func (c *Client) ExitCode() *int { + return c.exitCode +} + +func (c *Client) Report() *TestReport { + return c.report +} + +func (c *Client) Mode() TestMode { + return c.mode +} + +func (c *Client) SetModeJson() { + c.SetJSON(true) + c.reportingChan = nil + c.reportingFile = "" +} + +func (c *Client) SetModeLive() <-chan *StreamIntervalReport { + c.SetJSON(false) // having JSON == true will cause reporting to fail + c.live = true + c.reportingChan = make(chan *StreamIntervalReport, 10000) + f, err := ioutil.TempFile("", "iperf_") + if err != nil { + log.Fatalf("failed to create logfile: %v", err) + } + c.reportingFile = f.Name() + c.SetLogFile(c.reportingFile) + return c.reportingChan +} + +func (c *Client) Start() (err error) { + //todo: Need to build the string based on the options above that are set + cmd, err := c.commandString() + if err != nil { + return err + } + fmt.Println(cmd) + var exit chan int + c.outputStream, c.errorStream, exit, c.cancel, err = ExecuteAsyncWithCancel(cmd) + if err != nil { + return err + } + c.Running = true + //go func() { + // ds := DebugScanner{Silent: false} + // ds.Scan(c.outputStream) + //}() + //go func() { + // ds := DebugScanner{Silent: false} + // ds.Scan(c.errorStream) + //}() + go func() { + var reporter *Reporter + if c.live { + reporter = &Reporter{ + ReportingChannel: c.reportingChan, + LogFile: c.reportingFile, + } + reporter.Start() + } else { + testOutput, err := ioutil.ReadAll(c.outputStream) + if err != nil { + return + } + c.report, err = Loads(string(testOutput)) + } + exitCode := <-exit + c.exitCode = &exitCode + c.Running = false + c.Done <- true + if reporter != nil { + reporter.Stop() + } + }() + return nil +} + +func (c *Client) Stop() { + if c.Running && c.cancel != nil { + c.cancel() + os.Remove(c.reportingFile) + c.Done <- true + } } diff --git a/cmd/main.go b/cmd/main.go index b62a1a9..0d86a3b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,13 +2,17 @@ package main import ( "fmt" + "github.com/BGrewell/go-conversions" "github.com/BGrewell/go-iperf" "time" ) func main() { + s := iperf.NewServer() - c := iperf.NewClient() + c := iperf.NewClient("127.0.0.1") + includeServer := true + c.IncludeServer = &includeServer fmt.Println(s.Id) fmt.Println(c.Id) @@ -16,10 +20,27 @@ func main() { if err != nil { fmt.Println(err) } - for s.Running { + + err = c.Start() + if err != nil { + fmt.Println(err) + } + + for c.Running { time.Sleep(1 * time.Second) } + fmt.Println("stopping server") + s.Stop() + + fmt.Printf("Client exit code: %d\n", *c.ExitCode) fmt.Printf("Server exit code: %d\n", *s.ExitCode) iperf.Cleanup() + if c.Report.Error != "" { + fmt.Println(c.Report.Error) + } else { + fmt.Printf("Recv Rate: %s\n", conversions.IntBitRateToString(int64(c.Report.End.SumReceived.BitsPerSecond))) + fmt.Printf("Send Rate: %s\n", conversions.IntBitRateToString(int64(c.Report.End.SumSent.BitsPerSecond))) + } + } diff --git a/execute.go b/execute.go index 4fd09f3..4ebada9 100644 --- a/execute.go +++ b/execute.go @@ -1,41 +1,82 @@ package iperf import ( + "context" "io" "os/exec" "strings" "syscall" ) -func Execute(cmd string, outPipe io.ReadCloser, errPipe io.ReadCloser, exit chan <- int) (err error) { +func ExecuteAsync(cmd string) (outPipe io.ReadCloser, errPipe io.ReadCloser, exitCode chan int, err error) { + exitCode = make(chan int) cmdParts := strings.Fields(cmd) binary, err := exec.LookPath(cmdParts[0]) if err != nil { - return err + return nil, nil, nil, err } exe := exec.Command(binary, cmdParts[1:]...) outPipe, err = exe.StdoutPipe() if err != nil { - return err + return nil, nil, nil, err } errPipe, err = exe.StderrPipe() if err != nil { - return err + return nil, nil, nil, err } err = exe.Start() if err != nil { - return err + return nil, nil, nil, err } go func() { if err := exe.Wait(); err != nil { if exiterr, ok := err.(*exec.ExitError); ok { if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { - exit <- status.ExitStatus() + exitCode <- status.ExitStatus() } } } else { - exit <- 0 + exitCode <- 0 } }() - return nil + return outPipe, errPipe, exitCode, nil +} + +func ExecuteAsyncWithCancel(cmd string) (stdOut io.ReadCloser, stdErr io.ReadCloser, exitCode chan int, cancelToken context.CancelFunc, err error) { + exitCode = make(chan int) + ctx, cancel := context.WithCancel(context.Background()) + cmdParts := strings.Fields(cmd) + binary, err := exec.LookPath(cmdParts[0]) + if err != nil { + defer cancel() + return nil, nil, nil, nil, err + } + exe := exec.CommandContext(ctx, binary, cmdParts[1:]...) + stdOut, err = exe.StdoutPipe() + if err != nil { + defer cancel() + return nil, nil, nil, nil, err + } + stdErr, err = exe.StderrPipe() + if err != nil { + defer cancel() + return nil, nil, nil, nil, err + } + err = exe.Start() + if err != nil { + defer cancel() + return nil, nil, nil, nil, err + } + go func() { + if err := exe.Wait(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + exitCode <- status.ExitStatus() + } + } + } else { + exitCode <- 0 + } + }() + return stdOut, stdErr, exitCode, cancel, nil } diff --git a/go.mod b/go.mod index 076e299..352be87 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,14 @@ module github.com/BGrewell/go-iperf go 1.15 -require github.com/google/uuid v1.1.2 +require ( + github.com/BGrewell/go-conversions v0.0.0-20201203155646-5e189e4ca087 + github.com/BGrewell/go-execute v0.0.0-20201203155726-b7c037ebde49 // indirect + github.com/BGrewell/tail v1.0.0 + github.com/fatih/gomodifytags v1.13.0 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/google/uuid v1.1.2 + github.com/hpcloud/tail v1.0.0 // indirect + gopkg.in/fsnotify.v1 v1.4.7 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect +) diff --git a/go.sum b/go.sum index c8547fe..fe98da7 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,25 @@ +github.com/BGrewell/go-conversions v0.0.0-20201203155646-5e189e4ca087 h1:/ZR3IHtTSPqwzYQSfZYwGjrmDI5c1u2jEDDtT75Fmqw= +github.com/BGrewell/go-conversions v0.0.0-20201203155646-5e189e4ca087/go.mod h1:XtXcz/MP04vhr6c6R/5gZPJZVQpbXlFsKTHr5yy/5sU= +github.com/BGrewell/go-execute v0.0.0-20201203155726-b7c037ebde49 h1:HV+WdlqXjmzP59lhgb100vTSIcDuKuLmK11KdnPY0cg= +github.com/BGrewell/go-execute v0.0.0-20201203155726-b7c037ebde49/go.mod h1:vQZr3vuuuKuknvi74K22ne7NzOlwKTPKXOFMVr9Qa6A= +github.com/BGrewell/tail v1.0.0 h1:sG+Uvv+UApHtj5z+AWWB9i5m2SCH0RLfxYqXujYQo+Q= +github.com/BGrewell/tail v1.0.0/go.mod h1:0PFYWAobUZKZLEYIxxmjFgnfvCLA600LkFbGO9KFIRA= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/gomodifytags v1.13.0 h1:fmhwoecjZ5c34Q2chjRB9cL8Rgag+1TOSMy+grissMc= +github.com/fatih/gomodifytags v1.13.0/go.mod h1:TbUyEjH1Zo0GkJd2Q52oVYqYcJ0eGNqG8bsiOb75P9c= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/tools v0.0.0-20180824175216-6c1c5e93cdc1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/iperf.go b/iperf.go index 2673b82..41e6c15 100644 --- a/iperf.go +++ b/iperf.go @@ -10,8 +10,8 @@ import ( ) var ( - Debug = true - binaryDir = "" + Debug = true + binaryDir = "" binaryLocation = "" ) @@ -67,4 +67,4 @@ func extractEmbeddedBinaries(files []string) (err error) { } } return nil -} \ No newline at end of file +} diff --git a/report.go b/report.go index ed8358d..5d879df 100644 --- a/report.go +++ b/report.go @@ -1 +1,393 @@ package iperf + +import ( + "bytes" + "encoding/json" + "io/ioutil" +) + +type StreamInterval struct { + Streams []*StreamIntervalReport `json:"streams"` + Sum *StreamIntervalSumReport `json:"sum"` +} + +func (si *StreamInterval) String() string { + b, err := json.Marshal(si) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type StreamIntervalReport struct { + Socket int `json:"socket"` + StartInterval float32 `json:"start"` + EndInterval float32 `json:"end"` + Seconds float32 `json:"seconds"` + Bytes int `json:"bytes"` + BitsPerSecond float64 `json:"bits_per_second"` + Omitted bool `json:"omitted"` +} + +func (sir *StreamIntervalReport) String() string { + b, err := json.Marshal(sir) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type StreamIntervalSumReport struct { + StartInterval float32 `json:"start"` + EndInterval float32 `json:"end"` + Seconds float32 `json:"seconds"` + Bytes int `json:"bytes"` + BitsPerSecond float64 `json:"bits_per_second"` + Omitted bool `json:"omitted"` +} + +func (sisr *StreamIntervalSumReport) String() string { + b, err := json.Marshal(sisr) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type StreamEndReport struct { + Sender TcpStreamEndReport `json:"sender"` + Receiver TcpStreamEndReport `json:"receiver"` + Udp UdpStreamEndReport `json:"udp"` +} + +func (ser *StreamEndReport) String() string { + b, err := json.Marshal(ser) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type UdpStreamEndReport struct { + Socket int `json:"socket"` + Start float32 `json:"start"` + End float32 `json:"end"` + Seconds float32 `json:"seconds"` + Bytes int `json:"bytes"` + BitsPerSecond float64 `json:"bits_per_second"` + JitterMs float32 `json:"jitter_ms"` + LostPackets int `json:"lost_packets"` + Packets int `json:"packets"` + LostPercent float32 `json:"lost_percent"` + OutOfOrder int `json:"out_of_order"` +} + +func (user *UdpStreamEndReport) String() string { + b, err := json.Marshal(user) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type TcpStreamEndReport struct { + Socket int `json:"socket"` + Start float32 `json:"start"` + End float32 `json:"end"` + Seconds float32 `json:"seconds"` + Bytes int `json:"bytes"` + BitsPerSecond float64 `json:"bits_per_second"` +} + +func (tser *TcpStreamEndReport) String() string { + b, err := json.Marshal(tser) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type StreamEndSumReport struct { + Start float32 `json:"start"` + End float32 `json:"end"` + Seconds float32 `json:"seconds"` + Bytes int `json:"bytes"` + BitsPerSecond float64 `json:"bits_per_second"` +} + +func (sesr *StreamEndSumReport) String() string { + b, err := json.Marshal(sesr) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type CpuUtilizationReport struct { + HostTotal float32 `json:"host_total"` + HostUser float32 `json:"host_user"` + HostSystem float32 `json:"host_system"` + RemoteTotal float32 `json:"remote_total"` + RemoteUser float32 `json:"remote_user"` + RemoteSystem float32 `json:"remote_system"` +} + +func (cur *CpuUtilizationReport) String() string { + b, err := json.Marshal(cur) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type ConnectionInfo struct { + Socket int `json:"socket"` + LocalHost string `json:"local_host"` + LocalPort int `json:"local_port"` + RemoteHost string `json:"remote_host"` + RemotePort int `json:"remote_port"` +} + +func (ci *ConnectionInfo) String() string { + b, err := json.Marshal(ci) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type TimestampInfo struct { + Time string `json:"time"` + TimeSecs int `json:"timesecs"` +} + +func (tsi *TimestampInfo) String() string { + b, err := json.Marshal(tsi) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type ConnectingToInfo struct { + Host string `json:"host"` + Port int `json:"port"` +} + +func (cti *ConnectingToInfo) String() string { + b, err := json.Marshal(cti) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type TestStartInfo struct { + Protocol string `json:"protocol"` + NumStreams int `json:"num_streams"` + BlkSize int `json:"blksize"` + Omit int `json:"omit"` + Duration int `json:"duration"` + Bytes int `json:"bytes"` + Blocks int `json:"blocks"` + Reverse int `json:"reverse"` +} + +func (tsi *TestStartInfo) String() string { + b, err := json.Marshal(tsi) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type StartInfo struct { + Connected []*ConnectionInfo `json:"connected"` + Version string `json:"version"` + SystemInfo string `json:"system_info"` + Timestamp TimestampInfo `json:"timestamp"` + ConnectingTo ConnectingToInfo `json:"connecting_to"` + Cookie string `json:"cookie"` + TcpMssDefault int `json:"tcp_mss_default"` + TestStart TestStartInfo `json:"test_start"` +} + +func (si *StartInfo) String() string { + b, err := json.Marshal(si) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type EndInfo struct { + Streams []*StreamEndReport `json:"streams"` + SumSent StreamEndSumReport `json:"sum_sent"` + SumReceived StreamEndSumReport `json:"sum_received"` + CpuReport CpuUtilizationReport `json:"cpu_utilization_percent"` +} + +func (ei *EndInfo) String() string { + b, err := json.Marshal(ei) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type ServerReport struct { + Start StartInfo `json:"start"` + Intervals []*StreamInterval `json:"intervals"` + End EndInfo `json:"end"` + Error string `json:"error"` +} + +func (sr *ServerReport) String() string { + b, err := json.Marshal(sr) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +type TestReport struct { + Start StartInfo `json:"start"` + Intervals []*StreamInterval `json:"intervals"` + End EndInfo `json:"end"` + Error string `json:"error"` + ServerOutputJson ServerReport `json:"server_output_json"` +} + +func (tr *TestReport) String() string { + b, err := json.Marshal(tr) + if err != nil { + return "error converting to json" + } + + var pretty bytes.Buffer + err = json.Indent(&pretty, b, "", " ") + if err != nil { + return "error converting json to indented format" + } + + return string(pretty.Bytes()) +} + +func Loads(jsonStr string) (report *TestReport, err error) { + r := &TestReport{} + err = json.Unmarshal([]byte(jsonStr), r) + return r, err +} + +func Load(filename string) (report *TestReport, err error) { + contents, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return Loads(string(contents)) +} diff --git a/reporter.go b/reporter.go index ed8358d..2d8d98c 100644 --- a/reporter.go +++ b/reporter.go @@ -1 +1,92 @@ package iperf + +import ( + "fmt" + "github.com/BGrewell/tail" + "github.com/BGrewell/go-conversions" + "log" + "strconv" + "strings" +) + +type Reporter struct { + ReportingChannel chan *StreamIntervalReport + LogFile string + running bool +} + +func (r *Reporter) Start() { + r.running = true + go r.runLogProcessor() +} + +func (r *Reporter) Stop() { + r.running = false + close(r.ReportingChannel) +} + +func (r *Reporter) runLogProcessor() { + tailer, err := tail.TailFile(r.LogFile, tail.Config{ + Follow: true, + ReOpen: true, + Poll: true, + MustExist: true, + }) + if err != nil { + log.Fatalf("failed to tail log file: %v", err) + } + for line := range tailer.Lines { + // TODO: For now this only cares about individual streams it ignores the sum lines + if len(line.Text) > 5 { + id := line.Text[1:4] + stream, err := strconv.Atoi(strings.TrimSpace(id)) + if err != nil { + continue + } + fields := strings.Fields(line.Text[5:]) + if len(fields) >= 6 { + if fields[0] == "local" { + continue + } + timeFields := strings.Split(fields[0], "-") + start, err := strconv.ParseFloat(timeFields[0], 32) + if err != nil { + log.Printf("failed to convert start time: %s\n", err) + } + end, err := strconv.ParseFloat(timeFields[1], 32) + transferedStr := fmt.Sprintf("%s%s", fields[2], fields[3]) + transferedBytes, err := conversions.StringBitRateToInt(transferedStr) + if err != nil { + log.Printf("failed to convert units: %s\n", err) + } + transferedBytes = transferedBytes / 8 + rateStr := fmt.Sprintf("%s%s", fields[4], fields[5]) + rate, err := conversions.StringBitRateToInt(rateStr) + if err != nil { + log.Printf("failed to convert units: %s\n", err) + } + omitted := false + if len(fields) >= 7 && fields[6] == "(omitted)" { + omitted = true + } + report := &StreamIntervalReport{ + Socket: stream, + StartInterval: float32(start), + EndInterval: float32(end), + Seconds: float32(end - start), + Bytes: int(transferedBytes), + BitsPerSecond: float64(rate), + Omitted: omitted, + } + r.ReportingChannel <- report + } + } + + if !r.running { + return + } + } +} + + + diff --git a/sample.json b/sample.json index e69de29..a761ab9 100644 --- a/sample.json +++ b/sample.json @@ -0,0 +1,291 @@ +{ + "start":{ + "connected":[ + { + "socket":4, + "local_host":"127.0.0.1", + "local_port":64200, + "remote_host":"127.0.0.1", + "remote_port":5201 + } + ], + "version":"iperf 3.1.3", + "system_info":"CYGWIN_NT-10.0 BGrewell-MOBL3 2.5.1(0.297/5/3) 2016-04-21 22:14 x86_64", + "timestamp":{ + "time":"Tue, 02 Mar 2021 18:28:14 GMT", + "timesecs":1614709694 + }, + "connecting_to":{ + "host":"127.0.0.1", + "port":5201 + }, + "cookie":"BGrewell-MOBL3.1614709694.715824.0bf", + "tcp_mss_default":0, + "test_start":{ + "protocol":"TCP", + "num_streams":1, + "blksize":131072, + "omit":0, + "duration":10, + "bytes":0, + "blocks":0, + "reverse":0 + } + }, + "intervals":[ + { + "streams":[ + { + "socket":4, + "start":0, + "end":1.000554, + "seconds":1.000554, + "bytes":1635647488, + "bits_per_second":1.307794e+10, + "omitted":false + } + ], + "sum":{ + "start":0, + "end":1.000554, + "seconds":1.000554, + "bytes":1635647488, + "bits_per_second":1.307794e+10, + "omitted":false + } + }, + { + "streams":[ + { + "socket":4, + "start":1.000554, + "end":2.000056, + "seconds":0.999502, + "bytes":1527250944, + "bits_per_second":1.222409e+10, + "omitted":false + } + ], + "sum":{ + "start":1.000554, + "end":2.000056, + "seconds":0.999502, + "bytes":1527250944, + "bits_per_second":1.222409e+10, + "omitted":false + } + }, + { + "streams":[ + { + "socket":4, + "start":2.000056, + "end":3.000353, + "seconds":1.000297, + "bytes":1712062464, + "bits_per_second":1.369244e+10, + "omitted":false + } + ], + "sum":{ + "start":2.000056, + "end":3.000353, + "seconds":1.000297, + "bytes":1712062464, + "bits_per_second":1.369244e+10, + "omitted":false + } + }, + { + "streams":[ + { + "socket":4, + "start":3.000353, + "end":4.000669, + "seconds":1.000316, + "bytes":1730412544, + "bits_per_second":1.383893e+10, + "omitted":false + } + ], + "sum":{ + "start":3.000353, + "end":4.000669, + "seconds":1.000316, + "bytes":1730412544, + "bits_per_second":1.383893e+10, + "omitted":false + } + }, + { + "streams":[ + { + "socket":4, + "start":4.000669, + "end":5.000021, + "seconds":0.999352, + "bytes":1777467392, + "bits_per_second":1.422896e+10, + "omitted":false + } + ], + "sum":{ + "start":4.000669, + "end":5.000021, + "seconds":0.999352, + "bytes":1777467392, + "bits_per_second":1.422896e+10, + "omitted":false + } + }, + { + "streams":[ + { + "socket":4, + "start":5.000021, + "end":6.000122, + "seconds":1.000101, + "bytes":1673920512, + "bits_per_second":1.339001e+10, + "omitted":false + } + ], + "sum":{ + "start":5.000021, + "end":6.000122, + "seconds":1.000101, + "bytes":1673920512, + "bits_per_second":1.339001e+10, + "omitted":false + } + }, + { + "streams":[ + { + "socket":4, + "start":6.000122, + "end":7.000517, + "seconds":1.000395, + "bytes":1585446912, + "bits_per_second":1.267857e+10, + "omitted":false + } + ], + "sum":{ + "start":6.000122, + "end":7.000517, + "seconds":1.000395, + "bytes":1585446912, + "bits_per_second":1.267857e+10, + "omitted":false + } + }, + { + "streams":[ + { + "socket":4, + "start":7.000517, + "end":8.000053, + "seconds":0.999536, + "bytes":1642987520, + "bits_per_second":1.315000e+10, + "omitted":false + } + ], + "sum":{ + "start":7.000517, + "end":8.000053, + "seconds":0.999536, + "bytes":1642987520, + "bits_per_second":1.315000e+10, + "omitted":false + } + }, + { + "streams":[ + { + "socket":4, + "start":8.000053, + "end":9.000105, + "seconds":1.000052, + "bytes":1547173888, + "bits_per_second":1.237675e+10, + "omitted":false + } + ], + "sum":{ + "start":8.000053, + "end":9.000105, + "seconds":1.000052, + "bytes":1547173888, + "bits_per_second":1.237675e+10, + "omitted":false + } + }, + { + "streams":[ + { + "socket":4, + "start":9.000105, + "end":10.000386, + "seconds":1.000281, + "bytes":1773142016, + "bits_per_second":1.418115e+10, + "omitted":false + } + ], + "sum":{ + "start":9.000105, + "end":10.000386, + "seconds":1.000281, + "bytes":1773142016, + "bits_per_second":1.418115e+10, + "omitted":false + } + } + ], + "end":{ + "streams":[ + { + "sender":{ + "socket":4, + "start":0, + "end":10.000386, + "seconds":10.000386, + "bytes":16605511680, + "bits_per_second":1.328390e+10 + }, + "receiver":{ + "socket":4, + "start":0, + "end":10.000386, + "seconds":10.000386, + "bytes":16605511680, + "bits_per_second":1.328390e+10 + } + } + ], + "sum_sent":{ + "start":0, + "end":10.000386, + "seconds":10.000386, + "bytes":16605511680, + "bits_per_second":1.328390e+10 + }, + "sum_received":{ + "start":0, + "end":10.000386, + "seconds":10.000386, + "bytes":16605511680, + "bits_per_second":1.328390e+10 + }, + "cpu_utilization_percent":{ + "host_total":83.983525, + "host_user":5.155306, + "host_system":78.828219, + "remote_total":34.722681, + "remote_user":7.224950, + "remote_system":27.497731 + } + } +} \ No newline at end of file diff --git a/server.go b/server.go index e009f6e..5de6e84 100644 --- a/server.go +++ b/server.go @@ -1,44 +1,47 @@ package iperf import ( + "context" "fmt" "github.com/google/uuid" "io" + "time" ) func NewServer() *Server { - s := &Server{ - } + s := &Server{} s.Id = uuid.New().String() return s } type Server struct { SharedOptions - OneOff *bool - ExitCode *int - Running bool + Id string + OneOff *bool + ExitCode *int + Running bool outputStream io.ReadCloser - errorStream io.ReadCloser + errorStream io.ReadCloser + cancel context.CancelFunc } func (s *Server) Start() (err error) { - exit := make(chan int, 0) - err = Execute(fmt.Sprintf("%s -s", binaryLocation), s.outputStream, s.errorStream, exit) + var exit chan int + s.outputStream, s.errorStream, exit, s.cancel, err = ExecuteAsyncWithCancel(fmt.Sprintf("%s -s -J", binaryLocation)) if err != nil { return err } s.Running = true go func() { - ds := DebugScanner{} + ds := DebugScanner{Silent: true} ds.Scan(s.outputStream) }() go func() { - ds := DebugScanner{} + ds := DebugScanner{Silent: true} ds.Scan(s.errorStream) }() go func() { - exitCode := <- exit + exitCode := <-exit s.ExitCode = &exitCode s.Running = false }() @@ -46,5 +49,8 @@ func (s *Server) Start() (err error) { } func (s *Server) Stop() { - + if s.Running && s.cancel != nil { + s.cancel() + time.Sleep(100 * time.Millisecond) + } } diff --git a/shared.go b/shared.go index 738aeba..e0fe723 100644 --- a/shared.go +++ b/shared.go @@ -6,15 +6,20 @@ import ( "io" ) +type TestMode string +const ( + MODE_JSON TestMode = "json" + MODE_LIVE TestMode = "live" +) + type SharedOptions struct { - Id string - Port *int - Format *rune + Port *int + Format *rune Interval *int } type DebugScanner struct { - + Silent bool } func (ds *DebugScanner) Scan(buff io.ReadCloser) { @@ -26,6 +31,8 @@ func (ds *DebugScanner) Scan(buff io.ReadCloser) { scanner.Split(bufio.ScanWords) for scanner.Scan() { text := scanner.Text() - fmt.Println(text) + if !ds.Silent { + fmt.Println(text) + } } -} \ No newline at end of file +} diff --git a/tests/server/server.go b/tests/server/server.go index 3e4cfe4..8ecc046 100644 --- a/tests/server/server.go +++ b/tests/server/server.go @@ -1,5 +1,23 @@ package main +import ( + "fmt" + "github.com/BGrewell/go-iperf" + "os" + "time" +) + func main() { - $END$ + s := iperf.NewServer() + err := s.Start() + if err != nil { + fmt.Println("failed to start server") + os.Exit(-1) + } + + for s.Running { + time.Sleep(1 * time.Second) + } + + fmt.Println("server has exited") }