func (c *Client) StartDownload(ctx context.Context) (<-chan spec.Measurement, error) {
	return c.start(ctx, c.download, params.DownloadURLPath)
}
StartDownload discovers a ndt7 server (if needed) and starts a download. On
success it returns a channel where measurements are emitted. This channel is
closed when the download ends. On failure, the error is non nil and you
should not attempt using the channel. A side effect of starting the download
is that, if you did not specify a server FQDN, we will discover a server
for you and store that value into the c.FQDN field.
package ndt7
import (
	"context"
	"errors"
	"net/http"
	"net/url"
	"runtime"
	"time"
	"github.com/gorilla/websocket"
	"github.com/m-lab/locate/api/locate"
	v2 "github.com/m-lab/locate/api/v2"
	"github.com/m-lab/ndt7-client-go/internal/download"
	"github.com/m-lab/ndt7-client-go/internal/params"
	"github.com/m-lab/ndt7-client-go/internal/upload"
	"github.com/m-lab/ndt7-client-go/internal/websocketx"
	"github.com/m-lab/ndt7-client-go/spec"
)
const (
	libraryName = "ndt7-client-go"
	libraryVersion = "0.5.0"
)
var (
	ErrServiceUnsupported = errors.New("unsupported service url")
	ErrNoTargets = errors.New("no targets available")
)
type Locator interface {
	Nearest(ctx context.Context, service string) ([]v2.Target, error)
}
type connectFn = func(
	dialer websocket.Dialer,
	ctx context.Context, urlStr string,
	requestHeader http.Header,
) (*websocket.Conn, *http.Response, error)
type testFn = func(
	ctx context.Context, conn websocketx.Conn, ch chan<- spec.Measurement,
) error
const DefaultWebSocketHandshakeTimeout = 7 * time.Second
type LatestMeasurements struct {
	Server         spec.Measurement
	Client         spec.Measurement
	ConnectionInfo *spec.ConnectionInfo
}
type Client struct {
	ClientName string
	ClientVersion string
	Dialer websocket.Dialer
	FQDN string
	Server string
	ServiceURL *url.URL
	Locate Locator
	Scheme string
	connect connectFn
	download testFn
	upload testFn
	targets []v2.Target
	tIndex  map[string]int
	results map[spec.TestKind]*LatestMeasurements
}
func makeUserAgent(clientName, clientVersion string) string {
	return clientName + "/" + clientVersion + " " + libraryName + "/" + libraryVersion
}
func NewClient(clientName, clientVersion string) *Client {
	results := map[spec.TestKind]*LatestMeasurements{
		spec.TestDownload: {},
		spec.TestUpload:   {},
	}
	return &Client{
		ClientName:    clientName,
		ClientVersion: clientVersion,
		connect: func(
			dialer websocket.Dialer, ctx context.Context, urlStr string,
			requestHeader http.Header) (*websocket.Conn, *http.Response, error,
		) {
			return dialer.DialContext(ctx, urlStr, requestHeader)
		},
		Dialer: websocket.Dialer{
			HandshakeTimeout: DefaultWebSocketHandshakeTimeout,
		},
		download: download.Run,
		Locate: locate.NewClient(
			makeUserAgent(clientName, clientVersion),
		),
		tIndex:  map[string]int{},
		upload:  upload.Run,
		Scheme:  "wss",
		results: results,
	}
}
func (c *Client) doConnect(ctx context.Context, serviceURL string) (*websocket.Conn, error) {
	URL, _ := url.Parse(serviceURL)
	q := URL.Query()
	q.Set("client_arch", runtime.GOARCH)
	q.Set("client_library_name", libraryName)
	q.Set("client_library_version", libraryVersion)
	q.Set("client_name", c.ClientName)
	q.Set("client_os", runtime.GOOS)
	q.Set("client_version", c.ClientVersion)
	URL.RawQuery = q.Encode()
	headers := http.Header{}
	headers.Add("Sec-WebSocket-Protocol", params.SecWebSocketProtocol)
	headers.Add("User-Agent", makeUserAgent(c.ClientName, c.ClientVersion))
	conn, _, err := c.connect(c.Dialer, ctx, URL.String(), headers)
	return conn, err
}
func (c *Client) getURLforPath(ctx context.Context, p string) (string, error) {
	if c.Server != "" && (p == params.DownloadURLPath || p == params.UploadURLPath) {
		return (&url.URL{
			Scheme: c.Scheme,
			Host:   c.Server,
			Path:   p,
		}).String(), nil
	}
	if c.ServiceURL != nil && (c.ServiceURL.Path == params.DownloadURLPath || c.ServiceURL.Path == params.UploadURLPath) {
		c.Scheme = c.ServiceURL.Scheme
		return c.ServiceURL.String(), nil
	} else if c.ServiceURL != nil {
		return "", ErrServiceUnsupported
	}
	if len(c.targets) == 0 {
		targets, err := c.Locate.Nearest(ctx, "ndt/ndt7")
		if err != nil {
			return "", err
		}
		c.targets = targets
	}
	k := c.Scheme + "://" + p
	if c.tIndex[k] < len(c.targets) {
		r := c.targets[c.tIndex[k]].URLs[k]
		c.tIndex[k]++
		return r, nil
	}
	return "", ErrNoTargets
}
func (c *Client) start(ctx context.Context, f testFn, p string) (<-chan spec.Measurement, error) {
	s, err := c.getURLforPath(ctx, p)
	if err != nil {
		return nil, err
	}
	u, err := url.Parse(s)
	if err != nil {
		return nil, err
	}
	c.FQDN = u.Hostname()
	conn, err := c.doConnect(ctx, u.String())
	if err != nil {
		return nil, err
	}
	ch := make(chan spec.Measurement)
	go c.collectData(ctx, f, conn, ch)
	return ch, nil
}
func (c *Client) collectData(ctx context.Context, f testFn, conn websocketx.Conn, outch chan<- spec.Measurement) {
	inch := make(chan spec.Measurement)
	defer close(outch)
	go f(ctx, conn, inch)
	for m := range inch {
		switch m.Origin {
		case spec.OriginClient:
			c.results[m.Test].Client = m
		case spec.OriginServer:
			if m.ConnectionInfo != nil {
				c.results[m.Test].ConnectionInfo = m.ConnectionInfo
			}
			c.results[m.Test].Server = m
		}
		outch <- m
	}
}