<commit_before>package statsd

import (
	"fmt"
	"log"
	"net"
	"os"
	"strings"
	"time"

	"github.com/quipo/statsd/event"
)

// Logger interface compatible with log.Logger
type Logger interface {
	Println(v ...interface{})
}

// UDPPayloadSize is the number of bytes to send at one go through the udp socket.
// SendEvents will try to pack as many events into one udp packet.
// Change this value as per network capabilities
// For example to change to 16KB
//  import "github.com/quipo/statsd"
//  func init() {
//   statsd.UDPPayloadSize = 16 * 1024
//  }
var UDPPayloadSize int = 512

// Hostname is exported so clients can set it to something different than the default
var Hostname string

var errNotConnected = fmt.Errorf("cannot send stats, not connected to StatsD server")

func init() {
	host, err := os.Hostname()
	if nil == err {
		Hostname = host
	}
}

// StatsdClient is a client library to send events to StatsD
type StatsdClient struct {
	conn           net.Conn
	addr           string
	prefix         string
	eventStringTpl string
	Logger         Logger
}

// NewStatsdClient - Factory
func NewStatsdClient(addr string, prefix string) *StatsdClient {
	// allow %HOST% in the prefix string
	prefix = strings.Replace(prefix, "%HOST%", Hostname, 1)
	return &StatsdClient{
		addr:           addr,
		prefix:         prefix,
		Logger:         log.New(os.Stdout, "[StatsdClient] ", log.Ldate|log.Ltime),
		eventStringTpl: "%s%s:%s",
	}
}

// String returns the StatsD server address
func (c *StatsdClient) String() string {
	return c.addr
}

// CreateSocket creates a UDP connection to a StatsD server
func (c *StatsdClient) CreateSocket() error {
	conn, err := net.DialTimeout("udp", c.addr, 5*time.Second)
	if err != nil {
		return err
	}
	c.conn = conn
	return nil
}

// CreateTCPSocket creates a TCP connection to a StatsD server
func (c *StatsdClient) CreateTCPSocket() error {
	conn, err := net.DialTimeout("tcp", c.addr, 5*time.Second)
	if err != nil {
		return err
	}
	c.conn = conn
	c.eventStringTpl = "%s%s:%s\n"
	return nil
}

// Close the UDP connection
func (c *StatsdClient) Close() error {
	if nil == c.conn {
		return nil
	}
	return c.conn.Close()
}

// See statsd data types here: http://statsd.readthedocs.org/en/latest/types.html
// or also https://github.com/b/statsd_spec

// Incr - Increment a counter metric. Often used to note a particular event
func (c *StatsdClient) Incr(stat string, count int64) error {
	if 0 != count {
		return c.send(stat, "%d|c", count)
	}
	return nil
}

// Decr - Decrement a counter metric. Often used to note a particular event
func (c *StatsdClient) Decr(stat string, count int64) error {
	if 0 != count {
		return c.send(stat, "%d|c", -count)
	}
	return nil
}

// Timing - Track a duration event
// the time delta must be given in milliseconds
func (c *StatsdClient) Timing(stat string, delta int64) error {
	return c.send(stat, "%d|ms", delta)
}

// PrecisionTiming - Track a duration event
// the time delta has to be a duration
func (c *StatsdClient) PrecisionTiming(stat string, delta time.Duration) error {
	return c.send(stat, fmt.Sprintf("%.6f%s|ms", float64(delta)/float64(time.Millisecond), "%d"), 0)
}

// Gauge - Gauges are a constant data type. They are not subject to averaging,
// and they don’t change unless you change them. That is, once you set a gauge value,
// it will be a flat line on the graph until you change it again. If you specify
// delta to be true, that specifies that the gauge should be updated, not set. Due to the
// underlying protocol, you can't explicitly set a gauge to a negative number without
// first setting it to zero.
func (c *StatsdClient) Gauge(stat string, value int64) error {
	if value < 0 {
		c.send(stat, "%d|g", 0)
		return c.send(stat, "%d|g", value)
	}
	return c.send(stat, "%d|g", value)
}

// GaugeDelta -- Send a change for a gauge
func (c *StatsdClient) GaugeDelta(stat string, value int64) error {
	// Gauge Deltas are always sent with a leading '+' or '-'. The '-' takes care of itself but the '+' must added by hand
	if value < 0 {
		return c.send(stat, "%d|g", value)
	}
	return c.send(stat, "+%d|g", value)
}

// FGauge -- Send a floating point value for a gauge
func (c *StatsdClient) FGauge(stat string, value float64) error {
	if value < 0 {
		c.send(stat, "%d|g", 0)
		return c.send(stat, "%g|g", value)
	}
	return c.send(stat, "%g|g", value)
}

// FGaugeDelta -- Send a floating point change for a gauge
func (c *StatsdClient) FGaugeDelta(stat string, value float64) error {
	if value < 0 {
		return c.send(stat, "%g|g", value)
	}
	return c.send(stat, "+%g|g", value)
}

// Absolute - Send absolute-valued metric (not averaged/aggregated)
func (c *StatsdClient) Absolute(stat string, value int64) error {
	return c.send(stat, "%d|a", value)
}

// FAbsolute - Send absolute-valued floating point metric (not averaged/aggregated)
func (c *StatsdClient) FAbsolute(stat string, value float64) error {
	return c.send(stat, "%g|a", value)
}

// Total - Send a metric that is continously increasing, e.g. read operations since boot
func (c *StatsdClient) Total(stat string, value int64) error {
	return c.send(stat, "%d|t", value)
}

// write a UDP packet with the statsd event
func (c *StatsdClient) send(stat string, format string, value interface{}) error {
	if c.conn == nil {
		return errNotConnected
	}
	stat = strings.Replace(stat, "%HOST%", Hostname, 1)
	// if sending tcp append a newline
	format = fmt.Sprintf(c.eventStringTpl, c.prefix, stat, format)
	_, err := fmt.Fprintf(c.conn, format, value)
	return err
}

// SendEvent - Sends stats from an event object
func (c *StatsdClient) SendEvent(e event.Event) error {
	if c.conn == nil {
		return errNotConnected
	}
	for _, stat := range e.Stats() {
		//fmt.Printf("SENDING EVENT %s%s\n", c.prefix, strings.Replace(stat, "%HOST%", Hostname, 1))
		_, err := fmt.Fprintf(c.conn, "%s%s", c.prefix, strings.Replace(stat, "%HOST%", Hostname, 1))
		if nil != err {
			return err
		}
	}
	return nil
}

// SendEvents - Sends stats from all the event objects.
// Tries to bundle many together into one fmt.Fprintf based on UDPPayloadSize.
func (c *StatsdClient) SendEvents(events map[string]event.Event) error {
	if c.conn == nil {
		return fmt.Errorf("cannot send stats, not connected to StatsD server")
	}

	var n int
	var stats []string = make([]string, 0)

	for _, e := range events {
		for _, stat := range e.Stats() {

			stat = fmt.Sprintf("%s%s", c.prefix, strings.Replace(stat, "%HOST%", Hostname, 1))
			_n := n + len(stat) + 1

			if _n > UDPPayloadSize {
				// with this last event, the UDP payload would be too big
				if _, err := fmt.Fprintf(c.conn, strings.Join(stats, "\n")); err != nil {
					return err
				}
				// reset payload after flushing, and add the last event
				stats = []string{stat}
				n = len(stat)
				continue
			}

			// can fit more into the current payload
			n = _n
			stats = append(stats, stat)
		}
	}

	if len(stats) != 0 {
		if _, err := fmt.Fprintf(c.conn, strings.Join(stats, "\n")); err != nil {
			return err
		}
	}

	return nil
}
<commit_msg>pre-allocate error message<commit_after>package statsd

import (
	"fmt"
	"log"
	"net"
	"os"
	"strings"
	"time"

	"github.com/quipo/statsd/event"
)

// Logger interface compatible with log.Logger
type Logger interface {
	Println(v ...interface{})
}

// UDPPayloadSize is the number of bytes to send at one go through the udp socket.
// SendEvents will try to pack as many events into one udp packet.
// Change this value as per network capabilities
// For example to change to 16KB
//  import "github.com/quipo/statsd"
//  func init() {
//   statsd.UDPPayloadSize = 16 * 1024
//  }
var UDPPayloadSize int = 512

// Hostname is exported so clients can set it to something different than the default
var Hostname string

var errNotConnected = fmt.Errorf("cannot send stats, not connected to StatsD server")

func init() {
	host, err := os.Hostname()
	if nil == err {
		Hostname = host
	}
}

// StatsdClient is a client library to send events to StatsD
type StatsdClient struct {
	conn           net.Conn
	addr           string
	prefix         string
	eventStringTpl string
	Logger         Logger
}

// NewStatsdClient - Factory
func NewStatsdClient(addr string, prefix string) *StatsdClient {
	// allow %HOST% in the prefix string
	prefix = strings.Replace(prefix, "%HOST%", Hostname, 1)
	return &StatsdClient{
		addr:           addr,
		prefix:         prefix,
		Logger:         log.New(os.Stdout, "[StatsdClient] ", log.Ldate|log.Ltime),
		eventStringTpl: "%s%s:%s",
	}
}

// String returns the StatsD server address
func (c *StatsdClient) String() string {
	return c.addr
}

// CreateSocket creates a UDP connection to a StatsD server
func (c *StatsdClient) CreateSocket() error {
	conn, err := net.DialTimeout("udp", c.addr, 5*time.Second)
	if err != nil {
		return err
	}
	c.conn = conn
	return nil
}

// CreateTCPSocket creates a TCP connection to a StatsD server
func (c *StatsdClient) CreateTCPSocket() error {
	conn, err := net.DialTimeout("tcp", c.addr, 5*time.Second)
	if err != nil {
		return err
	}
	c.conn = conn
	c.eventStringTpl = "%s%s:%s\n"
	return nil
}

// Close the UDP connection
func (c *StatsdClient) Close() error {
	if nil == c.conn {
		return nil
	}
	return c.conn.Close()
}

// See statsd data types here: http://statsd.readthedocs.org/en/latest/types.html
// or also https://github.com/b/statsd_spec

// Incr - Increment a counter metric. Often used to note a particular event
func (c *StatsdClient) Incr(stat string, count int64) error {
	if 0 != count {
		return c.send(stat, "%d|c", count)
	}
	return nil
}

// Decr - Decrement a counter metric. Often used to note a particular event
func (c *StatsdClient) Decr(stat string, count int64) error {
	if 0 != count {
		return c.send(stat, "%d|c", -count)
	}
	return nil
}

// Timing - Track a duration event
// the time delta must be given in milliseconds
func (c *StatsdClient) Timing(stat string, delta int64) error {
	return c.send(stat, "%d|ms", delta)
}

// PrecisionTiming - Track a duration event
// the time delta has to be a duration
func (c *StatsdClient) PrecisionTiming(stat string, delta time.Duration) error {
	return c.send(stat, fmt.Sprintf("%.6f%s|ms", float64(delta)/float64(time.Millisecond), "%d"), 0)
}

// Gauge - Gauges are a constant data type. They are not subject to averaging,
// and they don’t change unless you change them. That is, once you set a gauge value,
// it will be a flat line on the graph until you change it again. If you specify
// delta to be true, that specifies that the gauge should be updated, not set. Due to the
// underlying protocol, you can't explicitly set a gauge to a negative number without
// first setting it to zero.
func (c *StatsdClient) Gauge(stat string, value int64) error {
	if value < 0 {
		c.send(stat, "%d|g", 0)
		return c.send(stat, "%d|g", value)
	}
	return c.send(stat, "%d|g", value)
}

// GaugeDelta -- Send a change for a gauge
func (c *StatsdClient) GaugeDelta(stat string, value int64) error {
	// Gauge Deltas are always sent with a leading '+' or '-'. The '-' takes care of itself but the '+' must added by hand
	if value < 0 {
		return c.send(stat, "%d|g", value)
	}
	return c.send(stat, "+%d|g", value)
}

// FGauge -- Send a floating point value for a gauge
func (c *StatsdClient) FGauge(stat string, value float64) error {
	if value < 0 {
		c.send(stat, "%d|g", 0)
		return c.send(stat, "%g|g", value)
	}
	return c.send(stat, "%g|g", value)
}

// FGaugeDelta -- Send a floating point change for a gauge
func (c *StatsdClient) FGaugeDelta(stat string, value float64) error {
	if value < 0 {
		return c.send(stat, "%g|g", value)
	}
	return c.send(stat, "+%g|g", value)
}

// Absolute - Send absolute-valued metric (not averaged/aggregated)
func (c *StatsdClient) Absolute(stat string, value int64) error {
	return c.send(stat, "%d|a", value)
}

// FAbsolute - Send absolute-valued floating point metric (not averaged/aggregated)
func (c *StatsdClient) FAbsolute(stat string, value float64) error {
	return c.send(stat, "%g|a", value)
}

// Total - Send a metric that is continously increasing, e.g. read operations since boot
func (c *StatsdClient) Total(stat string, value int64) error {
	return c.send(stat, "%d|t", value)
}

// write a UDP packet with the statsd event
func (c *StatsdClient) send(stat string, format string, value interface{}) error {
	if c.conn == nil {
		return errNotConnected
	}
	stat = strings.Replace(stat, "%HOST%", Hostname, 1)
	// if sending tcp append a newline
	format = fmt.Sprintf(c.eventStringTpl, c.prefix, stat, format)
	_, err := fmt.Fprintf(c.conn, format, value)
	return err
}

// SendEvent - Sends stats from an event object
func (c *StatsdClient) SendEvent(e event.Event) error {
	if c.conn == nil {
		return errNotConnected
	}
	for _, stat := range e.Stats() {
		//fmt.Printf("SENDING EVENT %s%s\n", c.prefix, strings.Replace(stat, "%HOST%", Hostname, 1))
		_, err := fmt.Fprintf(c.conn, "%s%s", c.prefix, strings.Replace(stat, "%HOST%", Hostname, 1))
		if nil != err {
			return err
		}
	}
	return nil
}

// SendEvents - Sends stats from all the event objects.
// Tries to bundle many together into one fmt.Fprintf based on UDPPayloadSize.
func (c *StatsdClient) SendEvents(events map[string]event.Event) error {
	if c.conn == nil {
		return errNotConnected
	}

	var n int
	var stats []string = make([]string, 0)

	for _, e := range events {
		for _, stat := range e.Stats() {

			stat = fmt.Sprintf("%s%s", c.prefix, strings.Replace(stat, "%HOST%", Hostname, 1))
			_n := n + len(stat) + 1

			if _n > UDPPayloadSize {
				// with this last event, the UDP payload would be too big
				if _, err := fmt.Fprintf(c.conn, strings.Join(stats, "\n")); err != nil {
					return err
				}
				// reset payload after flushing, and add the last event
				stats = []string{stat}
				n = len(stat)
				continue
			}

			// can fit more into the current payload
			n = _n
			stats = append(stats, stat)
		}
	}

	if len(stats) != 0 {
		if _, err := fmt.Fprintf(c.conn, strings.Join(stats, "\n")); err != nil {
			return err
		}
	}

	return nil
}
<|endoftext|>


<commit_before>// Copyright 2016 Yeung Shu Hung and The Go Authors.
// All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This file implements the web server side for FastCGI
// as specified in http://www.fastcgi.com/drupal/node/22

// A part of this file is from golang package net/http/cgi,
// in particular https://golang.org/src/net/http/cgi/host.go

package gofast

import (
	"bufio"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"strconv"
	"strings"
	"sync"
)

// Request hold information of a standard
// FastCGI request
type Request struct {
	ID       uint16
	Params   map[string]string
	Stdin    io.Reader
	KeepConn bool
}

// client is the default implementation of Client
type client struct {
	conn   *conn
	chanID chan uint16
}

// AllocID implements Client.AllocID
func (c *client) AllocID() (reqID uint16) {
	reqID = <-c.chanID
	return
}

// ReleaseID implements Client.ReleaseID
func (c *client) ReleaseID(reqID uint16) {
	go func() {
		// release the ID back to channel for reuse
		// use goroutine to prevent blocking ReleaseID
		c.chanID <- reqID
	}()
}

// Do implements Client.Do
func (c *client) Do(req *Request) (resp *ResponsePipe, err error) {

	resp = NewResponsePipe()

	// read all from stdin and determine the content length
	stdin := []byte{}
	if req.Stdin != nil {
		stdin, err = ioutil.ReadAll(req.Stdin)
		if err != nil {
			return
		}
		req.Params["CONTENT_LENGTH"] = fmt.Sprintf("%d", len(stdin))
	} else {
		req.Params["CONTENT_LENGTH"] = "0"
	}

	// FIXME: add other role implementation, add role field to Request
	err = c.conn.writeBeginRequest(req.ID, uint16(roleResponder), 0)
	if err != nil {
		resp.Close()
		return
	}
	err = c.conn.writePairs(typeParams, req.ID, req.Params)
	if err != nil {
		resp.Close()
		return
	}
	err = c.conn.writeRecord(typeStdin, req.ID, stdin)
	if err != nil {
		resp.Close()
		return
	}

	// NOTE: all errors return before goroutine (readLoop)
	go func() {
		var rec record

		defer c.ReleaseID(req.ID)
		defer resp.Close()
	readLoop:
		for {
			if err := rec.read(c.conn.rwc); err != nil {
				break
			}

			// different output type for different stream
			switch rec.h.Type {
			case typeStdout:
				resp.stdOutWriter.Write(rec.content())
			case typeStderr:
				resp.stdErrWriter.Write(rec.content())
			case typeEndRequest:
				break readLoop
			default:
				panic(fmt.Sprintf("unexpected type %#v in readLoop", rec.h.Type))
			}
		}
	}()

	return
}

// NewRequest implements Client.NewRequest
func (c *client) NewRequest(r *http.Request) (req *Request) {
	req = &Request{
		ID:     c.AllocID(),
		Params: make(map[string]string),
	}

	// if no http request, return here
	if r == nil {
		return
	}

	// define some required cgi parameters
	// with the given http request
	req.Params["SERVER_SOFTWARE"] = "go"
	req.Params["SERVER_NAME"] = r.Host
	req.Params["SERVER_PROTOCOL"] = "HTTP/1.1"
	req.Params["HTTP_HOST"] = r.Host
	req.Params["GATEWAY_INTERFACE"] = "CGI/1.1"
	req.Params["REQUEST_METHOD"] = r.Method
	req.Params["QUERY_STRING"] = r.URL.RawQuery
	req.Params["REQUEST_URI"] = r.URL.RequestURI()

	/*
		// FIXME: add these parameter automatically
		// from net/cgi Handler.ServeHTTP
		// should add later
		"PATH_INFO=" + pathInfo,
		"SCRIPT_NAME=" + root,
		"SCRIPT_FILENAME=" + h.Path,
		"SERVER_PORT=" + port,
	*/

	// pass body (io.ReadCloser) to stdio
	req.Stdin = r.Body

	return
}

// Client is a client interface of FastCGI
// application process through given
// connection (net.Conn)
type Client interface {

	// Do takes care of a proper FastCGI request
	Do(req *Request) (resp *ResponsePipe, err error)

	// NewRequest returns a standard FastCGI request
	// with a unique request ID allocted by the client
	NewRequest(*http.Request) *Request

	// AllocID allocates a new reqID.
	// It blocks if all possible uint16 IDs are allocated.
	AllocID() uint16

	// ReleaseID releases a reqID.
	// It never blocks.
	ReleaseID(uint16)
}

// NewClient returns a Client of the given
// connection (net.Conn).
//
// limit is the maximum number of request that the
// applcation support. 0 means the maximum number
// available for 16bit request id (65536).
// Default 0.
//
func NewClient(conn net.Conn, limit uint32) Client {
	cid := make(chan uint16)

	if limit == 0 || limit > 65536 {
		limit = 65536
	}
	go func(maxID uint16) {
		for i := uint16(0); i < maxID; i++ {
			cid <- i
		}
		cid <- uint16(maxID)
	}(uint16(limit - 1))

	return &client{
		conn:   newConn(conn),
		chanID: cid,
	}
}

// NewResponsePipe returns an initialized new ResponsePipe struct
func NewResponsePipe() (p *ResponsePipe) {
	p = new(ResponsePipe)
	p.stdOutReader, p.stdOutWriter = io.Pipe()
	p.stdErrReader, p.stdErrWriter = io.Pipe()
	return
}

// ResponsePipe contains readers and writers that handles
// all FastCGI output streams
type ResponsePipe struct {
	stdOutReader io.Reader
	stdOutWriter io.WriteCloser
	stdErrReader io.Reader
	stdErrWriter io.WriteCloser
}

// Close close all writers
func (pipes *ResponsePipe) Close() {
	pipes.stdOutWriter.Close()
	pipes.stdErrWriter.Close()
}

// WriteTo writes the given output into http.ResponseWriter
func (pipes *ResponsePipe) WriteTo(rw http.ResponseWriter, ew io.Writer) (err error) {
	wg := new(sync.WaitGroup)
	wg.Add(2)

	go func() {
		defer wg.Done()
		err = pipes.writeResponse(rw)
	}()

	go func() {
		defer wg.Done()
		err = pipes.writeError(ew)
	}()

	// blocks until all reads and writes are done
	wg.Wait()
	return
}

func (pipes *ResponsePipe) writeError(w io.Writer) (err error) {
	_, err = io.Copy(w, pipes.stdErrReader)
	if err != nil {
		err = fmt.Errorf("gofast: copy error: %v", err.Error())
	}
	return
}

// writeTo writes the given output into http.ResponseWriter
func (pipes *ResponsePipe) writeResponse(w http.ResponseWriter) (err error) {
	linebody := bufio.NewReaderSize(pipes.stdOutReader, 1024)
	headers := make(http.Header)
	statusCode := 0
	headerLines := 0
	sawBlankLine := false

	for {
		var line []byte
		var isPrefix bool
		line, isPrefix, err = linebody.ReadLine()
		if isPrefix {
			w.WriteHeader(http.StatusInternalServerError)
			err = fmt.Errorf("gofast: long header line from subprocess")
			return
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			err = fmt.Errorf("gofast: error reading headers: %v", err)
			return
		}
		if len(line) == 0 {
			sawBlankLine = true
			break
		}
		headerLines++
		parts := strings.SplitN(string(line), ":", 2)
		if len(parts) < 2 {
			err = fmt.Errorf("gofast: bogus header line: %s", string(line))
			continue
		}
		header, val := parts[0], parts[1]
		header = strings.TrimSpace(header)
		val = strings.TrimSpace(val)
		switch {
		case header == "Status":
			if len(val) < 3 {
				err = fmt.Errorf("gofast: bogus status (short): %q", val)
				return
			}
			var code int
			code, err = strconv.Atoi(val[0:3])
			if err != nil {
				err = fmt.Errorf("gofast: bogus status: %q\nline was %q",
					val, line)
				return
			}
			statusCode = code
		default:
			headers.Add(header, val)
		}
	}
	if headerLines == 0 || !sawBlankLine {
		w.WriteHeader(http.StatusInternalServerError)
		err = fmt.Errorf("gofast: no headers")
		return
	}

	if loc := headers.Get("Location"); loc != "" {
		/*
			if strings.HasPrefix(loc, "/") && h.PathLocationHandler != nil {
				h.handleInternalRedirect(rw, req, loc)
				return
			}
		*/
		if statusCode == 0 {
			statusCode = http.StatusFound
		}
	}

	if statusCode == 0 && headers.Get("Content-Type") == "" {
		w.WriteHeader(http.StatusInternalServerError)
		err = fmt.Errorf("gofast: missing required Content-Type in headers")
		return
	}

	if statusCode == 0 {
		statusCode = http.StatusOK
	}

	// Copy headers to rw's headers, after we've decided not to
	// go into handleInternalRedirect, which won't want its rw
	// headers to have been touched.
	for k, vv := range headers {
		for _, v := range vv {
			w.Header().Add(k, v)
		}
	}

	w.WriteHeader(statusCode)

	_, err = io.Copy(w, linebody)
	if err != nil {
		err = fmt.Errorf("gofast: copy error: %v", err)
	}
	return
}
<commit_msg>Fix CONTENT_LENGTH issue with 0<commit_after>// Copyright 2016 Yeung Shu Hung and The Go Authors.
// All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This file implements the web server side for FastCGI
// as specified in http://www.fastcgi.com/drupal/node/22

// A part of this file is from golang package net/http/cgi,
// in particular https://golang.org/src/net/http/cgi/host.go

package gofast

import (
	"bufio"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"strconv"
	"strings"
	"sync"
)

// Request hold information of a standard
// FastCGI request
type Request struct {
	ID       uint16
	Params   map[string]string
	Stdin    io.Reader
	KeepConn bool
}

// client is the default implementation of Client
type client struct {
	conn   *conn
	chanID chan uint16
}

// AllocID implements Client.AllocID
func (c *client) AllocID() (reqID uint16) {
	reqID = <-c.chanID
	return
}

// ReleaseID implements Client.ReleaseID
func (c *client) ReleaseID(reqID uint16) {
	go func() {
		// release the ID back to channel for reuse
		// use goroutine to prevent blocking ReleaseID
		c.chanID <- reqID
	}()
}

// Do implements Client.Do
func (c *client) Do(req *Request) (resp *ResponsePipe, err error) {

	resp = NewResponsePipe()

	// read all from stdin and determine the content length
	stdin := []byte{}
	if req.Stdin != nil {
		stdin, err = ioutil.ReadAll(req.Stdin)
		if err != nil {
			return
		}
		req.Params["CONTENT_LENGTH"] = fmt.Sprintf("%d", len(stdin))
	} else {
		req.Params["CONTENT_LENGTH"] = ""
	}

	// FIXME: add other role implementation, add role field to Request
	err = c.conn.writeBeginRequest(req.ID, uint16(roleResponder), 0)
	if err != nil {
		resp.Close()
		return
	}
	err = c.conn.writePairs(typeParams, req.ID, req.Params)
	if err != nil {
		resp.Close()
		return
	}
	err = c.conn.writeRecord(typeStdin, req.ID, stdin)
	if err != nil {
		resp.Close()
		return
	}

	// NOTE: all errors return before goroutine (readLoop)
	go func() {
		var rec record

		defer c.ReleaseID(req.ID)
		defer resp.Close()
	readLoop:
		for {
			if err := rec.read(c.conn.rwc); err != nil {
				break
			}

			// different output type for different stream
			switch rec.h.Type {
			case typeStdout:
				resp.stdOutWriter.Write(rec.content())
			case typeStderr:
				resp.stdErrWriter.Write(rec.content())
			case typeEndRequest:
				break readLoop
			default:
				panic(fmt.Sprintf("unexpected type %#v in readLoop", rec.h.Type))
			}
		}
	}()

	return
}

// NewRequest implements Client.NewRequest
func (c *client) NewRequest(r *http.Request) (req *Request) {
	req = &Request{
		ID:     c.AllocID(),
		Params: make(map[string]string),
	}

	// if no http request, return here
	if r == nil {
		return
	}

	// define some required cgi parameters
	// with the given http request
	req.Params["SERVER_SOFTWARE"] = "go"
	req.Params["SERVER_NAME"] = r.Host
	req.Params["SERVER_PROTOCOL"] = "HTTP/1.1"
	req.Params["HTTP_HOST"] = r.Host
	req.Params["GATEWAY_INTERFACE"] = "CGI/1.1"
	req.Params["REQUEST_METHOD"] = r.Method
	req.Params["QUERY_STRING"] = r.URL.RawQuery
	req.Params["REQUEST_URI"] = r.URL.RequestURI()

	/*
		// FIXME: add these parameter automatically
		// from net/cgi Handler.ServeHTTP
		// should add later
		"PATH_INFO=" + pathInfo,
		"SCRIPT_NAME=" + root,
		"SCRIPT_FILENAME=" + h.Path,
		"SERVER_PORT=" + port,
	*/

	// pass body (io.ReadCloser) to stdio
	req.Stdin = r.Body

	return
}

// Client is a client interface of FastCGI
// application process through given
// connection (net.Conn)
type Client interface {

	// Do takes care of a proper FastCGI request
	Do(req *Request) (resp *ResponsePipe, err error)

	// NewRequest returns a standard FastCGI request
	// with a unique request ID allocted by the client
	NewRequest(*http.Request) *Request

	// AllocID allocates a new reqID.
	// It blocks if all possible uint16 IDs are allocated.
	AllocID() uint16

	// ReleaseID releases a reqID.
	// It never blocks.
	ReleaseID(uint16)
}

// NewClient returns a Client of the given
// connection (net.Conn).
//
// limit is the maximum number of request that the
// applcation support. 0 means the maximum number
// available for 16bit request id (65536).
// Default 0.
//
func NewClient(conn net.Conn, limit uint32) Client {
	cid := make(chan uint16)

	if limit == 0 || limit > 65536 {
		limit = 65536
	}
	go func(maxID uint16) {
		for i := uint16(0); i < maxID; i++ {
			cid <- i
		}
		cid <- uint16(maxID)
	}(uint16(limit - 1))

	return &client{
		conn:   newConn(conn),
		chanID: cid,
	}
}

// NewResponsePipe returns an initialized new ResponsePipe struct
func NewResponsePipe() (p *ResponsePipe) {
	p = new(ResponsePipe)
	p.stdOutReader, p.stdOutWriter = io.Pipe()
	p.stdErrReader, p.stdErrWriter = io.Pipe()
	return
}

// ResponsePipe contains readers and writers that handles
// all FastCGI output streams
type ResponsePipe struct {
	stdOutReader io.Reader
	stdOutWriter io.WriteCloser
	stdErrReader io.Reader
	stdErrWriter io.WriteCloser
}

// Close close all writers
func (pipes *ResponsePipe) Close() {
	pipes.stdOutWriter.Close()
	pipes.stdErrWriter.Close()
}

// WriteTo writes the given output into http.ResponseWriter
func (pipes *ResponsePipe) WriteTo(rw http.ResponseWriter, ew io.Writer) (err error) {
	wg := new(sync.WaitGroup)
	wg.Add(2)

	go func() {
		defer wg.Done()
		err = pipes.writeResponse(rw)
	}()

	go func() {
		defer wg.Done()
		err = pipes.writeError(ew)
	}()

	// blocks until all reads and writes are done
	wg.Wait()
	return
}

func (pipes *ResponsePipe) writeError(w io.Writer) (err error) {
	_, err = io.Copy(w, pipes.stdErrReader)
	if err != nil {
		err = fmt.Errorf("gofast: copy error: %v", err.Error())
	}
	return
}

// writeTo writes the given output into http.ResponseWriter
func (pipes *ResponsePipe) writeResponse(w http.ResponseWriter) (err error) {
	linebody := bufio.NewReaderSize(pipes.stdOutReader, 1024)
	headers := make(http.Header)
	statusCode := 0
	headerLines := 0
	sawBlankLine := false

	for {
		var line []byte
		var isPrefix bool
		line, isPrefix, err = linebody.ReadLine()
		if isPrefix {
			w.WriteHeader(http.StatusInternalServerError)
			err = fmt.Errorf("gofast: long header line from subprocess")
			return
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			err = fmt.Errorf("gofast: error reading headers: %v", err)
			return
		}
		if len(line) == 0 {
			sawBlankLine = true
			break
		}
		headerLines++
		parts := strings.SplitN(string(line), ":", 2)
		if len(parts) < 2 {
			err = fmt.Errorf("gofast: bogus header line: %s", string(line))
			continue
		}
		header, val := parts[0], parts[1]
		header = strings.TrimSpace(header)
		val = strings.TrimSpace(val)
		switch {
		case header == "Status":
			if len(val) < 3 {
				err = fmt.Errorf("gofast: bogus status (short): %q", val)
				return
			}
			var code int
			code, err = strconv.Atoi(val[0:3])
			if err != nil {
				err = fmt.Errorf("gofast: bogus status: %q\nline was %q",
					val, line)
				return
			}
			statusCode = code
		default:
			headers.Add(header, val)
		}
	}
	if headerLines == 0 || !sawBlankLine {
		w.WriteHeader(http.StatusInternalServerError)
		err = fmt.Errorf("gofast: no headers")
		return
	}

	if loc := headers.Get("Location"); loc != "" {
		/*
			if strings.HasPrefix(loc, "/") && h.PathLocationHandler != nil {
				h.handleInternalRedirect(rw, req, loc)
				return
			}
		*/
		if statusCode == 0 {
			statusCode = http.StatusFound
		}
	}

	if statusCode == 0 && headers.Get("Content-Type") == "" {
		w.WriteHeader(http.StatusInternalServerError)
		err = fmt.Errorf("gofast: missing required Content-Type in headers")
		return
	}

	if statusCode == 0 {
		statusCode = http.StatusOK
	}

	// Copy headers to rw's headers, after we've decided not to
	// go into handleInternalRedirect, which won't want its rw
	// headers to have been touched.
	for k, vv := range headers {
		for _, v := range vv {
			w.Header().Add(k, v)
		}
	}

	w.WriteHeader(statusCode)

	_, err = io.Copy(w, linebody)
	if err != nil {
		err = fmt.Errorf("gofast: copy error: %v", err)
	}
	return
}
<|endoftext|>


<commit_before>// A simple chat client to talk to the simple chat server (./server.go)
// To run the client, use "go run client.go {username}" where username is your username
// This will listen for chat room events and display them
// (someone entered the room, left the room, or chatted something)
// To chat a message, simply type something after you run the program and press the enter key
//
// reference https://gist.github.com/iwanbk/2295233
//           http://golang.org/pkg/net/
package main

import (
  "fmt"
  "os"
  "net"
  "bufio"
  "regexp"
  "strings"
  "./util"
)

// input message regular expression (look for a command /whatever)
var standardInputMessageRegex, _ = regexp.Compile(`^\/([^\s]*)\s*(.*)$`)
// chat server command /command [username] body contents
var chatServerResponseRegex, _ = regexp.Compile(`^\/([^\s]*)\s?(?:\[([^\]]*)\])?\s*(.*)$`)

// container for chat server Command details
type Command struct {
  // "leave", "message", "enter"
  Command string
  Username string
  Body string
}

// program main
func main() {
  username, properties := getConfig();

  conn, err := net.Dial("tcp", properties.Hostname + ":" + properties.Port)
  util.CheckForError(err, "Connection refused")
  defer conn.Close()

  // we're listening to chat server commands *and* user terminal commands
  go watchForConnectionInput(username, properties, conn)
  for true {
    watchForConsoleInput(conn)
  }
}

// parse out the arguments to be used when connecting to the chat server
func getConfig() (string, util.Properties) {
  if (len(os.Args) >= 2) {
    username := os.Args[1]
    properties := util.LoadConfig()
    return username, properties
  } else {
    println("You must provide the username as the first parameter ")
    os.Exit(1)
    return "", util.Properties{}
  }
}

// keep watching for console input
// send the "message" command to the chat server when we have some
func watchForConsoleInput(conn net.Conn) {
  reader := bufio.NewReader(os.Stdin)

  for true {
    message, err := reader.ReadString('\n')
    util.CheckForError(err, "Lost console connection")

    message = strings.TrimSpace(message)
    if (message != "") {
      command := parseInput(message)

      if (command.Command == "") {
        // there is no command so treat this as a simple message to be sent out
        sendCommand("message", message, conn);
      } else {
        switch command.Command {

          // enter a room
          case "enter":
            sendCommand("enter", command.Body, conn)

          // ignore someone
          case "ignore":
            sendCommand("ignore", command.Body, conn)

          // leave a room
          case "leave":
            // leave the current room (we aren't allowing multiple rooms)
            sendCommand("leave", "", conn)

          // disconnect from the chat server
          case "disconnect":
            sendCommand("disconnect", "", conn)

          default:
            fmt.Printf("Unknown command\"%s\"", command.Command)
        }
      }
    }
  }
}

// listen for any commands that come from the chat server
// like someone entered the room, said something, or left the room
func watchForConnectionInput(username string, properties util.Properties, conn net.Conn) {
  reader := bufio.NewReader(conn)

  for true {
    message, err := reader.ReadString('\n')
    util.CheckForError(err, "Lost server connection");
    message = strings.TrimSpace(message)
    if (message != "") {
      Command := parseCommand(message)
      switch Command.Command {

        // the handshake - send out our username
        case "ready":
          sendCommand("user", username, conn)

        // the user has connected to the chat server
        case "connect":
          fmt.Printf(properties.HasEnteredTheLobbyMessage + "\n", Command.Username)

        // the user has disconnected
        case "disconnect":
          fmt.Printf(properties.HasLeftTheLobbyMessage + "\n", Command.Username)

        // the user has entered a room
        case "enter":
          fmt.Printf(properties.HasEnteredTheRoomMessage + "\n", Command.Username, Command.Body)

        // the user has left a room
        case "leave":
          fmt.Printf(properties.HasLeftTheRoomMessage + "\n", Command.Username, Command.Body)

        // the user has sent a message
        case "message":
          if (Command.Username != username) {
            fmt.Printf(properties.ReceivedAMessage + "\n", Command.Username, Command.Body)
          }

        // the user has connected to the chat server
        case "ignoring":
          fmt.Printf(properties.IgnoringMessage + "\n", Command.Body)
      }
    }
  }
}

// send a command to the chat server
// commands are in the form of /command {command specific body content}\n
func sendCommand(command string, body string, conn net.Conn) {
  message := fmt.Sprintf("/%v %v\n", util.Encode(command), util.Encode(body));
  conn.Write([]byte(message))
}

// parse the input message and return an Command
// if there is a command the "Command" will != "", otherwise just Body will exist
func parseInput(message string) Command {
  res := standardInputMessageRegex.FindAllStringSubmatch(message, -1)
  if (len(res) == 1) {
    // there is a command
    return Command {
      Command: res[0][1],
      Body: res[0][2],
    }
  } else {
    return Command {
      Body: util.Decode(message),
    }
  }
}

// look for "/Command [name] body contents" where [name] is optional
func parseCommand(message string) Command {
  res := chatServerResponseRegex.FindAllStringSubmatch(message, -1)
  if (len(res) == 1) {
    // we've got a match
    return Command {
      Command: util.Decode(res[0][1]),
      Username: util.Decode(res[0][2]),
      Body: util.Decode(res[0][3]),
    }
  } else {
    // it's irritating that I can't return a nil value here - must be something I'm missing
    return Command{}
  }
}
<commit_msg>fix log message<commit_after>// A simple chat client to talk to the simple chat server (./server.go)
// To run the client, use "go run client.go {username}" where username is your username
// This will listen for chat room events and display them
// (someone entered the room, left the room, or chatted something)
// To chat a message, simply type something after you run the program and press the enter key
//
// reference https://gist.github.com/iwanbk/2295233
//           http://golang.org/pkg/net/
package main

import (
  "fmt"
  "os"
  "net"
  "bufio"
  "regexp"
  "strings"
  "./util"
)

// input message regular expression (look for a command /whatever)
var standardInputMessageRegex, _ = regexp.Compile(`^\/([^\s]*)\s*(.*)$`)
// chat server command /command [username] body contents
var chatServerResponseRegex, _ = regexp.Compile(`^\/([^\s]*)\s?(?:\[([^\]]*)\])?\s*(.*)$`)

// container for chat server Command details
type Command struct {
  // "leave", "message", "enter"
  Command string
  Username string
  Body string
}

// program main
func main() {
  username, properties := getConfig();

  conn, err := net.Dial("tcp", properties.Hostname + ":" + properties.Port)
  util.CheckForError(err, "Connection refused")
  defer conn.Close()

  // we're listening to chat server commands *and* user terminal commands
  go watchForConnectionInput(username, properties, conn)
  for true {
    watchForConsoleInput(conn)
  }
}

// parse out the arguments to be used when connecting to the chat server
func getConfig() (string, util.Properties) {
  if (len(os.Args) >= 2) {
    username := os.Args[1]
    properties := util.LoadConfig()
    return username, properties
  } else {
    println("You must provide the username as the first parameter ")
    os.Exit(1)
    return "", util.Properties{}
  }
}

// keep watching for console input
// send the "message" command to the chat server when we have some
func watchForConsoleInput(conn net.Conn) {
  reader := bufio.NewReader(os.Stdin)

  for true {
    message, err := reader.ReadString('\n')
    util.CheckForError(err, "Lost console connection")

    message = strings.TrimSpace(message)
    if (message != "") {
      command := parseInput(message)

      if (command.Command == "") {
        // there is no command so treat this as a simple message to be sent out
        sendCommand("message", message, conn);
      } else {
        switch command.Command {

          // enter a room
          case "enter":
            sendCommand("enter", command.Body, conn)

          // ignore someone
          case "ignore":
            sendCommand("ignore", command.Body, conn)

          // leave a room
          case "leave":
            // leave the current room (we aren't allowing multiple rooms)
            sendCommand("leave", "", conn)

          // disconnect from the chat server
          case "disconnect":
            sendCommand("disconnect", "", conn)

          default:
            fmt.Printf("Unknown command \"%s\"\n", command.Command)
        }
      }
    }
  }
}

// listen for any commands that come from the chat server
// like someone entered the room, said something, or left the room
func watchForConnectionInput(username string, properties util.Properties, conn net.Conn) {
  reader := bufio.NewReader(conn)

  for true {
    message, err := reader.ReadString('\n')
    util.CheckForError(err, "Lost server connection");
    message = strings.TrimSpace(message)
    if (message != "") {
      Command := parseCommand(message)
      switch Command.Command {

        // the handshake - send out our username
        case "ready":
          sendCommand("user", username, conn)

        // the user has connected to the chat server
        case "connect":
          fmt.Printf(properties.HasEnteredTheLobbyMessage + "\n", Command.Username)

        // the user has disconnected
        case "disconnect":
          fmt.Printf(properties.HasLeftTheLobbyMessage + "\n", Command.Username)

        // the user has entered a room
        case "enter":
          fmt.Printf(properties.HasEnteredTheRoomMessage + "\n", Command.Username, Command.Body)

        // the user has left a room
        case "leave":
          fmt.Printf(properties.HasLeftTheRoomMessage + "\n", Command.Username, Command.Body)

        // the user has sent a message
        case "message":
          if (Command.Username != username) {
            fmt.Printf(properties.ReceivedAMessage + "\n", Command.Username, Command.Body)
          }

        // the user has connected to the chat server
        case "ignoring":
          fmt.Printf(properties.IgnoringMessage + "\n", Command.Body)
      }
    }
  }
}

// send a command to the chat server
// commands are in the form of /command {command specific body content}\n
func sendCommand(command string, body string, conn net.Conn) {
  message := fmt.Sprintf("/%v %v\n", util.Encode(command), util.Encode(body));
  conn.Write([]byte(message))
}

// parse the input message and return an Command
// if there is a command the "Command" will != "", otherwise just Body will exist
func parseInput(message string) Command {
  res := standardInputMessageRegex.FindAllStringSubmatch(message, -1)
  if (len(res) == 1) {
    // there is a command
    return Command {
      Command: res[0][1],
      Body: res[0][2],
    }
  } else {
    return Command {
      Body: util.Decode(message),
    }
  }
}

// look for "/Command [name] body contents" where [name] is optional
func parseCommand(message string) Command {
  res := chatServerResponseRegex.FindAllStringSubmatch(message, -1)
  if (len(res) == 1) {
    // we've got a match
    return Command {
      Command: util.Decode(res[0][1]),
      Username: util.Decode(res[0][2]),
      Body: util.Decode(res[0][3]),
    }
  } else {
    // it's irritating that I can't return a nil value here - must be something I'm missing
    return Command{}
  }
}
<|endoftext|>


<commit_before>package stun

import (
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"runtime"
	"sync"
	"sync/atomic"
	"time"
)

// Dial connects to the address on the named network and then
// initializes Client on that connection, returning error if any.
func Dial(network, address string) (*Client, error) {
	conn, err := net.Dial(network, address)
	if err != nil {
		return nil, err
	}
	return NewClient(ClientOptions{
		Connection: conn,
	})
}

// ClientOptions are used to initialize Client.
type ClientOptions struct {
	Agent       ClientAgent
	Connection  Connection
	TimeoutRate time.Duration // defaults to 100 ms
}

const defaultTimeoutRate = time.Millisecond * 100

// ErrNoConnection means that ClientOptions.Connection is nil.
var ErrNoConnection = errors.New("no connection provided")

// NewClient initializes new Client from provided options,
// starting internal goroutines and using default options fields
// if necessary. Call Close method after using Client to release
// resources.
func NewClient(options ClientOptions) (*Client, error) {
	c := &Client{
		close:       make(chan struct{}),
		c:           options.Connection,
		a:           options.Agent,
		gcRate:      options.TimeoutRate,
		clock:       systemClock,
		rto:         int64(time.Millisecond * 500),
		t:           make(map[transactionID]*clientTransaction, 100),
		maxAttempts: 7,
	}
	if c.c == nil {
		return nil, ErrNoConnection
	}
	if c.a == nil {
		c.a = NewAgent(AgentOptions{})
	}
	if c.gcRate == 0 {
		c.gcRate = defaultTimeoutRate
	}
	if err := c.a.SetHandler(c.handleAgentCallback); err != nil {
		return nil, err
	}
	c.wg.Add(2)
	go c.readUntilClosed()
	go c.collectUntilClosed()
	runtime.SetFinalizer(c, clientFinalizer)
	return c, nil
}

func clientFinalizer(c *Client) {
	if c == nil {
		return
	}
	err := c.Close()
	if err == ErrClientClosed {
		return
	}
	if err == nil {
		log.Println("client: called finalizer on non-closed client")
		return
	}
	log.Println("client: called finalizer on non-closed client:", err)
}

// Connection wraps Reader, Writer and Closer interfaces.
type Connection interface {
	io.Reader
	io.Writer
	io.Closer
}

// ClientAgent is Agent implementation that is used by Client to
// process transactions.
type ClientAgent interface {
	Process(*Message) error
	Close() error
	Start(id [TransactionIDSize]byte, deadline time.Time) error
	Stop(id [TransactionIDSize]byte) error
	Collect(time.Time) error
	SetHandler(h Handler) error
}

// Client simulates "connection" to STUN server.
type Client struct {
	a           ClientAgent
	c           Connection
	close       chan struct{}
	gcRate      time.Duration
	rto         int64 // time.Duration
	maxAttempts int32
	closed      bool
	closedMux   sync.RWMutex
	wg          sync.WaitGroup
	clock       Clock

	t    map[transactionID]*clientTransaction
	tMux sync.RWMutex
}

// clientTransaction represents transaction in progress.
// If transaction is succeed or failed, f will be called
// provided by event.
// Concurrent access is invalid.
type clientTransaction struct {
	id      transactionID
	attempt int32
	h       Handler
	start   time.Time
	rto     time.Duration
	raw     []byte
}

var clientTransactionPool = &sync.Pool{
	New: func() interface{} {
		return &clientTransaction{
			raw: make([]byte, 1500),
		}
	},
}

func acquireClientTransaction() *clientTransaction {
	return clientTransactionPool.Get().(*clientTransaction)
}

func putClientTransaction(t *clientTransaction) {
	clientTransactionPool.Put(t)
}

func (t clientTransaction) nextTimeout(now time.Time) time.Time {
	return now.Add(time.Duration(t.attempt) * t.rto)
}

// Start registers transaction with provided id, deadline and callback.
// Could return ErrAgentClosed, ErrTransactionExists.
// Callback f is guaranteed to be eventually called. See AgentFn for
// callback processing constraints.
func (c *Client) start(t *clientTransaction) error {
	c.tMux.Lock()
	defer c.tMux.Unlock()
	if c.closed {
		return ErrAgentClosed
	}
	_, exists := c.t[t.id]
	if exists {
		return ErrTransactionExists
	}
	c.t[t.id] = t
	return nil
}

// Clock abstracts the source of current time.
type Clock interface {
	Now() time.Time
}

type systemClockService struct{}

func (systemClockService) Now() time.Time { return time.Now() }

var systemClock = systemClockService{}

// SetRTO sets current RTO value.
func (c *Client) SetRTO(rto time.Duration) {
	atomic.StoreInt64(&c.rto, int64(rto))
}

// StopErr occurs when Client fails to stop transaction while
// processing error.
type StopErr struct {
	Err   error // value returned by Stop()
	Cause error // error that caused Stop() call
}

func (e StopErr) Error() string {
	return fmt.Sprintf("error while stopping due to %s: %s",
		sprintErr(e.Cause), sprintErr(e.Err),
	)
}

// CloseErr indicates client close failure.
type CloseErr struct {
	AgentErr      error
	ConnectionErr error
}

func sprintErr(err error) string {
	if err == nil {
		return "<nil>"
	}
	return err.Error()
}

func (c CloseErr) Error() string {
	return fmt.Sprintf("failed to close: %s (connection), %s (agent)",
		sprintErr(c.ConnectionErr), sprintErr(c.AgentErr),
	)
}

func (c *Client) readUntilClosed() {
	defer c.wg.Done()
	m := new(Message)
	m.Raw = make([]byte, 1024)
	for {
		select {
		case <-c.close:
			return
		default:
		}
		_, err := m.ReadFrom(c.c)
		if err == nil {
			if pErr := c.a.Process(m); pErr == ErrAgentClosed {
				return
			}
		}
	}
}

func closedOrPanic(err error) {
	if err == nil || err == ErrAgentClosed {
		return
	}
	panic(err)
}

func (c *Client) collectUntilClosed() {
	t := time.NewTicker(c.gcRate)
	defer c.wg.Done()
	for {
		select {
		case <-c.close:
			t.Stop()
			return
		case gcTime := <-t.C:
			closedOrPanic(c.a.Collect(gcTime))
		}
	}
}

// ErrClientClosed indicates that client is closed.
var ErrClientClosed = errors.New("client is closed")

// Close stops internal connection and agent, returning CloseErr on error.
func (c *Client) Close() error {
	if err := c.checkInit(); err != nil {
		return err
	}
	c.closedMux.Lock()
	if c.closed {
		c.closedMux.Unlock()
		return ErrClientClosed
	}
	c.closed = true
	c.closedMux.Unlock()
	agentErr, connErr := c.a.Close(), c.c.Close()
	close(c.close)
	c.wg.Wait()
	if agentErr == nil && connErr == nil {
		return nil
	}
	return CloseErr{
		AgentErr:      agentErr,
		ConnectionErr: connErr,
	}
}

// Indicate sends indication m to server. Shorthand to Start call
// with zero deadline and callback.
func (c *Client) Indicate(m *Message) error {
	return c.Start(m, nil)
}

// callbackWaitHandler blocks on wait() call until callback is called.
type callbackWaitHandler struct {
	handler   Handler
	callback  func(event Event)
	cond      *sync.Cond
	processed bool
}

func (s *callbackWaitHandler) HandleEvent(e Event) {
	if s.callback == nil {
		panic("s.callback is nil")
	}
	s.callback(e)
	s.cond.L.Lock()
	s.processed = true
	s.cond.Broadcast()
	s.cond.L.Unlock()
}

func (s *callbackWaitHandler) wait() {
	s.cond.L.Lock()
	for !s.processed {
		s.cond.Wait()
	}
	s.cond.L.Unlock()
}

func (s *callbackWaitHandler) setCallback(f func(event Event)) {
	if f == nil {
		panic("f is nil")
	}
	s.callback = f
	if s.handler == nil {
		s.handler = s.HandleEvent
	}
}

func (s *callbackWaitHandler) reset() {
	s.processed = false
	s.callback = nil
}

var callbackWaitHandlerPool = sync.Pool{
	New: func() interface{} {
		return &callbackWaitHandler{
			cond: sync.NewCond(new(sync.Mutex)),
		}
	},
}

// ErrClientNotInitialized means that client connection or agent is nil.
var ErrClientNotInitialized = errors.New("client not initialized")

func (c *Client) checkInit() error {
	if c == nil || c.c == nil || c.a == nil || c.close == nil {
		return ErrClientNotInitialized
	}
	return nil
}

// Do is Start wrapper that waits until callback is called. If no callback
// provided, Indicate is called instead.
//
// Do has cpu overhead due to blocking, see BenchmarkClient_Do.
// Use Start method for less overhead.
func (c *Client) Do(m *Message, f func(Event)) error {
	if err := c.checkInit(); err != nil {
		return err
	}
	if f == nil {
		return c.Indicate(m)
	}
	h := callbackWaitHandlerPool.Get().(*callbackWaitHandler)
	h.setCallback(f)
	defer func() {
		h.reset()
		callbackWaitHandlerPool.Put(h)
	}()
	if err := c.Start(m, h.handler); err != nil {
		return err
	}
	h.wait()
	return nil
}

func (c *Client) delete(id transactionID) {
	c.tMux.Lock()
	if c.t != nil {
		t, ok := c.t[id]
		if ok {
			putClientTransaction(t)
		}
		delete(c.t, id)
	}
	c.tMux.Unlock()
}

func (c *Client) handleAgentCallback(e Event) {
	c.tMux.Lock()
	if c.t == nil {
		c.tMux.Unlock()
		return
	}
	t, found := c.t[e.TransactionID]
	if found {
		delete(c.t, t.id)
	}
	c.tMux.Unlock()
	if !found {
		// Ignoring.
		return
	}
	h := t.h

	if atomic.LoadInt32(&c.maxAttempts) < t.attempt || e.Error == nil {
		// Transaction completed.
		putClientTransaction(t)
		h(e)
		return
	}

	// Doing re-transmission.
	t.attempt++
	if err := c.start(t); err != nil {
		putClientTransaction(t)
		e.Error = err
		h(e)
		return
	}

	// Starting transaction in agent.
	now := c.clock.Now()
	d := t.nextTimeout(now)
	if err := c.a.Start(t.id, d); err != nil {
		c.delete(t.id)
		e.Error = err
		h(e)
		return
	}

	// Writing message to connection again.
	_, err := c.c.Write(t.raw)
	if err != nil {
		c.delete(t.id)
		e.Error = err

		// Stopping transaction instead of waiting until deadline.
		if stopErr := c.a.Stop(t.id); stopErr != nil {
			e.Error = StopErr{
				Err:   stopErr,
				Cause: err,
			}
		}
		h(e)
		return
	}

}

// Start starts transaction (if f set) and writes message to server, handler
// is called asynchronously.
func (c *Client) Start(m *Message, h Handler) error {
	if err := c.checkInit(); err != nil {
		return err
	}
	c.closedMux.RLock()
	closed := c.closed
	c.closedMux.RUnlock()
	if closed {
		return ErrClientClosed
	}
	if h != nil {
		// Starting transaction only if h is set. Useful for indications.
		t := acquireClientTransaction()
		t.id = m.TransactionID
		t.start = c.clock.Now()
		t.h = h
		t.rto = time.Duration(atomic.LoadInt64(&c.rto))
		t.attempt = 0
		t.raw = append(t.raw[:0], m.Raw...)
		d := t.nextTimeout(t.start)
		if err := c.start(t); err != nil {
			return err
		}
		if err := c.a.Start(m.TransactionID, d); err != nil {
			return err
		}
	}
	_, err := m.WriteTo(c.c)
	if err != nil && h != nil {
		c.delete(m.TransactionID)
		// Stopping transaction instead of waiting until deadline.
		if stopErr := c.a.Stop(m.TransactionID); stopErr != nil {
			return StopErr{
				Err:   stopErr,
				Cause: err,
			}
		}
	}
	return err
}
<commit_msg>client: update start method<commit_after>package stun

import (
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"runtime"
	"sync"
	"sync/atomic"
	"time"
)

// Dial connects to the address on the named network and then
// initializes Client on that connection, returning error if any.
func Dial(network, address string) (*Client, error) {
	conn, err := net.Dial(network, address)
	if err != nil {
		return nil, err
	}
	return NewClient(ClientOptions{
		Connection: conn,
	})
}

// ClientOptions are used to initialize Client.
type ClientOptions struct {
	Agent       ClientAgent
	Connection  Connection
	TimeoutRate time.Duration // defaults to 100 ms
}

const defaultTimeoutRate = time.Millisecond * 100

// ErrNoConnection means that ClientOptions.Connection is nil.
var ErrNoConnection = errors.New("no connection provided")

// NewClient initializes new Client from provided options,
// starting internal goroutines and using default options fields
// if necessary. Call Close method after using Client to release
// resources.
func NewClient(options ClientOptions) (*Client, error) {
	c := &Client{
		close:       make(chan struct{}),
		c:           options.Connection,
		a:           options.Agent,
		gcRate:      options.TimeoutRate,
		clock:       systemClock,
		rto:         int64(time.Millisecond * 500),
		t:           make(map[transactionID]*clientTransaction, 100),
		maxAttempts: 7,
	}
	if c.c == nil {
		return nil, ErrNoConnection
	}
	if c.a == nil {
		c.a = NewAgent(AgentOptions{})
	}
	if c.gcRate == 0 {
		c.gcRate = defaultTimeoutRate
	}
	if err := c.a.SetHandler(c.handleAgentCallback); err != nil {
		return nil, err
	}
	c.wg.Add(2)
	go c.readUntilClosed()
	go c.collectUntilClosed()
	runtime.SetFinalizer(c, clientFinalizer)
	return c, nil
}

func clientFinalizer(c *Client) {
	if c == nil {
		return
	}
	err := c.Close()
	if err == ErrClientClosed {
		return
	}
	if err == nil {
		log.Println("client: called finalizer on non-closed client")
		return
	}
	log.Println("client: called finalizer on non-closed client:", err)
}

// Connection wraps Reader, Writer and Closer interfaces.
type Connection interface {
	io.Reader
	io.Writer
	io.Closer
}

// ClientAgent is Agent implementation that is used by Client to
// process transactions.
type ClientAgent interface {
	Process(*Message) error
	Close() error
	Start(id [TransactionIDSize]byte, deadline time.Time) error
	Stop(id [TransactionIDSize]byte) error
	Collect(time.Time) error
	SetHandler(h Handler) error
}

// Client simulates "connection" to STUN server.
type Client struct {
	a           ClientAgent
	c           Connection
	close       chan struct{}
	gcRate      time.Duration
	rto         int64 // time.Duration
	maxAttempts int32
	closed      bool
	closedMux   sync.RWMutex
	wg          sync.WaitGroup
	clock       Clock

	t    map[transactionID]*clientTransaction
	tMux sync.RWMutex
}

// clientTransaction represents transaction in progress.
// If transaction is succeed or failed, f will be called
// provided by event.
// Concurrent access is invalid.
type clientTransaction struct {
	id      transactionID
	attempt int32
	h       Handler
	start   time.Time
	rto     time.Duration
	raw     []byte
}

var clientTransactionPool = &sync.Pool{
	New: func() interface{} {
		return &clientTransaction{
			raw: make([]byte, 1500),
		}
	},
}

func acquireClientTransaction() *clientTransaction {
	return clientTransactionPool.Get().(*clientTransaction)
}

func putClientTransaction(t *clientTransaction) {
	clientTransactionPool.Put(t)
}

func (t clientTransaction) nextTimeout(now time.Time) time.Time {
	return now.Add(time.Duration(t.attempt) * t.rto)
}

// start registers transaction.
//
// Could return ErrClientClosed, ErrTransactionExists.
func (c *Client) start(t *clientTransaction) error {
	c.tMux.Lock()
	defer c.tMux.Unlock()
	if c.closed {
		return ErrClientClosed
	}
	_, exists := c.t[t.id]
	if exists {
		return ErrTransactionExists
	}
	c.t[t.id] = t
	return nil
}

// Clock abstracts the source of current time.
type Clock interface {
	Now() time.Time
}

type systemClockService struct{}

func (systemClockService) Now() time.Time { return time.Now() }

var systemClock = systemClockService{}

// SetRTO sets current RTO value.
func (c *Client) SetRTO(rto time.Duration) {
	atomic.StoreInt64(&c.rto, int64(rto))
}

// StopErr occurs when Client fails to stop transaction while
// processing error.
type StopErr struct {
	Err   error // value returned by Stop()
	Cause error // error that caused Stop() call
}

func (e StopErr) Error() string {
	return fmt.Sprintf("error while stopping due to %s: %s",
		sprintErr(e.Cause), sprintErr(e.Err),
	)
}

// CloseErr indicates client close failure.
type CloseErr struct {
	AgentErr      error
	ConnectionErr error
}

func sprintErr(err error) string {
	if err == nil {
		return "<nil>"
	}
	return err.Error()
}

func (c CloseErr) Error() string {
	return fmt.Sprintf("failed to close: %s (connection), %s (agent)",
		sprintErr(c.ConnectionErr), sprintErr(c.AgentErr),
	)
}

func (c *Client) readUntilClosed() {
	defer c.wg.Done()
	m := new(Message)
	m.Raw = make([]byte, 1024)
	for {
		select {
		case <-c.close:
			return
		default:
		}
		_, err := m.ReadFrom(c.c)
		if err == nil {
			if pErr := c.a.Process(m); pErr == ErrAgentClosed {
				return
			}
		}
	}
}

func closedOrPanic(err error) {
	if err == nil || err == ErrAgentClosed {
		return
	}
	panic(err)
}

func (c *Client) collectUntilClosed() {
	t := time.NewTicker(c.gcRate)
	defer c.wg.Done()
	for {
		select {
		case <-c.close:
			t.Stop()
			return
		case gcTime := <-t.C:
			closedOrPanic(c.a.Collect(gcTime))
		}
	}
}

// ErrClientClosed indicates that client is closed.
var ErrClientClosed = errors.New("client is closed")

// Close stops internal connection and agent, returning CloseErr on error.
func (c *Client) Close() error {
	if err := c.checkInit(); err != nil {
		return err
	}
	c.closedMux.Lock()
	if c.closed {
		c.closedMux.Unlock()
		return ErrClientClosed
	}
	c.closed = true
	c.closedMux.Unlock()
	agentErr, connErr := c.a.Close(), c.c.Close()
	close(c.close)
	c.wg.Wait()
	if agentErr == nil && connErr == nil {
		return nil
	}
	return CloseErr{
		AgentErr:      agentErr,
		ConnectionErr: connErr,
	}
}

// Indicate sends indication m to server. Shorthand to Start call
// with zero deadline and callback.
func (c *Client) Indicate(m *Message) error {
	return c.Start(m, nil)
}

// callbackWaitHandler blocks on wait() call until callback is called.
type callbackWaitHandler struct {
	handler   Handler
	callback  func(event Event)
	cond      *sync.Cond
	processed bool
}

func (s *callbackWaitHandler) HandleEvent(e Event) {
	if s.callback == nil {
		panic("s.callback is nil")
	}
	s.callback(e)
	s.cond.L.Lock()
	s.processed = true
	s.cond.Broadcast()
	s.cond.L.Unlock()
}

func (s *callbackWaitHandler) wait() {
	s.cond.L.Lock()
	for !s.processed {
		s.cond.Wait()
	}
	s.cond.L.Unlock()
}

func (s *callbackWaitHandler) setCallback(f func(event Event)) {
	if f == nil {
		panic("f is nil")
	}
	s.callback = f
	if s.handler == nil {
		s.handler = s.HandleEvent
	}
}

func (s *callbackWaitHandler) reset() {
	s.processed = false
	s.callback = nil
}

var callbackWaitHandlerPool = sync.Pool{
	New: func() interface{} {
		return &callbackWaitHandler{
			cond: sync.NewCond(new(sync.Mutex)),
		}
	},
}

// ErrClientNotInitialized means that client connection or agent is nil.
var ErrClientNotInitialized = errors.New("client not initialized")

func (c *Client) checkInit() error {
	if c == nil || c.c == nil || c.a == nil || c.close == nil {
		return ErrClientNotInitialized
	}
	return nil
}

// Do is Start wrapper that waits until callback is called. If no callback
// provided, Indicate is called instead.
//
// Do has cpu overhead due to blocking, see BenchmarkClient_Do.
// Use Start method for less overhead.
func (c *Client) Do(m *Message, f func(Event)) error {
	if err := c.checkInit(); err != nil {
		return err
	}
	if f == nil {
		return c.Indicate(m)
	}
	h := callbackWaitHandlerPool.Get().(*callbackWaitHandler)
	h.setCallback(f)
	defer func() {
		h.reset()
		callbackWaitHandlerPool.Put(h)
	}()
	if err := c.Start(m, h.handler); err != nil {
		return err
	}
	h.wait()
	return nil
}

func (c *Client) delete(id transactionID) {
	c.tMux.Lock()
	if c.t != nil {
		t, ok := c.t[id]
		if ok {
			putClientTransaction(t)
		}
		delete(c.t, id)
	}
	c.tMux.Unlock()
}

func (c *Client) handleAgentCallback(e Event) {
	c.tMux.Lock()
	if c.t == nil {
		c.tMux.Unlock()
		return
	}
	t, found := c.t[e.TransactionID]
	if found {
		delete(c.t, t.id)
	}
	c.tMux.Unlock()
	if !found {
		// Ignoring.
		return
	}
	h := t.h

	if atomic.LoadInt32(&c.maxAttempts) < t.attempt || e.Error == nil {
		// Transaction completed.
		putClientTransaction(t)
		h(e)
		return
	}

	// Doing re-transmission.
	t.attempt++
	if err := c.start(t); err != nil {
		putClientTransaction(t)
		e.Error = err
		h(e)
		return
	}

	// Starting transaction in agent.
	now := c.clock.Now()
	d := t.nextTimeout(now)
	if err := c.a.Start(t.id, d); err != nil {
		c.delete(t.id)
		e.Error = err
		h(e)
		return
	}

	// Writing message to connection again.
	_, err := c.c.Write(t.raw)
	if err != nil {
		c.delete(t.id)
		e.Error = err

		// Stopping transaction instead of waiting until deadline.
		if stopErr := c.a.Stop(t.id); stopErr != nil {
			e.Error = StopErr{
				Err:   stopErr,
				Cause: err,
			}
		}
		h(e)
		return
	}

}

// Start starts transaction (if f set) and writes message to server, handler
// is called asynchronously.
func (c *Client) Start(m *Message, h Handler) error {
	if err := c.checkInit(); err != nil {
		return err
	}
	c.closedMux.RLock()
	closed := c.closed
	c.closedMux.RUnlock()
	if closed {
		return ErrClientClosed
	}
	if h != nil {
		// Starting transaction only if h is set. Useful for indications.
		t := acquireClientTransaction()
		t.id = m.TransactionID
		t.start = c.clock.Now()
		t.h = h
		t.rto = time.Duration(atomic.LoadInt64(&c.rto))
		t.attempt = 0
		t.raw = append(t.raw[:0], m.Raw...)
		d := t.nextTimeout(t.start)
		if err := c.start(t); err != nil {
			return err
		}
		if err := c.a.Start(m.TransactionID, d); err != nil {
			return err
		}
	}
	_, err := m.WriteTo(c.c)
	if err != nil && h != nil {
		c.delete(m.TransactionID)
		// Stopping transaction instead of waiting until deadline.
		if stopErr := c.a.Stop(m.TransactionID); stopErr != nil {
			return StopErr{
				Err:   stopErr,
				Cause: err,
			}
		}
	}
	return err
}
<|endoftext|>


<commit_before>package main

import (
	"errors"
	"fmt"
	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"
	"strings"
	"time"
)

type DockerDeployClient struct {
	Mode                 string
	SSHPort              int
	SSHUser              string
	SSHHost              string
	SSHPassword          string
	ProjectName          string
	ComposeFile          string
	StartTime            int
	RemoteWorkingDir     string
	LocalWorkingDir      string
	LocalArtifact        string
	ServiceDiscoveryPort int
	ClearVolumes         bool
	config               *ssh.ClientConfig
	sshClient            *ssh.Client
}

func (c *DockerDeployClient) connect() error {
	c.config = &ssh.ClientConfig{
		User: c.SSHUser,
		Auth: []ssh.AuthMethod{
			ssh.Password(c.SSHPassword),
		},
	}

	client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", c.SSHHost, c.SSHPort), c.config)
	if err != nil {
		return err
	}
	c.sshClient = client
	return nil
}

func (c *DockerDeployClient) disconnect() error {
	return c.sshClient.Close()
}

func (c *DockerDeployClient) executeCommand(command string, sudo bool) (string, error) {
	session, err := c.sshClient.NewSession()
	if err != nil {
		return "", err
	}
	defer session.Close()

	if sudo {
		session.Stdin = strings.NewReader(fmt.Sprintf("%v\n", c.SSHPassword))
	}

	output, err := session.CombinedOutput(command)
	log.Printf("Command: %v", command)
	log.Printf("Output: %v", string(output))
	if err != nil {
		return "", errors.New(fmt.Sprintf("%v: %v", err.Error(), string(output)))
	}
	return string(output), nil
}

func (c *DockerDeployClient) findLocalArtifact() error {
	if c.LocalArtifact != "" {
		if _, err := os.Stat(c.LocalArtifact); err != nil {
			return errors.New(fmt.Sprintf("Could not find artifact at %v.", c.LocalArtifact))
		}
		if path.Ext(c.LocalArtifact) != ".zip" {
			return errors.New(fmt.Sprintf("Given artifact %v is no zip file.", c.LocalArtifact))
		}
		log.Printf("Local artifact found: %v", c.LocalArtifact)
		return nil
	}

	if c.LocalWorkingDir == "" {
		return errors.New("No local working directory specified.")
	}

	if _, err := os.Stat(c.LocalWorkingDir); err != nil {
		return errors.New(fmt.Sprintf("Local working directory \"%v\" does not exist!", c.LocalWorkingDir))
	}

	d, err := os.Open(c.LocalWorkingDir)
	if err != nil {
		return errors.New(fmt.Sprintf("Could not open local working directory: %v", err.Error()))
	}
	defer d.Close()

	files, err := d.Readdir(-1)
	if err != nil {
		return err
	}

	for _, file := range files {
		if file.Mode().IsRegular() {
			fpath := path.Join(c.LocalWorkingDir, file.Name())
			if path.Ext(fpath) == ".zip" {
				c.LocalArtifact = fpath
				log.Printf("Local artifact found: %v", c.LocalArtifact)
				return nil
			}
		}
	}
	return errors.New(fmt.Sprintf("Could not find local artifact in working directory %v", c.LocalWorkingDir))
}

func (c *DockerDeployClient) unzipArtifact() error {
	output, err := c.executeCommand("which unzip", false)
	if err != nil {
		return errors.New(fmt.Sprintf("Unzip not installed. %v", output))
	}

	output, err = c.executeCommand(fmt.Sprintf("cd %v && unzip -o %v && rm %v", c.RemoteWorkingDir, path.Base(c.LocalArtifact), path.Base(c.LocalArtifact)), false)
	if err != nil {
		return errors.New(fmt.Sprintf("Could not unzip artifact: %v %v", err.Error(), output))
	}
	return nil
}

func (c *DockerDeployClient) copyArtifact() error {
	err := c.copyFile(c.LocalArtifact, path.Join(c.RemoteWorkingDir, path.Base(c.LocalArtifact)))
	return err
}

func (c *DockerDeployClient) copyFile(source string, target string) error {
	sftp, err := sftp.NewClient(c.sshClient)
	if err != nil {
		log.Fatalf("Could not initialize SFTP connection: %v", err)
	}
	defer sftp.Close()

	tf, err := sftp.Create(target)
	if err != nil {
		return err
	}
	defer tf.Close()

	sf, err := os.Open(source)
	if err != nil {
		return err
	}
	defer sf.Close()

	n, err := io.Copy(tf, sf)
	if err != nil {
		return err
	}

	log.Printf("Artifact from %v to %v copied. %v Bytes transferred.", source, target, n)
	return nil
}

func (c *DockerDeployClient) prepareRemoteWorkdir() error {
	if c.RemoteWorkingDir == "" {
		return errors.New("No remote working directory specified.")
	}

	command := fmt.Sprintf("mkdir -p %s && cd %s && pwd && rm -rf *", c.RemoteWorkingDir, c.RemoteWorkingDir)
	_, err := c.executeCommand(command, false)
	if err != nil {
		return err
	}
	return nil
}

func (c *DockerDeployClient) checkDockerInstallation() error {
	output, err := c.executeCommand("which docker", false)
	if err != nil {
		return errors.New(fmt.Sprintf("Docker not installed. %v", output))
	}
	output, err = c.executeCommand("which docker-compose", false)
	if err != nil {
		return errors.New(fmt.Sprintf("Docker Compose not installed. %v", output))
	}
	if c.ComposeFile == "" {
		return errors.New("No compose file specified.")
	}
	if c.ProjectName == "" {
		return errors.New("No project name specified.")
	}
	return nil
}

func (c *DockerDeployClient) stopComposition() error {
	_, err := c.executeCommand(fmt.Sprintf("cd %v && sudo -S docker-compose -p %v -f %v stop", c.RemoteWorkingDir, c.ProjectName, c.ComposeFile), true)
	return err
}

func (c *DockerDeployClient) removeComposition() error {
	var err error = nil
	if c.ClearVolumes {
		_, err = c.executeCommand(fmt.Sprintf("cd %v && sudo -S docker-compose -p %v -f %v rm -v --force", c.RemoteWorkingDir, c.ProjectName, c.ComposeFile), true)
	} else {
		_, err = c.executeCommand(fmt.Sprintf("cd %v && sudo -S docker-compose -p %v -f %v rm --force", c.RemoteWorkingDir, c.ProjectName, c.ComposeFile), true)
	}
	return err
}

func (c *DockerDeployClient) buildComposition() error {
	_, err := c.executeCommand(fmt.Sprintf("cd %v && sudo -S docker-compose -p %v -f %v build", c.RemoteWorkingDir, c.ProjectName, c.ComposeFile), true)
	return err
}

func (c *DockerDeployClient) runComposition() error {
	_, err := c.executeCommand(fmt.Sprintf("cd %v && sudo -S docker-compose -p %v -f %v up -d", c.RemoteWorkingDir, c.ProjectName, c.ComposeFile), true)
	return err
}

func (c *DockerDeployClient) serviceDiscoveryTest() error {
	iterations := 0
	var response []byte
	ticks := time.Duration(c.StartTime/5) * time.Second

	for _ = range time.Tick(ticks) {
		iterations += 1
		if iterations >= 6 {
			break
		}

		res, err := http.Get(fmt.Sprintf("http://%v:%d/api/projectUp/%v", c.SSHHost, c.ServiceDiscoveryPort, c.ProjectName))
		if err != nil {
			log.Printf("#%d: Service Discovery is not reachable: %v", iterations, err.Error())
			continue
		}

		response, err = ioutil.ReadAll(res.Body)
		res.Body.Close()
		if err == nil && string(response) == "true" {
			log.Printf("#%d: Composition was started: %d %v", iterations, res.StatusCode, string(response))
			return nil
		}

		log.Printf("#%d: Composition could not be started: %d %v", iterations, res.StatusCode, string(response))
	}

	return errors.New("Service Discovery test failed.")
}

func (c *DockerDeployClient) remoteCleanUp() error {
	if c.RemoteWorkingDir == "" {
		return errors.New("No remote working directory specified.")
	}

	command := fmt.Sprintf("rm -rf %s", c.RemoteWorkingDir)
	_, err := c.executeCommand(command, false)
	if err != nil {
		return err
	}
	return nil
}
<commit_msg>Create subfolder on remote for each deployment based on the name of the artifact to enable concurrent deployments.<commit_after>package main

import (
	"errors"
	"fmt"
	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"
	"strings"
	"time"
)

type DockerDeployClient struct {
	Mode                 string
	SSHPort              int
	SSHUser              string
	SSHHost              string
	SSHPassword          string
	ProjectName          string
	ComposeFile          string
	StartTime            int
	RemoteWorkingDir     string
	LocalWorkingDir      string
	LocalArtifact        string
	ServiceDiscoveryPort int
	ClearVolumes         bool
	config               *ssh.ClientConfig
	sshClient            *ssh.Client
}

func (c *DockerDeployClient) connect() error {
	c.config = &ssh.ClientConfig{
		User: c.SSHUser,
		Auth: []ssh.AuthMethod{
			ssh.Password(c.SSHPassword),
		},
	}

	client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", c.SSHHost, c.SSHPort), c.config)
	if err != nil {
		return err
	}
	c.sshClient = client
	return nil
}

func (c *DockerDeployClient) disconnect() error {
	return c.sshClient.Close()
}

func (c *DockerDeployClient) executeCommand(command string, sudo bool) (string, error) {
	session, err := c.sshClient.NewSession()
	if err != nil {
		return "", err
	}
	defer session.Close()

	if sudo {
		session.Stdin = strings.NewReader(fmt.Sprintf("%v\n", c.SSHPassword))
	}

	output, err := session.CombinedOutput(command)
	log.Printf("Command: %v", command)
	log.Printf("Output: %v", string(output))
	if err != nil {
		return "", errors.New(fmt.Sprintf("%v: %v", err.Error(), string(output)))
	}
	return string(output), nil
}

func (c *DockerDeployClient) findLocalArtifact() error {
	if c.LocalArtifact != "" {
		if _, err := os.Stat(c.LocalArtifact); err != nil {
			return errors.New(fmt.Sprintf("Could not find artifact at %v.", c.LocalArtifact))
		}
		if path.Ext(c.LocalArtifact) != ".zip" {
			return errors.New(fmt.Sprintf("Given artifact %v is no zip file.", c.LocalArtifact))
		}
		log.Printf("Local artifact found: %v", c.LocalArtifact)
		return nil
	}

	if c.LocalWorkingDir == "" {
		return errors.New("No local working directory specified.")
	}

	if _, err := os.Stat(c.LocalWorkingDir); err != nil {
		return errors.New(fmt.Sprintf("Local working directory \"%v\" does not exist!", c.LocalWorkingDir))
	}

	d, err := os.Open(c.LocalWorkingDir)
	if err != nil {
		return errors.New(fmt.Sprintf("Could not open local working directory: %v", err.Error()))
	}
	defer d.Close()

	files, err := d.Readdir(-1)
	if err != nil {
		return err
	}

	for _, file := range files {
		if file.Mode().IsRegular() {
			fpath := path.Join(c.LocalWorkingDir, file.Name())
			if path.Ext(fpath) == ".zip" {
				c.LocalArtifact = fpath
				log.Printf("Local artifact found: %v", c.LocalArtifact)
				return nil
			}
		}
	}
	return errors.New(fmt.Sprintf("Could not find local artifact in working directory %v", c.LocalWorkingDir))
}

func (c *DockerDeployClient) unzipArtifact() error {
	output, err := c.executeCommand("which unzip", false)
	if err != nil {
		return errors.New(fmt.Sprintf("Unzip not installed. %v", output))
	}

	output, err = c.executeCommand(fmt.Sprintf("cd %v && unzip -o %v && rm %v", c.RemoteWorkingDir, path.Base(c.LocalArtifact), path.Base(c.LocalArtifact)), false)
	if err != nil {
		return errors.New(fmt.Sprintf("Could not unzip artifact: %v %v", err.Error(), output))
	}
	return nil
}

func (c *DockerDeployClient) copyArtifact() error {
	err := c.copyFile(c.LocalArtifact, path.Join(c.RemoteWorkingDir, path.Base(c.LocalArtifact)))
	return err
}

func (c *DockerDeployClient) copyFile(source string, target string) error {
	sftp, err := sftp.NewClient(c.sshClient)
	if err != nil {
		log.Fatalf("Could not initialize SFTP connection: %v", err)
	}
	defer sftp.Close()

	tf, err := sftp.Create(target)
	if err != nil {
		return err
	}
	defer tf.Close()

	sf, err := os.Open(source)
	if err != nil {
		return err
	}
	defer sf.Close()

	n, err := io.Copy(tf, sf)
	if err != nil {
		return err
	}

	log.Printf("Artifact from %v to %v copied. %v Bytes transferred.", source, target, n)
	return nil
}

func (c *DockerDeployClient) prepareRemoteWorkdir() error {
	if c.RemoteWorkingDir == "" {
		return errors.New("No remote working directory specified.")
	}

	subfolder := strings.TrimSuffix(path.Base(c.LocalArtifact), path.Ext(c.LocalArtifact))
	c.RemoteWorkingDir = path.Join(c.RemoteWorkingDir, subfolder)

	command := fmt.Sprintf("mkdir -p %s && cd %s && pwd && rm -rf *", c.RemoteWorkingDir, c.RemoteWorkingDir)
	_, err := c.executeCommand(command, false)
	if err != nil {
		return err
	}
	return nil
}

func (c *DockerDeployClient) checkDockerInstallation() error {
	output, err := c.executeCommand("which docker", false)
	if err != nil {
		return errors.New(fmt.Sprintf("Docker not installed. %v", output))
	}
	output, err = c.executeCommand("which docker-compose", false)
	if err != nil {
		return errors.New(fmt.Sprintf("Docker Compose not installed. %v", output))
	}
	if c.ComposeFile == "" {
		return errors.New("No compose file specified.")
	}
	if c.ProjectName == "" {
		return errors.New("No project name specified.")
	}
	return nil
}

func (c *DockerDeployClient) stopComposition() error {
	_, err := c.executeCommand(fmt.Sprintf("cd %v && sudo -S docker-compose -p %v -f %v stop", c.RemoteWorkingDir, c.ProjectName, c.ComposeFile), true)
	return err
}

func (c *DockerDeployClient) removeComposition() error {
	var err error = nil
	if c.ClearVolumes {
		_, err = c.executeCommand(fmt.Sprintf("cd %v && sudo -S docker-compose -p %v -f %v rm -v --force", c.RemoteWorkingDir, c.ProjectName, c.ComposeFile), true)
	} else {
		_, err = c.executeCommand(fmt.Sprintf("cd %v && sudo -S docker-compose -p %v -f %v rm --force", c.RemoteWorkingDir, c.ProjectName, c.ComposeFile), true)
	}
	return err
}

func (c *DockerDeployClient) buildComposition() error {
	_, err := c.executeCommand(fmt.Sprintf("cd %v && sudo -S docker-compose -p %v -f %v build", c.RemoteWorkingDir, c.ProjectName, c.ComposeFile), true)
	return err
}

func (c *DockerDeployClient) runComposition() error {
	_, err := c.executeCommand(fmt.Sprintf("cd %v && sudo -S docker-compose -p %v -f %v up -d", c.RemoteWorkingDir, c.ProjectName, c.ComposeFile), true)
	return err
}

func (c *DockerDeployClient) serviceDiscoveryTest() error {
	iterations := 0
	var response []byte
	ticks := time.Duration(c.StartTime/5) * time.Second

	for _ = range time.Tick(ticks) {
		iterations += 1
		if iterations >= 6 {
			break
		}

		res, err := http.Get(fmt.Sprintf("http://%v:%d/api/projectUp/%v", c.SSHHost, c.ServiceDiscoveryPort, c.ProjectName))
		if err != nil {
			log.Printf("#%d: Service Discovery is not reachable: %v", iterations, err.Error())
			continue
		}

		response, err = ioutil.ReadAll(res.Body)
		res.Body.Close()
		if err == nil && string(response) == "true" {
			log.Printf("#%d: Composition was started: %d %v", iterations, res.StatusCode, string(response))
			return nil
		}

		log.Printf("#%d: Composition could not be started: %d %v", iterations, res.StatusCode, string(response))
	}

	return errors.New("Service Discovery test failed.")
}

func (c *DockerDeployClient) remoteCleanUp() error {
	if c.RemoteWorkingDir == "" {
		return errors.New("No remote working directory specified.")
	}

	command := fmt.Sprintf("rm -rf %s", c.RemoteWorkingDir)
	_, err := c.executeCommand(command, false)
	if err != nil {
		return err
	}
	return nil
}
<|endoftext|>


<commit_before>package sensorsanalytics

import (
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"time"
)

// Client sensoranalytics client
type Client struct {
	consumer        Consumer
	projectName     *string
	enableTimeFree  bool
	appVersion      *string
	superProperties map[string]interface{}
	namePattern     *regexp.Regexp
}

// NewClient create new client
func NewClient(consumer Consumer, projectName string, timeFree bool) (*Client, error) {
	var c Client
	c.consumer = consumer
	if projectName == "" {
		return &c, errors.New("project_name must not be empty")
	}
	c.projectName = &projectName
	c.enableTimeFree = timeFree
	namePattern, err := regexp.Compile("^([a-zA-Z_$][a-zA-Z0-9_$]{0,99}$)")
	if err != nil {
		return nil, err
	}
	c.namePattern = namePattern
	c.ClearSuperProperties()
	return &c, nil
}

func (c *Client) match(input string) bool {
	if c.namePattern.Match([]byte(input)) {
		for _, keyword := range FieldKeywords {
			if keyword == input {
				return false
			}
		}
		return true
	}
	return false
}

func (c *Client) now() int64 {
	return time.Now().Unix() * 1000
}

// RegisterSuperProperties 设置每个事件都带有的一些公共属性，当 track 的 properties 和 super properties 有相同的 key 时，将采用 track 的
// :param superProperties 公共属性
func (c *Client) RegisterSuperProperties(superProperties map[string]interface{}) {
	for k, v := range superProperties {
		c.superProperties[k] = v
	}
}

// ClearSuperProperties 删除所有已设置的事件公共属性
func (c *Client) ClearSuperProperties() {
	c.superProperties = map[string]interface{}{
		"$lib":         "golang",
		"$lib_version": SDKVersion,
	}
}

// Track 跟踪一个用户的行为。
// :param distinctID: 用户的唯一标识
// :param eventName: 事件名称
// :param properties: 事件的属性
func (c *Client) Track(distinctID string, eventName string, properties map[string]interface{}, isLoginID bool) error {
	allProperties := c.superProperties
	if properties != nil {
		for k, v := range properties {
			allProperties[k] = v
		}
	}
	return c.trackEvent("track", eventName, distinctID, "", allProperties, isLoginID)
}

// TrackSignup 这个接口是一个较为复杂的功能，请在使用前先阅读相关说明:http://www.sensorsdata.cn/manual/track_signup.html，
// 并在必要时联系我们的技术支持人员。
// :param distinct_id: 用户注册之后的唯一标识
// :param original_id: 用户注册前的唯一标识
// :param properties: 事件的属性
func (c *Client) TrackSignup(distinctID string, originalID string, properties map[string]interface{}) error {
	if len(originalID) == 0 {
		return fmt.Errorf("%s: %s", ErrIllegalDataException, "property [original_id] must not be empty")
	}
	if len(originalID) > 255 {
		return fmt.Errorf("%s: %s", ErrIllegalDataException, "the max length of property [original_id] is 255")
	}
	allProperties := c.superProperties
	if properties != nil {
		for key, value := range properties {
			allProperties[key] = value
		}
	}
	return c.trackEvent("track_signup", "$SignUp", distinctID, originalID, allProperties, false)
}

func (c *Client) normalizeData(data map[string]interface{}) (map[string]interface{}, error) {
	// 检查 distinct_id
	distinctIDI, ok := data["distinct_id"]
	if !ok {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [distinct_id] must not be empty")
	}
	distinctID, ok := distinctIDI.(string)
	if !ok || len(distinctID) == 0 {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [distinct_id] must not be empty")
	}
	if len(distinctID) > 255 {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "the max length of [distinct_id] is 255")
	}
	// 检查 time
	tsI, ok := data["time"]
	if !ok {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [time] must not be empty")
	}
	ts, ok := tsI.(int64)
	if !ok {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [time] must be int64")
	}
	tsNum := len(strconv.FormatInt(ts, 10))
	if tsNum < 10 || tsNum > 13 {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [time] must be a timestamp in microseconds")
	}
	if tsNum == 10 {
		ts *= 1000
	}
	data["time"] = ts

	// 检查 event name
	eventI, ok := data["event"]
	if ok {
		event, ok := eventI.(string)
		if !ok {
			return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [event] must no be empty")
		}
		if !c.match(event) {
			return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("event name must be a valid variable name. [event=%s]", event))
		}
	}
	// 检查 project name
	projectI, ok := data["project"]
	if ok {
		project, ok := projectI.(string)
		if !ok {
			return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [project] must no be empty")
		}
		if !c.match(project) {
			return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("project name must be a valid variable name. [project=%s]", project))
		}
	}
	// 检查 properties
	var eventType string
	eventTypeI, ok := data["type"]
	if ok {
		eventType = eventTypeI.(string)

	}
	propertiesi, ok := data["properties"]
	if ok {
		properties, ok := propertiesi.(map[string]interface{})
		if ok {
			for key, value := range properties {
				if len(key) > 255 {
					return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("the max length of property key is 256. [key=%s]", key))
				}
				if !c.match(key) {
					return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("the property key must be a valid variable name. [key=%s]", key))
				}
				switch value.(type) {
				case string:
					v, ok := value.(string)
					if ok {
						if len(v) > 8192 {
							return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("the max length of property value is 8192. [value=%s]", value))
						}
					}
				case int, int32, int64, float32, float64, []string, bool:
					continue
				default:
					return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("default: property value must be a str/int/float/list. [key=%s, value=%s]", key, reflect.TypeOf(value)))
				}
			}
		} else {
			return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "properties must be a map[string]interface{}")
		}
	}
	return data, nil
}

func (c *Client) getLibProperties() map[string]interface{} {
	libProperties := map[string]interface{}{
		"$lib":         "golang",
		"$lib_version": SDKVersion,
		"$lib_method":  "code",
	}
	if appVersion, ok := c.superProperties["$app_version"]; ok {
		libProperties["$app_version"] = appVersion
	}
	return libProperties
}

// getCommonProperties 构造所有 Event 通用的属性
func (c *Client) getCommonProperties() map[string]interface{} {
	commonProperties := map[string]interface{}{
		"$lib":         "golang",
		"$lib_version": SDKVersion,
	}
	if c.appVersion != nil {
		commonProperties["$app_version"] = c.appVersion
	}
	return commonProperties
}

// extractUserTime 如果用户传入了 $time 字段，则不使用当前时间。
func (c *Client) extractUserTime(properties map[string]interface{}) *int64 {
	if properties != nil {
		ti, ok := properties["$time"]
		if ok {
			t, ok := ti.(int64)
			if ok {
				delete(properties, "$time")
				return &t
			}
		}
	}
	return nil
}

// ProfileSet 直接设置一个用户的 Profile，如果已存在则覆盖
// :param distinct_id: 用户的唯一标识
// :param profiles: 用户属性
func (c *Client) ProfileSet(distinctID string, profiles map[string]interface{}, isLoginID bool) error {
	return c.trackEvent("profile_set", "", distinctID, "", profiles, isLoginID)
}

// ProfileSetOnce 直接设置一个用户的 Profile，如果某个 Profile 已存在则不设置。
// :param distinct_id: 用户的唯一标识
// :param profiles: 用户属性
func (c *Client) ProfileSetOnce(distinctID string, profiles map[string]interface{}, isLoginID bool) error {
	return c.trackEvent("profile_set_once", "", distinctID, "", profiles, isLoginID)
}

// ProfileIncrement 增减/减少一个用户的某一个或者多个数值类型的 Profile。
// :param distinct_id: 用户的唯一标识
// :param profiles: 用户属性
func (c *Client) ProfileIncrement(distinctID string, profiles map[string]interface{}, isLoginID bool) error {
	return c.trackEvent("profile_increment", "", distinctID, "", profiles, isLoginID)
}

// ProfileAppend 追加一个用户的某一个或者多个集合类型的 Profile。
// :param distinct_id: 用户的唯一标识
// :param profiles: 用户属性
func (c *Client) ProfileAppend(distinctID string, profiles map[string]interface{}, isLoginID bool) error {
	return c.trackEvent("profile_append", "", distinctID, "", profiles, isLoginID)
}

// ProfileUnset 删除一个用户的一个或者多个 Profile。
// :param distinct_id: 用户的唯一标识
// :param profile_keys: 用户属性键值列表
func (c *Client) ProfileUnset(distinctID string, profileKeys []string, isLoginID bool) error {
	profileMap := make(map[string]interface{}, len(profileKeys))
	for _, v := range profileKeys {
		profileMap[v] = true
	}
	return c.trackEvent("profile_unset", "", distinctID, "", profileMap, isLoginID)
}

// ProfileDelete 删除整个用户的信息。
// :param distinct_id: 用户的唯一标识
func (c *Client) ProfileDelete(distinctID string, isLoginID bool) error {
	return c.trackEvent("profile_delete", "", distinctID, "", map[string]interface{}{}, isLoginID)
}

func (c *Client) trackEvent(eventType string, eventName string, distinctID string, originalID string, properties map[string]interface{}, isLoginID bool) error {
	var eventTime int64
	t := c.extractUserTime(properties)
	if t != nil {
		eventTime = *t
	} else {
		eventTime = c.now()
	}
	if isLoginID {
		properties["$is_login_id"] = true
	}
	data := map[string]interface{}{
		"type":        eventType,
		"time":        eventTime,
		"distinct_id": distinctID,
		"properties":  properties,
		"lib":         c.getLibProperties(),
	}
	if c.projectName != nil {
		data["project"] = *c.projectName
	}
	if eventType == "track" || eventType == "track_signup" {
		data["event"] = eventName
	}
	if eventType == "track_signup" {
		data["original_id"] = originalID
	}
	if c.enableTimeFree {
		data["time_free"] = true
	}
	data, err := c.normalizeData(data)
	if err != nil {
		return err
	}
	return c.consumer.Send(data)
}

// Flush 对于不立即发送数据的 Consumer，调用此接口应当立即进行已有数据的发送。
func (c *Client) Flush() error {
	return c.consumer.Flush()
}

// Close 在进程结束或者数据发送完成时，应当调用此接口，以保证所有数据被发送完毕。如果发生意外，此方法将抛出异常。
func (c *Client) Close() error {
	return c.consumer.Close()
}
<commit_msg>Remove eventType<commit_after>package sensorsanalytics

import (
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"time"
)

// Client sensoranalytics client
type Client struct {
	consumer        Consumer
	projectName     *string
	enableTimeFree  bool
	appVersion      *string
	superProperties map[string]interface{}
	namePattern     *regexp.Regexp
}

// NewClient create new client
func NewClient(consumer Consumer, projectName string, timeFree bool) (*Client, error) {
	var c Client
	c.consumer = consumer
	if projectName == "" {
		return &c, errors.New("project_name must not be empty")
	}
	c.projectName = &projectName
	c.enableTimeFree = timeFree
	namePattern, err := regexp.Compile("^([a-zA-Z_$][a-zA-Z0-9_$]{0,99}$)")
	if err != nil {
		return nil, err
	}
	c.namePattern = namePattern
	c.ClearSuperProperties()
	return &c, nil
}

func (c *Client) match(input string) bool {
	if c.namePattern.Match([]byte(input)) {
		for _, keyword := range FieldKeywords {
			if keyword == input {
				return false
			}
		}
		return true
	}
	return false
}

func (c *Client) now() int64 {
	return time.Now().Unix() * 1000
}

// RegisterSuperProperties 设置每个事件都带有的一些公共属性，当 track 的 properties 和 super properties 有相同的 key 时，将采用 track 的
// :param superProperties 公共属性
func (c *Client) RegisterSuperProperties(superProperties map[string]interface{}) {
	for k, v := range superProperties {
		c.superProperties[k] = v
	}
}

// ClearSuperProperties 删除所有已设置的事件公共属性
func (c *Client) ClearSuperProperties() {
	c.superProperties = map[string]interface{}{
		"$lib":         "golang",
		"$lib_version": SDKVersion,
	}
}

// Track 跟踪一个用户的行为。
// :param distinctID: 用户的唯一标识
// :param eventName: 事件名称
// :param properties: 事件的属性
func (c *Client) Track(distinctID string, eventName string, properties map[string]interface{}, isLoginID bool) error {
	allProperties := c.superProperties
	if properties != nil {
		for k, v := range properties {
			allProperties[k] = v
		}
	}
	return c.trackEvent("track", eventName, distinctID, "", allProperties, isLoginID)
}

// TrackSignup 这个接口是一个较为复杂的功能，请在使用前先阅读相关说明:http://www.sensorsdata.cn/manual/track_signup.html，
// 并在必要时联系我们的技术支持人员。
// :param distinct_id: 用户注册之后的唯一标识
// :param original_id: 用户注册前的唯一标识
// :param properties: 事件的属性
func (c *Client) TrackSignup(distinctID string, originalID string, properties map[string]interface{}) error {
	if len(originalID) == 0 {
		return fmt.Errorf("%s: %s", ErrIllegalDataException, "property [original_id] must not be empty")
	}
	if len(originalID) > 255 {
		return fmt.Errorf("%s: %s", ErrIllegalDataException, "the max length of property [original_id] is 255")
	}
	allProperties := c.superProperties
	if properties != nil {
		for key, value := range properties {
			allProperties[key] = value
		}
	}
	return c.trackEvent("track_signup", "$SignUp", distinctID, originalID, allProperties, false)
}

func (c *Client) normalizeData(data map[string]interface{}) (map[string]interface{}, error) {
	// 检查 distinct_id
	distinctIDI, ok := data["distinct_id"]
	if !ok {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [distinct_id] must not be empty")
	}
	distinctID, ok := distinctIDI.(string)
	if !ok || len(distinctID) == 0 {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [distinct_id] must not be empty")
	}
	if len(distinctID) > 255 {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "the max length of [distinct_id] is 255")
	}
	// 检查 time
	tsI, ok := data["time"]
	if !ok {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [time] must not be empty")
	}
	ts, ok := tsI.(int64)
	if !ok {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [time] must be int64")
	}
	tsNum := len(strconv.FormatInt(ts, 10))
	if tsNum < 10 || tsNum > 13 {
		return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [time] must be a timestamp in microseconds")
	}
	if tsNum == 10 {
		ts *= 1000
	}
	data["time"] = ts

	// 检查 event name
	eventI, ok := data["event"]
	if ok {
		event, ok := eventI.(string)
		if !ok {
			return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [event] must no be empty")
		}
		if !c.match(event) {
			return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("event name must be a valid variable name. [event=%s]", event))
		}
	}
	// 检查 project name
	projectI, ok := data["project"]
	if ok {
		project, ok := projectI.(string)
		if !ok {
			return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "property [project] must no be empty")
		}
		if !c.match(project) {
			return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("project name must be a valid variable name. [project=%s]", project))
		}
	}
	// 检查 properties
	propertiesi, ok := data["properties"]
	if ok {
		properties, ok := propertiesi.(map[string]interface{})
		if ok {
			for key, value := range properties {
				if len(key) > 255 {
					return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("the max length of property key is 256. [key=%s]", key))
				}
				if !c.match(key) {
					return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("the property key must be a valid variable name. [key=%s]", key))
				}
				switch value.(type) {
				case string:
					v, ok := value.(string)
					if ok {
						if len(v) > 8192 {
							return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("the max length of property value is 8192. [value=%s]", value))
						}
					}
				case int, int32, int64, float32, float64, []string, bool:
					continue
				default:
					return data, fmt.Errorf("%s: %s", ErrIllegalDataException, fmt.Sprintf("default: property value must be a str/int/float/list. [key=%s, value=%s]", key, reflect.TypeOf(value)))
				}
			}
		} else {
			return data, fmt.Errorf("%s: %s", ErrIllegalDataException, "properties must be a map[string]interface{}")
		}
	}
	return data, nil
}

func (c *Client) getLibProperties() map[string]interface{} {
	libProperties := map[string]interface{}{
		"$lib":         "golang",
		"$lib_version": SDKVersion,
		"$lib_method":  "code",
	}
	if appVersion, ok := c.superProperties["$app_version"]; ok {
		libProperties["$app_version"] = appVersion
	}
	return libProperties
}

// getCommonProperties 构造所有 Event 通用的属性
func (c *Client) getCommonProperties() map[string]interface{} {
	commonProperties := map[string]interface{}{
		"$lib":         "golang",
		"$lib_version": SDKVersion,
	}
	if c.appVersion != nil {
		commonProperties["$app_version"] = c.appVersion
	}
	return commonProperties
}

// extractUserTime 如果用户传入了 $time 字段，则不使用当前时间。
func (c *Client) extractUserTime(properties map[string]interface{}) *int64 {
	if properties != nil {
		ti, ok := properties["$time"]
		if ok {
			t, ok := ti.(int64)
			if ok {
				delete(properties, "$time")
				return &t
			}
		}
	}
	return nil
}

// ProfileSet 直接设置一个用户的 Profile，如果已存在则覆盖
// :param distinct_id: 用户的唯一标识
// :param profiles: 用户属性
func (c *Client) ProfileSet(distinctID string, profiles map[string]interface{}, isLoginID bool) error {
	return c.trackEvent("profile_set", "", distinctID, "", profiles, isLoginID)
}

// ProfileSetOnce 直接设置一个用户的 Profile，如果某个 Profile 已存在则不设置。
// :param distinct_id: 用户的唯一标识
// :param profiles: 用户属性
func (c *Client) ProfileSetOnce(distinctID string, profiles map[string]interface{}, isLoginID bool) error {
	return c.trackEvent("profile_set_once", "", distinctID, "", profiles, isLoginID)
}

// ProfileIncrement 增减/减少一个用户的某一个或者多个数值类型的 Profile。
// :param distinct_id: 用户的唯一标识
// :param profiles: 用户属性
func (c *Client) ProfileIncrement(distinctID string, profiles map[string]interface{}, isLoginID bool) error {
	return c.trackEvent("profile_increment", "", distinctID, "", profiles, isLoginID)
}

// ProfileAppend 追加一个用户的某一个或者多个集合类型的 Profile。
// :param distinct_id: 用户的唯一标识
// :param profiles: 用户属性
func (c *Client) ProfileAppend(distinctID string, profiles map[string]interface{}, isLoginID bool) error {
	return c.trackEvent("profile_append", "", distinctID, "", profiles, isLoginID)
}

// ProfileUnset 删除一个用户的一个或者多个 Profile。
// :param distinct_id: 用户的唯一标识
// :param profile_keys: 用户属性键值列表
func (c *Client) ProfileUnset(distinctID string, profileKeys []string, isLoginID bool) error {
	profileMap := make(map[string]interface{}, len(profileKeys))
	for _, v := range profileKeys {
		profileMap[v] = true
	}
	return c.trackEvent("profile_unset", "", distinctID, "", profileMap, isLoginID)
}

// ProfileDelete 删除整个用户的信息。
// :param distinct_id: 用户的唯一标识
func (c *Client) ProfileDelete(distinctID string, isLoginID bool) error {
	return c.trackEvent("profile_delete", "", distinctID, "", map[string]interface{}{}, isLoginID)
}

func (c *Client) trackEvent(eventType string, eventName string, distinctID string, originalID string, properties map[string]interface{}, isLoginID bool) error {
	var eventTime int64
	t := c.extractUserTime(properties)
	if t != nil {
		eventTime = *t
	} else {
		eventTime = c.now()
	}
	if isLoginID {
		properties["$is_login_id"] = true
	}
	data := map[string]interface{}{
		"type":        eventType,
		"time":        eventTime,
		"distinct_id": distinctID,
		"properties":  properties,
		"lib":         c.getLibProperties(),
	}
	if c.projectName != nil {
		data["project"] = *c.projectName
	}
	if eventType == "track" || eventType == "track_signup" {
		data["event"] = eventName
	}
	if eventType == "track_signup" {
		data["original_id"] = originalID
	}
	if c.enableTimeFree {
		data["time_free"] = true
	}
	data, err := c.normalizeData(data)
	if err != nil {
		return err
	}
	return c.consumer.Send(data)
}

// Flush 对于不立即发送数据的 Consumer，调用此接口应当立即进行已有数据的发送。
func (c *Client) Flush() error {
	return c.consumer.Flush()
}

// Close 在进程结束或者数据发送完成时，应当调用此接口，以保证所有数据被发送完毕。如果发生意外，此方法将抛出异常。
func (c *Client) Close() error {
	return c.consumer.Close()
}
<|endoftext|>


<commit_before>package gunfish

// PushClient is interface to send payload to GCM and APNs.
type PushClient interface {
	Send(Request) (*Response, error)
}
<commit_msg>change return value of Send(): *Response -> Response<commit_after>package gunfish

// PushClient is interface to send payload to GCM and APNs.
type PushClient interface {
	Send(Request) (Response, error)
}
<|endoftext|>