<commit_before>// Package syno provides a client to access the Synology NAS APIs.
//
// The APIs are documented in various PDFs here:
// https://global.download.synology.com/ftp/Document/DeveloperGuide/
package syno

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

var (
	errURLMisconfigured = errors.New("syno: client URL misconfigured")
)

// Error is the integer error code returned by the Synology API.
type Error int

// Error returns a human readable error string for the error if one is known.
func (e Error) Error() string {
	if s, ok := errStrings[e]; ok {
		return fmt.Sprint("syno: ", s, " (", int(e), ")")
	}
	return fmt.Sprintf("syno: error code %d", int(e))
}

const (
	ErrorUnknown                          = Error(100)
	ErrorInvalidParameter                 = Error(101)
	ErrorInvalidAPI                       = Error(102)
	ErrorInvalidMethod                    = Error(103)
	ErrorUnsupportedVersion               = Error(104)
	ErrorPermissionDenied                 = Error(105)
	ErrorSessionTimeout                   = Error(106)
	ErrorSessionInterruptedDuplicateLogin = Error(107)
)

var errStrings = map[Error]string{
	ErrorUnknown:                          "unknown API error",
	ErrorInvalidParameter:                 "invalid parameter",
	ErrorInvalidAPI:                       "invalid API",
	ErrorInvalidMethod:                    "invalid method",
	ErrorUnsupportedVersion:               "unsupported version",
	ErrorPermissionDenied:                 "permission denined",
	ErrorSessionTimeout:                   "session timeout error",
	ErrorSessionInterruptedDuplicateLogin: "session interrupted with duplicated login",
}

func dropEmpty(p url.Values) url.Values {
	for k, v := range p {
		if len(v) == 1 && v[0] == "" {
			p.Del(k)
		}
	}
	return p
}

// Request represents an API request to Synology.
type Request struct {
	Path    string
	API     string
	Version string
	Method  string
	Params  url.Values
	SID     string
}

// MarshalRequest can be implemented by a type that can be serialized to a
// Request.
type MarshalRequest interface {
	MarshalRequest() (*Request, error)
}

// Client provides access to the Synology API.
type Client struct {
	url       *url.URL
	transport http.RoundTripper
	sid       string
}

// Call makes a request obtained from marshaling the given argument and calls
// Do with it.
func (c *Client) Call(r MarshalRequest, data interface{}) error {
	req, err := r.MarshalRequest()
	if err != nil {
		return err
	}
	return c.Do(req, data)
}

// Do performs an API request and unmarshals the "Data" into the passed in
// argument. If data is nil, it is ignored.
func (c *Client) Do(r *Request, data interface{}) error {
	v := make(url.Values)
	v.Add("api", r.API)
	v.Add("version", r.Version)
	v.Add("method", r.Method)

	if r.SID != "" {
		v.Add("_sid", r.SID)
	} else if c.sid != "" {
		v.Add("_sid", c.sid)
	}

	for k, l := range r.Params {
		for _, e := range l {
			v.Add(k, e)
		}
	}

	hreq := &http.Request{
		Method: "GET",
		URL: c.url.ResolveReference(&url.URL{
			Path:     r.Path,
			RawQuery: v.Encode(),
		}),
		Header: make(http.Header),
	}
	hres, err := c.transport.RoundTrip(hreq)
	if err != nil {
		return err
	}
	defer hres.Body.Close()

	var synologyResponse struct {
		Success bool
		Error   struct{ Code Error }
		Data    json.RawMessage
	}
	if err := json.NewDecoder(hres.Body).Decode(&synologyResponse); err != nil {
		return err
	}
	if !synologyResponse.Success {
		return synologyResponse.Error.Code
	}
	if data != nil {
		if err := json.Unmarshal(synologyResponse.Data, data); err != nil {
			return err
		}
	}
	return nil
}

// ClientOption allows configuring various aspects of the Client.
type ClientOption func(*Client) error

// ClientURL configures the base Client URL. All API requests are made relative
// to this URL.
func ClientURL(u *url.URL) ClientOption {
	return func(c *Client) error {
		c.url = u
		return nil
	}
}

// ClientRawURL configures the base Client URL. All API requests are made
// relative to this URL.
func ClientRawURL(u string) ClientOption {
	return func(c *Client) error {
		var err error
		c.url, err = url.Parse(u)
		return err
	}
}

// ClientTransport configures the Transport for the Client. If not specified
// http.DefaultTransport is used.
func ClientTransport(t http.RoundTripper) ClientOption {
	return func(c *Client) error {
		c.transport = t
		return nil
	}
}

// ClientSID configures a default "sid" to include for authenticating an
// account.
func ClientSID(sid string) ClientOption {
	return func(c *Client) error {
		c.sid = sid
		return nil
	}
}

// ClientLogin configures the Client with a "sid" from the given credentials.
// It does so when the client is being initialized, so the ordering of this
// option should typically be after all the other options have been specified.
func ClientLogin(l AuthLogin) ClientOption {
	return func(c *Client) error {
		var res AuthLoginResponse
		l.Format = "sid"
		if err := c.Call(l, &res); err != nil {
			return err
		}
		c.sid = res.SID
		return nil
	}
}

// NewClient creates a new client with the given options.
func NewClient(options ...ClientOption) (*Client, error) {
	var c Client
	for _, o := range options {
		if err := o(&c); err != nil {
			return nil, err
		}
	}
	if c.url == nil || !c.url.IsAbs() {
		return nil, errURLMisconfigured
	}
	if c.transport == nil {
		c.transport = http.DefaultTransport
	}
	return &c, nil
}

const (
	authLoginPath    = "auth.cgi"
	authLoginAPI     = "SYNO.API.Auth"
	authLoginVersion = "3"
)

// AuthLogin logs in an account. The response is AuthLoginResponse.
type AuthLogin struct {
	Account  string
	Password string
	Session  string
	Format   string
	OTPCode  string
}

// MarshalRequest serializes the instance to a Request.
func (a AuthLogin) MarshalRequest() (*Request, error) {
	return &Request{
		Path:    authLoginPath,
		API:     authLoginAPI,
		Version: authLoginVersion,
		Method:  "login",
		Params: dropEmpty(url.Values{
			"account":  []string{a.Account},
			"passwd":   []string{a.Password},
			"session":  []string{a.Session},
			"format":   []string{a.Format},
			"otp_code": []string{a.OTPCode},
		}),
	}, nil
}

// AuthLoginResponse is the response from an AuthLogin request.
type AuthLoginResponse struct {
	SID    string
	Cookie string
}

const (
	downloadTaskPath    = "DownloadStation/task.cgi"
	downloadTaskAPI     = "SYNO.DownloadStation.Task"
	downloadTaskVersion = "1"
)

// DownloadTaskList perfoms a list call for download tasks.
type DownloadTaskList struct {
	Offset     int
	Limit      int
	Additional []string
}

// MarshalRequest serializes the instance to a Request.
func (d DownloadTaskList) MarshalRequest() (*Request, error) {
	v := url.Values{}
	if d.Offset != 0 {
		v.Add("offset", strconv.Itoa(d.Offset))
	}
	if d.Limit != 0 {
		v.Add("limit", strconv.Itoa(d.Limit))
	}
	if len(d.Additional) > 0 {
		v.Add("additional", strings.Join(d.Additional, ","))
	}

	return &Request{
		Path:    downloadTaskPath,
		API:     downloadTaskAPI,
		Version: downloadTaskVersion,
		Method:  "list",
		Params:  v,
	}, nil
}

// DownloadTaskCreate creates a new download task. It does not have a response.
type DownloadTaskCreate struct {
	URI           string
	Username      string
	Password      string
	UnzipPassword string
	Destination   string
}

// MarshalRequest serializes the instance to a Request.
func (d DownloadTaskCreate) MarshalRequest() (*Request, error) {
	return &Request{
		Path:    downloadTaskPath,
		API:     downloadTaskAPI,
		Version: downloadTaskVersion,
		Method:  "create",
		Params: dropEmpty(url.Values{
			"uri":            []string{d.URI},
			"username":       []string{d.Username},
			"password":       []string{d.Password},
			"unzip_password": []string{d.UnzipPassword},
			"destination":    []string{d.Destination},
		}),
	}, nil
}
<commit_msg>make paths absolute<commit_after>// Package syno provides a client to access the Synology NAS APIs.
//
// The APIs are documented in various PDFs here:
// https://global.download.synology.com/ftp/Document/DeveloperGuide/
package syno

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

var (
	errURLMisconfigured = errors.New("syno: client URL misconfigured")
)

// Error is the integer error code returned by the Synology API.
type Error int

// Error returns a human readable error string for the error if one is known.
func (e Error) Error() string {
	if s, ok := errStrings[e]; ok {
		return fmt.Sprint("syno: ", s, " (", int(e), ")")
	}
	return fmt.Sprintf("syno: error code %d", int(e))
}

const (
	ErrorUnknown                          = Error(100)
	ErrorInvalidParameter                 = Error(101)
	ErrorInvalidAPI                       = Error(102)
	ErrorInvalidMethod                    = Error(103)
	ErrorUnsupportedVersion               = Error(104)
	ErrorPermissionDenied                 = Error(105)
	ErrorSessionTimeout                   = Error(106)
	ErrorSessionInterruptedDuplicateLogin = Error(107)
)

var errStrings = map[Error]string{
	ErrorUnknown:                          "unknown API error",
	ErrorInvalidParameter:                 "invalid parameter",
	ErrorInvalidAPI:                       "invalid API",
	ErrorInvalidMethod:                    "invalid method",
	ErrorUnsupportedVersion:               "unsupported version",
	ErrorPermissionDenied:                 "permission denined",
	ErrorSessionTimeout:                   "session timeout error",
	ErrorSessionInterruptedDuplicateLogin: "session interrupted with duplicated login",
}

func dropEmpty(p url.Values) url.Values {
	for k, v := range p {
		if len(v) == 1 && v[0] == "" {
			p.Del(k)
		}
	}
	return p
}

// Request represents an API request to Synology.
type Request struct {
	Path    string
	API     string
	Version string
	Method  string
	Params  url.Values
	SID     string
}

// MarshalRequest can be implemented by a type that can be serialized to a
// Request.
type MarshalRequest interface {
	MarshalRequest() (*Request, error)
}

// Client provides access to the Synology API.
type Client struct {
	url       *url.URL
	transport http.RoundTripper
	sid       string
}

// Call makes a request obtained from marshaling the given argument and calls
// Do with it.
func (c *Client) Call(r MarshalRequest, data interface{}) error {
	req, err := r.MarshalRequest()
	if err != nil {
		return err
	}
	return c.Do(req, data)
}

// Do performs an API request and unmarshals the "Data" into the passed in
// argument. If data is nil, it is ignored.
func (c *Client) Do(r *Request, data interface{}) error {
	v := make(url.Values)
	v.Add("api", r.API)
	v.Add("version", r.Version)
	v.Add("method", r.Method)

	if r.SID != "" {
		v.Add("_sid", r.SID)
	} else if c.sid != "" {
		v.Add("_sid", c.sid)
	}

	for k, l := range r.Params {
		for _, e := range l {
			v.Add(k, e)
		}
	}

	hreq := &http.Request{
		Method: "GET",
		URL: c.url.ResolveReference(&url.URL{
			Path:     r.Path,
			RawQuery: v.Encode(),
		}),
		Header: make(http.Header),
	}
	hres, err := c.transport.RoundTrip(hreq)
	if err != nil {
		return err
	}
	defer hres.Body.Close()

	var synologyResponse struct {
		Success bool
		Error   struct{ Code Error }
		Data    json.RawMessage
	}
	if err := json.NewDecoder(hres.Body).Decode(&synologyResponse); err != nil {
		return err
	}
	if !synologyResponse.Success {
		return synologyResponse.Error.Code
	}
	if data != nil {
		if err := json.Unmarshal(synologyResponse.Data, data); err != nil {
			return err
		}
	}
	return nil
}

// ClientOption allows configuring various aspects of the Client.
type ClientOption func(*Client) error

// ClientURL configures the base Client URL. All API requests are made relative
// to this URL.
func ClientURL(u *url.URL) ClientOption {
	return func(c *Client) error {
		c.url = u
		return nil
	}
}

// ClientRawURL configures the base Client URL. All API requests are made
// relative to this URL.
func ClientRawURL(u string) ClientOption {
	return func(c *Client) error {
		var err error
		c.url, err = url.Parse(u)
		return err
	}
}

// ClientTransport configures the Transport for the Client. If not specified
// http.DefaultTransport is used.
func ClientTransport(t http.RoundTripper) ClientOption {
	return func(c *Client) error {
		c.transport = t
		return nil
	}
}

// ClientSID configures a default "sid" to include for authenticating an
// account.
func ClientSID(sid string) ClientOption {
	return func(c *Client) error {
		c.sid = sid
		return nil
	}
}

// ClientLogin configures the Client with a "sid" from the given credentials.
// It does so when the client is being initialized, so the ordering of this
// option should typically be after all the other options have been specified.
func ClientLogin(l AuthLogin) ClientOption {
	return func(c *Client) error {
		var res AuthLoginResponse
		l.Format = "sid"
		if err := c.Call(l, &res); err != nil {
			return err
		}
		c.sid = res.SID
		return nil
	}
}

// NewClient creates a new client with the given options.
func NewClient(options ...ClientOption) (*Client, error) {
	var c Client
	for _, o := range options {
		if err := o(&c); err != nil {
			return nil, err
		}
	}
	if c.url == nil || !c.url.IsAbs() {
		return nil, errURLMisconfigured
	}
	if c.transport == nil {
		c.transport = http.DefaultTransport
	}
	return &c, nil
}

const (
	authLoginPath    = "/webapi/auth.cgi"
	authLoginAPI     = "SYNO.API.Auth"
	authLoginVersion = "3"
)

// AuthLogin logs in an account. The response is AuthLoginResponse.
type AuthLogin struct {
	Account  string
	Password string
	Session  string
	Format   string
	OTPCode  string
}

// MarshalRequest serializes the instance to a Request.
func (a AuthLogin) MarshalRequest() (*Request, error) {
	return &Request{
		Path:    authLoginPath,
		API:     authLoginAPI,
		Version: authLoginVersion,
		Method:  "login",
		Params: dropEmpty(url.Values{
			"account":  []string{a.Account},
			"passwd":   []string{a.Password},
			"session":  []string{a.Session},
			"format":   []string{a.Format},
			"otp_code": []string{a.OTPCode},
		}),
	}, nil
}

// AuthLoginResponse is the response from an AuthLogin request.
type AuthLoginResponse struct {
	SID    string
	Cookie string
}

const (
	downloadTaskPath    = "/webapi/DownloadStation/task.cgi"
	downloadTaskAPI     = "SYNO.DownloadStation.Task"
	downloadTaskVersion = "1"
)

// DownloadTaskList perfoms a list call for download tasks.
type DownloadTaskList struct {
	Offset     int
	Limit      int
	Additional []string
}

// MarshalRequest serializes the instance to a Request.
func (d DownloadTaskList) MarshalRequest() (*Request, error) {
	v := url.Values{}
	if d.Offset != 0 {
		v.Add("offset", strconv.Itoa(d.Offset))
	}
	if d.Limit != 0 {
		v.Add("limit", strconv.Itoa(d.Limit))
	}
	if len(d.Additional) > 0 {
		v.Add("additional", strings.Join(d.Additional, ","))
	}

	return &Request{
		Path:    downloadTaskPath,
		API:     downloadTaskAPI,
		Version: downloadTaskVersion,
		Method:  "list",
		Params:  v,
	}, nil
}

// DownloadTaskCreate creates a new download task. It does not have a response.
type DownloadTaskCreate struct {
	URI           string
	Username      string
	Password      string
	UnzipPassword string
	Destination   string
}

// MarshalRequest serializes the instance to a Request.
func (d DownloadTaskCreate) MarshalRequest() (*Request, error) {
	return &Request{
		Path:    downloadTaskPath,
		API:     downloadTaskAPI,
		Version: downloadTaskVersion,
		Method:  "create",
		Params: dropEmpty(url.Values{
			"uri":            []string{d.URI},
			"username":       []string{d.Username},
			"password":       []string{d.Password},
			"unzip_password": []string{d.UnzipPassword},
			"destination":    []string{d.Destination},
		}),
	}, nil
}
<|endoftext|>


<commit_before>package tailer

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/ActiveState/tail"
	"github.com/golang/glog"
)

// observe file and add to tailer
func (s *Tailer) addToTail(filePath string) error {
	go s.tailFile(filePath)
	return nil
}

// TailFile tail -f the file and emit with publisher
func (s *Tailer) tailFile(filename string) {
	defer func() {
		glog.Warningf("Stop %s", filename)
		s.waitGroup.Done()
	}()
	s.waitGroup.Add(1)
	base := filepath.Base(filename)
	if RegexNotWatch.MatchString(base) {
		glog.Warningf("Skip %s", filename)
		return
	}
	glog.Warningf("Tail %s", filename)
	t, err := tail.TailFile(filename, tail.Config{
		Follow: true, Location: &tail.SeekInfo{0, os.SEEK_END},
	})
	if err != nil {
		glog.Errorf("initial tail file error: %v", err)
		return
	}
	for line := range t.Lines {
		if s.matchLine != nil {
			if s.matchLine.MatchString(line.Text) {
				err = s.publisher.Publish([]byte(fmt.Sprintf("%s: %s", base, line.Text)))
				if err != nil {
					glog.Errorf("publish error: %v", err)
				}
			}
		} else {
			err = s.publisher.Publish([]byte(fmt.Sprintf("%s: %s", base, line.Text)))
			if err != nil {
				glog.Errorf("publish error: %v", err)
			}
		}
	}
	err = t.Wait()
	if err != nil {
		glog.Errorf("wait error: %v", err)
	}
}
<commit_msg>refactory the if-else block<commit_after>package tailer

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/ActiveState/tail"
	"github.com/golang/glog"
)

// observe file and add to tailer
func (s *Tailer) addToTail(filePath string) error {
	go s.tailFile(filePath)
	return nil
}

// TailFile tail -f the file and emit with publisher
func (s *Tailer) tailFile(filename string) {
	defer func() {
		glog.Warningf("Stop %s", filename)
		s.waitGroup.Done()
	}()
	s.waitGroup.Add(1)
	base := filepath.Base(filename)
	if RegexNotWatch.MatchString(base) {
		glog.Warningf("Skip %s", filename)
		return
	}
	glog.Warningf("Tail %s", filename)
	t, err := tail.TailFile(filename, tail.Config{
		Follow: true, Location: &tail.SeekInfo{0, os.SEEK_END},
	})
	if err != nil {
		glog.Errorf("initial tail file error: %v", err)
		return
	}
	for line := range t.Lines {
		if s.matchLine != nil && !s.matchLine.MatchString(line.Text) {
			continue
		} else {
			err = s.publisher.Publish([]byte(fmt.Sprintf("%s: %s", base, line.Text)))
			if err != nil {
				glog.Errorf("publish error: %v", err)
			}
		}
	}
	err = t.Wait()
	if err != nil {
		glog.Errorf("wait error: %v", err)
	}
}
<|endoftext|>


<commit_before>package task

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"

	"github.com/kardianos/osext"
	"gopkg.in/yaml.v2"
)

var (
	CurrentDirectory, _ = osext.ExecutableFolder()
	TaskFilePath        = filepath.Join(CurrentDirectory, "Taskfile.yml")
	ShExists            bool
	ShPath              string

	Tasks = make(map[string]*Task)
)

func init() {
	var err error
	ShPath, err = exec.LookPath("sh")
	if err != nil {
		return
	}
	ShExists = true
}

type Task struct {
	Cmds      []string
	Deps      []string
	Source    string
	Generates string
}

type TaskNotFoundError struct {
	taskName string
}

func (err *TaskNotFoundError) Error() string {
	return fmt.Sprintf(`Task "%s" not found`, err.taskName)
}

type TaskRunError struct {
	taskName string
	err      error
}

func (err *TaskRunError) Error() string {
	return fmt.Sprintf(`Failed to run task "%s": %v`, err.taskName, err.err)
}

func Run() {
	log.SetFlags(0)

	args := os.Args[1:]
	if len(args) == 0 {
		log.Fatal("No argument given")
	}

	file, err := ioutil.ReadFile(TaskFilePath)
	if err != nil {
		if os.IsNotExist(err) {
			log.Fatal("Taskfile.yml not found")
		}
		log.Fatal(err)
	}

	if err = yaml.Unmarshal(file, &Tasks); err != nil {
		log.Fatal(err)
	}

	for _, a := range args {
		if err = RunTask(a); err != nil {
			log.Fatal(err)
		}
	}
}

func RunTask(name string) error {
	t, ok := Tasks[name]
	if !ok {
		return &TaskNotFoundError{name}
	}

	for _, d := range t.Deps {
		if err := RunTask(d); err != nil {
			return err
		}
	}

	for _, c := range t.Cmds {
		if err := runCommand(c); err != nil {
			return &TaskRunError{name, err}
		}
	}
	return nil
}

func runCommand(c string) error {
	var cmd *exec.Cmd
	if ShExists {
		cmd = exec.Command(ShPath, "-c", c)
	} else {
		cmd = exec.Command("cmd", "/C", c)
	}
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return err
	}
	return nil
}
<commit_msg>Use relative path<commit_after>package task

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"

	"gopkg.in/yaml.v2"
)

var (
	TaskFilePath = "Taskfile.yml"
	ShExists     bool
	ShPath       string

	Tasks = make(map[string]*Task)
)

func init() {
	var err error
	ShPath, err = exec.LookPath("sh")
	if err != nil {
		return
	}
	ShExists = true
}

type Task struct {
	Cmds      []string
	Deps      []string
	Source    string
	Generates string
}

type TaskNotFoundError struct {
	taskName string
}

func (err *TaskNotFoundError) Error() string {
	return fmt.Sprintf(`Task "%s" not found`, err.taskName)
}

type TaskRunError struct {
	taskName string
	err      error
}

func (err *TaskRunError) Error() string {
	return fmt.Sprintf(`Failed to run task "%s": %v`, err.taskName, err.err)
}

func Run() {
	log.SetFlags(0)

	args := os.Args[1:]
	if len(args) == 0 {
		log.Fatal("No argument given")
	}

	file, err := ioutil.ReadFile(TaskFilePath)
	if err != nil {
		if os.IsNotExist(err) {
			log.Fatal("Taskfile.yml not found")
		}
		log.Fatal(err)
	}

	if err = yaml.Unmarshal(file, &Tasks); err != nil {
		log.Fatal(err)
	}

	for _, a := range args {
		if err = RunTask(a); err != nil {
			log.Fatal(err)
		}
	}
}

func RunTask(name string) error {
	t, ok := Tasks[name]
	if !ok {
		return &TaskNotFoundError{name}
	}

	for _, d := range t.Deps {
		if err := RunTask(d); err != nil {
			return err
		}
	}

	for _, c := range t.Cmds {
		if err := runCommand(c); err != nil {
			return &TaskRunError{name, err}
		}
	}
	return nil
}

func runCommand(c string) error {
	var cmd *exec.Cmd
	if ShExists {
		cmd = exec.Command(ShPath, "-c", c)
	} else {
		cmd = exec.Command("cmd", "/C", c)
	}
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return err
	}
	return nil
}
<|endoftext|>


<commit_before>package task

import (
	"context"
	"fmt"
	"io"
	"os"
	"strings"
	"sync/atomic"

	"github.com/go-task/task/v2/internal/compiler"
	compilerv1 "github.com/go-task/task/v2/internal/compiler/v1"
	compilerv2 "github.com/go-task/task/v2/internal/compiler/v2"
	"github.com/go-task/task/v2/internal/execext"
	"github.com/go-task/task/v2/internal/logger"
	"github.com/go-task/task/v2/internal/output"
	"github.com/go-task/task/v2/internal/taskfile"
	"github.com/go-task/task/v2/internal/taskfile/read"
	"github.com/go-task/task/v2/internal/taskfile/version"

	"github.com/Masterminds/semver"
	"golang.org/x/sync/errgroup"
)

const (
	// MaximumTaskCall is the max number of times a task can be called.
	// This exists to prevent infinite loops on cyclic dependencies
	MaximumTaskCall = 100
)

// Executor executes a Taskfile
type Executor struct {
	Taskfile *taskfile.Taskfile
	Dir      string
	Force    bool
	Watch    bool
	Verbose  bool
	Silent   bool
	Dry      bool
	Summary  bool

	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer

	Logger      *logger.Logger
	Compiler    compiler.Compiler
	Output      output.Output
	OutputStyle string

	taskvars taskfile.Vars

	taskCallCount map[string]*int32
}

// Run runs Task
func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
	// check if given tasks exist
	for _, c := range calls {
		if _, ok := e.Taskfile.Tasks[c.Task]; !ok {
			// FIXME: move to the main package
			e.PrintTasksHelp()
			return &taskNotFoundError{taskName: c.Task}
		}
	}

	if e.Summary {
		e.printTaskDetails(calls[0].Task)
		return nil
	}

	if e.Watch {
		return e.watchTasks(calls...)
	}

	for _, c := range calls {
		if err := e.RunTask(ctx, c); err != nil {
			return err
		}
	}
	return nil
}

func (e *Executor) printTaskDetails(task string) {
	t := e.Taskfile.Tasks[task]
	s := t.Summary
	if s == "" {
		e.Logger.Errf("task: There is no detailed description for task: %s", task)
		return
	}

	e.Logger.Outf("task: " + task)
	e.Logger.Outf("")

	printTaskDetailedDescription(s, e.Logger)
	printTaskDependencies(t.Deps, e.Logger)

	e.Logger.Outf("")
	e.Logger.Outf("commands:")

	for _, c := range t.Cmds {
		e.Logger.Outf(" - %s", c.Cmd)
	}
}

func printTaskDependencies(deps []*taskfile.Dep, logger *logger.Logger) {
	hasDependencies := len(deps) > 0
	if hasDependencies {
		logger.Outf("")
		logger.Outf("dependencies:")

		for _, d := range deps {
			logger.Outf(" - %s", d.Task)
		}
	}
}

func printTaskDetailedDescription(description string, Logger *logger.Logger) {
	lines := strings.Split(description, "\n")
	for i, line := range lines {
		notLastLine := i+1 < len(lines)
		if notLastLine || line != "" {
			Logger.Outf(line)
		}
	}

}

// Setup setups Executor's internal state
func (e *Executor) Setup() error {
	var err error
	e.Taskfile, err = read.Taskfile(e.Dir)
	if err != nil {
		return err
	}
	e.taskvars, err = read.Taskvars(e.Dir)
	if err != nil {
		return err
	}

	v, err := semver.NewConstraint(e.Taskfile.Version)
	if err != nil {
		return fmt.Errorf(`task: could not parse taskfile version "%s": %v`, e.Taskfile.Version, err)
	}

	if e.Stdin == nil {
		e.Stdin = os.Stdin
	}
	if e.Stdout == nil {
		e.Stdout = os.Stdout
	}
	if e.Stderr == nil {
		e.Stderr = os.Stderr
	}
	e.Logger = &logger.Logger{
		Stdout:  e.Stdout,
		Stderr:  e.Stderr,
		Verbose: e.Verbose,
	}
	switch {
	case version.IsV1(v):
		e.Compiler = &compilerv1.CompilerV1{
			Dir:    e.Dir,
			Vars:   e.taskvars,
			Logger: e.Logger,
		}
	case version.IsV2(v), version.IsV21(v), version.IsV22(v):
		e.Compiler = &compilerv2.CompilerV2{
			Dir:          e.Dir,
			Taskvars:     e.taskvars,
			TaskfileVars: e.Taskfile.Vars,
			Expansions:   e.Taskfile.Expansions,
			Logger:       e.Logger,
		}
	case version.IsV23(v):
		return fmt.Errorf(`task: Taskfile versions greater than v2.3 not implemented in the version of Task`)
	}

	if !version.IsV21(v) && e.Taskfile.Output != "" {
		return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
	}
	if !version.IsV22(v) && len(e.Taskfile.Includes) > 0 {
		return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`)
	}
	if e.OutputStyle != "" {
		e.Taskfile.Output = e.OutputStyle
	}
	switch e.Taskfile.Output {
	case "", "interleaved":
		e.Output = output.Interleaved{}
	case "group":
		e.Output = output.Group{}
	case "prefixed":
		e.Output = output.Prefixed{}
	default:
		return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output)
	}

	if !version.IsV21(v) {
		err := fmt.Errorf(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)

		for _, task := range e.Taskfile.Tasks {
			if task.IgnoreError {
				return err
			}
			for _, cmd := range task.Cmds {
				if cmd.IgnoreError {
					return err
				}
			}
		}
	}

	e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
	for k := range e.Taskfile.Tasks {
		e.taskCallCount[k] = new(int32)
	}
	return nil
}

// RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
	t, err := e.CompiledTask(call)
	if err != nil {
		return err
	}
	if !e.Watch && atomic.AddInt32(e.taskCallCount[call.Task], 1) >= MaximumTaskCall {
		return &MaximumTaskCallExceededError{task: call.Task}
	}

	if err := e.runDeps(ctx, t); err != nil {
		return err
	}

	if !e.Force {
		upToDate, err := e.isTaskUpToDate(ctx, t)
		if err != nil {
			return err
		}
		if upToDate {
			if !e.Silent {
				e.Logger.Errf(`task: Task "%s" is up to date`, t.Task)
			}
			return nil
		}
	}

	for i := range t.Cmds {
		if err := e.runCommand(ctx, t, call, i); err != nil {
			if err2 := e.statusOnError(t); err2 != nil {
				e.Logger.VerboseErrf("task: error cleaning status on error: %v", err2)
			}

			if execext.IsExitError(err) && t.IgnoreError {
				e.Logger.VerboseErrf("task: task error ignored: %v", err)
				continue
			}

			return &taskRunError{t.Task, err}
		}
	}
	return nil
}

func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
	g, ctx := errgroup.WithContext(ctx)

	for _, d := range t.Deps {
		d := d

		g.Go(func() error {
			return e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
		})
	}

	return g.Wait()
}

func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfile.Call, i int) error {
	cmd := t.Cmds[i]

	switch {
	case cmd.Task != "":
		return e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
	case cmd.Cmd != "":
		if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
			e.Logger.Errf(cmd.Cmd)
		}

		if e.Dry {
			return nil
		}

		stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix)
		stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix)
		defer stdOut.Close()
		defer stdErr.Close()

		err := execext.RunCommand(ctx, &execext.RunCommandOptions{
			Command: cmd.Cmd,
			Dir:     t.Dir,
			Env:     getEnviron(t),
			Stdin:   e.Stdin,
			Stdout:  stdOut,
			Stderr:  stdErr,
		})
		if execext.IsExitError(err) && cmd.IgnoreError {
			e.Logger.VerboseErrf("task: command error ignored: %v", err)
			return nil
		}
		return err
	default:
		return nil
	}
}

func getEnviron(t *taskfile.Task) []string {
	if t.Env == nil {
		return nil
	}

	environ := os.Environ()
	for k, v := range t.Env.ToStringMap() {
		environ = append(environ, fmt.Sprintf("%s=%s", k, v))
	}
	return environ
}
<commit_msg>rename to summary<commit_after>package task

import (
	"context"
	"fmt"
	"io"
	"os"
	"strings"
	"sync/atomic"

	"github.com/go-task/task/v2/internal/compiler"
	compilerv1 "github.com/go-task/task/v2/internal/compiler/v1"
	compilerv2 "github.com/go-task/task/v2/internal/compiler/v2"
	"github.com/go-task/task/v2/internal/execext"
	"github.com/go-task/task/v2/internal/logger"
	"github.com/go-task/task/v2/internal/output"
	"github.com/go-task/task/v2/internal/taskfile"
	"github.com/go-task/task/v2/internal/taskfile/read"
	"github.com/go-task/task/v2/internal/taskfile/version"

	"github.com/Masterminds/semver"
	"golang.org/x/sync/errgroup"
)

const (
	// MaximumTaskCall is the max number of times a task can be called.
	// This exists to prevent infinite loops on cyclic dependencies
	MaximumTaskCall = 100
)

// Executor executes a Taskfile
type Executor struct {
	Taskfile *taskfile.Taskfile
	Dir      string
	Force    bool
	Watch    bool
	Verbose  bool
	Silent   bool
	Dry      bool
	Summary  bool

	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer

	Logger      *logger.Logger
	Compiler    compiler.Compiler
	Output      output.Output
	OutputStyle string

	taskvars taskfile.Vars

	taskCallCount map[string]*int32
}

// Run runs Task
func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
	// check if given tasks exist
	for _, c := range calls {
		if _, ok := e.Taskfile.Tasks[c.Task]; !ok {
			// FIXME: move to the main package
			e.PrintTasksHelp()
			return &taskNotFoundError{taskName: c.Task}
		}
	}

	if e.Summary {
		e.printTaskSummary(calls[0].Task)
		return nil
	}

	if e.Watch {
		return e.watchTasks(calls...)
	}

	for _, c := range calls {
		if err := e.RunTask(ctx, c); err != nil {
			return err
		}
	}
	return nil
}

func (e *Executor) printTaskSummary(task string) {
	t := e.Taskfile.Tasks[task]
	s := t.Summary
	if s == "" {
		e.Logger.Errf("task: There is no detailed description for task: %s", task)
		return
	}

	e.Logger.Outf("task: " + task)
	e.Logger.Outf("")

	printTaskDetailedDescription(s, e.Logger)
	printTaskDependencies(t.Deps, e.Logger)

	e.Logger.Outf("")
	e.Logger.Outf("commands:")

	for _, c := range t.Cmds {
		e.Logger.Outf(" - %s", c.Cmd)
	}
}

func printTaskDependencies(deps []*taskfile.Dep, logger *logger.Logger) {
	hasDependencies := len(deps) > 0
	if hasDependencies {
		logger.Outf("")
		logger.Outf("dependencies:")

		for _, d := range deps {
			logger.Outf(" - %s", d.Task)
		}
	}
}

func printTaskDetailedDescription(description string, Logger *logger.Logger) {
	lines := strings.Split(description, "\n")
	for i, line := range lines {
		notLastLine := i+1 < len(lines)
		if notLastLine || line != "" {
			Logger.Outf(line)
		}
	}

}

// Setup setups Executor's internal state
func (e *Executor) Setup() error {
	var err error
	e.Taskfile, err = read.Taskfile(e.Dir)
	if err != nil {
		return err
	}
	e.taskvars, err = read.Taskvars(e.Dir)
	if err != nil {
		return err
	}

	v, err := semver.NewConstraint(e.Taskfile.Version)
	if err != nil {
		return fmt.Errorf(`task: could not parse taskfile version "%s": %v`, e.Taskfile.Version, err)
	}

	if e.Stdin == nil {
		e.Stdin = os.Stdin
	}
	if e.Stdout == nil {
		e.Stdout = os.Stdout
	}
	if e.Stderr == nil {
		e.Stderr = os.Stderr
	}
	e.Logger = &logger.Logger{
		Stdout:  e.Stdout,
		Stderr:  e.Stderr,
		Verbose: e.Verbose,
	}
	switch {
	case version.IsV1(v):
		e.Compiler = &compilerv1.CompilerV1{
			Dir:    e.Dir,
			Vars:   e.taskvars,
			Logger: e.Logger,
		}
	case version.IsV2(v), version.IsV21(v), version.IsV22(v):
		e.Compiler = &compilerv2.CompilerV2{
			Dir:          e.Dir,
			Taskvars:     e.taskvars,
			TaskfileVars: e.Taskfile.Vars,
			Expansions:   e.Taskfile.Expansions,
			Logger:       e.Logger,
		}
	case version.IsV23(v):
		return fmt.Errorf(`task: Taskfile versions greater than v2.3 not implemented in the version of Task`)
	}

	if !version.IsV21(v) && e.Taskfile.Output != "" {
		return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
	}
	if !version.IsV22(v) && len(e.Taskfile.Includes) > 0 {
		return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`)
	}
	if e.OutputStyle != "" {
		e.Taskfile.Output = e.OutputStyle
	}
	switch e.Taskfile.Output {
	case "", "interleaved":
		e.Output = output.Interleaved{}
	case "group":
		e.Output = output.Group{}
	case "prefixed":
		e.Output = output.Prefixed{}
	default:
		return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output)
	}

	if !version.IsV21(v) {
		err := fmt.Errorf(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)

		for _, task := range e.Taskfile.Tasks {
			if task.IgnoreError {
				return err
			}
			for _, cmd := range task.Cmds {
				if cmd.IgnoreError {
					return err
				}
			}
		}
	}

	e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
	for k := range e.Taskfile.Tasks {
		e.taskCallCount[k] = new(int32)
	}
	return nil
}

// RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
	t, err := e.CompiledTask(call)
	if err != nil {
		return err
	}
	if !e.Watch && atomic.AddInt32(e.taskCallCount[call.Task], 1) >= MaximumTaskCall {
		return &MaximumTaskCallExceededError{task: call.Task}
	}

	if err := e.runDeps(ctx, t); err != nil {
		return err
	}

	if !e.Force {
		upToDate, err := e.isTaskUpToDate(ctx, t)
		if err != nil {
			return err
		}
		if upToDate {
			if !e.Silent {
				e.Logger.Errf(`task: Task "%s" is up to date`, t.Task)
			}
			return nil
		}
	}

	for i := range t.Cmds {
		if err := e.runCommand(ctx, t, call, i); err != nil {
			if err2 := e.statusOnError(t); err2 != nil {
				e.Logger.VerboseErrf("task: error cleaning status on error: %v", err2)
			}

			if execext.IsExitError(err) && t.IgnoreError {
				e.Logger.VerboseErrf("task: task error ignored: %v", err)
				continue
			}

			return &taskRunError{t.Task, err}
		}
	}
	return nil
}

func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
	g, ctx := errgroup.WithContext(ctx)

	for _, d := range t.Deps {
		d := d

		g.Go(func() error {
			return e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
		})
	}

	return g.Wait()
}

func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfile.Call, i int) error {
	cmd := t.Cmds[i]

	switch {
	case cmd.Task != "":
		return e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
	case cmd.Cmd != "":
		if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
			e.Logger.Errf(cmd.Cmd)
		}

		if e.Dry {
			return nil
		}

		stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix)
		stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix)
		defer stdOut.Close()
		defer stdErr.Close()

		err := execext.RunCommand(ctx, &execext.RunCommandOptions{
			Command: cmd.Cmd,
			Dir:     t.Dir,
			Env:     getEnviron(t),
			Stdin:   e.Stdin,
			Stdout:  stdOut,
			Stderr:  stdErr,
		})
		if execext.IsExitError(err) && cmd.IgnoreError {
			e.Logger.VerboseErrf("task: command error ignored: %v", err)
			return nil
		}
		return err
	default:
		return nil
	}
}

func getEnviron(t *taskfile.Task) []string {
	if t.Env == nil {
		return nil
	}

	environ := os.Environ()
	for k, v := range t.Env.ToStringMap() {
		environ = append(environ, fmt.Sprintf("%s=%s", k, v))
	}
	return environ
}
<|endoftext|>


<commit_before>package main

import "fmt"
import "log"
import "os"
import "errors"
import "strings"
import "unicode"
import "github.com/nlopes/slack"
import "github.com/tadgh/go-toggl"
import "encoding/gob"
import "time"
import "runtime"

const file = "./test.gob"

func Check(err error) {
	if err != nil {
		_, file, line, _ := runtime.Caller(1)
		fmt.Println(line, "\t", file, "\n", err)
		os.Exit(1)
	}
}

func pingTogglApi(apiKey string) error {
	ts := toggl.OpenSession(apiKey)
	_, err := ts.GetAccount()
	if err != nil {
		fmt.Println(os.Stderr, "Error: %s\n", err)
		return err
	}
	return nil
}

func Save(path string, object interface{}) error {
	file, err := os.Create(path)
	if err == nil {
		encoder := gob.NewEncoder(file)
		encoder.Encode(object)
	}
	file.Close()
	return err
}

func Load(path string, object interface{}) error {
	file, err := os.Open(path)
	if err == nil {
		decoder := gob.NewDecoder(file)
		err = decoder.Decode(object)
	}
	file.Close()
	return err
}

func createTimeEntry(apiKey, description string, start time.Time, duration time.Duration, pid, tid int) *toggl.TimeEntry {
	ts := toggl.OpenSession(apiKey)
	te, err := ts.CreateTimeEntry(pid, 0, start, duration, description)
	if err != nil {
		log.Fatal("Error uploading time entry! %v", err)
	}
	return &te
}

func stopTimer(apiKey string) (*toggl.TimeEntry, error) {
	ts := toggl.OpenSession(apiKey)
	te, err := ts.GetActiveTimeEntry()
	if err != nil {
		return nil, err
	}
	if te.ID <= 0 {
		return nil, errors.New("No timer is currently running!")
	}
	te, err = ts.StopTimeEntry(te)
	if err != nil {
		return nil, err
	}
	return &te, nil
}

func startTimer(apiKey, description string, pid int) *toggl.TimeEntry {
	ts := toggl.OpenSession(apiKey)
	te, err := ts.StartTimeEntryForProject(description, pid)

	if err != nil {
		fmt.Println(err)
	}
	sessionMap[apiKey] = te
	return &te
}

func getProjectWithName(apiKey, projectName string) int {
	ts := toggl.OpenSession(apiKey)
	acc, err := ts.GetAccount()
	var retVal int
	if err != nil {
		fmt.Println(err)
	}
	for _, project := range acc.Data.Projects {
		//case insensitive string comparison
		if strings.EqualFold(project.Name, projectName) {
			retVal = project.ID
		}
	}
	return retVal
}

type BotCommand struct {
	Channel string
	Event   *slack.MessageEvent
	UserId  string
}

type ReplyChannel struct {
	Channel      string
	Attachment   *slack.Attachment
	DisplayTitle string
}

var (
	api               *slack.Client
	botCommandChannel chan *BotCommand
	botReplyChannel   chan ReplyChannel
	botId             string
	userMap           map[string]string
	sessionMap        map[string]toggl.TimeEntry
)

func handleBotCommands(replyChannel chan ReplyChannel) {
	commands := map[string]string{
		"register": "Register yourself with togglbot. `@togglbot register MY_TOGGL_API_KEY`",
		"start":    "Start a timer for a given project and description. `@togglbot start <PROJECT_NAME> <EVERYTHING_ELSE_IS_DESCRIPTION>`",
		"stop":     "Stops any current timer session. `@togglbot stop`",
		"track":    "adds a toggl entry with an optional description to a project for a given time range and date (optional)."
			+ "`@togglbot track icancope 9am-5pm` OR `@togglbot track icancope 9am-5pm 2017/08/11 description",
	}

	for {
		incomingCommand := <-botCommandChannel

		commandArray := strings.Fields(strings.ToLower(incomingCommand.Event.Text))
		if strings.EqualFold(commandArray[0], "<@"+botId+">") {
			commandArray = commandArray[1:]
		}
		var reply ReplyChannel
		reply.Channel = incomingCommand.Channel

		switch commandArray[0] {
		case "help":
			reply.DisplayTitle = "Help!"
			fields := make([]slack.AttachmentField, 0)
			for k, v := range commands {
				fields = append(fields, slack.AttachmentField{
					Title: "<bot> " + k,
					Value: v,
				})
			}
			attachment := &slack.Attachment{
				Pretext:    "TogglBot Command List",
				Color:      "#B733FF",
				Fields:     fields,
				MarkdownIn: []string{"fields"},
			}
			reply.Attachment = attachment
			replyChannel <- reply
		case "register":
			togglApiKey := commandArray[1]
			err := pingTogglApi(togglApiKey)
			if err != nil {
				reply.DisplayTitle = "Failed to register. Bad api key?"
			} else {
				userMap[incomingCommand.Event.User] = togglApiKey
				err := Save(file, userMap)
				Check(err)
				reply.DisplayTitle = "Successfully registered!"
			}
			replyChannel <- reply
		}

		if reply.DisplayTitle != "" {
			continue
		}
		togglApiKey, ok := userMap[incomingCommand.Event.User]
		if !ok {
			reply.DisplayTitle = "You have not registered with togglbot yet. Try @togglbot register API_KEY_HERE"
			replyChannel <- reply
			continue
		}

		switch commandArray[0] {
		case "start":
			if len(commandArray) <= 2 {
				reply.DisplayTitle = "Please provide a project name and description! `@togglbot start PROJECT_NAME DESCRIPTION`"
				replyChannel <- reply
				break
			}
			project := commandArray[1]
			description := strings.Join(commandArray[2:], " ")
			pid := getProjectWithName(togglApiKey, project)
			fmt.Printf("%v", pid)
			startTimer(togglApiKey, description, pid)
			reply.DisplayTitle = "Timer started! *get back to work peon*"
			replyChannel <- reply
		case "stop":
			te, err := stopTimer(togglApiKey)
			if err != nil {
				reply.DisplayTitle = "couldn't stop timer: " + err.Error()
				replyChannel <- reply
				break
			}
			dur, err := time.ParseDuration(fmt.Sprintf("%vs", te.Duration))
			if err != nil {
				log.Fatal("Unparseable duration! %v", te.Duration)
			}
			reply.DisplayTitle = fmt.Sprintf("Timer Stopped. Worked for %v.", dur.String())
			replyChannel <- reply
		case "track":
			if len(commandArray) < 3 {
				reply.DisplayTitle = "Sorry, I don't have enough information to make an event for you. try `@togglbot track PROJECT_NAME 9:00AM-5:00PM YYYY/MM/DD TASK_DESCRIPTION`"
				replyChannel <- reply
				break
			}
			projectName := commandArray[1]
			pid := getProjectWithName(togglApiKey, projectName)
			timeRange := commandArray[2]

			description := "no description provided"
			entryDate := time.Now() // Defauly day is today

			if len(commandArray) > 3 {
				entryDate = parseDate(commandArray[3])

				if entryDate == nil {
					description = strings.Join(commandArray[3:], " ")
				} else {
					if len(commandArray) > 4 {
						description = strings.Join(commandArray[4:], " ")
					}
				}
			}

			startTime, duration, err := parseTimeRange(timeRange, entryDate)
			if err != nil {
				reply.DisplayTitle = err.Error()
				replyChannel <- reply
				break
			}
			createTimeEntry(togglApiKey, description, *startTime, *duration, pid, 0)
			reply.DisplayTitle = "Time entry created!"
			replyChannel <- reply
		default:
			reply.DisplayTitle = "Sorry, I don't understand that command. Try `@Togglbot help`"
			replyChannel <- reply
		}
	}
}

func parseDate(readingDate string) (*time.Date) {
	parsedDate, err := time.Parse("2006/1/2", readingDate)
	if err != nil {
		return nil
	}

	/* No error is returned if malformed date because if parsedDate == nil it will
	be assumed to be a description thus error will never be shown */

	return parsedDate
}

func parseTimeRange(timeRange string, parsedDate time.Date) (*time.Time, *time.Duration, error) {
	timeRange = strings.ToUpper(timeRange)
	fields := strings.Split(timeRange, "-")
	if len(fields) != 2 {
		return nil, nil, errors.New("Your date range is incorrectly formatted! Try something like: 9:00AM-5:00PM")
	}

	startTime, err := time.Parse(time.Kitchen, fields[0])
	if err != nil {
		return nil, nil, errors.New("Your date range is incorrectly formatted! Try something like: 9:00AM-5:00PM")
	}

	endTime, err := time.Parse(time.Kitchen, fields[1])
	if err != nil {
		return nil, nil, errors.New("Your date range is incorrectly formatted! Try something like: 9:00AM-5:00PM")
	}

	duration := endTime.Sub(startTime)

	if parsedDate == nil {
		dateToInput := time.Now()
	} else {
		dateToInput := parsedDate
	}

	// Kitchen time has only hours and PM/AM. Drop in today's date.
	fmt.Println("DATE:\t", dateToInput)
	startTime = time.Date(dateToInput.Year(), dateToInput.Month(), dateToInput.Day(), startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(), now.Location())
	fmt.Println("START TIME:\t", startTime)
	return &startTime, &duration, nil
}

func handleBotReplies() {
	for {
		reply := <-botReplyChannel
		params := slack.PostMessageParameters{}
		params.AsUser = true
		if reply.Attachment != nil {
			params.Attachments = []slack.Attachment{*reply.Attachment}
		}
		_, _, err := api.PostMessage(reply.Channel, reply.DisplayTitle, params)
		if err != nil {
			fmt.Println("FATAL SHIT")
			log.Fatal(err)
		}
	}
}

func main() {

	if len(os.Args) != 2 {
		fmt.Printf("usage: togglbot slack-bot-token")
		os.Exit(1)
	}

	//First attempt to deserialize a saved registration file, otherwise
	//create new
	err := Load(file, &userMap)
	if err != nil {
		fmt.Println("ERROR WAS DETECTED")
		log.Fatal(err)
		userMap = make(map[string]string)
	}

	sessionMap = make(map[string]toggl.TimeEntry)
	token := os.Args[1]
	api = slack.New(token)
	rtm := api.NewRTM()
	botCommandChannel = make(chan *BotCommand)
	botReplyChannel = make(chan ReplyChannel)
	go rtm.ManageConnection()
	go handleBotCommands(botReplyChannel)
	go handleBotReplies()
	go fmt.Println("TogglBot ready, ^C exits")
Loop:
	for {
		select {
		case msg := <-rtm.IncomingEvents:
			switch event := msg.Data.(type) {
			case *slack.ConnectedEvent:
				botId = event.Info.User.ID
			case *slack.MessageEvent:

				botCommand := &BotCommand{
					Channel: event.Channel,
					Event:   event,
					UserId:  event.User,
				}

				if isValidMessageEvent(event) {
					fmt.Println("Received event: ", event)
					botCommandChannel <- botCommand
				}
			case *slack.RTMError:
				fmt.Printf("ERROR: %s\n", event.Error())
			case *slack.InvalidAuthEvent:
				fmt.Printf("Invalid credentials")
				break Loop
			}
		}
	}

}

func isValidMessageEvent(event *slack.MessageEvent) bool {
	if event.Type != "message" {
		return false
	}
	if event.User == botId {
		return false
	}
	if strings.HasPrefix(event.Text, "<@"+botId+">") {
		return true
	}
	if strings.HasPrefix(event.Channel, "D") {
		return true
	}
	return false
}
<commit_msg>Fixed compile time errors<commit_after>package main

import "fmt"
import "log"
import "os"
import "errors"
import "strings"
import "github.com/nlopes/slack"
import "github.com/tadgh/go-toggl"
import "encoding/gob"
import "time"
import "runtime"

const file = "./test.gob"

func Check(err error) {
	if err != nil {
		_, file, line, _ := runtime.Caller(1)
		fmt.Println(line, "\t", file, "\n", err)
		os.Exit(1)
	}
}

func pingTogglApi(apiKey string) error {
	ts := toggl.OpenSession(apiKey)
	_, err := ts.GetAccount()
	if err != nil {
		fmt.Println(os.Stderr, "Error: %s\n", err)
		return err
	}
	return nil
}

func Save(path string, object interface{}) error {
	file, err := os.Create(path)
	if err == nil {
		encoder := gob.NewEncoder(file)
		encoder.Encode(object)
	}
	file.Close()
	return err
}

func Load(path string, object interface{}) error {
	file, err := os.Open(path)
	if err == nil {
		decoder := gob.NewDecoder(file)
		err = decoder.Decode(object)
	}
	file.Close()
	return err
}

func createTimeEntry(apiKey, description string, start time.Time, duration time.Duration, pid, tid int) *toggl.TimeEntry {
	ts := toggl.OpenSession(apiKey)
	te, err := ts.CreateTimeEntry(pid, 0, start, duration, description)
	if err != nil {
		log.Fatal("Error uploading time entry! %v", err)
	}
	return &te
}

func stopTimer(apiKey string) (*toggl.TimeEntry, error) {
	ts := toggl.OpenSession(apiKey)
	te, err := ts.GetActiveTimeEntry()
	if err != nil {
		return nil, err
	}
	if te.ID <= 0 {
		return nil, errors.New("No timer is currently running!")
	}
	te, err = ts.StopTimeEntry(te)
	if err != nil {
		return nil, err
	}
	return &te, nil
}

func startTimer(apiKey, description string, pid int) *toggl.TimeEntry {
	ts := toggl.OpenSession(apiKey)
	te, err := ts.StartTimeEntryForProject(description, pid)

	if err != nil {
		fmt.Println(err)
	}
	sessionMap[apiKey] = te
	return &te
}

func getProjectWithName(apiKey, projectName string) int {
	ts := toggl.OpenSession(apiKey)
	acc, err := ts.GetAccount()
	var retVal int
	if err != nil {
		fmt.Println(err)
	}
	for _, project := range acc.Data.Projects {
		//case insensitive string comparison
		if strings.EqualFold(project.Name, projectName) {
			retVal = project.ID
		}
	}
	return retVal
}

type BotCommand struct {
	Channel string
	Event   *slack.MessageEvent
	UserId  string
}

type ReplyChannel struct {
	Channel      string
	Attachment   *slack.Attachment
	DisplayTitle string
}

var (
	api               *slack.Client
	botCommandChannel chan *BotCommand
	botReplyChannel   chan ReplyChannel
	botId             string
	userMap           map[string]string
	sessionMap        map[string]toggl.TimeEntry
)

func handleBotCommands(replyChannel chan ReplyChannel) {
	commands := map[string]string{
		"register": "Register yourself with togglbot. `@togglbot register MY_TOGGL_API_KEY`",
		"start":    "Start a timer for a given project and description. `@togglbot start <PROJECT_NAME> <EVERYTHING_ELSE_IS_DESCRIPTION>`",
		"stop":     "Stops any current timer session. `@togglbot stop`",
		"track":    "adds a toggl entry with an optional description to a project for a given time range and date (optional). `@togglbot track icancope 9am-5pm` OR `@togglbot track icancope 9am-5pm 2017/08/11 description",
	}

	for {
		incomingCommand := <-botCommandChannel

		commandArray := strings.Fields(strings.ToLower(incomingCommand.Event.Text))
		if strings.EqualFold(commandArray[0], "<@"+botId+">") {
			commandArray = commandArray[1:]
		}
		var reply ReplyChannel
		reply.Channel = incomingCommand.Channel

		switch commandArray[0] {
		case "help":
			reply.DisplayTitle = "Help!"
			fields := make([]slack.AttachmentField, 0)
			for k, v := range commands {
				fields = append(fields, slack.AttachmentField{
					Title: "<bot> " + k,
					Value: v,
				})
			}
			attachment := &slack.Attachment{
				Pretext:    "TogglBot Command List",
				Color:      "#B733FF",
				Fields:     fields,
				MarkdownIn: []string{"fields"},
			}
			reply.Attachment = attachment
			replyChannel <- reply
		case "register":
			togglApiKey := commandArray[1]
			err := pingTogglApi(togglApiKey)
			if err != nil {
				reply.DisplayTitle = "Failed to register. Bad api key?"
			} else {
				userMap[incomingCommand.Event.User] = togglApiKey
				err := Save(file, userMap)
				Check(err)
				reply.DisplayTitle = "Successfully registered!"
			}
			replyChannel <- reply
		}

		if reply.DisplayTitle != "" {
			continue
		}
		togglApiKey, ok := userMap[incomingCommand.Event.User]
		if !ok {
			reply.DisplayTitle = "You have not registered with togglbot yet. Try @togglbot register API_KEY_HERE"
			replyChannel <- reply
			continue
		}

		switch commandArray[0] {
		case "start":
			if len(commandArray) <= 2 {
				reply.DisplayTitle = "Please provide a project name and description! `@togglbot start PROJECT_NAME DESCRIPTION`"
				replyChannel <- reply
				break
			}
			project := commandArray[1]
			description := strings.Join(commandArray[2:], " ")
			pid := getProjectWithName(togglApiKey, project)
			fmt.Printf("%v", pid)
			startTimer(togglApiKey, description, pid)
			reply.DisplayTitle = "Timer started! *get back to work peon*"
			replyChannel <- reply
		case "stop":
			te, err := stopTimer(togglApiKey)
			if err != nil {
				reply.DisplayTitle = "couldn't stop timer: " + err.Error()
				replyChannel <- reply
				break
			}
			dur, err := time.ParseDuration(fmt.Sprintf("%vs", te.Duration))
			if err != nil {
				log.Fatal("Unparseable duration! %v", te.Duration)
			}
			reply.DisplayTitle = fmt.Sprintf("Timer Stopped. Worked for %v.", dur.String())
			replyChannel <- reply
		case "track":
			if len(commandArray) < 3 {
				reply.DisplayTitle = "Sorry, I don't have enough information to make an event for you. try `@togglbot track PROJECT_NAME 9:00AM-5:00PM YYYY/MM/DD TASK_DESCRIPTION`"
				replyChannel <- reply
				break
			}
			projectName := commandArray[1]
			pid := getProjectWithName(togglApiKey, projectName)
			timeRange := commandArray[2]

			description := "no description provided"
			var entryDate time.Time;
			var dateError error;

			if len(commandArray) > 3 {
				entryDate, dateError = parseDate(commandArray[3])

				if dateError != nil {
					description = strings.Join(commandArray[3:], " ")
				} else {
					if len(commandArray) > 4 {
						description = strings.Join(commandArray[4:], " ")
					}
				}
			}

			startTime, duration, err := parseTimeRange(timeRange, entryDate)
			if err != nil {
				reply.DisplayTitle = err.Error()
				replyChannel <- reply
				break
			}
			createTimeEntry(togglApiKey, description, *startTime, *duration, pid, 0)
			reply.DisplayTitle = "Time entry created!"
			replyChannel <- reply
		default:
			reply.DisplayTitle = "Sorry, I don't understand that command. Try `@Togglbot help`"
			replyChannel <- reply
		}
	}
}

func parseDate(readingDate string) (time.Time, error) {
	parsedDate, err := time.Parse("2006/1/2", readingDate)
	if err != nil {
		return time.Now(), err
	}

	/* No error is returned if malformed date because if parsedDate == nil it will
	be assumed to be a description thus error will never be shown */

	return parsedDate, nil
}

func parseTimeRange(timeRange string, parsedDate time.Time) (*time.Time, *time.Duration, error) {
	timeRange = strings.ToUpper(timeRange)
	fields := strings.Split(timeRange, "-")
	if len(fields) != 2 {
		return nil, nil, errors.New("Your date range is incorrectly formatted! Try something like: 9:00AM-5:00PM")
	}

	startTime, err := time.Parse(time.Kitchen, fields[0])
	if err != nil {
		return nil, nil, errors.New("Your date range is incorrectly formatted! Try something like: 9:00AM-5:00PM")
	}

	endTime, err := time.Parse(time.Kitchen, fields[1])
	if err != nil {
		return nil, nil, errors.New("Your date range is incorrectly formatted! Try something like: 9:00AM-5:00PM")
	}

	duration := endTime.Sub(startTime)

	// Kitchen time has only hours and PM/AM. Drop in today's date.
	fmt.Println("DATE:\t", parsedDate)
	startTime = time.Date(parsedDate.Year(), parsedDate.Month(), parsedDate.Day(), startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(), parsedDate.Location())
	fmt.Println("START TIME:\t", startTime)
	return &startTime, &duration, nil
}

func handleBotReplies() {
	for {
		reply := <-botReplyChannel
		params := slack.PostMessageParameters{}
		params.AsUser = true
		if reply.Attachment != nil {
			params.Attachments = []slack.Attachment{*reply.Attachment}
		}
		_, _, err := api.PostMessage(reply.Channel, reply.DisplayTitle, params)
		if err != nil {
			fmt.Println("FATAL SHIT")
			log.Fatal(err)
		}
	}
}

func main() {

	if len(os.Args) != 2 {
		fmt.Printf("usage: togglbot slack-bot-token")
		os.Exit(1)
	}

	//First attempt to deserialize a saved registration file, otherwise
	//create new
	err := Load(file, &userMap)
	if err != nil {
		fmt.Println("ERROR WAS DETECTED")
		log.Fatal(err)
		userMap = make(map[string]string)
	}

	sessionMap = make(map[string]toggl.TimeEntry)
	token := os.Args[1]
	api = slack.New(token)
	rtm := api.NewRTM()
	botCommandChannel = make(chan *BotCommand)
	botReplyChannel = make(chan ReplyChannel)
	go rtm.ManageConnection()
	go handleBotCommands(botReplyChannel)
	go handleBotReplies()
	go fmt.Println("TogglBot ready, ^C exits")
Loop:
	for {
		select {
		case msg := <-rtm.IncomingEvents:
			switch event := msg.Data.(type) {
			case *slack.ConnectedEvent:
				botId = event.Info.User.ID
			case *slack.MessageEvent:

				botCommand := &BotCommand{
					Channel: event.Channel,
					Event:   event,
					UserId:  event.User,
				}

				if isValidMessageEvent(event) {
					fmt.Println("Received event: ", event)
					botCommandChannel <- botCommand
				}
			case *slack.RTMError:
				fmt.Printf("ERROR: %s\n", event.Error())
			case *slack.InvalidAuthEvent:
				fmt.Printf("Invalid credentials")
				break Loop
			}
		}
	}

}

func isValidMessageEvent(event *slack.MessageEvent) bool {
	if event.Type != "message" {
		return false
	}
	if event.User == botId {
		return false
	}
	if strings.HasPrefix(event.Text, "<@"+botId+">") {
		return true
	}
	if strings.HasPrefix(event.Channel, "D") {
		return true
	}
	return false
}
<|endoftext|>


<commit_before>package leakybucket

import (
	"fmt"
	"math"
	"sync"
	"testing"
	"time"
)

// CreateTest returns a test of bucket creation for a given storage backend.
// It is meant to be used by leakybucket implementers who wish to test this.
func CreateTest(s Storage) func(*testing.T) {
	return func(t *testing.T) {
		now := time.Now()
		bucket, err := s.Create("testbucket", 100, time.Minute)
		if err != nil {
			t.Fatal(err)
		}
		if capacity := bucket.Capacity(); capacity != 100 {
			t.Fatalf("expected capacity of %d, got %d", 100, capacity)
		}
		e := float64(1 * time.Second) // margin of error
		if error := float64(bucket.Reset().Sub(now.Add(time.Minute))); math.Abs(error) > e {
			t.Fatalf("expected reset time close to %s, got %s", now.Add(time.Minute),
				bucket.Reset())
		}
	}
}

// AddTest returns a test that adding to a single bucket works.
// It is meant to be used by leakybucket implementers who wish to test this.
func AddTest(s Storage) func(*testing.T) {
	return func(t *testing.T) {
		bucket, err := s.Create("testbucket", 10, time.Minute)
		if err != nil {
			t.Fatal(err)
		}

		if state, err := bucket.Add(1); err != nil {
			t.Fatal(err)
		} else if bucket.Remaining() != state.Remaining {
			t.Fatalf("expected bucket and state remaining to match, bucket is %d, state is %d",
				bucket.Remaining(), state.Remaining)
		} else if state.Remaining != 9 {
			t.Fatalf("expected 9 remaining, got %d", state.Remaining)
		}

		if state, err := bucket.Add(3); err != nil {
			t.Fatal(err)
		} else if bucket.Remaining() != state.Remaining {
			t.Fatalf("expected bucket and state remaining to match, bucket is %d, state is %d",
				bucket.Remaining(), state.Remaining)
		} else if state.Remaining != 6 {
			t.Fatalf("expected 6 remaining, got %d", state.Remaining)
		}

		if state, err := bucket.Add(6); err != nil {
			t.Fatal(err)
		} else if bucket.Remaining() != state.Remaining {
			t.Fatalf("expected bucket and state remaining to match, bucket is %d, state is %d",
				bucket.Remaining(), state.Remaining)
		} else if state.Remaining != 0 {
			t.Fatalf("expected 0 remaining, got %d", state.Remaining)
		}

		if _, err := bucket.Add(1); err == nil {
			t.Fatalf("expected ErrorFull, received no error")
		} else if fmt.Sprint(err) != "add exceeds free capacity" {
			t.Fatalf("expected ErrorFull, received %v", err)
		}
	}
}

// ThreadSafeAddTest returns a test that adding to a single bucket is thread-safe.
// It is meant to be used by leakybucket implementers who wish to test this.
func ThreadSafeAddTest(s Storage) func(*testing.T) {
	return func(t *testing.T) {
		// Make a bucket of size `n`. Spawn `n+1` goroutines that each try to take one token.
		// We should see the bucket transition through having `n-1`, `n-2`, ... 0 remaining capacity.
		// We should also witness one error when the bucket has reached capacity.
		n := 100
		bucket, err := s.Create("testbucket", uint(n), time.Minute)
		if err != nil {
			t.Fatal(err)
		}
		remaining := map[uint]bool{}     // record observed "remaining" counts. (ab)using map as set here
		remainingMutex := sync.RWMutex{} // maps are not threadsafe
		errors := []error{}              // record observed errors
		var wg sync.WaitGroup
		for i := 0; i < n+1; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
				state, err := bucket.Add(1)
				if err != nil {
					errors = append(errors, err)
				} else {
					remainingMutex.Lock()
					defer remainingMutex.Unlock()
					remaining[state.Remaining] = true
				}
			}()
		}
		wg.Wait()
		if len(remaining) != n {
			t.Fatalf("Did not observe correct bucket states: %#v, %#v", remaining, errors)
		}
		if len(errors) != 1 && errors[0] != ErrorFull {
			t.Fatalf("Did not observe one full error: %#v", errors)
		}
	}
}
<commit_msg>test: AddTest: better error comparison<commit_after>package leakybucket

import (
	"math"
	"sync"
	"testing"
	"time"
)

// CreateTest returns a test of bucket creation for a given storage backend.
// It is meant to be used by leakybucket implementers who wish to test this.
func CreateTest(s Storage) func(*testing.T) {
	return func(t *testing.T) {
		now := time.Now()
		bucket, err := s.Create("testbucket", 100, time.Minute)
		if err != nil {
			t.Fatal(err)
		}
		if capacity := bucket.Capacity(); capacity != 100 {
			t.Fatalf("expected capacity of %d, got %d", 100, capacity)
		}
		e := float64(1 * time.Second) // margin of error
		if error := float64(bucket.Reset().Sub(now.Add(time.Minute))); math.Abs(error) > e {
			t.Fatalf("expected reset time close to %s, got %s", now.Add(time.Minute),
				bucket.Reset())
		}
	}
}

// AddTest returns a test that adding to a single bucket works.
// It is meant to be used by leakybucket implementers who wish to test this.
func AddTest(s Storage) func(*testing.T) {
	return func(t *testing.T) {
		bucket, err := s.Create("testbucket", 10, time.Minute)
		if err != nil {
			t.Fatal(err)
		}

		if state, err := bucket.Add(1); err != nil {
			t.Fatal(err)
		} else if bucket.Remaining() != state.Remaining {
			t.Fatalf("expected bucket and state remaining to match, bucket is %d, state is %d",
				bucket.Remaining(), state.Remaining)
		} else if state.Remaining != 9 {
			t.Fatalf("expected 9 remaining, got %d", state.Remaining)
		}

		if state, err := bucket.Add(3); err != nil {
			t.Fatal(err)
		} else if bucket.Remaining() != state.Remaining {
			t.Fatalf("expected bucket and state remaining to match, bucket is %d, state is %d",
				bucket.Remaining(), state.Remaining)
		} else if state.Remaining != 6 {
			t.Fatalf("expected 6 remaining, got %d", state.Remaining)
		}

		if state, err := bucket.Add(6); err != nil {
			t.Fatal(err)
		} else if bucket.Remaining() != state.Remaining {
			t.Fatalf("expected bucket and state remaining to match, bucket is %d, state is %d",
				bucket.Remaining(), state.Remaining)
		} else if state.Remaining != 0 {
			t.Fatalf("expected 0 remaining, got %d", state.Remaining)
		}

		if _, err := bucket.Add(1); err == nil {
			t.Fatalf("expected ErrorFull, received no error")
		} else if err != ErrorFull {
			t.Fatalf("expected ErrorFull, received %v", err)
		}
	}
}

// ThreadSafeAddTest returns a test that adding to a single bucket is thread-safe.
// It is meant to be used by leakybucket implementers who wish to test this.
func ThreadSafeAddTest(s Storage) func(*testing.T) {
	return func(t *testing.T) {
		// Make a bucket of size `n`. Spawn `n+1` goroutines that each try to take one token.
		// We should see the bucket transition through having `n-1`, `n-2`, ... 0 remaining capacity.
		// We should also witness one error when the bucket has reached capacity.
		n := 100
		bucket, err := s.Create("testbucket", uint(n), time.Minute)
		if err != nil {
			t.Fatal(err)
		}
		remaining := map[uint]bool{}     // record observed "remaining" counts. (ab)using map as set here
		remainingMutex := sync.RWMutex{} // maps are not threadsafe
		errors := []error{}              // record observed errors
		var wg sync.WaitGroup
		for i := 0; i < n+1; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
				state, err := bucket.Add(1)
				if err != nil {
					errors = append(errors, err)
				} else {
					remainingMutex.Lock()
					defer remainingMutex.Unlock()
					remaining[state.Remaining] = true
				}
			}()
		}
		wg.Wait()
		if len(remaining) != n {
			t.Fatalf("Did not observe correct bucket states: %#v, %#v", remaining, errors)
		}
		if len(errors) != 1 && errors[0] != ErrorFull {
			t.Fatalf("Did not observe one full error: %#v", errors)
		}
	}
}
<|endoftext|>


<commit_before>package collection

import (
	"encoding/json"
	"errors"
	"fmt"
	"github.com/kuzzleio/sdk-go/types"
	"strconv"
)

type IDocument interface {
	Save()
	Refresh()
	SetContent()
	Publish()
	Exists()
	SetHeaders()
	Delete()
}

type Document struct {
	Id         string            `json:"_id"`
	Meta       *types.KuzzleMeta `json:"_meta"`
	Content    json.RawMessage   `json:"_source"`
	Version    int               `json:"_version"`
	Collection string            `json:"collection"`
	collection *Collection       `json:"-"`
}

type DocumentContent map[string]interface{}

func (documentContent DocumentContent) ToString() string {
	s, _ := json.Marshal(documentContent)

	return string(s)
}

func (d Document) SourceToMap() DocumentContent {
	sourceMap := DocumentContent{}

	json.Unmarshal(d.Content, &sourceMap)

	return sourceMap
}

// Helper function to initialize a document into Document using fetch query.
func (d *Document) Fetch(id string) (*Document, error) {
	if id == "" {
		return d, errors.New("Document.Fetch: missing document id")
	}

	doc, err := d.collection.FetchDocument(id, nil)

	if err != nil {
		return d, errors.New("Document.Fetch: an error occurred: " + fmt.Sprint(err))
	}

	d.Id = id
	d.Meta = doc.Meta
	d.Content = doc.Content
	d.Version = doc.Version
	d.Collection = doc.Collection
	d.collection = doc.collection

	return d, nil
}

// Subscribe listens to events concerning this document. Has no effect if the document does not have an ID
// (i.e. if the document has not yet been created as a persisted document).
func (d Document) Subscribe(options types.RoomOptions, ch chan<- *types.KuzzleNotification) chan *types.SubscribeResponse {
	if d.Id == "" {
		errorResponse := make(chan *types.SubscribeResponse, 1)
		errorResponse <- &types.SubscribeResponse{Error: errors.New("Document.Subscribe: cannot subscribe to a document if no ID has been provided")}

		return errorResponse
	}

	filters := map[string]map[string][]string{
		"ids": {
			"values": []string{d.Id},
		},
	}

	return d.collection.Subscribe(filters, options, ch)
}

/*
  Saves the document into Kuzzle.

  If this is a new document, will create it in Kuzzle and the id property will be made available.
  Otherwise, will replace the latest version of the document in Kuzzle by the current content of this object.
*/
func (d *Document) Save(options types.QueryOptions) (*Document, error) {
	if d.Id == "" {
		return d, errors.New("Document.Save: missing document id")
	}

	ch := make(chan *types.KuzzleResponse)

	query := &types.KuzzleRequest{
		Index:      d.collection.index,
		Collection: d.collection.collection,
		Controller: "document",
		Action:     "createOrReplace",
		Id:         d.Id,
		Body:       d.Content,
	}

	go d.collection.Kuzzle.Query(query, options, ch)

	res := <-ch

	if res.Error != nil {
		return d, errors.New(res.Error.Message)
	}

	return d, nil
}

/*
  Replaces the document with the latest version stored in Kuzzle.
*/
func (d *Document) Refresh(options types.QueryOptions) (*Document, error) {
	if d.Id == "" {
		return d, errors.New("Document.Refresh: missing document id")
	}

	ch := make(chan *types.KuzzleResponse)

	query := &types.KuzzleRequest{
		Index:      d.collection.index,
		Collection: d.collection.collection,
		Controller: "document",
		Action:     "get",
		Id:         d.Id,
	}

	go d.collection.Kuzzle.Query(query, options, ch)

	res := <-ch
	if res.Error != nil {
		return d, errors.New(res.Error.Message)
	}

	json.Unmarshal(res.Result, d)

	return d, nil
}

/*
  Sets the document id.
*/
func (d *Document) SetDocumentId(id string) *Document {
	if id != "" {
		d.Id = id
	}

	return d
}

/*
  Replaces the current document content with provided data.
  Changes made by this function won’t be applied until the save method is called.
  If replace is set to true, the entire content will be replaced, otherwise, only existing and new fields will be impacted.
*/
func (d *Document) SetContent(content DocumentContent, replace bool) *Document {
	if replace {
		d.Content, _ = json.Marshal(content)
	} else {
		source := DocumentContent{}
		json.Unmarshal(d.Content, &source)

		for attr, value := range content {
			source[attr] = value
		}

		d.Content, _ = json.Marshal(source)
	}

	return d
}

/*
  Helper function allowing to set headers while chaining calls.

  If the replace argument is set to true, replaces the current headers with the provided ones.
  Otherwise, appends the content to the current headers, only replacing already existing values.
*/
func (d Document) SetHeaders(content map[string]interface{}, replace bool) {
	d.collection.Kuzzle.SetHeaders(content, replace)
}

/*
  Sends the content of the document as a realtime message.
*/
func (d Document) Publish(options types.QueryOptions) (bool, error) {
	ch := make(chan *types.KuzzleResponse)

	type message struct {
		Id      string            `json:"_id,omitempty"`
		Version int               `json:"_version,omitempty"`
		Body    json.RawMessage   `json:"body"`
		Meta    *types.KuzzleMeta `json:"meta"`
	}

	query := &types.KuzzleRequest{
		Index:      d.collection.index,
		Collection: d.collection.collection,
		Controller: "realtime",
		Action:     "publish",
		Body: message{
			Id:      d.Id,
			Version: d.Version,
			Body:    d.Content,
			Meta:    d.Meta,
		},
	}

	go d.collection.Kuzzle.Query(query, options, ch)

	res := <-ch

	if res.Error != nil {
		return false, errors.New(res.Error.Message)
	}

	response := types.RealtimeResponse{}

	json.Unmarshal(res.Result, &response)

	return response.Published, nil
}

/*
  Checks if the document exists in Kuzzle.
*/
func (d Document) Exists(options types.QueryOptions) (bool, error) {
	if d.Id == "" {
		return false, errors.New("Document.Exists: missing document id")
	}

	ch := make(chan *types.KuzzleResponse)

	query := &types.KuzzleRequest{
		Index:      d.collection.index,
		Collection: d.collection.collection,
		Controller: "document",
		Action:     "exists",
		Id:         d.Id,
	}

	go d.collection.Kuzzle.Query(query, options, ch)

	res := <-ch

	if res.Error != nil {
		return false, errors.New(res.Error.Message)
	}

	exists, _ := strconv.ParseBool(string(res.Result))

	return exists, nil
}

/*
  Deletes the document in Kuzzle.
*/
func (d Document) Delete(options types.QueryOptions) (string, error) {
	if d.Id == "" {
		return "", errors.New("Document.Delete: missing document id")
	}

	ch := make(chan *types.KuzzleResponse)

	query := &types.KuzzleRequest{
		Index:      d.collection.index,
		Collection: d.collection.collection,
		Controller: "document",
		Action:     "delete",
		Id:         d.Id,
	}

	go d.collection.Kuzzle.Query(query, options, ch)

	res := <-ch

	if res.Error != nil {
		return "", errors.New(res.Error.Message)
	}

	document := Document{collection: d.collection}
	json.Unmarshal(res.Result, &document)

	return document.Id, nil
}
<commit_msg>Add missing fields in document struct<commit_after>package collection

import (
	"encoding/json"
	"errors"
	"fmt"
	"github.com/kuzzleio/sdk-go/types"
	"strconv"
)

type IDocument interface {
	Save()
	Refresh()
	SetContent()
	Publish()
	Exists()
	SetHeaders()
	Delete()
}

type Document struct {
	Id         string            `json:"_id"`
	Index      string 					 `json:"_index"`
	Meta       *types.KuzzleMeta `json:"_meta"`
	Shards     map[string]int    `json:"_shards"`
	Content    json.RawMessage   `json:"_source"`
	Version    int               `json:"_version"`
	Result     string            `json:"result"`
	Created    bool 						 `json:"created"`
	Collection string            `json:"_collection"`
	collection *Collection       `json:"-"`
}

type DocumentContent map[string]interface{}

func (documentContent DocumentContent) ToString() string {
	s, _ := json.Marshal(documentContent)

	return string(s)
}

func (d Document) SourceToMap() DocumentContent {
	sourceMap := DocumentContent{}

	json.Unmarshal(d.Content, &sourceMap)

	return sourceMap
}

// Helper function to initialize a document into Document using fetch query.
func (d *Document) Fetch(id string) (*Document, error) {
	if id == "" {
		return d, errors.New("Document.Fetch: missing document id")
	}

	doc, err := d.collection.FetchDocument(id, nil)

	if err != nil {
		return d, errors.New("Document.Fetch: an error occurred: " + fmt.Sprint(err))
	}

	d.Id = id
	d.Index = doc.Index
	d.Meta = doc.Meta
	d.Shards = doc.Shards
	d.Content = doc.Content
	d.Version = doc.Version
	d.Result = doc.Result
	d.Created = doc.Created
	d.Collection = doc.Collection
	d.collection = doc.collection

	return d, nil
}

// Subscribe listens to events concerning this document. Has no effect if the document does not have an ID
// (i.e. if the document has not yet been created as a persisted document).
func (d Document) Subscribe(options types.RoomOptions, ch chan<- *types.KuzzleNotification) chan *types.SubscribeResponse {
	if d.Id == "" {
		errorResponse := make(chan *types.SubscribeResponse, 1)
		errorResponse <- &types.SubscribeResponse{Error: errors.New("Document.Subscribe: cannot subscribe to a document if no ID has been provided")}

		return errorResponse
	}

	filters := map[string]map[string][]string{
		"ids": {
			"values": []string{d.Id},
		},
	}

	return d.collection.Subscribe(filters, options, ch)
}

/*
  Saves the document into Kuzzle.

  If this is a new document, will create it in Kuzzle and the id property will be made available.
  Otherwise, will replace the latest version of the document in Kuzzle by the current content of this object.
*/
func (d *Document) Save(options types.QueryOptions) (*Document, error) {
	if d.Id == "" {
		return d, errors.New("Document.Save: missing document id")
	}

	ch := make(chan *types.KuzzleResponse)

	query := &types.KuzzleRequest{
		Index:      d.collection.index,
		Collection: d.collection.collection,
		Controller: "document",
		Action:     "createOrReplace",
		Id:         d.Id,
		Body:       d.Content,
	}

	go d.collection.Kuzzle.Query(query, options, ch)

	res := <-ch

	if res.Error != nil {
		return d, errors.New(res.Error.Message)
	}

	return d, nil
}

/*
  Replaces the document with the latest version stored in Kuzzle.
*/
func (d *Document) Refresh(options types.QueryOptions) (*Document, error) {
	if d.Id == "" {
		return d, errors.New("Document.Refresh: missing document id")
	}

	ch := make(chan *types.KuzzleResponse)

	query := &types.KuzzleRequest{
		Index:      d.collection.index,
		Collection: d.collection.collection,
		Controller: "document",
		Action:     "get",
		Id:         d.Id,
	}

	go d.collection.Kuzzle.Query(query, options, ch)

	res := <-ch
	if res.Error != nil {
		return d, errors.New(res.Error.Message)
	}

	json.Unmarshal(res.Result, d)

	return d, nil
}

/*
  Sets the document id.
*/
func (d *Document) SetDocumentId(id string) *Document {
	if id != "" {
		d.Id = id
	}

	return d
}

/*
  Replaces the current document content with provided data.
  Changes made by this function won’t be applied until the save method is called.
  If replace is set to true, the entire content will be replaced, otherwise, only existing and new fields will be impacted.
*/
func (d *Document) SetContent(content DocumentContent, replace bool) *Document {
	if replace {
		d.Content, _ = json.Marshal(content)
	} else {
		source := DocumentContent{}
		json.Unmarshal(d.Content, &source)

		for attr, value := range content {
			source[attr] = value
		}

		d.Content, _ = json.Marshal(source)
	}

	return d
}

/*
  Helper function allowing to set headers while chaining calls.

  If the replace argument is set to true, replaces the current headers with the provided ones.
  Otherwise, appends the content to the current headers, only replacing already existing values.
*/
func (d Document) SetHeaders(content map[string]interface{}, replace bool) {
	d.collection.Kuzzle.SetHeaders(content, replace)
}

/*
  Sends the content of the document as a realtime message.
*/
func (d Document) Publish(options types.QueryOptions) (bool, error) {
	ch := make(chan *types.KuzzleResponse)

	type message struct {
		Id      string            `json:"_id,omitempty"`
		Version int               `json:"_version,omitempty"`
		Body    json.RawMessage   `json:"body"`
		Meta    *types.KuzzleMeta `json:"meta"`
	}

	query := &types.KuzzleRequest{
		Index:      d.collection.index,
		Collection: d.collection.collection,
		Controller: "realtime",
		Action:     "publish",
		Body: message{
			Id:      d.Id,
			Version: d.Version,
			Body:    d.Content,
			Meta:    d.Meta,
		},
	}

	go d.collection.Kuzzle.Query(query, options, ch)

	res := <-ch

	if res.Error != nil {
		return false, errors.New(res.Error.Message)
	}

	response := types.RealtimeResponse{}

	json.Unmarshal(res.Result, &response)

	return response.Published, nil
}

/*
  Checks if the document exists in Kuzzle.
*/
func (d Document) Exists(options types.QueryOptions) (bool, error) {
	if d.Id == "" {
		return false, errors.New("Document.Exists: missing document id")
	}

	ch := make(chan *types.KuzzleResponse)

	query := &types.KuzzleRequest{
		Index:      d.collection.index,
		Collection: d.collection.collection,
		Controller: "document",
		Action:     "exists",
		Id:         d.Id,
	}

	go d.collection.Kuzzle.Query(query, options, ch)

	res := <-ch

	if res.Error != nil {
		return false, errors.New(res.Error.Message)
	}

	exists, _ := strconv.ParseBool(string(res.Result))

	return exists, nil
}

/*
  Deletes the document in Kuzzle.
*/
func (d Document) Delete(options types.QueryOptions) (string, error) {
	if d.Id == "" {
		return "", errors.New("Document.Delete: missing document id")
	}

	ch := make(chan *types.KuzzleResponse)

	query := &types.KuzzleRequest{
		Index:      d.collection.index,
		Collection: d.collection.collection,
		Controller: "document",
		Action:     "delete",
		Id:         d.Id,
	}

	go d.collection.Kuzzle.Query(query, options, ch)

	res := <-ch

	if res.Error != nil {
		return "", errors.New(res.Error.Message)
	}

	document := Document{collection: d.collection}
	json.Unmarshal(res.Result, &document)

	return document.Id, nil
}
<|endoftext|>


<commit_before>// Copyright 2016 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package collector

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/common/log"
)

// constants from https://github.com/zfsonlinux/zfs/blob/master/lib/libspl/include/sys/kstat.h
// kept as strings for comparison thus avoiding conversion to int
const (
	KSTAT_DATA_CHAR   = "0"
	KSTAT_DATA_INT32  = "1"
	KSTAT_DATA_UINT32 = "2"
	KSTAT_DATA_INT64  = "3"
	KSTAT_DATA_UINT64 = "4"
	KSTAT_DATA_LONG   = "5"
	KSTAT_DATA_ULONG  = "6"
	KSTAT_DATA_STRING = "7"
)

func (c *zfsCollector) openProcFile(path string) (*os.File, error) {
	file, err := os.Open(procFilePath(path))
	if err != nil {
		// file not found error can occur if:
		// 1. zfs module is not loaded
		// 2. zfs version does not have the feature with metrics -- ok to ignore
		log.Debugf("Cannot open %q for reading", procFilePath(path))
		return nil, errZFSNotAvailable
	}
	return file, nil
}

func (c *zfsCollector) updateZfsStats(subsystem string, ch chan<- prometheus.Metric) error {
	file, err := c.openProcFile(filepath.Join(c.linuxProcpathBase, c.linuxPathMap[subsystem]))
	if err != nil {
		return err
	}
	defer file.Close()

	return c.parseProcfsFile(file, c.linuxPathMap[subsystem], func(s zfsSysctl, v uint64) {
		ch <- c.constSysctlMetric(subsystem, s, v)
	})
}

func (c *zfsCollector) updatePoolStats(ch chan<- prometheus.Metric) error {
	zpoolPaths, err := filepath.Glob(procFilePath(filepath.Join(c.linuxProcpathBase, c.linuxZpoolIoPath)))
	if err != nil {
		return err
	}

	if zpoolPaths == nil {
		return nil
	}

	for _, zpoolPath := range zpoolPaths {
		file, err := os.Open(zpoolPath)
		if err != nil {
			// this file should exist, but there is a race where an exporting pool can remove the files -- ok to ignore
			log.Debugf("Cannot open %q for reading", zpoolPath)
			return errZFSNotAvailable
		}

		err = c.parsePoolProcfsFile(file, zpoolPath, func(poolName string, s zfsSysctl, v uint64) {
			ch <- c.constPoolMetric(poolName, s, v)
		})
		file.Close()
		if err != nil {
			return err
		}
	}

	return nil
}

func (c *zfsCollector) parseProcfsFile(reader io.Reader, fmtExt string, handler func(zfsSysctl, uint64)) error {
	scanner := bufio.NewScanner(reader)

	parseLine := false
	for scanner.Scan() {
		parts := strings.Fields(scanner.Text())

		if !parseLine && len(parts) == 3 && parts[0] == "name" && parts[1] == "type" && parts[2] == "data" {
			// Start parsing from here.
			parseLine = true
			continue
		}

		if !parseLine || len(parts) < 3 {
			continue
		}

		// kstat data type (column 2) should be KSTAT_DATA_UINT64, otherwise ignore
		// TODO: when other KSTAT_DATA_* types arrive, much of this will need to be restructured
		if parts[1] == KSTAT_DATA_UINT64 {
			key := fmt.Sprintf("kstat.zfs.misc.%s.%s", fmtExt, parts[0])
			value, err := strconv.ParseUint(parts[2], 10, 64)
			if err != nil {
				return fmt.Errorf("could not parse expected integer value for %q", key)
			}
			handler(zfsSysctl(key), value)
		}
	}
	if !parseLine {
		return fmt.Errorf("did not parse a single %q metric", fmtExt)
	}

	return scanner.Err()
}

func (c *zfsCollector) parsePoolProcfsFile(reader io.Reader, zpoolPath string, handler func(string, zfsSysctl, uint64)) error {
	scanner := bufio.NewScanner(reader)

	parseLine := false
	var fields []string
	for scanner.Scan() {
		line := strings.Fields(scanner.Text())

		if !parseLine && len(line) >= 12 && line[0] == "nread" {
			//Start parsing from here.
			parseLine = true
			fields = make([]string, len(line))
			copy(fields, line)
			continue
		}
		if !parseLine {
			continue
		}

		zpoolPathElements := strings.Split(zpoolPath, "/")
		pathLen := len(zpoolPathElements)
		if pathLen < 2 {
			return fmt.Errorf("zpool path did not return at least two elements")
		}
		zpoolName := zpoolPathElements[pathLen-2]
		zpoolFile := zpoolPathElements[pathLen-1]

		for i, field := range fields {
			key := fmt.Sprintf("kstat.zfs.misc.%s.%s", zpoolFile, field)

			value, err := strconv.ParseUint(line[i], 10, 64)
			if err != nil {
				return fmt.Errorf("could not parse expected integer value for %q: %v", key, err)
			}
			handler(zpoolName, zfsSysctl(key), value)
		}
	}

	return scanner.Err()
}
<commit_msg>collector: fix golint problems in ZFS collector (#1132)<commit_after>// Copyright 2016 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package collector

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/common/log"
)

// constants from https://github.com/zfsonlinux/zfs/blob/master/lib/libspl/include/sys/kstat.h
// kept as strings for comparison thus avoiding conversion to int
const (
	kstatDataChar   = "0"
	kstatDataInt32  = "1"
	kstatDataUint32 = "2"
	kstatDataInt64  = "3"
	kstatDataUint64 = "4"
	kstatDataLong   = "5"
	kstatDataUlong  = "6"
	kstatDataString = "7"
)

func (c *zfsCollector) openProcFile(path string) (*os.File, error) {
	file, err := os.Open(procFilePath(path))
	if err != nil {
		// file not found error can occur if:
		// 1. zfs module is not loaded
		// 2. zfs version does not have the feature with metrics -- ok to ignore
		log.Debugf("Cannot open %q for reading", procFilePath(path))
		return nil, errZFSNotAvailable
	}
	return file, nil
}

func (c *zfsCollector) updateZfsStats(subsystem string, ch chan<- prometheus.Metric) error {
	file, err := c.openProcFile(filepath.Join(c.linuxProcpathBase, c.linuxPathMap[subsystem]))
	if err != nil {
		return err
	}
	defer file.Close()

	return c.parseProcfsFile(file, c.linuxPathMap[subsystem], func(s zfsSysctl, v uint64) {
		ch <- c.constSysctlMetric(subsystem, s, v)
	})
}

func (c *zfsCollector) updatePoolStats(ch chan<- prometheus.Metric) error {
	zpoolPaths, err := filepath.Glob(procFilePath(filepath.Join(c.linuxProcpathBase, c.linuxZpoolIoPath)))
	if err != nil {
		return err
	}

	if zpoolPaths == nil {
		return nil
	}

	for _, zpoolPath := range zpoolPaths {
		file, err := os.Open(zpoolPath)
		if err != nil {
			// this file should exist, but there is a race where an exporting pool can remove the files -- ok to ignore
			log.Debugf("Cannot open %q for reading", zpoolPath)
			return errZFSNotAvailable
		}

		err = c.parsePoolProcfsFile(file, zpoolPath, func(poolName string, s zfsSysctl, v uint64) {
			ch <- c.constPoolMetric(poolName, s, v)
		})
		file.Close()
		if err != nil {
			return err
		}
	}

	return nil
}

func (c *zfsCollector) parseProcfsFile(reader io.Reader, fmtExt string, handler func(zfsSysctl, uint64)) error {
	scanner := bufio.NewScanner(reader)

	parseLine := false
	for scanner.Scan() {
		parts := strings.Fields(scanner.Text())

		if !parseLine && len(parts) == 3 && parts[0] == "name" && parts[1] == "type" && parts[2] == "data" {
			// Start parsing from here.
			parseLine = true
			continue
		}

		if !parseLine || len(parts) < 3 {
			continue
		}

		// kstat data type (column 2) should be KSTAT_DATA_UINT64, otherwise ignore
		// TODO: when other KSTAT_DATA_* types arrive, much of this will need to be restructured
		if parts[1] == kstatDataUint64 {
			key := fmt.Sprintf("kstat.zfs.misc.%s.%s", fmtExt, parts[0])
			value, err := strconv.ParseUint(parts[2], 10, 64)
			if err != nil {
				return fmt.Errorf("could not parse expected integer value for %q", key)
			}
			handler(zfsSysctl(key), value)
		}
	}
	if !parseLine {
		return fmt.Errorf("did not parse a single %q metric", fmtExt)
	}

	return scanner.Err()
}

func (c *zfsCollector) parsePoolProcfsFile(reader io.Reader, zpoolPath string, handler func(string, zfsSysctl, uint64)) error {
	scanner := bufio.NewScanner(reader)

	parseLine := false
	var fields []string
	for scanner.Scan() {
		line := strings.Fields(scanner.Text())

		if !parseLine && len(line) >= 12 && line[0] == "nread" {
			//Start parsing from here.
			parseLine = true
			fields = make([]string, len(line))
			copy(fields, line)
			continue
		}
		if !parseLine {
			continue
		}

		zpoolPathElements := strings.Split(zpoolPath, "/")
		pathLen := len(zpoolPathElements)
		if pathLen < 2 {
			return fmt.Errorf("zpool path did not return at least two elements")
		}
		zpoolName := zpoolPathElements[pathLen-2]
		zpoolFile := zpoolPathElements[pathLen-1]

		for i, field := range fields {
			key := fmt.Sprintf("kstat.zfs.misc.%s.%s", zpoolFile, field)

			value, err := strconv.ParseUint(line[i], 10, 64)
			if err != nil {
				return fmt.Errorf("could not parse expected integer value for %q: %v", key, err)
			}
			handler(zpoolName, zfsSysctl(key), value)
		}
	}

	return scanner.Err()
}
<|endoftext|>


<commit_before>package collector

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"io"

	"github.com/prometheus/common/log"
)

const zfsArcstatsProcpath = "spl/kstat/zfs/arcstats"

func (p *zfsMetricProvider) PrepareUpdate() (err error) {

	err = p.prepareUpdateArcstats(zfsArcstatsProcpath)
	if err != nil {
		return
	}
	return nil
}

func (p *zfsMetricProvider) handleMiss(s zfsSysctl) (value zfsMetricValue, err error) {
	// all values are fetched in PrepareUpdate().
	return zfsErrorValue, fmt.Errorf("sysctl '%s' found")
}

func (p *zfsMetricProvider) prepareUpdateArcstats(zfsArcstatsProcpath string) (err error) {

	file, err := os.Open(procFilePath(zfsArcstatsProcpath))
	if err != nil {
		log.Debugf("Cannot open ZFS arcstats procfs file for reading. " +
			" Is the kernel module loaded?")
		return zfsNotAvailableError
	}
	defer file.Close()
	return p.parseArcstatsProcfsFile(file)
}

func (p *zfsMetricProvider) parseArcstatsProcfsFile(reader io.Reader) (err error) {

	scanner := bufio.NewScanner(reader)

	parseLine := false
	for scanner.Scan() {
		parts := strings.Fields(scanner.Text())

		if !parseLine && parts[0] == "name" && parts[1] == "type" && parts[2] == "data" {
			// Start parsing from here.
			parseLine = true
			continue
		}

		if !parseLine || len(parts) < 3 {
			continue
		}

		key := fmt.Sprintf("kstat.zfs.misc.arcstats.%s", parts[0])

		value, err := strconv.Atoi(parts[2])
		if err != nil {
			return fmt.Errorf("could not parse expected integer value for '%s'", key)
		}
		log.Debugf("%s = %d", key, value)
		p.values[zfsSysctl(key)] = zfsMetricValue(value)
	}
	if !parseLine {
		return errors.New("did not parse a single arcstat metrics")
	}

	return scanner.Err()
}
<commit_msg>Fix out-of-bounds on empty lines at the end of the procfs file.<commit_after>package collector

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"io"

	"github.com/prometheus/common/log"
)

const zfsArcstatsProcpath = "spl/kstat/zfs/arcstats"

func (p *zfsMetricProvider) PrepareUpdate() (err error) {

	err = p.prepareUpdateArcstats(zfsArcstatsProcpath)
	if err != nil {
		return
	}
	return nil
}

func (p *zfsMetricProvider) handleMiss(s zfsSysctl) (value zfsMetricValue, err error) {
	// all values are fetched in PrepareUpdate().
	return zfsErrorValue, fmt.Errorf("sysctl '%s' found")
}

func (p *zfsMetricProvider) prepareUpdateArcstats(zfsArcstatsProcpath string) (err error) {

	file, err := os.Open(procFilePath(zfsArcstatsProcpath))
	if err != nil {
		log.Debugf("Cannot open ZFS arcstats procfs file for reading. " +
			" Is the kernel module loaded?")
		return zfsNotAvailableError
	}
	defer file.Close()
	return p.parseArcstatsProcfsFile(file)
}

func (p *zfsMetricProvider) parseArcstatsProcfsFile(reader io.Reader) (err error) {

	scanner := bufio.NewScanner(reader)

	parseLine := false
	for scanner.Scan() {
		parts := strings.Fields(scanner.Text())

		if !parseLine && len(parts) == 3 && parts[0] == "name" && parts[1] == "type" && parts[2] == "data" {
			// Start parsing from here.
			parseLine = true
			continue
		}

		if !parseLine || len(parts) < 3 {
			continue
		}

		key := fmt.Sprintf("kstat.zfs.misc.arcstats.%s", parts[0])

		value, err := strconv.Atoi(parts[2])
		if err != nil {
			return fmt.Errorf("could not parse expected integer value for '%s'", key)
		}
		log.Debugf("%s = %d", key, value)
		p.values[zfsSysctl(key)] = zfsMetricValue(value)
	}
	if !parseLine {
		return errors.New("did not parse a single arcstat metrics")
	}

	return scanner.Err()
}
<|endoftext|>