<commit_before>package asana

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/manishrjain/asanawarrior/x"
	"github.com/pkg/errors"
)

var token = flag.String("token", "", "Token provided by Asana.")
var domain = flag.String("domain", "", "Workspace name, generally your domain name in Asana.")
var cache *acache = new(acache)

const (
	prefix = "https://app.asana.com/api/1.0"
	stamp  = "2006-01-02T15:04:05.999Z"
)

func runRequest(method, url string) ([]byte, error) {
RUNLOOP:
	req, err := http.NewRequest(method, url, nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Add("Authorization", "Bearer "+*token)
	client := &http.Client{
		Timeout: 10 * time.Minute,
	}

	resp, err := client.Do(req)
	if err != nil {
		log.Printf("runRequest method: [%v] url: [%v] err: [%v]", method, url, err)
		time.Sleep(5 * time.Second)
		goto RUNLOOP
	}

	defer resp.Body.Close()
	return ioutil.ReadAll(resp.Body)
}

func runGetter(i interface{}, suffix string, fields ...string) error {
	url := fmt.Sprintf("%s/%s?opt_fields=%s", prefix, suffix, strings.Join(fields, ","))
	body, err := runRequest("GET", url)
	if err != nil {
		return errors.Wrap(err, "runGetter")
	}
	if err := json.Unmarshal(body, i); err != nil {
		return errors.Wrap(err, "Unmarshal")
	}
	return nil
}

type Basic struct {
	Id    uint64 `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}
type BasicData struct {
	Data []Basic `json:"data"`
}
type BasicDataOne struct {
	Data Basic `json:"data"`
}

func getVarious(suffix string, opts ...string) ([]Basic, error) {
	var bd BasicData
	if err := runGetter(&bd, suffix, opts...); err != nil {
		return nil, err
	}
	return bd.Data, nil
}

type psec struct {
	Project Basic `json:"project"`
	Section Basic `json:"section"`
}

type task struct {
	Basic
	Assignee    Basic   `json:"assignee"`
	Tags        []Basic `json:"tags"`
	CompletedAt string  `json:"completed_at"`
	ModifiedAt  string  `json:"modified_at"`
	CreatedAt   string  `json:"created_at"`
	Memberships []psec  `json:"memberships"`
}

type tasks struct {
	Data []task `json:"data"`
}

type oneTask struct {
	Data task `json:"data"`
}

func convert(tsk task, proj, section string) (x.WarriorTask, error) {
	e := x.WarriorTask{}

	mts, err := time.Parse(stamp, tsk.ModifiedAt)
	if err != nil {
		return e, errors.Wrap(err, "asana modified at")
	}
	cts, err := time.Parse(stamp, tsk.CreatedAt)
	if err != nil {
		return e, errors.Wrap(err, "asana created at")
	}
	var dts time.Time
	if len(tsk.CompletedAt) > 0 {
		dts, err = time.Parse(stamp, tsk.CompletedAt)
		if err != nil {
			return e, errors.Wrap(err, "asana completed at")
		}
	}

	wt := x.WarriorTask{
		Name:      tsk.Name,
		Project:   proj,
		Xid:       tsk.Id,
		Assignee:  cache.User(tsk.Assignee.Id),
		Modified:  mts,
		Created:   cts,
		Completed: dts,
		Section:   section,
	}
	for _, tag := range tsk.Tags {
		wt.Tags = append(wt.Tags, cache.Tag(tag.Id))
	}
	return wt, nil
}

func getTasks(proj Basic, out chan x.WarriorTask, errc chan error) {
	var sectionName string
	var t tasks
	if err := runGetter(&t, fmt.Sprintf("projects/%d/tasks", proj.Id),
		"assignee,name,tags,completed_at,modified_at,created_at"); err != nil {
		errc <- errors.Wrapf(err, "getTasks for project: %v", proj.Name)
		return
	}

	for _, tsk := range t.Data {
		if len(tsk.Name) == 0 {
			// Don't sync such tasks.
			continue
		}
		if strings.HasSuffix(tsk.Name, ":") {
			sec := Basic{
				Id:   tsk.Id,
				Name: tsk.Name,
			}
			sectionName = cache.AddSection(proj.Id, sec)
			continue
		}

		wt, err := convert(tsk, proj.Name, sectionName)
		if err != nil {
			errc <- errors.Wrapf(err, "convert: getTasks for project: %v", proj.Name)
			return
		}
		out <- wt
	}
	errc <- nil
}

func GetTasks() ([]x.WarriorTask, error) {
	if err := cache.update(); err != nil {
		return nil, errors.Wrap(err, "cache.update")
	}

	out := make(chan x.WarriorTask, 100)
	projects := cache.Projects()
	errc := make(chan error, len(projects))
	for _, proj := range projects {
		go getTasks(proj, out, errc)
	}

	// Asana can send back the same task multiple times, if it's part of multiple projects.
	// So, let's dedup them.
	seen := make(map[uint64]bool)
	wtasks := make([]x.WarriorTask, 0, 100)
	done := make(chan struct{})
	go func() {
		for wt := range out {
			if _, has := seen[wt.Xid]; has {
				// ignore
				continue
			}
			wtasks = append(wtasks, wt)
			seen[wt.Xid] = true
		}
		done <- struct{}{}
	}()

	var rerr error
	for _ = range projects {
		if err := <-errc; err != nil {
			rerr = err
		}
	}
	close(out)
	<-done // Wait for all tasks to be picked up by goroutine.
	return wtasks, rerr
}

// runPost would run a PUT or POST to Asana. No locks should be acquired.
func runPost(method, suffix string, values url.Values) ([]byte, error) {
POSTLOOP:
	url := fmt.Sprintf("%s/%s", prefix, suffix)
	fmt.Println(url, values.Encode())
	req, err := http.NewRequest(method, url, bytes.NewBufferString(values.Encode()))
	if err != nil {
		log.Fatal(errors.Wrap(err, "runPost http.NewRequest"))
	}

	req.Header.Add("Authorization", "Bearer "+*token)
	req.Header.Add("content-type", "application/x-www-form-urlencoded")
	client := &http.Client{
		Timeout: 10 * time.Minute,
	}
	resp, err := client.Do(req)
	if err != nil {
		log.Printf("runPost url: [%v] err: [%v]", url, err)
		time.Sleep(5 * time.Second)
		goto POSTLOOP
	}
	defer resp.Body.Close()

	return ioutil.ReadAll(resp.Body)
}

func toTagIds(tnames []string) []string {
	var tags []string
	for _, t := range tnames {
		tid := cache.TagId(t)
		if tid == 0 {
			tid = cache.CreateTag(t)
			fmt.Printf("New Tag created. ID: %d", tid)
		}
		if tid > 0 {
			tags = append(tags, strconv.FormatUint(tid, 10))
		}
	}
	return tags
}

func removeProject(tid, pid uint64) error {
	v := url.Values{}
	v.Add("project", strconv.FormatUint(pid, 10))
	_, err := runPost("POST", fmt.Sprintf("tasks/%d/removeProject", tid), v)
	return err
}

func updateSection(tid, pid uint64, section string) error {
	v := url.Values{}
	v.Add("project", strconv.FormatUint(pid, 10))

	sid := cache.SectionId(pid, section)
	if sid > 0 {
		v.Add("section", strconv.FormatUint(sid, 10))
	}

	_, err := runPost("POST", fmt.Sprintf("tasks/%d/addProject", tid), v)
	return err
}

func AddNew(wt x.WarriorTask) (x.WarriorTask, error) {
	e := x.WarriorTask{}

	// Ensure that project actually exists before proceeding.
	pid := cache.ProjectId(wt.Project)
	if pid == 0 {
		return e, fmt.Errorf("Project not found: %v", wt.Project)
	}

	v := url.Values{}
	v.Add("workspace", strconv.FormatUint(cache.Workspace(), 10))
	v.Add("name", wt.Name)
	fmt.Println("Assignee", wt.Assignee)
	aid := cache.UserId(wt.Assignee)
	if aid > 0 {
		v.Add("assignee", strconv.FormatUint(aid, 10))
	}

	tags := toTagIds(wt.Tags)
	v.Add("tags", strings.Join(tags, ","))
	resp, err := runPost("POST", "tasks", v)
	if err != nil {
		return e, errors.Wrap(err, "AddNew runPost")
	}
	fmt.Println(string(resp))

	var ot oneTask
	if err := json.Unmarshal(resp, &ot); err != nil {
		return e, errors.Wrap(err, "AddNew unmarshal")
	}
	if ot.Data.Id == 0 {
		return e, fmt.Errorf("Unable to find ID assigned by Asana: %+v", ot.Data)
	}

	// Now set the project and section.
	if err := updateSection(ot.Data.Id, pid, wt.Section); err != nil {
		return e, errors.Wrap(err, "AddNew updateSection")
	}

	// Now retrieve the task back again so we can sync it up with TW.
	return GetOneTask(ot.Data.Id)
}

func diff(t1 []string, t2 []string) []string {
	m := make(map[string]bool)
	for _, s := range t2 {
		m[s] = true
	}

	var result []string
	for _, t := range t1 {
		if has := m[t]; !has {
			result = append(result, t)
		}
	}
	return result
}

func updateOneTag(tagid, taskid, instruction string, errc chan error) {
	v := url.Values{}
	v.Add("tag", tagid)
	suffix := fmt.Sprintf("tasks/%s/%s", taskid, instruction)
	_, err := runPost("POST", suffix, v)
	if err != nil {
		errc <- errors.Wrap(err, "updateTags")
		return
	}
	errc <- nil
}

func updateTags(tw x.WarriorTask, asana x.WarriorTask) error {
	taskid := strconv.FormatUint(tw.Xid, 10)
	add := diff(tw.Tags, asana.Tags)
	rem := diff(asana.Tags, tw.Tags)

	addids := toTagIds(add)
	remids := toTagIds(rem)
	sz := len(addids) + len(remids)

	errc := make(chan error, sz)
	for _, id := range addids {
		go updateOneTag(id, taskid, "addTag", errc)
	}
	for _, id := range remids {
		go updateOneTag(id, taskid, "removeTag", errc)
	}

	var rerr error
	for i := 0; i < sz; i++ {
		if err := <-errc; err != nil {
			rerr = err
		}
	}

	return rerr
}

func UpdateTask(tw x.WarriorTask, asana x.WarriorTask) error {
	v := url.Values{}
	if tw.Name != asana.Name {
		v.Add("name", tw.Name)
	}
	if tw.Assignee != asana.Assignee {
		a := cache.UserId(tw.Assignee)
		if a > 0 {
			v.Add("assignee", strconv.FormatUint(a, 10))
		}
	}
	if !tw.Completed.IsZero() && asana.Completed.IsZero() {
		v.Add("completed", "true")
	}

	if len(v) > 0 {
		resp, err := runPost("PUT", "tasks/"+strconv.FormatUint(tw.Xid, 10), v)
		if err != nil {
			return errors.Wrap(err, "UpdateAsanaTask")
		}
		fmt.Println(string(resp))
	}

	if err := updateTags(tw, asana); err != nil {
		return errors.Wrap(err, "asana.UpdateTask updateTags")
	}

	// Update project or section if changed.
	pid := cache.ProjectId(tw.Project)
	if pid > 0 && (tw.Project != asana.Project || tw.Section != asana.Section) {
		fmt.Printf("Updating project and section: %v %v\n", tw.Project, tw.Section)
		if err := updateSection(tw.Xid, pid, tw.Section); err != nil {
			return errors.Wrap(err, "asana.UpdateTask updateSection")
		}
		if tw.Project == asana.Project {
			return nil
		}
		// Project was changed. So, remove the last one.
		fmt.Printf("Removing from project: %v\n", asana.Project)
		if previd := cache.ProjectId(asana.Project); previd > 0 {
			if err := removeProject(tw.Xid, previd); err != nil {
				return err
			}
		}
	}
	return nil
}

func GetOneTask(taskid uint64) (x.WarriorTask, error) {
	e := x.WarriorTask{}
	var ot oneTask
	if err := runGetter(&ot, "tasks/"+strconv.FormatUint(taskid, 10)); err != nil {
		return e, errors.Wrap(err, "AddNew runGetter")
	}

	if len(ot.Data.Memberships) == 0 {
		return e, fmt.Errorf("Member of no project")
	}
	member := ot.Data.Memberships[0]

	sname := cache.SectionName(member.Project.Id, member.Section.Id)
	return convert(ot.Data, member.Project.Name, sname)
}

func Delete(taskid uint64) error {
	url := fmt.Sprintf("%s/tasks/%d", prefix, taskid)
	_, err := runRequest("DELETE", url)
	return err
}
<commit_msg>Allow task to be created in Asana, marked as completed<commit_after>package asana

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/manishrjain/asanawarrior/x"
	"github.com/pkg/errors"
)

var token = flag.String("token", "", "Token provided by Asana.")
var domain = flag.String("domain", "", "Workspace name, generally your domain name in Asana.")
var cache *acache = new(acache)

const (
	prefix = "https://app.asana.com/api/1.0"
	stamp  = "2006-01-02T15:04:05.999Z"
)

func runRequest(method, url string) ([]byte, error) {
RUNLOOP:
	req, err := http.NewRequest(method, url, nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Add("Authorization", "Bearer "+*token)
	client := &http.Client{
		Timeout: 10 * time.Minute,
	}

	resp, err := client.Do(req)
	if err != nil {
		log.Printf("runRequest method: [%v] url: [%v] err: [%v]", method, url, err)
		time.Sleep(5 * time.Second)
		goto RUNLOOP
	}

	defer resp.Body.Close()
	return ioutil.ReadAll(resp.Body)
}

func runGetter(i interface{}, suffix string, fields ...string) error {
	url := fmt.Sprintf("%s/%s?opt_fields=%s", prefix, suffix, strings.Join(fields, ","))
	body, err := runRequest("GET", url)
	if err != nil {
		return errors.Wrap(err, "runGetter")
	}
	if err := json.Unmarshal(body, i); err != nil {
		return errors.Wrap(err, "Unmarshal")
	}
	return nil
}

type Basic struct {
	Id    uint64 `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}
type BasicData struct {
	Data []Basic `json:"data"`
}
type BasicDataOne struct {
	Data Basic `json:"data"`
}

func getVarious(suffix string, opts ...string) ([]Basic, error) {
	var bd BasicData
	if err := runGetter(&bd, suffix, opts...); err != nil {
		return nil, err
	}
	return bd.Data, nil
}

type psec struct {
	Project Basic `json:"project"`
	Section Basic `json:"section"`
}

type task struct {
	Basic
	Assignee    Basic   `json:"assignee"`
	Tags        []Basic `json:"tags"`
	CompletedAt string  `json:"completed_at"`
	ModifiedAt  string  `json:"modified_at"`
	CreatedAt   string  `json:"created_at"`
	Memberships []psec  `json:"memberships"`
}

type tasks struct {
	Data []task `json:"data"`
}

type oneTask struct {
	Data task `json:"data"`
}

func convert(tsk task, proj, section string) (x.WarriorTask, error) {
	e := x.WarriorTask{}

	mts, err := time.Parse(stamp, tsk.ModifiedAt)
	if err != nil {
		return e, errors.Wrap(err, "asana modified at")
	}
	cts, err := time.Parse(stamp, tsk.CreatedAt)
	if err != nil {
		return e, errors.Wrap(err, "asana created at")
	}
	var dts time.Time
	if len(tsk.CompletedAt) > 0 {
		dts, err = time.Parse(stamp, tsk.CompletedAt)
		if err != nil {
			return e, errors.Wrap(err, "asana completed at")
		}
	}

	wt := x.WarriorTask{
		Name:      tsk.Name,
		Project:   proj,
		Xid:       tsk.Id,
		Assignee:  cache.User(tsk.Assignee.Id),
		Modified:  mts,
		Created:   cts,
		Completed: dts,
		Section:   section,
	}
	for _, tag := range tsk.Tags {
		wt.Tags = append(wt.Tags, cache.Tag(tag.Id))
	}
	return wt, nil
}

func getTasks(proj Basic, out chan x.WarriorTask, errc chan error) {
	var sectionName string
	var t tasks
	if err := runGetter(&t, fmt.Sprintf("projects/%d/tasks", proj.Id),
		"assignee,name,tags,completed_at,modified_at,created_at"); err != nil {
		errc <- errors.Wrapf(err, "getTasks for project: %v", proj.Name)
		return
	}

	for _, tsk := range t.Data {
		if len(tsk.Name) == 0 {
			// Don't sync such tasks.
			continue
		}
		if strings.HasSuffix(tsk.Name, ":") {
			sec := Basic{
				Id:   tsk.Id,
				Name: tsk.Name,
			}
			sectionName = cache.AddSection(proj.Id, sec)
			continue
		}

		wt, err := convert(tsk, proj.Name, sectionName)
		if err != nil {
			errc <- errors.Wrapf(err, "convert: getTasks for project: %v", proj.Name)
			return
		}
		out <- wt
	}
	errc <- nil
}

func GetTasks() ([]x.WarriorTask, error) {
	if err := cache.update(); err != nil {
		return nil, errors.Wrap(err, "cache.update")
	}

	out := make(chan x.WarriorTask, 100)
	projects := cache.Projects()
	errc := make(chan error, len(projects))
	for _, proj := range projects {
		go getTasks(proj, out, errc)
	}

	// Asana can send back the same task multiple times, if it's part of multiple projects.
	// So, let's dedup them.
	seen := make(map[uint64]bool)
	wtasks := make([]x.WarriorTask, 0, 100)
	done := make(chan struct{})
	go func() {
		for wt := range out {
			if _, has := seen[wt.Xid]; has {
				// ignore
				continue
			}
			wtasks = append(wtasks, wt)
			seen[wt.Xid] = true
		}
		done <- struct{}{}
	}()

	var rerr error
	for _ = range projects {
		if err := <-errc; err != nil {
			rerr = err
		}
	}
	close(out)
	<-done // Wait for all tasks to be picked up by goroutine.
	return wtasks, rerr
}

// runPost would run a PUT or POST to Asana. No locks should be acquired.
func runPost(method, suffix string, values url.Values) ([]byte, error) {
POSTLOOP:
	url := fmt.Sprintf("%s/%s", prefix, suffix)
	fmt.Println(url, values.Encode())
	req, err := http.NewRequest(method, url, bytes.NewBufferString(values.Encode()))
	if err != nil {
		log.Fatal(errors.Wrap(err, "runPost http.NewRequest"))
	}

	req.Header.Add("Authorization", "Bearer "+*token)
	req.Header.Add("content-type", "application/x-www-form-urlencoded")
	client := &http.Client{
		Timeout: 10 * time.Minute,
	}
	resp, err := client.Do(req)
	if err != nil {
		log.Printf("runPost url: [%v] err: [%v]", url, err)
		time.Sleep(5 * time.Second)
		goto POSTLOOP
	}
	defer resp.Body.Close()

	return ioutil.ReadAll(resp.Body)
}

func toTagIds(tnames []string) []string {
	var tags []string
	for _, t := range tnames {
		tid := cache.TagId(t)
		if tid == 0 {
			tid = cache.CreateTag(t)
			fmt.Printf("New Tag created. ID: %d", tid)
		}
		if tid > 0 {
			tags = append(tags, strconv.FormatUint(tid, 10))
		}
	}
	return tags
}

func removeProject(tid, pid uint64) error {
	v := url.Values{}
	v.Add("project", strconv.FormatUint(pid, 10))
	_, err := runPost("POST", fmt.Sprintf("tasks/%d/removeProject", tid), v)
	return err
}

func updateSection(tid, pid uint64, section string) error {
	v := url.Values{}
	v.Add("project", strconv.FormatUint(pid, 10))

	sid := cache.SectionId(pid, section)
	if sid > 0 {
		v.Add("section", strconv.FormatUint(sid, 10))
	}

	_, err := runPost("POST", fmt.Sprintf("tasks/%d/addProject", tid), v)
	return err
}

func AddNew(wt x.WarriorTask) (x.WarriorTask, error) {
	e := x.WarriorTask{}

	// Ensure that project actually exists before proceeding.
	pid := cache.ProjectId(wt.Project)
	if pid == 0 {
		return e, fmt.Errorf("Project not found: %v", wt.Project)
	}

	v := url.Values{}
	v.Add("workspace", strconv.FormatUint(cache.Workspace(), 10))
	v.Add("name", wt.Name)
	aid := cache.UserId(wt.Assignee)
	if aid > 0 {
		v.Add("assignee", strconv.FormatUint(aid, 10))
	}
	if !wt.Completed.IsZero() {
		v.Add("completed", "true")
	}

	tags := toTagIds(wt.Tags)
	v.Add("tags", strings.Join(tags, ","))
	resp, err := runPost("POST", "tasks", v)
	if err != nil {
		return e, errors.Wrap(err, "AddNew runPost")
	}
	fmt.Println(string(resp))

	var ot oneTask
	if err := json.Unmarshal(resp, &ot); err != nil {
		return e, errors.Wrap(err, "AddNew unmarshal")
	}
	if ot.Data.Id == 0 {
		return e, fmt.Errorf("Unable to find ID assigned by Asana: %+v", ot.Data)
	}

	// Now set the project and section.
	if err := updateSection(ot.Data.Id, pid, wt.Section); err != nil {
		return e, errors.Wrap(err, "AddNew updateSection")
	}

	// Now retrieve the task back again so we can sync it up with TW.
	return GetOneTask(ot.Data.Id)
}

func diff(t1 []string, t2 []string) []string {
	m := make(map[string]bool)
	for _, s := range t2 {
		m[s] = true
	}

	var result []string
	for _, t := range t1 {
		if has := m[t]; !has {
			result = append(result, t)
		}
	}
	return result
}

func updateOneTag(tagid, taskid, instruction string, errc chan error) {
	v := url.Values{}
	v.Add("tag", tagid)
	suffix := fmt.Sprintf("tasks/%s/%s", taskid, instruction)
	_, err := runPost("POST", suffix, v)
	if err != nil {
		errc <- errors.Wrap(err, "updateTags")
		return
	}
	errc <- nil
}

func updateTags(tw x.WarriorTask, asana x.WarriorTask) error {
	taskid := strconv.FormatUint(tw.Xid, 10)
	add := diff(tw.Tags, asana.Tags)
	rem := diff(asana.Tags, tw.Tags)

	addids := toTagIds(add)
	remids := toTagIds(rem)
	sz := len(addids) + len(remids)

	errc := make(chan error, sz)
	for _, id := range addids {
		go updateOneTag(id, taskid, "addTag", errc)
	}
	for _, id := range remids {
		go updateOneTag(id, taskid, "removeTag", errc)
	}

	var rerr error
	for i := 0; i < sz; i++ {
		if err := <-errc; err != nil {
			rerr = err
		}
	}

	return rerr
}

func UpdateTask(tw x.WarriorTask, asana x.WarriorTask) error {
	v := url.Values{}
	if tw.Name != asana.Name {
		v.Add("name", tw.Name)
	}
	if tw.Assignee != asana.Assignee {
		a := cache.UserId(tw.Assignee)
		if a > 0 {
			v.Add("assignee", strconv.FormatUint(a, 10))
		}
	}
	if !tw.Completed.IsZero() && asana.Completed.IsZero() {
		v.Add("completed", "true")
	}

	if len(v) > 0 {
		resp, err := runPost("PUT", "tasks/"+strconv.FormatUint(tw.Xid, 10), v)
		if err != nil {
			return errors.Wrap(err, "UpdateAsanaTask")
		}
		fmt.Println(string(resp))
	}

	if err := updateTags(tw, asana); err != nil {
		return errors.Wrap(err, "asana.UpdateTask updateTags")
	}

	// Update project or section if changed.
	pid := cache.ProjectId(tw.Project)
	if pid > 0 && (tw.Project != asana.Project || tw.Section != asana.Section) {
		fmt.Printf("Updating project and section: %v %v\n", tw.Project, tw.Section)
		if err := updateSection(tw.Xid, pid, tw.Section); err != nil {
			return errors.Wrap(err, "asana.UpdateTask updateSection")
		}
		if tw.Project == asana.Project {
			return nil
		}
		// Project was changed. So, remove the last one.
		fmt.Printf("Removing from project: %v\n", asana.Project)
		if previd := cache.ProjectId(asana.Project); previd > 0 {
			if err := removeProject(tw.Xid, previd); err != nil {
				return err
			}
		}
	}
	return nil
}

func GetOneTask(taskid uint64) (x.WarriorTask, error) {
	e := x.WarriorTask{}
	var ot oneTask
	if err := runGetter(&ot, "tasks/"+strconv.FormatUint(taskid, 10)); err != nil {
		return e, errors.Wrap(err, "AddNew runGetter")
	}

	if len(ot.Data.Memberships) == 0 {
		return e, fmt.Errorf("Member of no project")
	}
	member := ot.Data.Memberships[0]

	sname := cache.SectionName(member.Project.Id, member.Section.Id)
	return convert(ot.Data, member.Project.Name, sname)
}

func Delete(taskid uint64) error {
	url := fmt.Sprintf("%s/tasks/%d", prefix, taskid)
	_, err := runRequest("DELETE", url)
	return err
}
<|endoftext|>


<commit_before>package astilectron

import (
	"fmt"
	"net"
	"os/exec"
	"runtime"
	"time"

	"github.com/asticode/go-astikit"
)

// Versions
const (
	DefaultAcceptTCPTimeout   = 30 * time.Second
	DefaultVersionAstilectron = "0.42.0"
	DefaultVersionElectron    = "11.1.0"
)

// Misc vars
var (
	validOSes = map[string]bool{
		"darwin":  true,
		"linux":   true,
		"windows": true,
	}
)

// App event names
const (
	EventNameAppClose               = "app.close"
	EventNameAppCmdQuit             = "app.cmd.quit" // Sends an event to Electron to properly quit the app
	EventNameAppCmdStop             = "app.cmd.stop" // Cancel the context which results in exiting abruptly Electron's app
	EventNameAppCrash               = "app.crash"
	EventNameAppErrorAccept         = "app.error.accept"
	EventNameAppEventReady          = "app.event.ready"
	EventNameAppEventSecondInstance = "app.event.second.instance"
	EventNameAppNoAccept            = "app.no.accept"
	EventNameAppTooManyAccept       = "app.too.many.accept"
)

// Astilectron represents an object capable of interacting with Astilectron
type Astilectron struct {
	dispatcher   *dispatcher
	displayPool  *displayPool
	dock         *Dock
	executer     Executer
	identifier   *identifier
	l            astikit.SeverityLogger
	listener     net.Listener
	options      Options
	paths        *Paths
	provisioner  Provisioner
	reader       *reader
	stderrWriter *astikit.WriterAdapter
	stdoutWriter *astikit.WriterAdapter
	supported    *Supported
	worker       *astikit.Worker
	writer       *writer
}

// Options represents Astilectron options
type Options struct {
	AcceptTCPTimeout   time.Duration
	AppName            string
	AppIconDarwinPath  string // Darwin systems requires a specific .icns file
	AppIconDefaultPath string
	BaseDirectoryPath  string
	DataDirectoryPath  string
	ElectronSwitches   []string
	SingleInstance     bool
	SkipSetup          bool // If true, the user must handle provisioning and executing astilectron.
	TCPPort            *int // The port to listen on.
	VersionAstilectron string
	VersionElectron    string
}

// Supported represents Astilectron supported features
type Supported struct {
	Notification *bool `json:"notification"`
}

// New creates a new Astilectron instance
func New(l astikit.StdLogger, o Options) (a *Astilectron, err error) {
	// Validate the OS
	if !IsValidOS(runtime.GOOS) {
		err = fmt.Errorf("OS %s is invalid", runtime.GOOS)
		return
	}

	if o.VersionAstilectron == "" {
		o.VersionAstilectron = DefaultVersionAstilectron
	}
	if o.VersionElectron == "" {
		o.VersionElectron = DefaultVersionElectron
	}

	// Init
	a = &Astilectron{
		dispatcher:  newDispatcher(),
		displayPool: newDisplayPool(),
		executer:    DefaultExecuter,
		identifier:  newIdentifier(),
		l:           astikit.AdaptStdLogger(l),
		options:     o,
		provisioner: newDefaultProvisioner(l),
		worker:      astikit.NewWorker(astikit.WorkerOptions{Logger: l}),
	}

	// Set paths
	if a.paths, err = newPaths(runtime.GOOS, runtime.GOARCH, o); err != nil {
		err = fmt.Errorf("creating new paths failed: %w", err)
		return
	}

	// Add default listeners
	a.On(EventNameAppCmdStop, func(e Event) (deleteListener bool) {
		a.Stop()
		return
	})
	a.On(EventNameDisplayEventAdded, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameDisplayEventMetricsChanged, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameDisplayEventRemoved, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameAppCmdQuit, func(e Event) (deleteListener bool) {
		a.Stop()
		return
	})
	return
}

// IsValidOS validates the OS
func IsValidOS(os string) (ok bool) {
	_, ok = validOSes[os]
	return
}

// SetProvisioner sets the provisioner
func (a *Astilectron) SetProvisioner(p Provisioner) *Astilectron {
	a.provisioner = p
	return a
}

// SetExecuter sets the executer
func (a *Astilectron) SetExecuter(e Executer) *Astilectron {
	a.executer = e
	return a
}

// On implements the Listenable interface
func (a *Astilectron) On(eventName string, l Listener) {
	a.dispatcher.addListener(targetIDApp, eventName, l)
}

// Start starts Astilectron
func (a *Astilectron) Start() (err error) {
	// Log
	a.l.Debug("Starting...")

	// Provision
	if !a.options.SkipSetup {
		if err = a.provision(); err != nil {
			return fmt.Errorf("provisioning failed: %w", err)
		}
	}

	// Unfortunately communicating with Electron through stdin/stdout doesn't work on Windows so all communications
	// will be done through TCP
	if err = a.listenTCP(); err != nil {
		return fmt.Errorf("listening failed: %w", err)
	}

	// Execute
	if !a.options.SkipSetup {
		if err = a.execute(); err != nil {
			return fmt.Errorf("executing failed: %w", err)
		}
	} else {
		synchronousFunc(a.worker.Context(), a, nil, "app.event.ready")
	}
	return nil
}

// provision provisions Astilectron
func (a *Astilectron) provision() error {
	a.l.Debug("Provisioning...")
	return a.provisioner.Provision(a.worker.Context(), a.options.AppName, runtime.GOOS, runtime.GOARCH, a.options.VersionAstilectron, a.options.VersionElectron, *a.paths)
}

// listenTCP creates a TCP server for astilectron to connect to
// and listens to the first TCP connection coming its way (this should be Astilectron).
func (a *Astilectron) listenTCP() (err error) {
	// Log
	a.l.Debug("Listening...")

	addr := "127.0.0.1:"
	if a.options.TCPPort != nil {
		addr += fmt.Sprint(*a.options.TCPPort)
	}
	// Listen
	if a.listener, err = net.Listen("tcp", addr); err != nil {
		return fmt.Errorf("tcp net.Listen failed: %w", err)
	}

	// Check a connection has been accepted quickly enough
	var chanAccepted = make(chan bool)
	go a.watchNoAccept(a.options.AcceptTCPTimeout, chanAccepted)

	// Accept connections
	go a.acceptTCP(chanAccepted)
	return
}

// watchNoAccept checks whether a TCP connection is accepted quickly enough
func (a *Astilectron) watchNoAccept(timeout time.Duration, chanAccepted chan bool) {
	//check timeout
	if timeout == 0 {
		timeout = DefaultAcceptTCPTimeout
	}
	var t = time.NewTimer(timeout)
	defer t.Stop()
	for {
		select {
		case <-chanAccepted:
			return
		case <-t.C:
			a.l.Errorf("No TCP connection has been accepted in the past %s", timeout)
			a.dispatcher.dispatch(Event{Name: EventNameAppNoAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			return
		}
	}
}

// watchAcceptTCP accepts TCP connections
func (a *Astilectron) acceptTCP(chanAccepted chan bool) {
	for i := 0; i <= 1; i++ {
		// Accept
		var conn net.Conn
		var err error
		if conn, err = a.listener.Accept(); err != nil {
			a.l.Errorf("%s while TCP accepting", err)
			a.dispatcher.dispatch(Event{Name: EventNameAppErrorAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			return
		}

		// We only accept the first connection which should be Astilectron, close the next one and stop
		// the app
		if i > 0 {
			a.l.Errorf("Too many TCP connections")
			a.dispatcher.dispatch(Event{Name: EventNameAppTooManyAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			conn.Close()
			return
		}

		// Let the timer know a connection has been accepted
		chanAccepted <- true

		// Create reader and writer
		a.writer = newWriter(conn, a.l)
		a.reader = newReader(a.worker.Context(), a.l, a.dispatcher, conn)
		go a.reader.read()
	}
}

// execute executes Astilectron in Electron
func (a *Astilectron) execute() (err error) {
	// Log
	a.l.Debug("Executing...")

	// Create command
	var singleInstance string
	if a.options.SingleInstance {
		singleInstance = "true"
	} else {
		singleInstance = "false"
	}
	var cmd = exec.CommandContext(a.worker.Context(), a.paths.AppExecutable(), append([]string{a.paths.AstilectronApplication(), a.listener.Addr().String(), singleInstance}, a.options.ElectronSwitches...)...)
	a.stderrWriter = astikit.NewWriterAdapter(astikit.WriterAdapterOptions{
		Callback: func(i []byte) { a.l.Debugf("Stderr says: %s", i) },
		Split:    []byte("\n"),
	})
	a.stdoutWriter = astikit.NewWriterAdapter(astikit.WriterAdapterOptions{
		Callback: func(i []byte) { a.l.Debugf("Stdout says: %s", i) },
		Split:    []byte("\n"),
	})
	cmd.Stderr = a.stderrWriter
	cmd.Stdout = a.stdoutWriter

	// Execute command
	if err = a.executeCmd(cmd); err != nil {
		return fmt.Errorf("executing cmd failed: %w", err)
	}
	return
}

// executeCmd executes the command
func (a *Astilectron) executeCmd(cmd *exec.Cmd) (err error) {
	// Execute
	var e Event
	if e, err = synchronousFunc(a.worker.Context(), a, func() error { return a.executer(a.l, a, cmd) }, EventNameAppEventReady); err != nil {
		err = fmt.Errorf("executer failed: %w", err)
		return
	}

	// Update display pool
	if e.Displays != nil {
		a.displayPool.update(e.Displays)
	}

	// Create dock
	a.dock = newDock(a.worker.Context(), a.dispatcher, a.identifier, a.writer)

	// Update supported features
	a.supported = e.Supported
	return
}

// watchCmd watches the cmd execution
func (a *Astilectron) watchCmd(cmd *exec.Cmd) {
	a.worker.NewTask().Do(func() {
		// Wait
		if err := cmd.Wait(); err != nil {
			a.l.Errorf("'%v' exited with code: %v", cmd.Path, cmd.ProcessState.ExitCode())
		}

		// Check the context to determine whether it was a crash
		if a.worker.Context().Err() == nil {
			a.l.Debug("App has crashed")
			a.dispatcher.dispatch(Event{Name: EventNameAppCrash, TargetID: targetIDApp})
		} else {
			a.l.Debug("App has closed")
			a.dispatcher.dispatch(Event{Name: EventNameAppClose, TargetID: targetIDApp})
		}
		a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
	})
}

// Close closes Astilectron properly
func (a *Astilectron) Close() {
	a.l.Debug("Closing...")
	a.worker.Stop()
	if a.listener != nil {
		a.listener.Close()
	}
	if a.reader != nil {
		a.reader.close()
	}
	if a.stderrWriter != nil {
		a.stderrWriter.Close()
	}
	if a.stdoutWriter != nil {
		a.stdoutWriter.Close()
	}
	if a.writer != nil {
		a.writer.close()
	}
}

// HandleSignals handles signals
func (a *Astilectron) HandleSignals(hs ...astikit.SignalHandler) {
	a.worker.HandleSignals(hs...)
}

// Stop orders Astilectron to stop
func (a *Astilectron) Stop() {
	a.l.Debug("Stopping...")
	a.worker.Stop()
}

// Wait is a blocking pattern
func (a *Astilectron) Wait() {
	a.worker.Wait()
}

// Quit quits the app
func (a *Astilectron) Quit() error {
	return a.writer.write(Event{Name: EventNameAppCmdQuit})
}

// Paths returns the paths
func (a *Astilectron) Paths() Paths {
	return *a.paths
}

// Displays returns the displays
func (a *Astilectron) Displays() []*Display {
	return a.displayPool.all()
}

// Dock returns the dock
func (a *Astilectron) Dock() *Dock {
	return a.dock
}

// PrimaryDisplay returns the primary display
func (a *Astilectron) PrimaryDisplay() *Display {
	return a.displayPool.primary()
}

// NewMenu creates a new app menu
func (a *Astilectron) NewMenu(i []*MenuItemOptions) *Menu {
	return newMenu(a.worker.Context(), targetIDApp, i, a.dispatcher, a.identifier, a.writer)
}

// NewWindow creates a new window
func (a *Astilectron) NewWindow(url string, o *WindowOptions) (*Window, error) {
	return newWindow(a.worker.Context(), a.l, a.options, a.Paths(), url, o, a.dispatcher, a.identifier, a.writer)
}

// NewWindowInDisplay creates a new window in a specific display
// This overrides the center attribute
func (a *Astilectron) NewWindowInDisplay(d *Display, url string, o *WindowOptions) (*Window, error) {
	if o.X != nil {
		*o.X += d.Bounds().X
	} else {
		o.X = astikit.IntPtr(d.Bounds().X)
	}
	if o.Y != nil {
		*o.Y += d.Bounds().Y
	} else {
		o.Y = astikit.IntPtr(d.Bounds().Y)
	}
	return newWindow(a.worker.Context(), a.l, a.options, a.Paths(), url, o, a.dispatcher, a.identifier, a.writer)
}

// NewTray creates a new tray
func (a *Astilectron) NewTray(o *TrayOptions) *Tray {
	return newTray(a.worker.Context(), o, a.dispatcher, a.identifier, a.writer)
}

// NewNotification creates a new notification
func (a *Astilectron) NewNotification(o *NotificationOptions) *Notification {
	return newNotification(a.worker.Context(), o, a.supported != nil && a.supported.Notification != nil && *a.supported.Notification, a.dispatcher, a.identifier, a.writer)
}
<commit_msg>Bumped astilectron<commit_after>package astilectron

import (
	"fmt"
	"net"
	"os/exec"
	"runtime"
	"time"

	"github.com/asticode/go-astikit"
)

// Versions
const (
	DefaultAcceptTCPTimeout   = 30 * time.Second
	DefaultVersionAstilectron = "0.43.1"
	DefaultVersionElectron    = "11.1.0"
)

// Misc vars
var (
	validOSes = map[string]bool{
		"darwin":  true,
		"linux":   true,
		"windows": true,
	}
)

// App event names
const (
	EventNameAppClose               = "app.close"
	EventNameAppCmdQuit             = "app.cmd.quit" // Sends an event to Electron to properly quit the app
	EventNameAppCmdStop             = "app.cmd.stop" // Cancel the context which results in exiting abruptly Electron's app
	EventNameAppCrash               = "app.crash"
	EventNameAppErrorAccept         = "app.error.accept"
	EventNameAppEventReady          = "app.event.ready"
	EventNameAppEventSecondInstance = "app.event.second.instance"
	EventNameAppNoAccept            = "app.no.accept"
	EventNameAppTooManyAccept       = "app.too.many.accept"
)

// Astilectron represents an object capable of interacting with Astilectron
type Astilectron struct {
	dispatcher   *dispatcher
	displayPool  *displayPool
	dock         *Dock
	executer     Executer
	identifier   *identifier
	l            astikit.SeverityLogger
	listener     net.Listener
	options      Options
	paths        *Paths
	provisioner  Provisioner
	reader       *reader
	stderrWriter *astikit.WriterAdapter
	stdoutWriter *astikit.WriterAdapter
	supported    *Supported
	worker       *astikit.Worker
	writer       *writer
}

// Options represents Astilectron options
type Options struct {
	AcceptTCPTimeout   time.Duration
	AppName            string
	AppIconDarwinPath  string // Darwin systems requires a specific .icns file
	AppIconDefaultPath string
	BaseDirectoryPath  string
	DataDirectoryPath  string
	ElectronSwitches   []string
	SingleInstance     bool
	SkipSetup          bool // If true, the user must handle provisioning and executing astilectron.
	TCPPort            *int // The port to listen on.
	VersionAstilectron string
	VersionElectron    string
}

// Supported represents Astilectron supported features
type Supported struct {
	Notification *bool `json:"notification"`
}

// New creates a new Astilectron instance
func New(l astikit.StdLogger, o Options) (a *Astilectron, err error) {
	// Validate the OS
	if !IsValidOS(runtime.GOOS) {
		err = fmt.Errorf("OS %s is invalid", runtime.GOOS)
		return
	}

	if o.VersionAstilectron == "" {
		o.VersionAstilectron = DefaultVersionAstilectron
	}
	if o.VersionElectron == "" {
		o.VersionElectron = DefaultVersionElectron
	}

	// Init
	a = &Astilectron{
		dispatcher:  newDispatcher(),
		displayPool: newDisplayPool(),
		executer:    DefaultExecuter,
		identifier:  newIdentifier(),
		l:           astikit.AdaptStdLogger(l),
		options:     o,
		provisioner: newDefaultProvisioner(l),
		worker:      astikit.NewWorker(astikit.WorkerOptions{Logger: l}),
	}

	// Set paths
	if a.paths, err = newPaths(runtime.GOOS, runtime.GOARCH, o); err != nil {
		err = fmt.Errorf("creating new paths failed: %w", err)
		return
	}

	// Add default listeners
	a.On(EventNameAppCmdStop, func(e Event) (deleteListener bool) {
		a.Stop()
		return
	})
	a.On(EventNameDisplayEventAdded, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameDisplayEventMetricsChanged, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameDisplayEventRemoved, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameAppCmdQuit, func(e Event) (deleteListener bool) {
		a.Stop()
		return
	})
	return
}

// IsValidOS validates the OS
func IsValidOS(os string) (ok bool) {
	_, ok = validOSes[os]
	return
}

// SetProvisioner sets the provisioner
func (a *Astilectron) SetProvisioner(p Provisioner) *Astilectron {
	a.provisioner = p
	return a
}

// SetExecuter sets the executer
func (a *Astilectron) SetExecuter(e Executer) *Astilectron {
	a.executer = e
	return a
}

// On implements the Listenable interface
func (a *Astilectron) On(eventName string, l Listener) {
	a.dispatcher.addListener(targetIDApp, eventName, l)
}

// Start starts Astilectron
func (a *Astilectron) Start() (err error) {
	// Log
	a.l.Debug("Starting...")

	// Provision
	if !a.options.SkipSetup {
		if err = a.provision(); err != nil {
			return fmt.Errorf("provisioning failed: %w", err)
		}
	}

	// Unfortunately communicating with Electron through stdin/stdout doesn't work on Windows so all communications
	// will be done through TCP
	if err = a.listenTCP(); err != nil {
		return fmt.Errorf("listening failed: %w", err)
	}

	// Execute
	if !a.options.SkipSetup {
		if err = a.execute(); err != nil {
			return fmt.Errorf("executing failed: %w", err)
		}
	} else {
		synchronousFunc(a.worker.Context(), a, nil, "app.event.ready")
	}
	return nil
}

// provision provisions Astilectron
func (a *Astilectron) provision() error {
	a.l.Debug("Provisioning...")
	return a.provisioner.Provision(a.worker.Context(), a.options.AppName, runtime.GOOS, runtime.GOARCH, a.options.VersionAstilectron, a.options.VersionElectron, *a.paths)
}

// listenTCP creates a TCP server for astilectron to connect to
// and listens to the first TCP connection coming its way (this should be Astilectron).
func (a *Astilectron) listenTCP() (err error) {
	// Log
	a.l.Debug("Listening...")

	addr := "127.0.0.1:"
	if a.options.TCPPort != nil {
		addr += fmt.Sprint(*a.options.TCPPort)
	}
	// Listen
	if a.listener, err = net.Listen("tcp", addr); err != nil {
		return fmt.Errorf("tcp net.Listen failed: %w", err)
	}

	// Check a connection has been accepted quickly enough
	var chanAccepted = make(chan bool)
	go a.watchNoAccept(a.options.AcceptTCPTimeout, chanAccepted)

	// Accept connections
	go a.acceptTCP(chanAccepted)
	return
}

// watchNoAccept checks whether a TCP connection is accepted quickly enough
func (a *Astilectron) watchNoAccept(timeout time.Duration, chanAccepted chan bool) {
	//check timeout
	if timeout == 0 {
		timeout = DefaultAcceptTCPTimeout
	}
	var t = time.NewTimer(timeout)
	defer t.Stop()
	for {
		select {
		case <-chanAccepted:
			return
		case <-t.C:
			a.l.Errorf("No TCP connection has been accepted in the past %s", timeout)
			a.dispatcher.dispatch(Event{Name: EventNameAppNoAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			return
		}
	}
}

// watchAcceptTCP accepts TCP connections
func (a *Astilectron) acceptTCP(chanAccepted chan bool) {
	for i := 0; i <= 1; i++ {
		// Accept
		var conn net.Conn
		var err error
		if conn, err = a.listener.Accept(); err != nil {
			a.l.Errorf("%s while TCP accepting", err)
			a.dispatcher.dispatch(Event{Name: EventNameAppErrorAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			return
		}

		// We only accept the first connection which should be Astilectron, close the next one and stop
		// the app
		if i > 0 {
			a.l.Errorf("Too many TCP connections")
			a.dispatcher.dispatch(Event{Name: EventNameAppTooManyAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			conn.Close()
			return
		}

		// Let the timer know a connection has been accepted
		chanAccepted <- true

		// Create reader and writer
		a.writer = newWriter(conn, a.l)
		a.reader = newReader(a.worker.Context(), a.l, a.dispatcher, conn)
		go a.reader.read()
	}
}

// execute executes Astilectron in Electron
func (a *Astilectron) execute() (err error) {
	// Log
	a.l.Debug("Executing...")

	// Create command
	var singleInstance string
	if a.options.SingleInstance {
		singleInstance = "true"
	} else {
		singleInstance = "false"
	}
	var cmd = exec.CommandContext(a.worker.Context(), a.paths.AppExecutable(), append([]string{a.paths.AstilectronApplication(), a.listener.Addr().String(), singleInstance}, a.options.ElectronSwitches...)...)
	a.stderrWriter = astikit.NewWriterAdapter(astikit.WriterAdapterOptions{
		Callback: func(i []byte) { a.l.Debugf("Stderr says: %s", i) },
		Split:    []byte("\n"),
	})
	a.stdoutWriter = astikit.NewWriterAdapter(astikit.WriterAdapterOptions{
		Callback: func(i []byte) { a.l.Debugf("Stdout says: %s", i) },
		Split:    []byte("\n"),
	})
	cmd.Stderr = a.stderrWriter
	cmd.Stdout = a.stdoutWriter

	// Execute command
	if err = a.executeCmd(cmd); err != nil {
		return fmt.Errorf("executing cmd failed: %w", err)
	}
	return
}

// executeCmd executes the command
func (a *Astilectron) executeCmd(cmd *exec.Cmd) (err error) {
	// Execute
	var e Event
	if e, err = synchronousFunc(a.worker.Context(), a, func() error { return a.executer(a.l, a, cmd) }, EventNameAppEventReady); err != nil {
		err = fmt.Errorf("executer failed: %w", err)
		return
	}

	// Update display pool
	if e.Displays != nil {
		a.displayPool.update(e.Displays)
	}

	// Create dock
	a.dock = newDock(a.worker.Context(), a.dispatcher, a.identifier, a.writer)

	// Update supported features
	a.supported = e.Supported
	return
}

// watchCmd watches the cmd execution
func (a *Astilectron) watchCmd(cmd *exec.Cmd) {
	a.worker.NewTask().Do(func() {
		// Wait
		if err := cmd.Wait(); err != nil {
			a.l.Errorf("'%v' exited with code: %v", cmd.Path, cmd.ProcessState.ExitCode())
		}

		// Check the context to determine whether it was a crash
		if a.worker.Context().Err() == nil {
			a.l.Debug("App has crashed")
			a.dispatcher.dispatch(Event{Name: EventNameAppCrash, TargetID: targetIDApp})
		} else {
			a.l.Debug("App has closed")
			a.dispatcher.dispatch(Event{Name: EventNameAppClose, TargetID: targetIDApp})
		}
		a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
	})
}

// Close closes Astilectron properly
func (a *Astilectron) Close() {
	a.l.Debug("Closing...")
	a.worker.Stop()
	if a.listener != nil {
		a.listener.Close()
	}
	if a.reader != nil {
		a.reader.close()
	}
	if a.stderrWriter != nil {
		a.stderrWriter.Close()
	}
	if a.stdoutWriter != nil {
		a.stdoutWriter.Close()
	}
	if a.writer != nil {
		a.writer.close()
	}
}

// HandleSignals handles signals
func (a *Astilectron) HandleSignals(hs ...astikit.SignalHandler) {
	a.worker.HandleSignals(hs...)
}

// Stop orders Astilectron to stop
func (a *Astilectron) Stop() {
	a.l.Debug("Stopping...")
	a.worker.Stop()
}

// Wait is a blocking pattern
func (a *Astilectron) Wait() {
	a.worker.Wait()
}

// Quit quits the app
func (a *Astilectron) Quit() error {
	return a.writer.write(Event{Name: EventNameAppCmdQuit})
}

// Paths returns the paths
func (a *Astilectron) Paths() Paths {
	return *a.paths
}

// Displays returns the displays
func (a *Astilectron) Displays() []*Display {
	return a.displayPool.all()
}

// Dock returns the dock
func (a *Astilectron) Dock() *Dock {
	return a.dock
}

// PrimaryDisplay returns the primary display
func (a *Astilectron) PrimaryDisplay() *Display {
	return a.displayPool.primary()
}

// NewMenu creates a new app menu
func (a *Astilectron) NewMenu(i []*MenuItemOptions) *Menu {
	return newMenu(a.worker.Context(), targetIDApp, i, a.dispatcher, a.identifier, a.writer)
}

// NewWindow creates a new window
func (a *Astilectron) NewWindow(url string, o *WindowOptions) (*Window, error) {
	return newWindow(a.worker.Context(), a.l, a.options, a.Paths(), url, o, a.dispatcher, a.identifier, a.writer)
}

// NewWindowInDisplay creates a new window in a specific display
// This overrides the center attribute
func (a *Astilectron) NewWindowInDisplay(d *Display, url string, o *WindowOptions) (*Window, error) {
	if o.X != nil {
		*o.X += d.Bounds().X
	} else {
		o.X = astikit.IntPtr(d.Bounds().X)
	}
	if o.Y != nil {
		*o.Y += d.Bounds().Y
	} else {
		o.Y = astikit.IntPtr(d.Bounds().Y)
	}
	return newWindow(a.worker.Context(), a.l, a.options, a.Paths(), url, o, a.dispatcher, a.identifier, a.writer)
}

// NewTray creates a new tray
func (a *Astilectron) NewTray(o *TrayOptions) *Tray {
	return newTray(a.worker.Context(), o, a.dispatcher, a.identifier, a.writer)
}

// NewNotification creates a new notification
func (a *Astilectron) NewNotification(o *NotificationOptions) *Notification {
	return newNotification(a.worker.Context(), o, a.supported != nil && a.supported.Notification != nil && *a.supported.Notification, a.dispatcher, a.identifier, a.writer)
}
<|endoftext|>


<commit_before>package series

import (
	"math"
	"sort"
	"time"
)

const (
	_ = iota
	// ConsolidateAverage represents an average consolidation type.
	ConsolidateAverage
	// ConsolidateFirst represents a first value consolidation type.
	ConsolidateFirst
	// ConsolidateLast represents a last value consolidation type.
	ConsolidateLast
	// ConsolidateMax represents a maximal value consolidation type.
	ConsolidateMax
	// ConsolidateMin represents a minimal value consolidation type.
	ConsolidateMin
	// ConsolidateSum represents a sum consolidation type.
	ConsolidateSum
)

const (
	// OperatorNone represents a null operation type.
	OperatorNone = iota
	// OperatorAverage represents an average operation type.
	OperatorAverage
	// OperatorSum represents a sum operation type.
	OperatorSum
)

type bucket struct {
	startTime time.Time
	points    []Point
}

// Consolidate consolidates points buckets based on consolidation function.
func (b bucket) Consolidate(consolidation int) Point {
	point := Point{
		Value: Value(math.NaN()),
		Time:  b.startTime,
	}

	length := len(b.points)
	if length == 0 {
		return point
	}

	switch consolidation {
	case ConsolidateAverage:
		sum := 0.0
		sumCount := 0
		for _, p := range b.points {
			if p.Value.IsNaN() {
				continue
			}

			sum += float64(p.Value)
			sumCount++
		}

		if sumCount > 0 {
			point.Value = Value(sum / float64(sumCount))
		}

		if length == 1 {
			point.Time = b.points[0].Time
		} else {
			// Interpolate median time
			point.Time = b.points[0].Time.Add(b.points[length-1].Time.Sub(b.points[0].Time) / 2)
		}

	case ConsolidateSum:
		sum := 0.0
		sumCount := 0
		for _, p := range b.points {
			if p.Value.IsNaN() {
				continue
			}

			sum += float64(p.Value)
			sumCount++
		}

		if sumCount > 0 {
			point.Value = Value(sum)
		}

		point.Time = b.points[length-1].Time

	case ConsolidateFirst:
		point = b.points[0]

	case ConsolidateLast:
		point = b.points[length-1]

	case ConsolidateMax:
		for _, p := range b.points {
			if !p.Value.IsNaN() && p.Value > point.Value || point.Value.IsNaN() {
				point = p
			}
		}

	case ConsolidateMin:
		for _, p := range b.points {
			if !p.Value.IsNaN() && p.Value < point.Value || point.Value.IsNaN() {
				point = p
			}
		}
	}

	return point
}

// Normalize aligns multiple point series on a common time step, consolidates points samples if necessary.
func Normalize(series []Series, startTime, endTime time.Time, sample int, consolidation int) ([]Series, error) {
	if sample <= 0 {
		return nil, ErrInvalidSample
	}

	length := len(series)
	if length == 0 {
		return nil, ErrEmptySeries
	}

	result := make([]Series, length)
	buckets := make([][]bucket, length)

	// Calculate the common step for all series based on time range and requested sampling
	step := endTime.Sub(startTime) / time.Duration(sample)

	// Dispatch points into proper time step buckets and then apply consolidation function
	for i, s := range series {
		if s.Points == nil {
			continue
		}

		buckets[i] = make([]bucket, sample)

		// Initialize time steps
		for j := 0; j < sample; j++ {
			buckets[i][j] = bucket{
				startTime: startTime.Add(time.Duration(j) * step),
				points:    make([]Point, 0),
			}
		}

		for _, p := range s.Points {
			// Discard series points out of time specs range
			if p.Time.Before(startTime) || p.Time.After(endTime) {
				continue
			}

			// Stop if index goes beyond the requested sample
			idx := int64(float64(p.Time.UnixNano()-startTime.UnixNano())/float64(step.Nanoseconds())+1) - 1
			if idx >= int64(sample) {
				continue
			}

			buckets[i][idx].points = append(buckets[i][idx].points, p)
		}

		result[i] = Series{
			Points:  make([]Point, sample),
			Summary: make(map[string]Value),
		}

		// Consolidate point buckets
		unknownGaps := map[int][]int{}
		unknownGapLast := 0
		unknownLast := -1

		for j := range buckets[i] {
			result[i].Points[j] = buckets[i][j].Consolidate(consolidation)

			// Align consolidated points timestamps among normalized series lists
			result[i].Points[j].Time = buckets[i][j].startTime.Add(time.Duration(step.Seconds() * float64(j))).
				Round(time.Second)

			if result[i].Points[j].Value.IsNaN() {
				if unknownLast != -1 {
					gap := j - unknownLast

					if _, ok := unknownGaps[gap]; !ok {
						unknownGaps[gap] = []int{}
					}

					// Check for first value if gap is consistent
					if unknownGapLast != -1 {
						if gap == unknownGapLast {
							unknownGaps[gap] = append(unknownGaps[gap], j-gap*2)
							unknownGapLast = -1
						} else {
							unknownGapLast = gap
						}
					}

					unknownGaps[gap] = append(unknownGaps[gap], j)
				}

				unknownLast = j
			}
		}

		// Cleanup series from regular gaps
		for _, indexes := range unknownGaps {
			if len(indexes) < 2 {
				continue
			}

			sort.Sort(sort.Reverse(sort.IntSlice(indexes)))
			for _, idx := range indexes {
				result[i].Points = append(result[i].Points[:idx], result[i].Points[idx+1:]...)
			}
		}
	}

	return result, nil
}

// Average returns a new series averaging each datapoints.
func Average(series []Series) (Series, error) {
	return applyOperator(series, OperatorAverage)
}

// Sum returns a new series summing each datapoints.
func Sum(series []Series) (Series, error) {
	return applyOperator(series, OperatorSum)
}

func applyOperator(series []Series, operator int) (Series, error) {
	length := len(series)
	if length == 0 {
		return Series{}, ErrEmptySeries
	}

	count := len(series[0].Points)

	result := Series{
		Points:  make([]Point, count),
		Summary: make(map[string]Value),
	}

	for i := 0; i < count; i++ {
		sumCount := 0

		result.Points[i].Time = series[0].Points[i].Time

		for _, s := range series {
			if s.Points == nil {
				continue
			} else if len(s.Points) != count {
				return Series{}, ErrUnnormalizedSeries
			} else if s.Points[i].Value.IsNaN() {
				continue
			}

			result.Points[i].Value += s.Points[i].Value
			sumCount++
		}

		if sumCount == 0 {
			result.Points[i].Value = Value(math.NaN())
		} else if operator == OperatorAverage {
			result.Points[i].Value /= Value(sumCount)
		}
	}

	return result, nil
}
<commit_msg>Prevent panic when cleaning point gaps<commit_after>package series

import (
	"math"
	"sort"
	"time"
)

const (
	_ = iota
	// ConsolidateAverage represents an average consolidation type.
	ConsolidateAverage
	// ConsolidateFirst represents a first value consolidation type.
	ConsolidateFirst
	// ConsolidateLast represents a last value consolidation type.
	ConsolidateLast
	// ConsolidateMax represents a maximal value consolidation type.
	ConsolidateMax
	// ConsolidateMin represents a minimal value consolidation type.
	ConsolidateMin
	// ConsolidateSum represents a sum consolidation type.
	ConsolidateSum
)

const (
	// OperatorNone represents a null operation type.
	OperatorNone = iota
	// OperatorAverage represents an average operation type.
	OperatorAverage
	// OperatorSum represents a sum operation type.
	OperatorSum
)

type bucket struct {
	startTime time.Time
	points    []Point
}

// Consolidate consolidates points buckets based on consolidation function.
func (b bucket) Consolidate(consolidation int) Point {
	point := Point{
		Value: Value(math.NaN()),
		Time:  b.startTime,
	}

	length := len(b.points)
	if length == 0 {
		return point
	}

	switch consolidation {
	case ConsolidateAverage:
		sum := 0.0
		sumCount := 0
		for _, p := range b.points {
			if p.Value.IsNaN() {
				continue
			}

			sum += float64(p.Value)
			sumCount++
		}

		if sumCount > 0 {
			point.Value = Value(sum / float64(sumCount))
		}

		if length == 1 {
			point.Time = b.points[0].Time
		} else {
			// Interpolate median time
			point.Time = b.points[0].Time.Add(b.points[length-1].Time.Sub(b.points[0].Time) / 2)
		}

	case ConsolidateSum:
		sum := 0.0
		sumCount := 0
		for _, p := range b.points {
			if p.Value.IsNaN() {
				continue
			}

			sum += float64(p.Value)
			sumCount++
		}

		if sumCount > 0 {
			point.Value = Value(sum)
		}

		point.Time = b.points[length-1].Time

	case ConsolidateFirst:
		point = b.points[0]

	case ConsolidateLast:
		point = b.points[length-1]

	case ConsolidateMax:
		for _, p := range b.points {
			if !p.Value.IsNaN() && p.Value > point.Value || point.Value.IsNaN() {
				point = p
			}
		}

	case ConsolidateMin:
		for _, p := range b.points {
			if !p.Value.IsNaN() && p.Value < point.Value || point.Value.IsNaN() {
				point = p
			}
		}
	}

	return point
}

// Normalize aligns multiple point series on a common time step, consolidates points samples if necessary.
func Normalize(series []Series, startTime, endTime time.Time, sample int, consolidation int) ([]Series, error) {
	if sample <= 0 {
		return nil, ErrInvalidSample
	}

	length := len(series)
	if length == 0 {
		return nil, ErrEmptySeries
	}

	result := make([]Series, length)
	buckets := make([][]bucket, length)

	// Calculate the common step for all series based on time range and requested sampling
	step := endTime.Sub(startTime) / time.Duration(sample)

	// Dispatch points into proper time step buckets and then apply consolidation function
	for i, s := range series {
		if s.Points == nil {
			continue
		}

		buckets[i] = make([]bucket, sample)

		// Initialize time steps
		for j := 0; j < sample; j++ {
			buckets[i][j] = bucket{
				startTime: startTime.Add(time.Duration(j) * step),
				points:    make([]Point, 0),
			}
		}

		for _, p := range s.Points {
			// Discard series points out of time specs range
			if p.Time.Before(startTime) || p.Time.After(endTime) {
				continue
			}

			// Stop if index goes beyond the requested sample
			idx := int64(float64(p.Time.UnixNano()-startTime.UnixNano())/float64(step.Nanoseconds())+1) - 1
			if idx >= int64(sample) {
				continue
			}

			buckets[i][idx].points = append(buckets[i][idx].points, p)
		}

		result[i] = Series{
			Points:  make([]Point, sample),
			Summary: make(map[string]Value),
		}

		// Consolidate point buckets
		unknownGaps := map[int][]int{}
		unknownGapLast := 0
		unknownLast := -1

		for j := range buckets[i] {
			result[i].Points[j] = buckets[i][j].Consolidate(consolidation)

			// Align consolidated points timestamps among normalized series lists
			result[i].Points[j].Time = buckets[i][j].startTime.Add(time.Duration(step.Seconds() * float64(j))).
				Round(time.Second)

			if result[i].Points[j].Value.IsNaN() {
				if unknownLast != -1 {
					gap := j - unknownLast

					if _, ok := unknownGaps[gap]; !ok {
						unknownGaps[gap] = []int{}
					}

					// Check for first value if gap is consistent
					if unknownGapLast != -1 {
						if gap == unknownGapLast {
							unknownGaps[gap] = append(unknownGaps[gap], j-gap*2)
							unknownGapLast = -1
						} else {
							unknownGapLast = gap
						}
					}

					unknownGaps[gap] = append(unknownGaps[gap], j)
				}

				unknownLast = j
			}
		}

		// Cleanup series from regular gaps
		for _, indexes := range unknownGaps {
			if len(indexes) < 2 {
				continue
			}

			sort.Sort(sort.Reverse(sort.IntSlice(indexes)))
			for _, idx := range indexes {
				if idx < len(result[i].Points) { // FIXME: find why in some cases index is greater than number of points
					result[i].Points = append(result[i].Points[:idx], result[i].Points[idx+1:]...)
				}
			}
		}
	}

	return result, nil
}

// Average returns a new series averaging each datapoints.
func Average(series []Series) (Series, error) {
	return applyOperator(series, OperatorAverage)
}

// Sum returns a new series summing each datapoints.
func Sum(series []Series) (Series, error) {
	return applyOperator(series, OperatorSum)
}

func applyOperator(series []Series, operator int) (Series, error) {
	length := len(series)
	if length == 0 {
		return Series{}, ErrEmptySeries
	}

	count := len(series[0].Points)

	result := Series{
		Points:  make([]Point, count),
		Summary: make(map[string]Value),
	}

	for i := 0; i < count; i++ {
		sumCount := 0

		result.Points[i].Time = series[0].Points[i].Time

		for _, s := range series {
			if s.Points == nil {
				continue
			} else if len(s.Points) != count {
				return Series{}, ErrUnnormalizedSeries
			} else if s.Points[i].Value.IsNaN() {
				continue
			}

			result.Points[i].Value += s.Points[i].Value
			sumCount++
		}

		if sumCount == 0 {
			result.Points[i].Value = Value(math.NaN())
		} else if operator == OperatorAverage {
			result.Points[i].Value /= Value(sumCount)
		}
	}

	return result, nil
}
<|endoftext|>


<commit_before>package server

import (
	"context"
	"fmt"
	"net/http"

	"github.com/influxdata/chronograf"
	"github.com/influxdata/chronograf/oauth2"
	"github.com/influxdata/chronograf/organizations"
	"github.com/influxdata/chronograf/roles"
)

// AuthorizedToken extracts the token and validates; if valid the next handler
// will be run.  The principal will be sent to the next handler via the request's
// Context.  It is up to the next handler to determine if the principal has access.
// On failure, will return http.StatusForbidden.
func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next http.Handler) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log := logger.
			WithField("component", "token_auth").
			WithField("remote_addr", r.RemoteAddr).
			WithField("method", r.Method).
			WithField("url", r.URL)

		ctx := r.Context()
		// We do not check the authorization of the principal.  Those
		// served further down the chain should do so.
		principal, err := auth.Validate(ctx, r)
		if err != nil {
			log.Error("Invalid principal")
			w.WriteHeader(http.StatusForbidden)
			return
		}

		// If the principal is valid we will extend its lifespan
		// into the future
		principal, err = auth.Extend(ctx, w, principal)
		if err != nil {
			log.Error("Unable to extend principal")
			w.WriteHeader(http.StatusForbidden)
			return
		}

		// Send the principal to the next handler
		ctx = context.WithValue(ctx, oauth2.PrincipalKey, principal)
		next.ServeHTTP(w, r.WithContext(ctx))
		return
	})
}

// AuthorizedUser extracts the user name and provider from context. If the
// user and provider can be found on the context, we look up the user by their
// name and provider. If the user is found, we verify that the user has at at
// least the role supplied.
func AuthorizedUser(
	store DataStore,
	useAuth bool,
	role string,
	logger chronograf.Logger,
	next http.HandlerFunc,
) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !useAuth {
			ctx := r.Context()
			defaultOrg, err := store.Organizations(ctx).DefaultOrganization(ctx)
			if err != nil {
				unknownErrorWithMessage(w, err, logger)
				return
			}
			// To access resources (servers, sources, databases, layouts) within a DataStore,
			// an organization and a role are required even if you are a super admin or are
			// not using auth. Every user's current organization is set on context to filter
			// the resources accessed within a DataStore, including for super admin or when
			// not using auth. In this way, a DataStore can treat all requests the same,
			// including those from a super admin and when not using auth.
			//
			// As for roles, in the case of super admin or when not using auth, the user's
			// role on context (though not on their JWT or user) is set to be admin. In order
			// to access all resources belonging to their current organization.
			ctx = context.WithValue(ctx, organizations.ContextKey, fmt.Sprintf("%d", defaultOrg.ID))
			ctx = context.WithValue(ctx, roles.ContextKey, roles.AdminRoleName)
			r = r.WithContext(ctx)
			next(w, r)
			return
		}

		log := logger.
			WithField("component", "role_auth").
			WithField("remote_addr", r.RemoteAddr).
			WithField("method", r.Method).
			WithField("url", r.URL)

		ctx := r.Context()

		p, err := getValidPrincipal(ctx)
		if err != nil {
			log.Error("Failed to retrieve principal from context")
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}
		scheme, err := getScheme(ctx)
		if err != nil {
			log.Error("Failed to retrieve scheme from context")
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}

		// This is as if the user was logged into the default organization
		if p.Organization == "" {
			defaultOrg, err := store.Organizations(ctx).DefaultOrganization(ctx)
			if err != nil {
				unknownErrorWithMessage(w, err, logger)
				return
			}
			p.Organization = fmt.Sprintf("%d", defaultOrg.ID)
		}

		// validate that the organization exists
		orgID, err := parseOrganizationID(p.Organization)
		if err != nil {
			log.Error("Failed to validate organization on context")
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}
		_, err = store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &orgID})
		if err != nil {
			log.Error(fmt.Sprintf("Failed to retrieve organization %d from organizations store", orgID))
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}

		ctx = context.WithValue(ctx, organizations.ContextKey, p.Organization)
		serverCtx := context.WithValue(ctx, SuperAdminKey, true)
		// To access resources (servers, sources, databases, layouts) within a DataStore,
		// an organization and a role are required even if you are a super admin or are
		// not using auth. Every user's current organization is set on context to filter
		// the resources accessed within a DataStore, including for super admin or when
		// not using auth. In this way, a DataStore can treat all requests the same,
		// including those from a super admin and when not using auth.
		//
		// As for roles, in the case of super admin or when not using auth, the user's
		// role on context (though not on their JWT or user) is set to be admin. In order
		// to access all resources belonging to their current organization.
		serverCtx = context.WithValue(serverCtx, roles.ContextKey, roles.AdminRoleName)
		// TODO: seems silly to look up a user twice
		u, err := store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{
			Name:     &p.Subject,
			Provider: &p.Issuer,
			Scheme:   &scheme,
		})

		if err != nil {
			log.Error("Failed to retrieve user")
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}

		if u.SuperAdmin {
			// This context is where superadmin gets set for all things
			r = r.WithContext(serverCtx)
			next(w, r)
			return
		}

		u, err = store.Users(ctx).Get(ctx, chronograf.UserQuery{
			Name:     &p.Subject,
			Provider: &p.Issuer,
			Scheme:   &scheme,
		})
		if err != nil {
			log.Error("Failed to retrieve user")
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}

		if hasAuthorizedRole(u, role) {
			// use the first role, since there should only ever be one
			// for any particular organization and hasAuthorizedRole
			// should ensure that at least one role for the org exists
			ctx = context.WithValue(ctx, roles.ContextKey, u.Roles[0].Name)
			r = r.WithContext(ctx)
			next(w, r)
			return
		}

		Error(w, http.StatusUnauthorized, "User is not authorized", logger)
		return
	})
}

func hasAuthorizedRole(u *chronograf.User, role string) bool {
	if u == nil {
		return false
	}

	switch role {
	case roles.ViewerRoleName:
		for _, r := range u.Roles {
			switch r.Name {
			case roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
				return true
			}
		}
	case roles.EditorRoleName:
		for _, r := range u.Roles {
			switch r.Name {
			case roles.EditorRoleName, roles.AdminRoleName:
				return true
			}
		}
	case roles.AdminRoleName:
		for _, r := range u.Roles {
			switch r.Name {
			case roles.AdminRoleName:
				return true
			}
		}
	case roles.SuperAdminStatus:
		// SuperAdmins should have been authorized before this.
		// This is only meant to restrict access for non-superadmins.
		return false
	}

	return false
}
<commit_msg>Add verification that user does not have more than one role in org<commit_after>package server

import (
	"context"
	"fmt"
	"net/http"

	"github.com/influxdata/chronograf"
	"github.com/influxdata/chronograf/oauth2"
	"github.com/influxdata/chronograf/organizations"
	"github.com/influxdata/chronograf/roles"
)

// AuthorizedToken extracts the token and validates; if valid the next handler
// will be run.  The principal will be sent to the next handler via the request's
// Context.  It is up to the next handler to determine if the principal has access.
// On failure, will return http.StatusForbidden.
func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next http.Handler) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log := logger.
			WithField("component", "token_auth").
			WithField("remote_addr", r.RemoteAddr).
			WithField("method", r.Method).
			WithField("url", r.URL)

		ctx := r.Context()
		// We do not check the authorization of the principal.  Those
		// served further down the chain should do so.
		principal, err := auth.Validate(ctx, r)
		if err != nil {
			log.Error("Invalid principal")
			w.WriteHeader(http.StatusForbidden)
			return
		}

		// If the principal is valid we will extend its lifespan
		// into the future
		principal, err = auth.Extend(ctx, w, principal)
		if err != nil {
			log.Error("Unable to extend principal")
			w.WriteHeader(http.StatusForbidden)
			return
		}

		// Send the principal to the next handler
		ctx = context.WithValue(ctx, oauth2.PrincipalKey, principal)
		next.ServeHTTP(w, r.WithContext(ctx))
		return
	})
}

// AuthorizedUser extracts the user name and provider from context. If the
// user and provider can be found on the context, we look up the user by their
// name and provider. If the user is found, we verify that the user has at at
// least the role supplied.
func AuthorizedUser(
	store DataStore,
	useAuth bool,
	role string,
	logger chronograf.Logger,
	next http.HandlerFunc,
) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !useAuth {
			ctx := r.Context()
			defaultOrg, err := store.Organizations(ctx).DefaultOrganization(ctx)
			if err != nil {
				unknownErrorWithMessage(w, err, logger)
				return
			}
			// To access resources (servers, sources, databases, layouts) within a DataStore,
			// an organization and a role are required even if you are a super admin or are
			// not using auth. Every user's current organization is set on context to filter
			// the resources accessed within a DataStore, including for super admin or when
			// not using auth. In this way, a DataStore can treat all requests the same,
			// including those from a super admin and when not using auth.
			//
			// As for roles, in the case of super admin or when not using auth, the user's
			// role on context (though not on their JWT or user) is set to be admin. In order
			// to access all resources belonging to their current organization.
			ctx = context.WithValue(ctx, organizations.ContextKey, fmt.Sprintf("%d", defaultOrg.ID))
			ctx = context.WithValue(ctx, roles.ContextKey, roles.AdminRoleName)
			r = r.WithContext(ctx)
			next(w, r)
			return
		}

		log := logger.
			WithField("component", "role_auth").
			WithField("remote_addr", r.RemoteAddr).
			WithField("method", r.Method).
			WithField("url", r.URL)

		ctx := r.Context()

		p, err := getValidPrincipal(ctx)
		if err != nil {
			log.Error("Failed to retrieve principal from context")
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}
		scheme, err := getScheme(ctx)
		if err != nil {
			log.Error("Failed to retrieve scheme from context")
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}

		// This is as if the user was logged into the default organization
		if p.Organization == "" {
			defaultOrg, err := store.Organizations(ctx).DefaultOrganization(ctx)
			if err != nil {
				unknownErrorWithMessage(w, err, logger)
				return
			}
			p.Organization = fmt.Sprintf("%d", defaultOrg.ID)
		}

		// validate that the organization exists
		orgID, err := parseOrganizationID(p.Organization)
		if err != nil {
			log.Error("Failed to validate organization on context")
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}
		_, err = store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &orgID})
		if err != nil {
			log.Error(fmt.Sprintf("Failed to retrieve organization %d from organizations store", orgID))
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}

		ctx = context.WithValue(ctx, organizations.ContextKey, p.Organization)
		serverCtx := context.WithValue(ctx, SuperAdminKey, true)
		// To access resources (servers, sources, databases, layouts) within a DataStore,
		// an organization and a role are required even if you are a super admin or are
		// not using auth. Every user's current organization is set on context to filter
		// the resources accessed within a DataStore, including for super admin or when
		// not using auth. In this way, a DataStore can treat all requests the same,
		// including those from a super admin and when not using auth.
		//
		// As for roles, in the case of super admin or when not using auth, the user's
		// role on context (though not on their JWT or user) is set to be admin. In order
		// to access all resources belonging to their current organization.
		serverCtx = context.WithValue(serverCtx, roles.ContextKey, roles.AdminRoleName)
		// TODO: seems silly to look up a user twice
		u, err := store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{
			Name:     &p.Subject,
			Provider: &p.Issuer,
			Scheme:   &scheme,
		})

		if err != nil {
			log.Error("Failed to retrieve user")
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}

		if u.SuperAdmin {
			// This context is where superadmin gets set for all things
			r = r.WithContext(serverCtx)
			next(w, r)
			return
		}

		u, err = store.Users(ctx).Get(ctx, chronograf.UserQuery{
			Name:     &p.Subject,
			Provider: &p.Issuer,
			Scheme:   &scheme,
		})
		if err != nil {
			log.Error("Failed to retrieve user")
			Error(w, http.StatusUnauthorized, "User is not authorized", logger)
			return
		}

		if hasAuthorizedRole(u, role) {
			if len(u.Roles) != 1 {
				msg := `User %d has too many role in organization. User: %#v.Please report this log at https://github.com/influxdata/chronograf/issues/new"`
				log.Error(fmt.Sprint(msg, u.ID, u))
				unknownErrorWithMessage(w, fmt.Errorf("please have administrator check logs and report error"), logger)
				return
			}
			// use the first role, since there should only ever be one
			// for any particular organization and hasAuthorizedRole
			// should ensure that at least one role for the org exists
			ctx = context.WithValue(ctx, roles.ContextKey, u.Roles[0].Name)
			r = r.WithContext(ctx)
			next(w, r)
			return
		}

		Error(w, http.StatusUnauthorized, "User is not authorized", logger)
		return
	})
}

func hasAuthorizedRole(u *chronograf.User, role string) bool {
	if u == nil {
		return false
	}

	switch role {
	case roles.ViewerRoleName:
		for _, r := range u.Roles {
			switch r.Name {
			case roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
				return true
			}
		}
	case roles.EditorRoleName:
		for _, r := range u.Roles {
			switch r.Name {
			case roles.EditorRoleName, roles.AdminRoleName:
				return true
			}
		}
	case roles.AdminRoleName:
		for _, r := range u.Roles {
			switch r.Name {
			case roles.AdminRoleName:
				return true
			}
		}
	case roles.SuperAdminStatus:
		// SuperAdmins should have been authorized before this.
		// This is only meant to restrict access for non-superadmins.
		return false
	}

	return false
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"flag"
	"log"
	"net"
	"time"
)

var port = flag.String("port", ":5050", "The port to run the server on.")

var multiverseDL []byte
var multiverseModified time.Time

func main() {
	var err error
	log.Println("Downloading multiverse.")

	multiverseDL, multiverseModified, err = getMultiverseData()

	if err != nil {
		log.Fatalln("Unable to download multiverse.")
	}

	log.Println("Multiverse downloaded.")

	go updateMultiverse()

	log.Println("Starting server.")

	ln, err := net.Listen("tcp", *port)

	for {
		conn, err := ln.Accept()

		if err != nil {
			log.Printf("Error accepting connection: %s\n", err)
		} else {
			go provideDownload(conn)
		}
	}
}

func updateMultiverse() {
	dailyUpdate := time.Tick(time.Hour * 12)

	for {
		<-dailyUpdate
		log.Println("Checking for update.")
		mod, err := onlineModifiedAt()

		if mod == multiverseModified {
			log.Println("Multiverse up-to-date.")
			continue
		}

		if err != nil {
			continue
		}

		log.Println("Update found! Downloading...")

		newData, newMod, err := getMultiverseData()

		if err != nil {
			continue
		}

		log.Println("Update applied.")

		multiverseDL, multiverseModified = newData, newMod
	}
}

func provideDownload(conn net.Conn) {
	s := bufio.NewScanner(conn)
	s.Scan()

	switch text := s.Text(); text {
	case "multiverseMod":
		log.Println("Timestamp accessed.")
		conn.Write([]byte(multiverseModified.Format(lastModifiedFormat) + "\n"))
		conn.Close()
	case "multiverseDL":
		log.Println("Multiverse downloaded.")
		conn.Write(multiverseDL)
		conn.Close()
	default:
		log.Printf("Unrecognized request '%s'.\n", text)
	}
}
<commit_msg>Added some comments.<commit_after>package main

import (
	"bufio"
	"flag"
	"log"
	"net"
	"time"
)

var port = flag.String("port", ":5050", "The port to run the server on.")

var multiverseDL []byte
var multiverseModified time.Time

func main() {
	var err error
	log.Println("Downloading multiverse.")

	multiverseDL, multiverseModified, err = getMultiverseData()

	if err != nil {
		log.Fatalln("Unable to download multiverse.")
	}

	log.Println("Multiverse downloaded.")

	go updateMultiverse()

	log.Println("Starting server.")

	ln, err := net.Listen("tcp", *port)

	for {
		conn, err := ln.Accept()

		if err != nil {
			log.Printf("Error accepting connection: %s\n", err)
		} else {
			go provideDownload(conn)
		}
	}
}

func updateMultiverse() {
	dailyUpdate := time.Tick(time.Hour * 12)

	for {
		<-dailyUpdate
		log.Println("Checking for update.")
		mod, err := onlineModifiedAt()

		if mod == multiverseModified {
			log.Println("Multiverse up-to-date.")
			continue
		}

		if err != nil {
			continue
		}

		log.Println("Update found! Downloading...")

		newData, newMod, err := getMultiverseData()

		if err != nil {
			continue
		}

		log.Println("Update applied.")

		multiverseDL, multiverseModified = newData, newMod
	}
}

func provideDownload(conn net.Conn) {
	s := bufio.NewScanner(conn)
	s.Scan()

	switch text := s.Text(); text {

	// We want to know the modification time of the multiverse.
	case "multiverseMod":
		log.Println("Timestamp accessed.")
		conn.Write([]byte(multiverseModified.Format(lastModifiedFormat) + "\n"))
		conn.Close()

	// We want to download the multiverse.
	case "multiverseDL":
		log.Println("Multiverse downloaded.")
		conn.Write(multiverseDL)
		conn.Close()

	default:
		log.Printf("Unrecognized request '%s'.\n", text)
	}
}
<|endoftext|>


<commit_before>package schema2

import (
	"encoding/json"
	"errors"
	"fmt"

	"github.com/docker/distribution"
	"github.com/docker/distribution/digest"
	"github.com/docker/distribution/manifest"
)

const (
	// MediaTypeManifest specifies the mediaType for the current version.
	MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"

	// MediaTypeConfig specifies the mediaType for the image configuration.
	MediaTypeConfig = "application/vnd.docker.container.image.v1+json"

	// MediaTypeLayer is the mediaType used for layers referenced by the
	// manifest.
	MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip"

	// MediaTypeForeignLayer is the mediaType used for layers that must be
	// downloaded from foreign URLs.
	MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
)

var (
	// SchemaVersion provides a pre-initialized version structure for this
	// packages version of the manifest.
	SchemaVersion = manifest.Versioned{
		SchemaVersion: 2,
		MediaType:     MediaTypeManifest,
	}
)

func init() {
	schema2Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
		m := new(DeserializedManifest)
		err := m.UnmarshalJSON(b)
		if err != nil {
			return nil, distribution.Descriptor{}, err
		}

		dgst := digest.FromBytes(b)
		return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifest}, err
	}
	err := distribution.RegisterManifestSchema(MediaTypeManifest, schema2Func)
	if err != nil {
		panic(fmt.Sprintf("Unable to register manifest: %s", err))
	}
}

// Manifest defines a schema2 manifest.
type Manifest struct {
	manifest.Versioned

	// Config references the image configuration as a blob.
	Config distribution.Descriptor `json:"config"`

	// Layers lists descriptors for the layers referenced by the
	// configuration.
	Layers []distribution.Descriptor `json:"layers"`
}

// References returnes the descriptors of this manifests references.
func (m Manifest) References() []distribution.Descriptor {
	return m.Layers
}

// Target returns the target of this signed manifest.
func (m Manifest) Target() distribution.Descriptor {
	return m.Config
}

// DeserializedManifest wraps Manifest with a copy of the original JSON.
// It satisfies the distribution.Manifest interface.
type DeserializedManifest struct {
	Manifest

	// canonical is the canonical byte representation of the Manifest.
	canonical []byte
}

// FromStruct takes a Manifest structure, marshals it to JSON, and returns a
// DeserializedManifest which contains the manifest and its JSON representation.
func FromStruct(m Manifest) (*DeserializedManifest, error) {
	var deserialized DeserializedManifest
	deserialized.Manifest = m

	var err error
	deserialized.canonical, err = json.MarshalIndent(&m, "", "   ")
	return &deserialized, err
}

// UnmarshalJSON populates a new Manifest struct from JSON data.
func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
	m.canonical = make([]byte, len(b), len(b))
	// store manifest in canonical
	copy(m.canonical, b)

	// Unmarshal canonical JSON into Manifest object
	var manifest Manifest
	if err := json.Unmarshal(m.canonical, &manifest); err != nil {
		return err
	}

	m.Manifest = manifest

	return nil
}

// MarshalJSON returns the contents of canonical. If canonical is empty,
// marshals the inner contents.
func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
	if len(m.canonical) > 0 {
		return m.canonical, nil
	}

	return nil, errors.New("JSON representation not initialized in DeserializedManifest")
}

// Payload returns the raw content of the manifest. The contents can be used to
// calculate the content identifier.
func (m DeserializedManifest) Payload() (string, []byte, error) {
	return m.MediaType, m.canonical, nil
}
<commit_msg>Add plugin mediatype to distribution manifest.<commit_after>package schema2

import (
	"encoding/json"
	"errors"
	"fmt"

	"github.com/docker/distribution"
	"github.com/docker/distribution/digest"
	"github.com/docker/distribution/manifest"
)

const (
	// MediaTypeManifest specifies the mediaType for the current version.
	MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"

	// MediaTypeConfig specifies the mediaType for the image configuration.
	MediaTypeConfig = "application/vnd.docker.container.image.v1+json"

	// MediaTypePluginConfig specifies the mediaType for plugin configuration.
	MediaTypePluginConfig = "application/vnd.docker.plugin.v0+json"

	// MediaTypeLayer is the mediaType used for layers referenced by the
	// manifest.
	MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip"

	// MediaTypeForeignLayer is the mediaType used for layers that must be
	// downloaded from foreign URLs.
	MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
)

var (
	// SchemaVersion provides a pre-initialized version structure for this
	// packages version of the manifest.
	SchemaVersion = manifest.Versioned{
		SchemaVersion: 2,
		MediaType:     MediaTypeManifest,
	}
)

func init() {
	schema2Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
		m := new(DeserializedManifest)
		err := m.UnmarshalJSON(b)
		if err != nil {
			return nil, distribution.Descriptor{}, err
		}

		dgst := digest.FromBytes(b)
		return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifest}, err
	}
	err := distribution.RegisterManifestSchema(MediaTypeManifest, schema2Func)
	if err != nil {
		panic(fmt.Sprintf("Unable to register manifest: %s", err))
	}
}

// Manifest defines a schema2 manifest.
type Manifest struct {
	manifest.Versioned

	// Config references the image configuration as a blob.
	Config distribution.Descriptor `json:"config"`

	// Layers lists descriptors for the layers referenced by the
	// configuration.
	Layers []distribution.Descriptor `json:"layers"`
}

// References returnes the descriptors of this manifests references.
func (m Manifest) References() []distribution.Descriptor {
	return m.Layers
}

// Target returns the target of this signed manifest.
func (m Manifest) Target() distribution.Descriptor {
	return m.Config
}

// DeserializedManifest wraps Manifest with a copy of the original JSON.
// It satisfies the distribution.Manifest interface.
type DeserializedManifest struct {
	Manifest

	// canonical is the canonical byte representation of the Manifest.
	canonical []byte
}

// FromStruct takes a Manifest structure, marshals it to JSON, and returns a
// DeserializedManifest which contains the manifest and its JSON representation.
func FromStruct(m Manifest) (*DeserializedManifest, error) {
	var deserialized DeserializedManifest
	deserialized.Manifest = m

	var err error
	deserialized.canonical, err = json.MarshalIndent(&m, "", "   ")
	return &deserialized, err
}

// UnmarshalJSON populates a new Manifest struct from JSON data.
func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
	m.canonical = make([]byte, len(b), len(b))
	// store manifest in canonical
	copy(m.canonical, b)

	// Unmarshal canonical JSON into Manifest object
	var manifest Manifest
	if err := json.Unmarshal(m.canonical, &manifest); err != nil {
		return err
	}

	m.Manifest = manifest

	return nil
}

// MarshalJSON returns the contents of canonical. If canonical is empty,
// marshals the inner contents.
func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
	if len(m.canonical) > 0 {
		return m.canonical, nil
	}

	return nil, errors.New("JSON representation not initialized in DeserializedManifest")
}

// Payload returns the raw content of the manifest. The contents can be used to
// calculate the content identifier.
func (m DeserializedManifest) Payload() (string, []byte, error) {
	return m.MediaType, m.canonical, nil
}
<|endoftext|>


<commit_before>package marathon

import (
	"testing"
)

var c MarathonClient = MarathonClient{}

func TestCreateApplicationFromFile(t *testing.T) {

	envParams := make(map[string]string, 1)
	envParams["NODE_EXPORTER_VERSION"] = "1"

	opts := &CreateOptions{Wait: false, Force: true, ErrorOnMissingParams: true, EnvParams: envParams}
	app, _, err := c.parseApplicationFromFile("resources/marathon.json", opts)
	if err != nil {
		log.Panic("Expected success %v", err)
	}
	log.Debug("%v", app)
	if app.Labels["tags"] != "prom-metrics" {
		log.Panic("Expected Labels parsed correctly")
	}
	if app.Instances != int(2) {
		log.Panic("Expected Instances parsed correctly")
	}

}
<commit_msg>fix tests<commit_after>package marathon

import (
	"testing"
)

var c MarathonClient = MarathonClient{}

func TestCreateApplicationFromFile(t *testing.T) {

	envParams := make(map[string]string, 1)
	envParams["NODE_EXPORTER_VERSION"] = "1"

	opts := &CreateOptions{Wait: false, Force: true, ErrorOnMissingParams: true, EnvParams: envParams}
	app, _, err := c.ParseApplicationFromFile("resources/marathon.json", opts)
	if err != nil {
		log.Panic("Expected success %v", err)
	}
	log.Debug("%v", app)
	if app.Labels["tags"] != "prom-metrics" {
		log.Panic("Expected Labels parsed correctly")
	}
	if app.Instances != int(2) {
		log.Panic("Expected Instances parsed correctly")
	}

}
<|endoftext|>


<commit_before>// +build darwin

package memory

import (
	"io"
	"reflect"
	"strings"
	"testing"
)

type memoryGeneratorMock struct {
}

func (generator memoryGeneratorMock) Name() string {
	return "memory-tester"
}

func (generator memoryGeneratorMock) Start() error {
	return nil
}

func (generator memoryGeneratorMock) Output() (io.Reader, error) {
	return strings.NewReader(
		`Mach Virtual Memory Statistics: (page size of 4096 bytes)
Pages free:                               72827.
Pages active:                           2154445.
Pages inactive:                         1511468.
Pages speculative:                         8107.
Pages throttled:                              0.
Pages wired down:                        446975.
Pages purgeable:                         383371.
"Translation faults":                  97589077.
Pages copy-on-write:                    3305869.
Pages zero filled:                     50848672.
Pages reactivated:                         1999.
Pages purged:                           2496610.
File-backed pages:                       677870.
Anonymous pages:                        2996150.
Pages stored in compressor:                   0.
Pages occupied by compressor:                 0.
Decompressions:                               0.
Compressions:                                 0.
Pageins:                                6333901.
Pageouts:                                   353.
Swapins:                                      0.
Swapouts:                                     0.
`), nil
}

func (generator memoryGeneratorMock) Finish() error {
	return nil
}

func Test_GetMemory(t *testing.T) {
	_, err := Get()
	if err != nil {
		t.Errorf("error should be nil but got: %v", err)
	}
}

func Test_collectMemoryStats(t *testing.T) {
	got, err := collectMemoryStats(memoryGeneratorMock{})
	if err != nil {
		t.Errorf("error should be nil but got: %v", err)
	}
	pageSize := 4096
	expected := Memory{
		Total:    uint64((446975 + 2154445 + 1511468 + 8107 + 72827) * pageSize),
		Used:     uint64((446975 + 2154445 + 1511468 + 8107 - (383371 + 677870)) * pageSize),
		Cached:   uint64((383371 + 677870) * pageSize),
		Free:     uint64(72827 * pageSize),
		Active:   uint64(2154445 * pageSize),
		Inactive: uint64(1511468 * pageSize),
	}
	if !reflect.DeepEqual(got, expected) {
		t.Errorf("invalid memory value: %v (expected: %v)", got, expected)
	}
}
<commit_msg>add tests for collectSwapStats<commit_after>// +build darwin

package memory

import (
	"io"
	"reflect"
	"strings"
	"testing"
)

type memoryGeneratorMock struct {
}

func (generator memoryGeneratorMock) Start() error {
	return nil
}

func (generator memoryGeneratorMock) Output() (io.Reader, error) {
	return strings.NewReader(
		`Mach Virtual Memory Statistics: (page size of 4096 bytes)
Pages free:                               72827.
Pages active:                           2154445.
Pages inactive:                         1511468.
Pages speculative:                         8107.
Pages throttled:                              0.
Pages wired down:                        446975.
Pages purgeable:                         383371.
"Translation faults":                  97589077.
Pages copy-on-write:                    3305869.
Pages zero filled:                     50848672.
Pages reactivated:                         1999.
Pages purged:                           2496610.
File-backed pages:                       677870.
Anonymous pages:                        2996150.
Pages stored in compressor:                   0.
Pages occupied by compressor:                 0.
Decompressions:                               0.
Compressions:                                 0.
Pageins:                                6333901.
Pageouts:                                   353.
Swapins:                                      0.
Swapouts:                                     0.
`), nil
}

func (generator memoryGeneratorMock) Finish() error {
	return nil
}

func Test_GetMemory(t *testing.T) {
	_, err := Get()
	if err != nil {
		t.Errorf("error should be nil but got: %v", err)
	}
}

func Test_collectMemoryStats(t *testing.T) {
	got, err := collectMemoryStats(memoryGeneratorMock{})
	if err != nil {
		t.Errorf("error should be nil but got: %v", err)
	}
	pageSize := 4096
	expected := Memory{
		Total:    uint64((446975 + 2154445 + 1511468 + 8107 + 72827) * pageSize),
		Used:     uint64((446975 + 2154445 + 1511468 + 8107 - (383371 + 677870)) * pageSize),
		Cached:   uint64((383371 + 677870) * pageSize),
		Free:     uint64(72827 * pageSize),
		Active:   uint64(2154445 * pageSize),
		Inactive: uint64(1511468 * pageSize),
	}
	if !reflect.DeepEqual(got, expected) {
		t.Errorf("invalid memory value: %v (expected: %v)", got, expected)
	}
}

type swapGeneratorMock struct {
}

func (generator swapGeneratorMock) Start() error {
	return nil
}

func (generator swapGeneratorMock) Output() (io.Reader, error) {
	return strings.NewReader(
		`total = 4096.00M  used = 3184.75M  free = 911.25M  (encrypted)
`), nil
}

func (generator swapGeneratorMock) Finish() error {
	return nil
}

func Test_collectSwapStats(t *testing.T) {
	got, err := collectSwapStats(swapGeneratorMock{})
	if err != nil {
		t.Errorf("error should be nil but got: %v", err)
	}
	expected := memorySwap{
		total: uint64(4096.00 * 1024 * 1024),
		used:  uint64(3184.75 * 1024 * 1024),
		free:  uint64(911.25 * 1024 * 1024),
	}
	if !reflect.DeepEqual(got, expected) {
		t.Errorf("invalid memory swap value: %v (expected: %v)", got, expected)
	}
}
<|endoftext|>


<commit_before>package messages

import (
	"bytes"
	gio "github.com/gogo/protobuf/io"
	"github.com/stretchr/testify/require"
	"math"
	"testing"
)

func TestMessages(t *testing.T) {
	t.Run("builds a pickle doc string", func(t *testing.T) {
		pickleDocString := PickleStepArgument_PickleDocString{
			MediaType: "text/plain",
			Content:   "some\ncontent\n",
		}

		b := &bytes.Buffer{}
		writer := gio.NewDelimitedWriter(b)
		writer.WriteMsg(&pickleDocString)

		r := gio.NewDelimitedReader(b, math.MaxInt32)
		var decoded PickleStepArgument_PickleDocString
		r.ReadMsg(&decoded)
		require.Equal(t, "some\ncontent\n", decoded.Content)
	})

	t.Run("builds a step", func(t *testing.T) {
		step := &GherkinDocument_Feature_Step{
			Keyword: "Given",
			Text:    "the following message:",
			Location: &Location{
				Line:   11,
				Column: 4,
			},

			Argument: &GherkinDocument_Feature_Step_DocString_{
				DocString: &GherkinDocument_Feature_Step_DocString{
					Content: "Hello",
					Location: &Location{
						Line:   12,
						Column: 6,
					},
				},
			},
		}

		b := &bytes.Buffer{}
		writer := gio.NewDelimitedWriter(b)
		writer.WriteMsg(step)

		r := gio.NewDelimitedReader(b, 4096)
		var decoded GherkinDocument_Feature_Step
		r.ReadMsg(&decoded)
		require.Equal(t, "Hello", decoded.GetDocString().Content)
	})
}
<commit_msg>Add failing test for #901<commit_after>package messages

import (
	"bytes"
	fio "github.com/cucumber/messages-go/v10/io"
	gio "github.com/gogo/protobuf/io"
	"github.com/stretchr/testify/require"
	"math"
	"testing"
)

func TestMessages(t *testing.T) {
	t.Run("builds a pickle doc string", func(t *testing.T) {
		pickleDocString := PickleStepArgument_PickleDocString{
			MediaType: "text/plain",
			Content:   "some\ncontent\n",
		}

		b := &bytes.Buffer{}
		writer := gio.NewDelimitedWriter(b)
		require.NoError(t, writer.WriteMsg(&pickleDocString))

		r := gio.NewDelimitedReader(b, math.MaxInt32)
		var decoded PickleStepArgument_PickleDocString
		require.NoError(t, r.ReadMsg(&decoded))
		require.Equal(t, "some\ncontent\n", decoded.Content)
	})

	t.Run("builds a step", func(t *testing.T) {
		step := &GherkinDocument_Feature_Step{
			Keyword: "Given",
			Text:    "the following message:",
			Location: &Location{
				Line:   11,
				Column: 4,
			},

			Argument: &GherkinDocument_Feature_Step_DocString_{
				DocString: &GherkinDocument_Feature_Step_DocString{
					Content: "Hello",
					Location: &Location{
						Line:   12,
						Column: 6,
					},
				},
			},
		}

		b := &bytes.Buffer{}
		writer := gio.NewDelimitedWriter(b)
		require.NoError(t, writer.WriteMsg(step))

		r := gio.NewDelimitedReader(b, 4096)
		var decoded GherkinDocument_Feature_Step
		require.NoError(t, r.ReadMsg(&decoded))
		require.Equal(t, "Hello", decoded.GetDocString().Content)
	})

	t.Run("reads an attachment with a tiny string as NDJSON", func(t *testing.T) {
		attachment := &Attachment{
			Body: &Attachment_Text{Text: "Hello"},
		}
		b := &bytes.Buffer{}
		writer := fio.NewNdjsonWriter(b)
		require.NoError(t, writer.WriteMsg(attachment))
		r := fio.NewNdjsonReader(b)
		var decoded Attachment
		require.NoError(t, r.ReadMsg(&decoded))
		require.Equal(t, "Hello", decoded.GetText())
	})

	t.Run("reads an attachment with a 70k string as NDJSON", func(t *testing.T) {
		ba := make([]byte, 70000)
		for i := range ba {
			ba[i] = "x"[0]
		}
		s := string(ba)
		attachment := &Attachment{
			Body: &Attachment_Text{Text: s},
		}
		b := &bytes.Buffer{}
		writer := fio.NewNdjsonWriter(b)
		require.NoError(t, writer.WriteMsg(attachment))
		r := fio.NewNdjsonReader(b)
		var decoded Attachment
		require.NoError(t, r.ReadMsg(&decoded))
		require.Equal(t, s, decoded.GetText())
	})
}
<|endoftext|>