<commit_before>package main

import (
	"flag"
	"fmt"
	"go/parser"
	"go/token"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"gopkg.in/fsnotify.v1"
)

type ColorLogger log.Logger

const (
	logPrefix  = "\033[33m"
	logPostfix = "\033[0m"
)

var (
	watchedRun = ""
	watchedFmt = "."
	noRun      = flag.Bool("n", false, "only run gofmt")
	delay      = flag.Int("d", 1, "delay time before detecting file change")
	isPipe     = false
	exitCode   = 0
	lastReport = make(map[string]time.Time)
)

// func hasMainFunction(f os.File) Bool {
//
// }

func getPackageNameAndImport(sourceName string) (packageName string, imports []string) {
	fset := token.NewFileSet() // positions are relative to fset

	// Parse the file containing this very example
	// but stop after processing the imports.
	f, err := parser.ParseFile(fset, sourceName, nil, parser.ImportsOnly)
	if err != nil {
		fmt.Println(err)
		return
	}

	// Print the imports from the file's AST.
	packageName = f.Name.Name

	for _, s := range f.Imports {
		importedPackageName := s.Path.Value[1 : len(s.Path.Value)-1]
		imports = append(imports, importedPackageName)
	}

	return
}

func getMainFile(path string) string {
	fi, _ := ioutil.ReadDir(path)
	for _, f := range fi {
		fmt.Println(f.Name())
		if filepath.Ext(f.Name()) == ".go" {
			fmt.Println(isMainPackage(f.Name()))
		}
	}
	return ""
}

func isGoFile(f os.FileInfo) bool {
	// ignore non-Go files
	name := f.Name()
	return !f.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go")
}

func isMainPackage(sourceName string) bool {
	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, sourceName, nil, parser.ImportsOnly)

	return f.Name.Name == "main"
}

func visitFile(path string, info os.FileInfo, err error) error {
	if isGoFile(info) {
		format(path)
	}
	return nil
}

func format(path string) error {
	log.Println("gofmt -w", path, logPostfix)
	command := exec.Command("gofmt", "-w", path)
	command.Stdout = os.Stdout
	command.Stderr = os.Stderr
	command.Run()
	return nil
}

func main() {
	gowatchMain()
	os.Exit(exitCode)
}

func gowatchMain() {
	flag.Parse()

	log.SetPrefix(logPrefix)

	path, err := filepath.Abs(watchedFmt)
	if err != nil {
		panic(err)
	}

	// switch flag.NArg() {
	// case 0:
	// case 1:
	// 	watchedRun = flag.Arg(0)
	// case 2:
	// default:
	// }
	//
	// watchedRun = getMainFile(path)
	// fmt.Println(watchedRun)

	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		panic(err)
	}

	err = watcher.Add(path)
	if err != nil {
		panic(err)
	}
	defer watcher.Close()

	log.Println("watching", path, logPostfix)

	filepath.Walk(path, visitFile)

	fi, _ := os.Stdout.Stat()
	isPipe = fi.Mode()&os.ModeNamedPipe != 0

	for event := range watcher.Events {
		if event.Op == fsnotify.Create || event.Op == fsnotify.Write {
			if time.Since(lastReport[event.Name]) < time.Duration(*delay)*time.Second {
				continue
			}
			lastReport[event.Name] = time.Now()
			relPath, _ := filepath.Rel(path, event.Name)
			if isPipe {
				io.WriteString(os.Stdout, relPath+"\n")
			} else {
				if f, _ := os.Stat(event.Name); isGoFile(f) {
					log.Println("gofmt -w", relPath, logPostfix)
					format(event.Name)
					// if packageName, _ := getPackageNameAndImport(event.Name); packageName == "main" && !*noRun {
					// 	log.Println("run", relPath, logPostfix)
					// 	command = exec.Command("go", "run", event.Name)
					// 	command.Stdout = os.Stdout
					// 	command.Stderr = os.Stderr
					// 	command.Run()
					// }
				}
			}
		}
	}
}
<commit_msg>Write a function to detect main file<commit_after>package main

import (
	"flag"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"gopkg.in/fsnotify.v1"
)

type ColorLogger log.Logger

const (
	logPrefix  = "\033[33m"
	logPostfix = "\033[0m"
)

var (
	watchedRun = ""
	watchedFmt = "."
	noRun      = flag.Bool("n", false, "only run gofmt")
	delay      = flag.Int("d", 1, "delay time before detecting file change")
	isPipe     = false
	exitCode   = 0
	lastReport = make(map[string]time.Time)
)

// func hasMainFunction(f os.File) Bool {
//
// }

func getPackageNameAndImport(sourceName string) (packageName string, imports []string) {
	fset := token.NewFileSet() // positions are relative to fset

	// Parse the file containing this very example
	// but stop after processing the imports.
	f, err := parser.ParseFile(fset, sourceName, nil, parser.ImportsOnly)
	if err != nil {
		fmt.Println(err)
		return
	}

	// Print the imports from the file's AST.
	packageName = f.Name.Name

	for _, s := range f.Imports {
		importedPackageName := s.Path.Value[1 : len(s.Path.Value)-1]
		imports = append(imports, importedPackageName)
	}

	return
}

func getMainFile(path string) string {
	fi, _ := ioutil.ReadDir(path)
	for _, f := range fi {
		fmt.Println(f.Name())
		if filepath.Ext(f.Name()) == ".go" {
			fmt.Println(isMainPackage(f.Name()))
		}
	}
	return ""
}

func isGoFile(f os.FileInfo) bool {
	// ignore non-Go files
	name := f.Name()
	return !f.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go")
}

func isMainPackage(sourceName string) bool {
	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, sourceName, nil, parser.ImportsOnly)

	return f.Name.Name == "main"
}

func isMainFile(sourceName string) bool {
	fset := token.NewFileSet()

	f, err := parser.ParseFile(fset, sourceName, nil, 0)
	if err != nil {
		return false
	}

	if o := f.Scope.Lookup("main"); f.Name.Name == "main" && o != nil && o.Kind == ast.Fun {
		return true
	}

	return false
}

func visitFile(path string, info os.FileInfo, err error) error {
	if isGoFile(info) {
		format(path)
		if isMainFile(path) {
			fmt.Println("main file -", path)
		}
	}
	return nil
}

func format(path string) error {
	log.Println("gofmt -w", path, logPostfix)
	command := exec.Command("gofmt", "-w", path)
	command.Stdout = os.Stdout
	command.Stderr = os.Stderr
	command.Run()
	return nil
}

func main() {
	gowatchMain()
	os.Exit(exitCode)
}

func gowatchMain() {
	flag.Parse()

	log.SetPrefix(logPrefix)

	path, err := filepath.Abs(watchedFmt)
	if err != nil {
		panic(err)
	}

	// switch flag.NArg() {
	// case 0:
	// case 1:
	// 	watchedRun = flag.Arg(0)
	// case 2:
	// default:
	// }
	//
	// watchedRun = getMainFile(path)
	// fmt.Println(watchedRun)

	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		panic(err)
	}

	err = watcher.Add(path)
	if err != nil {
		panic(err)
	}
	defer watcher.Close()

	log.Println("watching", path, logPostfix)

	filepath.Walk(path, visitFile)

	fi, _ := os.Stdout.Stat()
	isPipe = fi.Mode()&os.ModeNamedPipe != 0

	for event := range watcher.Events {
		if event.Op == fsnotify.Create || event.Op == fsnotify.Write {
			if time.Since(lastReport[event.Name]) < time.Duration(*delay)*time.Second {
				continue
			}
			lastReport[event.Name] = time.Now()
			relPath, _ := filepath.Rel(path, event.Name)
			if isPipe {
				io.WriteString(os.Stdout, relPath+"\n")
			} else {
				if f, _ := os.Stat(event.Name); isGoFile(f) {
					log.Println("gofmt -w", relPath, logPostfix)
					format(event.Name)
					// if packageName, _ := getPackageNameAndImport(event.Name); packageName == "main" && !*noRun {
					// 	log.Println("run", relPath, logPostfix)
					// 	command = exec.Command("go", "run", event.Name)
					// 	command.Stdout = os.Stdout
					// 	command.Stderr = os.Stderr
					// 	command.Run()
					// }
				}
			}
		}
	}
}
<|endoftext|>


<commit_before>package addrs

import (
	"bytes"
	"fmt"
	"net"
	"sort"
	"strings"
)

// SortedIPs type is used to implement sort.Interface for slice of IPNet
type SortedIPs []*net.IPNet

// Returns length of slice
// Implements sort.Interface
func (arr SortedIPs) Len() int {
	return len(arr)
}

// Swap swaps two items in slice identified by indexes
// Implements sort.Interface
func (arr SortedIPs) Swap(i, j int) {
	arr[i], arr[j] = arr[j], arr[i]
}

// Less returns true if the item in slice at index i in slice
// should be sorted before the element with index j
// Implements sort.Interface
func (arr SortedIPs) Less(i, j int) bool {
	return lessAdrr(arr[i], arr[j])
}

func eqAddr(a *net.IPNet, b *net.IPNet) bool {
	return bytes.Equal(a.IP, b.IP) && bytes.Equal(a.Mask, b.Mask)
}

func lessAdrr(a *net.IPNet, b *net.IPNet) bool {
	if bytes.Equal(a.IP, b.IP) {
		return bytes.Compare(a.Mask, b.Mask) < 0
	}
	return bytes.Compare(a.IP, b.IP) < 0

}

// DiffAddr calculates the difference between two slices of AddrWithPrefix configuration.
// Returns a list of addresses that should be deleted and added to the current configuration to match newConfig.
func DiffAddr(newConfig []*net.IPNet, oldConfig []*net.IPNet) (toBeDeleted []*net.IPNet, toBeAdded []*net.IPNet) {
	var add []*net.IPNet
	var del []*net.IPNet
	//sort
	n := SortedIPs(newConfig)
	sort.Sort(&n)
	o := SortedIPs(oldConfig)
	sort.Sort(&o)

	//compare
	i := 0
	j := 0
	for i < len(n) && j < len(o) {
		if eqAddr(n[i], o[j]) {
			i++
			j++
		} else {
			if lessAdrr(n[i], o[j]) {
				add = append(add, n[i])
				i++
			} else {
				del = append(del, o[j])
				j++
			}
		}
	}

	for ; i < len(n); i++ {
		add = append(add, n[i])
	}

	for ; j < len(o); j++ {
		del = append(del, o[j])
	}
	return del, add
}

// Converts ipv4 configuration from protobuf representation to IPNet
func protoAddrToStruct(addrs []string) ([]*net.IPNet, error) {
	var result []*net.IPNet
	for _, addressWithPrefix := range addrs {
		if addressWithPrefix == "" {
			continue
		}
		parsedIPWithPrefix, _, err := ParseIPWithPrefix(addressWithPrefix)
		if err != nil {
			return result, err
		}
		result = append(result, parsedIPWithPrefix)
	}

	return result, nil
}

// ParseIPWithPrefix parses string representation of ip address into net.IPNet structure.
// If the prefix is missing default one is added (/32 for IPv4, /128 for IPv6)
func ParseIPWithPrefix(input string) (addr *net.IPNet, isIpv6 bool, err error) {
	defaultIpv4Mask := net.CIDRMask(32, 32)
	defaultIpv6Mask := net.CIDRMask(128, 128)

	hasPrefix := strings.Contains(input, "/")

	if hasPrefix {
		ip, network, err := net.ParseCIDR(input)
		if err != nil {
			return nil, false, err
		}
		network.IP = ip
		isIpv6, err = IsIPv6(ip.String())
		return network, isIpv6, err
	}

	// Ip prefix was not set
	ip := net.ParseIP(input)
	if ip == nil {
		return nil, false, fmt.Errorf("Unable to parse IP address: %v", input)
	}
	isIpv6, err = IsIPv6(ip.String())
	if err != nil {
		return
	}

	network := net.IPNet{}
	network.IP = ip
	if isIpv6 {
		network.Mask = defaultIpv6Mask
	} else {
		network.Mask = defaultIpv4Mask
	}
	return &network, isIpv6, nil
}

// IsIPv6 returns true if provided IP address is IPv6, false otherwise
func IsIPv6(address string) (bool, error) {
	if strings.Contains(address, ":") {
		return true, nil
	} else if strings.Contains(address, ".") {
		return false, nil
	} else {
		return false, fmt.Errorf("Unknown IP version. Address: %v", address)
	}
}
<commit_msg>utils/addrs: StrAddrsToStruct<commit_after>package addrs

import (
	"bytes"
	"fmt"
	"net"
	"sort"
	"strings"
)

// SortedIPs type is used to implement sort.Interface for slice of IPNet
type SortedIPs []*net.IPNet

// Returns length of slice
// Implements sort.Interface
func (arr SortedIPs) Len() int {
	return len(arr)
}

// Swap swaps two items in slice identified by indexes
// Implements sort.Interface
func (arr SortedIPs) Swap(i, j int) {
	arr[i], arr[j] = arr[j], arr[i]
}

// Less returns true if the item in slice at index i in slice
// should be sorted before the element with index j
// Implements sort.Interface
func (arr SortedIPs) Less(i, j int) bool {
	return lessAdrr(arr[i], arr[j])
}

func eqAddr(a *net.IPNet, b *net.IPNet) bool {
	return bytes.Equal(a.IP, b.IP) && bytes.Equal(a.Mask, b.Mask)
}

func lessAdrr(a *net.IPNet, b *net.IPNet) bool {
	if bytes.Equal(a.IP, b.IP) {
		return bytes.Compare(a.Mask, b.Mask) < 0
	}
	return bytes.Compare(a.IP, b.IP) < 0

}

// DiffAddr calculates the difference between two slices of AddrWithPrefix configuration.
// Returns a list of addresses that should be deleted and added to the current configuration to match newConfig.
func DiffAddr(newConfig []*net.IPNet, oldConfig []*net.IPNet) (toBeDeleted []*net.IPNet, toBeAdded []*net.IPNet) {
	var add []*net.IPNet
	var del []*net.IPNet
	//sort
	n := SortedIPs(newConfig)
	sort.Sort(&n)
	o := SortedIPs(oldConfig)
	sort.Sort(&o)

	//compare
	i := 0
	j := 0
	for i < len(n) && j < len(o) {
		if eqAddr(n[i], o[j]) {
			i++
			j++
		} else {
			if lessAdrr(n[i], o[j]) {
				add = append(add, n[i])
				i++
			} else {
				del = append(del, o[j])
				j++
			}
		}
	}

	for ; i < len(n); i++ {
		add = append(add, n[i])
	}

	for ; j < len(o); j++ {
		del = append(del, o[j])
	}
	return del, add
}

// StrAddrsToStruct converts slice of strings representing ipv4 addresses to IPNet structures
func StrAddrsToStruct(addrs []string) ([]*net.IPNet, error) {
	var result []*net.IPNet
	for _, addressWithPrefix := range addrs {
		if addressWithPrefix == "" {
			continue
		}
		parsedIPWithPrefix, _, err := ParseIPWithPrefix(addressWithPrefix)
		if err != nil {
			return result, err
		}
		result = append(result, parsedIPWithPrefix)
	}

	return result, nil
}

// ParseIPWithPrefix parses string representation of ip address into net.IPNet structure.
// If the prefix is missing default one is added (/32 for IPv4, /128 for IPv6)
func ParseIPWithPrefix(input string) (addr *net.IPNet, isIpv6 bool, err error) {
	defaultIpv4Mask := net.CIDRMask(32, 32)
	defaultIpv6Mask := net.CIDRMask(128, 128)

	hasPrefix := strings.Contains(input, "/")

	if hasPrefix {
		ip, network, err := net.ParseCIDR(input)
		if err != nil {
			return nil, false, err
		}
		network.IP = ip
		isIpv6, err = IsIPv6(ip.String())
		return network, isIpv6, err
	}

	// Ip prefix was not set
	ip := net.ParseIP(input)
	if ip == nil {
		return nil, false, fmt.Errorf("Unable to parse IP address: %v", input)
	}
	isIpv6, err = IsIPv6(ip.String())
	if err != nil {
		return
	}

	network := net.IPNet{}
	network.IP = ip
	if isIpv6 {
		network.Mask = defaultIpv6Mask
	} else {
		network.Mask = defaultIpv4Mask
	}
	return &network, isIpv6, nil
}

// IsIPv6 returns true if provided IP address is IPv6, false otherwise
func IsIPv6(address string) (bool, error) {
	if strings.Contains(address, ":") {
		return true, nil
	} else if strings.Contains(address, ".") {
		return false, nil
	} else {
		return false, fmt.Errorf("Unknown IP version. Address: %v", address)
	}
}
<|endoftext|>


<commit_before>package utils

import (
	"strings"

	"github.com/gistia/slackbot/robots"
)

type CmdHandler struct {
	name     string
	payload  *robots.Payload
	handlers map[string]func(*robots.Payload, Command)
	msgr     SlackHandler
}

func NewCmdHandler(p *robots.Payload, h SlackHandler, name string) CmdHandler {
	return CmdHandler{
		name:     name,
		payload:  p,
		handlers: map[string]func(*robots.Payload, Command){},
		msgr:     h,
	}
}

func (c *CmdHandler) Handle(cmd string, handler func(*robots.Payload, Command)) {
	c.handlers[cmd] = handler
}

func (c *CmdHandler) HandleMany(cmds []string, handler func(*robots.Payload, Command)) {
	for _, cmd := range cmds {
		c.handlers[cmd] = handler
	}
}

func (c *CmdHandler) HandleDefault(handler func(*robots.Payload, Command)) {
	c.handlers["_default"] = handler
}

func (c *CmdHandler) Process(s string) {
	cmd := NewCommand(s)

	if cmd.IsDefault() {
		if h := c.handlers["_default"]; h != nil {
			h(c.payload, cmd)
			return
		}

		c.msgr.Send(c.payload, "You must enter a command.\n")
		c.sendHelp()
		return
	}

	if cmd.Is("help") {
		c.sendHelp()
		return
	}

	for k := range c.handlers {
		if cmd.Is(k) {
			c.handlers[k](c.payload, cmd)
			return
		}
	}

	c.msgr.Send(c.payload, "Invalid command *"+cmd.Command+"*\n")
	c.sendHelp()
}

func (c *CmdHandler) sendHelp() {
	s := "*Usage:* `!" + c.name + " <command>`\n"
	if len(c.handlers) > 0 {
		cmds := ""
		for k := range c.handlers {
			if cmds != "" {
				cmds += ", "
			}
			cmds += "`" + k + "`"
		}

		s += "*Commands:* " + cmds + "\n"
	}
	c.msgr.Send(c.payload, s)
}

//--------------

type Command struct {
	Command   string
	Arguments []string
	Params    map[string]string
}

func NewCommand(c string) Command {
	params := map[string]string{}
	args := []string{}
	parts := strings.Split(c, " ")

	cmd := parts[0]
	parts = append(parts[:0], parts[1:]...)

	for len(parts) > 0 {
		p := parts[0]
		parts = append(parts[:0], parts[1:]...)

		r := strings.Split(p, ":")
		if len(r) > 1 {
			params[r[0]] = r[1]
		} else {
			args = append(args, r[0])
		}
	}

	return Command{Command: cmd, Arguments: args, Params: params}
}

func (c *Command) Arg(idx int) string {
	if len(c.Arguments) > idx {
		return c.Arguments[idx]
	}

	return ""
}

func (c *Command) HasArgs() bool {
	return len(c.Arguments) > 0
}

func (c *Command) Param(s string) string {
	return c.Params[s]
}

func (c *Command) IsDefault() bool {
	return c.Command == ""
}

func (c *Command) Is(cmds ...string) bool {
	for _, cmd := range cmds {
		if c.Command == cmd {
			return true
		}
	}

	return false
}
<commit_msg>Doesn’t show help for default command<commit_after>package utils

import (
	"strings"

	"github.com/gistia/slackbot/robots"
)

type CmdHandler struct {
	name     string
	payload  *robots.Payload
	handlers map[string]func(*robots.Payload, Command)
	msgr     SlackHandler
}

func NewCmdHandler(p *robots.Payload, h SlackHandler, name string) CmdHandler {
	return CmdHandler{
		name:     name,
		payload:  p,
		handlers: map[string]func(*robots.Payload, Command){},
		msgr:     h,
	}
}

func (c *CmdHandler) Handle(cmd string, handler func(*robots.Payload, Command)) {
	c.handlers[cmd] = handler
}

func (c *CmdHandler) HandleMany(cmds []string, handler func(*robots.Payload, Command)) {
	for _, cmd := range cmds {
		c.handlers[cmd] = handler
	}
}

func (c *CmdHandler) HandleDefault(handler func(*robots.Payload, Command)) {
	c.handlers["_default"] = handler
}

func (c *CmdHandler) Process(s string) {
	cmd := NewCommand(s)

	if cmd.IsDefault() {
		if h := c.handlers["_default"]; h != nil {
			h(c.payload, cmd)
			return
		}

		c.msgr.Send(c.payload, "You must enter a command.\n")
		c.sendHelp()
		return
	}

	if cmd.Is("help") {
		c.sendHelp()
		return
	}

	for k := range c.handlers {
		if cmd.Is(k) {
			c.handlers[k](c.payload, cmd)
			return
		}
	}

	c.msgr.Send(c.payload, "Invalid command *"+cmd.Command+"*\n")
	c.sendHelp()
}

func (c *CmdHandler) sendHelp() {
	s := "*Usage:* `!" + c.name + " <command>`\n"
	if len(c.handlers) > 0 {
		cmds := ""
		for k := range c.handlers {
			if k == "_default" {
				continue
			}

			if cmds != "" {
				cmds += ", "
			}
			cmds += "`" + k + "`"
		}

		s += "*Commands:* " + cmds + "\n"
	}
	c.msgr.Send(c.payload, s)
}

//--------------

type Command struct {
	Command   string
	Arguments []string
	Params    map[string]string
}

func NewCommand(c string) Command {
	params := map[string]string{}
	args := []string{}
	parts := strings.Split(c, " ")

	cmd := parts[0]
	parts = append(parts[:0], parts[1:]...)

	for len(parts) > 0 {
		p := parts[0]
		parts = append(parts[:0], parts[1:]...)

		r := strings.Split(p, ":")
		if len(r) > 1 {
			params[r[0]] = r[1]
		} else {
			args = append(args, r[0])
		}
	}

	return Command{Command: cmd, Arguments: args, Params: params}
}

func (c *Command) Arg(idx int) string {
	if len(c.Arguments) > idx {
		return c.Arguments[idx]
	}

	return ""
}

func (c *Command) HasArgs() bool {
	return len(c.Arguments) > 0
}

func (c *Command) Param(s string) string {
	return c.Params[s]
}

func (c *Command) IsDefault() bool {
	return c.Command == ""
}

func (c *Command) Is(cmds ...string) bool {
	for _, cmd := range cmds {
		if c.Command == cmd {
			return true
		}
	}

	return false
}
<|endoftext|>


<commit_before>package Golf

import (
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
)

func makeTestHttpRequest(body io.Reader) *http.Request {
	req, err := http.NewRequest("GET", "/foo/bar", body)
	if err != nil {
		return nil
	}
	return req
}

func TestContextCreate(t *testing.T) {

	r := makeTestHttpRequest(nil)
	w := httptest.NewRecorder()
	app := New()
	ctx := NewContext(r, w, app)

	if ctx == nil {
		t.Errorf("Can not create context.")
	}
}
<commit_msg>Add cookie unittest for Context.<commit_after>package Golf

import (
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
)

func makeTestHttpRequest(body io.Reader) *http.Request {
	req, err := http.NewRequest("GET", "/foo/bar", body)
	if err != nil {
		return nil
	}
	return req
}

func TestContextCreate(t *testing.T) {
	r := makeTestHttpRequest(nil)
	w := httptest.NewRecorder()
	app := New()
	ctx := NewContext(r, w, app)
	if ctx == nil {
		t.Errorf("Can not create context.")
	}
}

func TestCookieSet(t *testing.T) {
	r := makeTestHttpRequest(nil)
	w := httptest.NewRecorder()
	app := New()
	ctx := NewContext(r, w, app)
	ctx.SetCookie("foo", "bar", 0)
	ctx.Send()
	if w.HeaderMap.Get("Set-Cookie") != `foo=bar; Path=/` {
		t.Errorf("Cookie test failed: %q != %q", w.HeaderMap.Get("Set-Cookie"), `foo=bar; Path=/`)
	}
}
<|endoftext|>


<commit_before>package jsonrpc

import (
	"context"
	"net/http"

	"github.com/intel-go/fastjson"
)

// Handler links a method of JSON-RPC request.
type Handler interface {
	ServeJSONRPC(c context.Context, params *fastjson.RawMessage) (result interface{}, err *Error)
}

// ServeHTTP provides basic JSON-RPC handling.
func (mr *MethodRepository) ServeHTTP(w http.ResponseWriter, r *http.Request) {

	rs, batch, err := ParseRequest(r)
	if err != nil {
		err := SendResponse(w, []*Response{
			{
				Version: Version,
				Error:   err,
			},
		}, false)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	resp := make([]*Response, len(rs))
	for i := range rs {
		resp[i] = mr.InvokeMethod(r.Context(), rs[i])
	}

	if err := SendResponse(w, resp, batch); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
	}
}

// InvokeMethod invokes JSON-RPC method.
func (mr *MethodRepository) InvokeMethod(c context.Context, r *Request) *Response {
	var h Handler
	res := NewResponse(r)
	h, res.Error = mr.TakeMethod(r)
	if res.Error != nil {
		return res
	}
	res.Result, res.Error = h.ServeJSONRPC(WithRequestID(c, r.ID), r.Params)
	if res.Error != nil {
		res.Result = nil
	}
	return res
}
<commit_msg>Add error response message when failed to encode a response body<commit_after>package jsonrpc

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

	"github.com/intel-go/fastjson"
)

// Handler links a method of JSON-RPC request.
type Handler interface {
	ServeJSONRPC(c context.Context, params *fastjson.RawMessage) (result interface{}, err *Error)
}

// ServeHTTP provides basic JSON-RPC handling.
func (mr *MethodRepository) ServeHTTP(w http.ResponseWriter, r *http.Request) {

	rs, batch, err := ParseRequest(r)
	if err != nil {
		err := SendResponse(w, []*Response{
			{
				Version: Version,
				Error:   err,
			},
		}, false)
		if err != nil {
			fmt.Fprint(w, "Failed to encode error objects")
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	resp := make([]*Response, len(rs))
	for i := range rs {
		resp[i] = mr.InvokeMethod(r.Context(), rs[i])
	}

	if err := SendResponse(w, resp, batch); err != nil {
		fmt.Fprint(w, "Failed to encode result objects")
		w.WriteHeader(http.StatusInternalServerError)
	}
}

// InvokeMethod invokes JSON-RPC method.
func (mr *MethodRepository) InvokeMethod(c context.Context, r *Request) *Response {
	var h Handler
	res := NewResponse(r)
	h, res.Error = mr.TakeMethod(r)
	if res.Error != nil {
		return res
	}
	res.Result, res.Error = h.ServeJSONRPC(WithRequestID(c, r.ID), r.Params)
	if res.Error != nil {
		res.Result = nil
	}
	return res
}
<|endoftext|>


<commit_before>// ui mode specific handlers

package main

import (
	"encoding/json"
	"sort"
	"strconv"
	"strings"

	"fmt"
	"github.com/nsf/termbox-go"
)

type ModeHandler interface {
	HandleKey(ev termbox.Event)
	Repaint()
}

type HandlerDefault struct {
	ui *UI
}

func (h *HandlerDefault) Repaint() {
	ui := h.ui
	_, height := termbox.Size()

	first := ui.offsetY
	last := clamp(ui.offsetY+height, 0, len(ui.filterMatches))
	total := len(ui.filterMatches) - 1
	filter := ""

	if ui.filterString != "" {
		filter = fmt.Sprintf("[filter: \"%s\"] ", ui.filterString)
	}

	line := fmt.Sprintf("%s[rows %d-%d of %d] :%d, %d", filter, first, last, total, ui.offsetX, ui.offsetY)
	writeLine(0, height-1, termbox.ColorDefault, termbox.ColorDefault, line)
}

func (h *HandlerDefault) HandleKey(ev termbox.Event) {
	ui := h.ui
	vw, vh := ui.viewSize()

	maxYOffset := clamp(len(ui.filterMatches)-(vh-2), 0, len(ui.filterMatches)-1)
	endOfLine := ui.endOfLine()

	switch {
	case ev.Key == termbox.KeyCtrlL:
		termbox.Sync()
	case ev.Key == termbox.KeyCtrlA:
		ui.offsetX = 0
	case ev.Key == termbox.KeyCtrlE:
		ui.offsetX = -endOfLine + vw
	case ev.Key == termbox.KeyArrowRight:
		ui.offsetX = clamp(ui.offsetX-5, -endOfLine+vw, 0)
	case ev.Key == termbox.KeyArrowLeft:
		ui.offsetX = clamp(ui.offsetX+5, -endOfLine+vw, 0)
	case ev.Key == termbox.KeyArrowUp:
		ui.offsetY = clamp(ui.offsetY-1, 0, maxYOffset)
	case ev.Key == termbox.KeyArrowDown:
		ui.offsetY = clamp(ui.offsetY+1, 0, maxYOffset)
	case ev.Ch == '/', ev.Key == termbox.KeyCtrlR:
		ui.handler = &HandlerFilter{*h}
		ui.offsetY = 0
	case ev.Key == termbox.KeyEnter:
		jsonObj := make(map[string]interface{})

		row := ui.rows[ui.offsetY]
		for i, col := range ui.columns {
			str := row[i]

			if v, err := strconv.ParseInt(str, 10, 64); err == nil {
				jsonObj[col] = v
			} else if v, err := strconv.ParseFloat(str, 64); err == nil {
				jsonObj[col] = v
			} else if v, err := strconv.ParseBool(str); err == nil {
				jsonObj[col] = v
			} else {
				jsonObj[col] = str
			}

		}

		jsonStr, err := json.MarshalIndent(jsonObj, "", "  ")
		// TODO: This should be handled
		if err != nil {
			panic(err)
		}

		ui.handler = NewPopup(h.ui, string(jsonStr))

	case ev.Key == termbox.KeySpace:
		ui.offsetY = clamp(ui.offsetY+vh, 0, maxYOffset)
	case ev.Ch == 'C':
		ui.handler = NewColumnSelect(h.ui)
		ui.offsetX = 0
	case ev.Ch == 'G':
		ui.offsetY = maxYOffset
	case ev.Ch == 'g':
		ui.offsetY = 0
	case ev.Ch == 'Z':
		ui.zebraStripe = !ui.zebraStripe
	case ev.Ch == 'X':
		for i, _ := range ui.columnOpts {
			ui.columnOpts[i].expanded = !globalExpanded
			// FIXME: Possibly not the best behavior
			ui.columnOpts[i].collapsed = false
		}
		globalExpanded = !globalExpanded
	case ev.Ch == '?':
		ui.handler = NewPopup(h.ui, HELP_TEXT)
	}
}

type HandlerFilter struct {
	HandlerDefault
}

func (h *HandlerFilter) Repaint() {
	ui := h.ui
	_, height := termbox.Size()

	line := fmt.Sprintf("FILTER [%d matches]: %s", len(ui.filterMatches), ui.filterString)
	writeLine(0, height-1, termbox.ColorWhite|termbox.AttrBold, termbox.ColorDefault, line)
	termbox.SetCursor(len(line), height-1)

}

func (h *HandlerFilter) HandleKey(ev termbox.Event) {
	ui := h.ui

	// Ch == 0 implies this was a special key
	if ev.Ch == 0 && ev.Key != termbox.KeySpace {
		if ev.Key == termbox.KeyEsc || ev.Key == termbox.KeyCtrlG {
			ui.switchToDefault()

			ui.filterString = ""
			ui.filterRows(false)
		} else if ev.Key == termbox.KeyEnter {
			ui.switchToDefault()
		} else if ev.Key == termbox.KeyDelete || ev.Key == termbox.KeyBackspace ||
			ev.Key == termbox.KeyBackspace2 {
			if sz := len(ui.filterString); sz > 0 {
				ui.filterString = ui.filterString[:sz-1]
				ui.filterRows(false)
			}
		} else if ev.Key == termbox.KeyCtrlW || ev.Key == termbox.KeyCtrlU {
			ui.filterString = ""
			ui.filterRows(false)
		} else {
			// Fallback to default handling for arrows etc
			// FIXME: is this really the best way to do this in go?
			def := &HandlerDefault{h.ui}
			def.HandleKey(ev)
		}
		return
	}

	if ev.Key == termbox.KeySpace {
		ui.filterString += " "
	} else {
		ui.filterString += string(ev.Ch)
	}

	ui.offsetY = 0
	ui.filterRows(true)
}

type HandlerColumnSelect struct {
	HandlerDefault
	column int
}

func NewColumnSelect(ui *UI) *HandlerColumnSelect {
	h := HandlerColumnSelect{
		HandlerDefault: HandlerDefault{ui},
		column:         0,
	}

	h.selectColumn(0)
	return &h
}

// TODO: also adjust x offset
func (h *HandlerColumnSelect) selectColumn(idx int) {
	h.ui.columnOpts[h.column].highlight = false
	h.column = idx

	if h.column >= 0 {
		h.ui.columnOpts[h.column].highlight = true
	}
}

func (h *HandlerColumnSelect) rowSorter(i, j int) bool {
	ui := h.ui

	row1 := ui.rows[ui.filterMatches[i]]
	row2 := ui.rows[ui.filterMatches[j]]

	v1, err1 := strconv.ParseFloat(row1[h.column], 32)
	v2, err2 := strconv.ParseFloat(row2[h.column], 32)

	if err1 == nil && err2 == nil {
		return v1 < v2
	}

	return row1[h.column] < row2[h.column]
}

func (h *HandlerColumnSelect) Repaint() {
	ui := h.ui
	_, height := termbox.Size()

	line := fmt.Sprintf("COLUMN SELECT (^g quit) [%s] %d", ui.columns[h.column], h.column)
	writeLine(0, height-1, termbox.ColorWhite|termbox.AttrBold, termbox.ColorDefault, line)
}

func (h *HandlerColumnSelect) HandleKey(ev termbox.Event) {
	ui := h.ui
	savedColumn := -1

	switch {
	case ev.Key == termbox.KeyCtrlA:
		h.selectColumn(ui.findFirstColumn())
	case ev.Key == termbox.KeyCtrlE:
		h.selectColumn(len(ui.columns) - 1)
	case ev.Key == termbox.KeyArrowRight:
		next := ui.findNextColumn(h.column, 1)
		h.selectColumn(clamp(next, 0, len(ui.columns)-1))
	case ev.Key == termbox.KeyArrowLeft:
		next := ui.findNextColumn(h.column, -1)
		h.selectColumn(clamp(next, 0, len(ui.columns)-1))
	case ev.Ch == '<':
		sort.SliceStable(ui.filterMatches, func(i, j int) bool {
			return h.rowSorter(i, j)
		})
	case ev.Ch == '>':
		sort.SliceStable(ui.filterMatches, func(i, j int) bool {
			return h.rowSorter(j, i)
		})
	case ev.Ch == 'w':
		ui.columnOpts[h.column].collapsed = !ui.columnOpts[h.column].collapsed
	case ev.Ch == 'x':
		ui.columnOpts[h.column].expanded = !ui.columnOpts[h.column].expanded
		if ui.columnOpts[h.column].expanded {
			ui.columnOpts[h.column].collapsed = false
		}
	case ev.Ch == '.':
		ui.columnOpts[h.column].pinned = !ui.columnOpts[h.column].pinned

		if ui.columnOpts[h.column].pinned {
			ui.columnOpts[h.column].collapsed = false
		}

	case ev.Key == termbox.KeyCtrlG, ev.Key == termbox.KeyEsc:
		savedColumn = h.column
		h.selectColumn(-1)
		ui.switchToDefault()
	default:
		// FIXME: ditto, is this the best way to do this?
		def := &HandlerDefault{h.ui}
		def.HandleKey(ev)
	}

	// find if we've gone off screen and readjust
	// TODO: this bit is buggy
	cursorPosition := 0
	for i, _ := range ui.columns {
		colOpts := ui.columnOpts[i]

		if i == h.column || i == savedColumn {
			break
		}

		if colOpts.collapsed {
			cursorPosition += 1
		} else if !colOpts.expanded {
			cursorPosition += clamp(colOpts.width, 0, MAX_CELL_WIDTH)
		} else {
			cursorPosition += colOpts.width
		}

		cursorPosition += len(CELL_SEPARATOR)
	}

	width, _ := ui.viewSize()
	if cursorPosition > width-ui.offsetX || cursorPosition < -ui.offsetX {
		ui.offsetX = -cursorPosition
	}
}

type HandlerPopup struct {
	HandlerDefault

	content          []string
	offsetX, offsetY int
}

func NewPopup(ui *UI, content string) *HandlerPopup {
	return &HandlerPopup{
		HandlerDefault: HandlerDefault{ui},
		content:        strings.Split(content, "\n"),
	}
}

func (h *HandlerPopup) Repaint() {
	width, height := termbox.Size()

	popupW := clamp(120, 40, width-5)
	popupH := clamp(len(h.content)+2, 10, height-5)

	x := width/2 - popupW/2
	y := height/2 - popupH/2

	fmtString := "%s%-" + strconv.Itoa(popupW) + "s%s"

	borders := [][]string{
		[]string{"┌─", "─┐"},
		[]string{"│ ", " │"},
		[]string{"└─", "─┘"},
	}

	for i := -1; i <= popupH; i += 1 {
		var border []string
		var content string

		if i == -1 {
			border = borders[0]
			content = strings.Repeat("─", popupW)
		} else if i < popupH {
			border = borders[1]
			if i+h.offsetY < len(h.content) {
				content = h.content[i+h.offsetY]
			} else {
				content = " "
			}
		} else {
			border = borders[2]
			content = strings.Repeat("─", popupW)
		}

		line := fmt.Sprintf(fmtString, border[0], content, border[1])
		writeString(x, y+i, termbox.ColorWhite, termbox.ColorDefault, line)
	}

	line := "POPUP (^g quit)"
	writeLine(0, height-1, termbox.ColorWhite|termbox.AttrBold, termbox.ColorDefault, line)

}

func (h *HandlerPopup) HandleKey(ev termbox.Event) {
	if ev.Key == termbox.KeyEsc || ev.Key == termbox.KeyCtrlG || ev.Ch == 'q' {
		h.ui.handler = &HandlerDefault{h.ui}
	}

	switch ev.Key {
	case termbox.KeyArrowLeft:
		h.offsetX = clamp(h.offsetX-5, 0, 9999)
	case termbox.KeyArrowRight:
		h.offsetX = clamp(h.offsetX+5, 0, 9999)
	case termbox.KeyArrowUp:
		h.offsetY = clamp(h.offsetY-1, 0, len(h.content)-5)
	case termbox.KeyArrowDown:
		h.offsetY = clamp(h.offsetY+1, 0, len(h.content)-5)
	}
}
<commit_msg>popup width-15<commit_after>// ui mode specific handlers

package main

import (
	"encoding/json"
	"sort"
	"strconv"
	"strings"

	"fmt"
	"github.com/nsf/termbox-go"
)

type ModeHandler interface {
	HandleKey(ev termbox.Event)
	Repaint()
}

type HandlerDefault struct {
	ui *UI
}

func (h *HandlerDefault) Repaint() {
	ui := h.ui
	_, height := termbox.Size()

	first := ui.offsetY
	last := clamp(ui.offsetY+height, 0, len(ui.filterMatches))
	total := len(ui.filterMatches) - 1
	filter := ""

	if ui.filterString != "" {
		filter = fmt.Sprintf("[filter: \"%s\"] ", ui.filterString)
	}

	line := fmt.Sprintf("%s[rows %d-%d of %d] :%d, %d", filter, first, last, total, ui.offsetX, ui.offsetY)
	writeLine(0, height-1, termbox.ColorDefault, termbox.ColorDefault, line)
}

func (h *HandlerDefault) HandleKey(ev termbox.Event) {
	ui := h.ui
	vw, vh := ui.viewSize()

	maxYOffset := clamp(len(ui.filterMatches)-(vh-2), 0, len(ui.filterMatches)-1)
	endOfLine := ui.endOfLine()

	switch {
	case ev.Key == termbox.KeyCtrlL:
		termbox.Sync()
	case ev.Key == termbox.KeyCtrlA:
		ui.offsetX = 0
	case ev.Key == termbox.KeyCtrlE:
		ui.offsetX = -endOfLine + vw
	case ev.Key == termbox.KeyArrowRight:
		ui.offsetX = clamp(ui.offsetX-5, -endOfLine+vw, 0)
	case ev.Key == termbox.KeyArrowLeft:
		ui.offsetX = clamp(ui.offsetX+5, -endOfLine+vw, 0)
	case ev.Key == termbox.KeyArrowUp:
		ui.offsetY = clamp(ui.offsetY-1, 0, maxYOffset)
	case ev.Key == termbox.KeyArrowDown:
		ui.offsetY = clamp(ui.offsetY+1, 0, maxYOffset)
	case ev.Ch == '/', ev.Key == termbox.KeyCtrlR:
		ui.handler = &HandlerFilter{*h}
		ui.offsetY = 0
	case ev.Key == termbox.KeyEnter:
		jsonObj := make(map[string]interface{})

		row := ui.rows[ui.offsetY]
		for i, col := range ui.columns {
			str := row[i]

			if v, err := strconv.ParseInt(str, 10, 64); err == nil {
				jsonObj[col] = v
			} else if v, err := strconv.ParseFloat(str, 64); err == nil {
				jsonObj[col] = v
			} else if v, err := strconv.ParseBool(str); err == nil {
				jsonObj[col] = v
			} else {
				jsonObj[col] = str
			}

		}

		jsonStr, err := json.MarshalIndent(jsonObj, "", "  ")
		// TODO: This should be handled
		if err != nil {
			panic(err)
		}

		ui.handler = NewPopup(h.ui, string(jsonStr))

	case ev.Key == termbox.KeySpace:
		ui.offsetY = clamp(ui.offsetY+vh, 0, maxYOffset)
	case ev.Ch == 'C':
		ui.handler = NewColumnSelect(h.ui)
		ui.offsetX = 0
	case ev.Ch == 'G':
		ui.offsetY = maxYOffset
	case ev.Ch == 'g':
		ui.offsetY = 0
	case ev.Ch == 'Z':
		ui.zebraStripe = !ui.zebraStripe
	case ev.Ch == 'X':
		for i, _ := range ui.columnOpts {
			ui.columnOpts[i].expanded = !globalExpanded
			// FIXME: Possibly not the best behavior
			ui.columnOpts[i].collapsed = false
		}
		globalExpanded = !globalExpanded
	case ev.Ch == '?':
		ui.handler = NewPopup(h.ui, HELP_TEXT)
	}
}

type HandlerFilter struct {
	HandlerDefault
}

func (h *HandlerFilter) Repaint() {
	ui := h.ui
	_, height := termbox.Size()

	line := fmt.Sprintf("FILTER [%d matches]: %s", len(ui.filterMatches), ui.filterString)
	writeLine(0, height-1, termbox.ColorWhite|termbox.AttrBold, termbox.ColorDefault, line)
	termbox.SetCursor(len(line), height-1)

}

func (h *HandlerFilter) HandleKey(ev termbox.Event) {
	ui := h.ui

	// Ch == 0 implies this was a special key
	if ev.Ch == 0 && ev.Key != termbox.KeySpace {
		if ev.Key == termbox.KeyEsc || ev.Key == termbox.KeyCtrlG {
			ui.switchToDefault()

			ui.filterString = ""
			ui.filterRows(false)
		} else if ev.Key == termbox.KeyEnter {
			ui.switchToDefault()
		} else if ev.Key == termbox.KeyDelete || ev.Key == termbox.KeyBackspace ||
			ev.Key == termbox.KeyBackspace2 {
			if sz := len(ui.filterString); sz > 0 {
				ui.filterString = ui.filterString[:sz-1]
				ui.filterRows(false)
			}
		} else if ev.Key == termbox.KeyCtrlW || ev.Key == termbox.KeyCtrlU {
			ui.filterString = ""
			ui.filterRows(false)
		} else {
			// Fallback to default handling for arrows etc
			// FIXME: is this really the best way to do this in go?
			def := &HandlerDefault{h.ui}
			def.HandleKey(ev)
		}
		return
	}

	if ev.Key == termbox.KeySpace {
		ui.filterString += " "
	} else {
		ui.filterString += string(ev.Ch)
	}

	ui.offsetY = 0
	ui.filterRows(true)
}

type HandlerColumnSelect struct {
	HandlerDefault
	column int
}

func NewColumnSelect(ui *UI) *HandlerColumnSelect {
	h := HandlerColumnSelect{
		HandlerDefault: HandlerDefault{ui},
		column:         0,
	}

	h.selectColumn(0)
	return &h
}

// TODO: also adjust x offset
func (h *HandlerColumnSelect) selectColumn(idx int) {
	h.ui.columnOpts[h.column].highlight = false
	h.column = idx

	if h.column >= 0 {
		h.ui.columnOpts[h.column].highlight = true
	}
}

func (h *HandlerColumnSelect) rowSorter(i, j int) bool {
	ui := h.ui

	row1 := ui.rows[ui.filterMatches[i]]
	row2 := ui.rows[ui.filterMatches[j]]

	v1, err1 := strconv.ParseFloat(row1[h.column], 32)
	v2, err2 := strconv.ParseFloat(row2[h.column], 32)

	if err1 == nil && err2 == nil {
		return v1 < v2
	}

	return row1[h.column] < row2[h.column]
}

func (h *HandlerColumnSelect) Repaint() {
	ui := h.ui
	_, height := termbox.Size()

	line := fmt.Sprintf("COLUMN SELECT (^g quit) [%s] %d", ui.columns[h.column], h.column)
	writeLine(0, height-1, termbox.ColorWhite|termbox.AttrBold, termbox.ColorDefault, line)
}

func (h *HandlerColumnSelect) HandleKey(ev termbox.Event) {
	ui := h.ui
	savedColumn := -1

	switch {
	case ev.Key == termbox.KeyCtrlA:
		h.selectColumn(ui.findFirstColumn())
	case ev.Key == termbox.KeyCtrlE:
		h.selectColumn(len(ui.columns) - 1)
	case ev.Key == termbox.KeyArrowRight:
		next := ui.findNextColumn(h.column, 1)
		h.selectColumn(clamp(next, 0, len(ui.columns)-1))
	case ev.Key == termbox.KeyArrowLeft:
		next := ui.findNextColumn(h.column, -1)
		h.selectColumn(clamp(next, 0, len(ui.columns)-1))
	case ev.Ch == '<':
		sort.SliceStable(ui.filterMatches, func(i, j int) bool {
			return h.rowSorter(i, j)
		})
	case ev.Ch == '>':
		sort.SliceStable(ui.filterMatches, func(i, j int) bool {
			return h.rowSorter(j, i)
		})
	case ev.Ch == 'w':
		ui.columnOpts[h.column].collapsed = !ui.columnOpts[h.column].collapsed
	case ev.Ch == 'x':
		ui.columnOpts[h.column].expanded = !ui.columnOpts[h.column].expanded
		if ui.columnOpts[h.column].expanded {
			ui.columnOpts[h.column].collapsed = false
		}
	case ev.Ch == '.':
		ui.columnOpts[h.column].pinned = !ui.columnOpts[h.column].pinned

		if ui.columnOpts[h.column].pinned {
			ui.columnOpts[h.column].collapsed = false
		}

	case ev.Key == termbox.KeyCtrlG, ev.Key == termbox.KeyEsc:
		savedColumn = h.column
		h.selectColumn(-1)
		ui.switchToDefault()
	default:
		// FIXME: ditto, is this the best way to do this?
		def := &HandlerDefault{h.ui}
		def.HandleKey(ev)
	}

	// find if we've gone off screen and readjust
	// TODO: this bit is buggy
	cursorPosition := 0
	for i, _ := range ui.columns {
		colOpts := ui.columnOpts[i]

		if i == h.column || i == savedColumn {
			break
		}

		if colOpts.collapsed {
			cursorPosition += 1
		} else if !colOpts.expanded {
			cursorPosition += clamp(colOpts.width, 0, MAX_CELL_WIDTH)
		} else {
			cursorPosition += colOpts.width
		}

		cursorPosition += len(CELL_SEPARATOR)
	}

	width, _ := ui.viewSize()
	if cursorPosition > width-ui.offsetX || cursorPosition < -ui.offsetX {
		ui.offsetX = -cursorPosition
	}
}

type HandlerPopup struct {
	HandlerDefault

	content          []string
	offsetX, offsetY int
}

func NewPopup(ui *UI, content string) *HandlerPopup {
	return &HandlerPopup{
		HandlerDefault: HandlerDefault{ui},
		content:        strings.Split(content, "\n"),
	}
}

func (h *HandlerPopup) Repaint() {
	width, height := termbox.Size()

	popupW := clamp(120, 50, width-15)
	popupH := clamp(len(h.content)+2, 10, height-5)

	x := width/2 - popupW/2
	y := height/2 - popupH/2

	fmtString := "%s%-" + strconv.Itoa(popupW) + "s%s"

	borders := [][]string{
		[]string{"┌─", "─┐"},
		[]string{"│ ", " │"},
		[]string{"└─", "─┘"},
	}

	for i := -1; i <= popupH; i += 1 {
		var border []string
		var content string

		if i == -1 {
			border = borders[0]
			content = strings.Repeat("─", popupW)
		} else if i < popupH {
			border = borders[1]
			if i+h.offsetY < len(h.content) {
				content = h.content[i+h.offsetY]
				if len(content) >= popupW {
					content = content[0:popupW]
				}
			} else {
				content = " "
			}
		} else {
			border = borders[2]
			content = strings.Repeat("─", popupW)
		}

		line := fmt.Sprintf(fmtString, border[0], content, border[1])
		writeString(x, y+i, termbox.ColorWhite, termbox.ColorDefault, line)
	}

	line := "POPUP (^g quit)"
	writeLine(0, height-1, termbox.ColorWhite|termbox.AttrBold, termbox.ColorDefault, line)

}

func (h *HandlerPopup) HandleKey(ev termbox.Event) {
	if ev.Key == termbox.KeyEsc || ev.Key == termbox.KeyCtrlG || ev.Ch == 'q' {
		h.ui.handler = &HandlerDefault{h.ui}
	}

	switch ev.Key {
	case termbox.KeyArrowLeft:
		h.offsetX = clamp(h.offsetX-5, 0, 9999)
	case termbox.KeyArrowRight:
		h.offsetX = clamp(h.offsetX+5, 0, 9999)
	case termbox.KeyArrowUp:
		h.offsetY = clamp(h.offsetY-1, 0, len(h.content)-5)
	case termbox.KeyArrowDown:
		h.offsetY = clamp(h.offsetY+1, 0, len(h.content)-5)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"crypto/rand"
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"
)

// CDNBackendServer is a backend server which will receive and respond to
// requests from the CDN.
type CDNBackendServer struct {
	Name     string
	Port     int
	TLSCerts []tls.Certificate
	handler  func(w http.ResponseWriter, r *http.Request)
	server   *httptest.Server
}

// ServeHTTP satisfies the http.HandlerFunc interface. Health check requests
// for `HEAD /` are always served 200 responses. Other requests are passed
// off to a custom handler provided by SwitchHandler.
func (s *CDNBackendServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Backend-Name", s.Name)
	if r.Method == "HEAD" && r.URL.Path == "/" {
		w.Header().Set("PING", "PONG")
		return
	}

	s.handler(w, r)
}

// ResetHandler sets the handler back to an empty function that will return
// a 200 response.
func (s *CDNBackendServer) ResetHandler() {
	s.handler = func(w http.ResponseWriter, r *http.Request) {}
}

// SwitchHandler sets the handler to a custom function. This is used by
// tests to pass in their own request inspection and response handler.
func (s *CDNBackendServer) SwitchHandler(h func(w http.ResponseWriter, r *http.Request)) {
	s.handler = h
}

// IsStarted checks whether the server is currently started.
func (s *CDNBackendServer) IsStarted() bool {
	return (s.server != nil)
}

// Stop closes all outstanding client connections and unbind the port.
// Resets server back to nil, as if the backend had been instantiated but
// Start() not called.
func (s *CDNBackendServer) Stop() {
	s.server.Close()
	s.server = nil
}

// Start resets the handler back to the default and starts the server on
// Port. It will exit immediately if it's unable to bind the port, due to
// permissions or a conflicting application.
func (s *CDNBackendServer) Start() {
	s.ResetHandler()

	addr := fmt.Sprintf(":%d", s.Port)
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal(err)
	}

	s.server = httptest.NewUnstartedServer(s)
	s.server.Listener = ln

	if len(s.TLSCerts) > 0 {
		s.server.TLS = &tls.Config{
			Certificates: s.TLSCerts,
		}
	}

	s.server.StartTLS()
	log.Printf("Started server on port %d", s.Port)
}

// Return a v4 (random) UUID string.
// This might not be strictly RFC4122 compliant, but it will do. Credit:
// https://groups.google.com/d/msg/golang-nuts/Rn13T6BZpgE/dBaYVJ4hB5gJ
func NewUUID() string {
	bs := make([]byte, 16)
	rand.Read(bs)
	bs[6] = (bs[6] & 0x0f) | 0x40
	bs[8] = (bs[8] & 0x3f) | 0x80

	return fmt.Sprintf("%x-%x-%x-%x-%x", bs[0:4], bs[4:6], bs[6:8], bs[8:10], bs[10:])
}

// Construct a new URL for edge. Always uses HTTPS. A random UUID is used in
// the path to ensure that it hasn't previously been cached. It is passed as
// a query param for / so that some of the tests can be run against a
// service that hasn't been configured to point at our test backends.
func NewUniqueEdgeURL() string {
	url := url.URL{
		Scheme: "https",
		Host:   *edgeHost,
		Path:   "/",
		RawQuery: url.Values{
			"nocache": []string{NewUUID()},
		}.Encode(),
	}

	return url.String()
}

// Construct a GET request (but not perform it) against edge. Uses
// NewUniqueEdgeURL() to ensure that it hasn't previously been cached. The
// request method field of the returned object can be later modified if
// required.
func NewUniqueEdgeGET(t *testing.T) *http.Request {
	url := NewUniqueEdgeURL()
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		t.Fatal(err)
	}

	return req
}

// Make an HTTP request using http.RoundTrip, which doesn't handle redirects
// or cookies, and return the response. If there are any errors then the
// calling test will be aborted so as not to operate on a nil response.
func RoundTripCheckError(t *testing.T, req *http.Request) *http.Response {
	start := time.Now()
	resp, err := client.RoundTrip(req)
	if duration := time.Since(start); duration > requestSlowThreshold {
		t.Error("Slow request, took:", duration)
	}
	if err != nil {
		t.Fatal(err)
	}

	return resp
}

// Reset all backends, ensuring that they are started, have the default
// handler function, and that the edge considers them healthy. It may take
// some time because we need to receive and respond to enough probe health
// checks to be considered up.
func ResetBackends(backends []*CDNBackendServer) {
	remainingBackendsStopped := false

	// Reverse priority order so that waitForBackend works.
	for i := len(backends); i > 0; i-- {
		backend := backends[i-1]

		if backend.IsStarted() {
			backend.ResetHandler()
		} else {
			if !remainingBackendsStopped {
				// Ensure all remaining unchecked backends are stopped so that
				// waitForBackend will work. We'll bring them back one-by-one.
				stopBackends(backends[0 : i-1])
				remainingBackendsStopped = true
			}

			backend.Start()
			err := waitForBackend(backend.Name)
			if err != nil {
				log.Fatal(err)
			}
		}
	}
}

// Ensure that a slice of backends are stopped.
func stopBackends(backends []*CDNBackendServer) {
	for _, backend := range backends {
		if backend.IsStarted() {
			backend.Stop()
		}
	}
}

// Wait for the backend to return with the header we expect. This is designed to
// confirm that requests are hitting this specific backend, rather than a lower-level
// backend that this overrides (for example, origin over a mirror)
//
func waitForBackend(expectedBackendName string) error {
	const maxRetries = 20
	const waitForCdnProbeToPropagate = time.Duration(5 * time.Second)
	const timeBetweenAttempts = time.Duration(2 * time.Second)

	var url string

	log.Printf("Checking health of %s...", expectedBackendName)
	for try := 0; try <= maxRetries; try++ {
		url = NewUniqueEdgeURL()
		req, _ := http.NewRequest("GET", url, nil)
		resp, err := client.RoundTrip(req)
		if err != nil {
			return err
		}
		if resp.Header.Get("Backend-Name") == expectedBackendName {
			if try != 0 {
				time.Sleep(waitForCdnProbeToPropagate)
			}
			log.Println(expectedBackendName + " is up!")
			return nil // all is well!
		}
		time.Sleep(timeBetweenAttempts)
	}

	return fmt.Errorf(
		"%s still not available after %d attempts",
		expectedBackendName,
		maxRetries,
	)

}

// Callback function to modify complete response.
type responseCallback func(w http.ResponseWriter)

// Wrapper for testRequestsCachedDuration() with a respTTL of zero.
// Meaning that the cached object doesn't expire.
func testRequestsCachedIndefinite(t *testing.T, respCB responseCallback) {
	testRequestsCachedDuration(t, respCB, time.Duration(0))
}

// Helper function to make three requests and test responses. If respTTL is:
//
//	- zero: no delay between requests, origin should only see one request,
//		and all response bodies should be identical (from cache).
//	- non-zero: first and second request without delay, origin should only
//		see one request and responses bodies should be identical, then after a
//		delay of respTTL + a buffer a third response should get a new response
//		directly from origin.
//
// A responseCallback, if not nil, will be called to modify the response
// before calling Write(body).
func testRequestsCachedDuration(t *testing.T, respCB responseCallback, respTTL time.Duration) {
	const responseCached = "first response"
	const responseNotCached = "subsequent response"
	var testCacheExpiry bool = respTTL > 0
	var respTTLWithBuffer time.Duration = respTTL + (respTTL / 4)
	var requestsExpectedCount int

	requestsReceivedCount := 0
	switch testCacheExpiry {
	case true:
		requestsExpectedCount = 2
	case false:
		requestsExpectedCount = 1
	}

	url := fmt.Sprintf("https://%s/%s", *edgeHost, NewUUID())
	req, _ := http.NewRequest("GET", url, nil)

	originServer.SwitchHandler(func(w http.ResponseWriter, r *http.Request) {
		if respCB != nil {
			respCB(w)
		}

		if requestsReceivedCount == 0 {
			w.Write([]byte(responseCached))
		} else {
			w.Write([]byte(responseNotCached))
		}

		requestsReceivedCount++
	})

	for requestCount := 0; requestCount < 3; requestCount++ {
		if testCacheExpiry && requestCount == 2 {
			time.Sleep(respTTLWithBuffer)
		}

		resp := RoundTripCheckError(t, req)

		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			t.Fatal(err)
		}

		var expectedBody string
		if testCacheExpiry && requestCount > 1 {
			expectedBody = responseNotCached
		} else {
			expectedBody = responseCached
		}

		if receivedBody := string(body); receivedBody != expectedBody {
			t.Errorf(
				"Incorrect response body. Expected %q, got %q",
				expectedBody,
				receivedBody,
			)
		}
	}

	if requestsReceivedCount != requestsExpectedCount {
		t.Errorf(
			"Origin received the wrong number of requests. Expected %d, got %d",
			requestsExpectedCount,
			requestsReceivedCount,
		)
	}
}

// Callback function to modify response headers.
type responseHeaderCallback func(h http.Header)

// Helper function to make three requests and verify that we get three
// unique and uncached responses back. A responseHeaderCallback, if not nil,
// will be called to modify the response headers.
func testThreeRequestsNotCached(t *testing.T, req *http.Request, headerCB responseHeaderCallback) {
	requestsReceivedCount := 0
	responseBodies := []string{
		"first response",
		"second response",
		"third response",
	}

	originServer.SwitchHandler(func(w http.ResponseWriter, r *http.Request) {
		if headerCB != nil {
			headerCB(w.Header())
		}
		w.Write([]byte(responseBodies[requestsReceivedCount]))
		requestsReceivedCount++
	})

	for _, expectedBody := range responseBodies {
		resp := RoundTripCheckError(t, req)

		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			t.Fatal(err)
		}

		if receivedBody := string(body); receivedBody != expectedBody {
			t.Errorf("Incorrect response body. Expected %q, got %q", expectedBody, receivedBody)
		}
	}
}
<commit_msg>Add function to check if a string contains...<commit_after>package main

import (
	"crypto/rand"
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"
)

// CDNBackendServer is a backend server which will receive and respond to
// requests from the CDN.
type CDNBackendServer struct {
	Name     string
	Port     int
	TLSCerts []tls.Certificate
	handler  func(w http.ResponseWriter, r *http.Request)
	server   *httptest.Server
}

// ServeHTTP satisfies the http.HandlerFunc interface. Health check requests
// for `HEAD /` are always served 200 responses. Other requests are passed
// off to a custom handler provided by SwitchHandler.
func (s *CDNBackendServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Backend-Name", s.Name)
	if r.Method == "HEAD" && r.URL.Path == "/" {
		w.Header().Set("PING", "PONG")
		return
	}

	s.handler(w, r)
}

// ResetHandler sets the handler back to an empty function that will return
// a 200 response.
func (s *CDNBackendServer) ResetHandler() {
	s.handler = func(w http.ResponseWriter, r *http.Request) {}
}

// SwitchHandler sets the handler to a custom function. This is used by
// tests to pass in their own request inspection and response handler.
func (s *CDNBackendServer) SwitchHandler(h func(w http.ResponseWriter, r *http.Request)) {
	s.handler = h
}

// IsStarted checks whether the server is currently started.
func (s *CDNBackendServer) IsStarted() bool {
	return (s.server != nil)
}

// Stop closes all outstanding client connections and unbind the port.
// Resets server back to nil, as if the backend had been instantiated but
// Start() not called.
func (s *CDNBackendServer) Stop() {
	s.server.Close()
	s.server = nil
}

// Start resets the handler back to the default and starts the server on
// Port. It will exit immediately if it's unable to bind the port, due to
// permissions or a conflicting application.
func (s *CDNBackendServer) Start() {
	s.ResetHandler()

	addr := fmt.Sprintf(":%d", s.Port)
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal(err)
	}

	s.server = httptest.NewUnstartedServer(s)
	s.server.Listener = ln

	if len(s.TLSCerts) > 0 {
		s.server.TLS = &tls.Config{
			Certificates: s.TLSCerts,
		}
	}

	s.server.StartTLS()
	log.Printf("Started server on port %d", s.Port)
}

// Return a v4 (random) UUID string.
// This might not be strictly RFC4122 compliant, but it will do. Credit:
// https://groups.google.com/d/msg/golang-nuts/Rn13T6BZpgE/dBaYVJ4hB5gJ
func NewUUID() string {
	bs := make([]byte, 16)
	rand.Read(bs)
	bs[6] = (bs[6] & 0x0f) | 0x40
	bs[8] = (bs[8] & 0x3f) | 0x80

	return fmt.Sprintf("%x-%x-%x-%x-%x", bs[0:4], bs[4:6], bs[6:8], bs[8:10], bs[10:])
}

// Construct a new URL for edge. Always uses HTTPS. A random UUID is used in
// the path to ensure that it hasn't previously been cached. It is passed as
// a query param for / so that some of the tests can be run against a
// service that hasn't been configured to point at our test backends.
func NewUniqueEdgeURL() string {
	url := url.URL{
		Scheme: "https",
		Host:   *edgeHost,
		Path:   "/",
		RawQuery: url.Values{
			"nocache": []string{NewUUID()},
		}.Encode(),
	}

	return url.String()
}

// Construct a GET request (but not perform it) against edge. Uses
// NewUniqueEdgeURL() to ensure that it hasn't previously been cached. The
// request method field of the returned object can be later modified if
// required.
func NewUniqueEdgeGET(t *testing.T) *http.Request {
	url := NewUniqueEdgeURL()
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		t.Fatal(err)
	}

	return req
}

// Make an HTTP request using http.RoundTrip, which doesn't handle redirects
// or cookies, and return the response. If there are any errors then the
// calling test will be aborted so as not to operate on a nil response.
func RoundTripCheckError(t *testing.T, req *http.Request) *http.Response {
	start := time.Now()
	resp, err := client.RoundTrip(req)
	if duration := time.Since(start); duration > requestSlowThreshold {
		t.Error("Slow request, took:", duration)
	}
	if err != nil {
		t.Fatal(err)
	}

	return resp
}

// Reset all backends, ensuring that they are started, have the default
// handler function, and that the edge considers them healthy. It may take
// some time because we need to receive and respond to enough probe health
// checks to be considered up.
func ResetBackends(backends []*CDNBackendServer) {
	remainingBackendsStopped := false

	// Reverse priority order so that waitForBackend works.
	for i := len(backends); i > 0; i-- {
		backend := backends[i-1]

		if backend.IsStarted() {
			backend.ResetHandler()
		} else {
			if !remainingBackendsStopped {
				// Ensure all remaining unchecked backends are stopped so that
				// waitForBackend will work. We'll bring them back one-by-one.
				stopBackends(backends[0 : i-1])
				remainingBackendsStopped = true
			}

			backend.Start()
			err := waitForBackend(backend.Name)
			if err != nil {
				log.Fatal(err)
			}
		}
	}
}

// Ensure that a slice of backends are stopped.
func stopBackends(backends []*CDNBackendServer) {
	for _, backend := range backends {
		if backend.IsStarted() {
			backend.Stop()
		}
	}
}

// Wait for the backend to return with the header we expect. This is designed to
// confirm that requests are hitting this specific backend, rather than a lower-level
// backend that this overrides (for example, origin over a mirror)
//
func waitForBackend(expectedBackendName string) error {
	const maxRetries = 20
	const waitForCdnProbeToPropagate = time.Duration(5 * time.Second)
	const timeBetweenAttempts = time.Duration(2 * time.Second)

	var url string

	log.Printf("Checking health of %s...", expectedBackendName)
	for try := 0; try <= maxRetries; try++ {
		url = NewUniqueEdgeURL()
		req, _ := http.NewRequest("GET", url, nil)
		resp, err := client.RoundTrip(req)
		if err != nil {
			return err
		}
		if resp.Header.Get("Backend-Name") == expectedBackendName {
			if try != 0 {
				time.Sleep(waitForCdnProbeToPropagate)
			}
			log.Println(expectedBackendName + " is up!")
			return nil // all is well!
		}
		time.Sleep(timeBetweenAttempts)
	}

	return fmt.Errorf(
		"%s still not available after %d attempts",
		expectedBackendName,
		maxRetries,
	)

}

// Callback function to modify complete response.
type responseCallback func(w http.ResponseWriter)

// Wrapper for testRequestsCachedDuration() with a respTTL of zero.
// Meaning that the cached object doesn't expire.
func testRequestsCachedIndefinite(t *testing.T, respCB responseCallback) {
	testRequestsCachedDuration(t, respCB, time.Duration(0))
}

// Helper function to make three requests and test responses. If respTTL is:
//
//	- zero: no delay between requests, origin should only see one request,
//		and all response bodies should be identical (from cache).
//	- non-zero: first and second request without delay, origin should only
//		see one request and responses bodies should be identical, then after a
//		delay of respTTL + a buffer a third response should get a new response
//		directly from origin.
//
// A responseCallback, if not nil, will be called to modify the response
// before calling Write(body).
func testRequestsCachedDuration(t *testing.T, respCB responseCallback, respTTL time.Duration) {
	const responseCached = "first response"
	const responseNotCached = "subsequent response"
	var testCacheExpiry bool = respTTL > 0
	var respTTLWithBuffer time.Duration = respTTL + (respTTL / 4)
	var requestsExpectedCount int

	requestsReceivedCount := 0
	switch testCacheExpiry {
	case true:
		requestsExpectedCount = 2
	case false:
		requestsExpectedCount = 1
	}

	url := fmt.Sprintf("https://%s/%s", *edgeHost, NewUUID())
	req, _ := http.NewRequest("GET", url, nil)

	originServer.SwitchHandler(func(w http.ResponseWriter, r *http.Request) {
		if respCB != nil {
			respCB(w)
		}

		if requestsReceivedCount == 0 {
			w.Write([]byte(responseCached))
		} else {
			w.Write([]byte(responseNotCached))
		}

		requestsReceivedCount++
	})

	for requestCount := 0; requestCount < 3; requestCount++ {
		if testCacheExpiry && requestCount == 2 {
			time.Sleep(respTTLWithBuffer)
		}

		resp := RoundTripCheckError(t, req)

		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			t.Fatal(err)
		}

		var expectedBody string
		if testCacheExpiry && requestCount > 1 {
			expectedBody = responseNotCached
		} else {
			expectedBody = responseCached
		}

		if receivedBody := string(body); receivedBody != expectedBody {
			t.Errorf(
				"Incorrect response body. Expected %q, got %q",
				expectedBody,
				receivedBody,
			)
		}
	}

	if requestsReceivedCount != requestsExpectedCount {
		t.Errorf(
			"Origin received the wrong number of requests. Expected %d, got %d",
			requestsExpectedCount,
			requestsReceivedCount,
		)
	}
}

// Callback function to modify response headers.
type responseHeaderCallback func(h http.Header)

// Helper function to make three requests and verify that we get three
// unique and uncached responses back. A responseHeaderCallback, if not nil,
// will be called to modify the response headers.
func testThreeRequestsNotCached(t *testing.T, req *http.Request, headerCB responseHeaderCallback) {
	requestsReceivedCount := 0
	responseBodies := []string{
		"first response",
		"second response",
		"third response",
	}

	originServer.SwitchHandler(func(w http.ResponseWriter, r *http.Request) {
		if headerCB != nil {
			headerCB(w.Header())
		}
		w.Write([]byte(responseBodies[requestsReceivedCount]))
		requestsReceivedCount++
	})

	for _, expectedBody := range responseBodies {
		resp := RoundTripCheckError(t, req)

		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			t.Fatal(err)
		}

		if receivedBody := string(body); receivedBody != expectedBody {
			t.Errorf("Incorrect response body. Expected %q, got %q", expectedBody, receivedBody)
		}
	}
}

func stringContainsOneOf(haystack string, needles []string) bool {
	for _, needle := range needles {
		if strings.Contains(haystack, needle) {
			return true
		}
	}

	return false
}
<|endoftext|>


<commit_before><commit_msg>Fix formatting<commit_after><|endoftext|>


<commit_before>package main

import (
    "./commands"
)

func main() {
    commands.Run()
}<commit_msg>fix compile error<commit_after>package main

import (
    "github.com/usk81/holster/commands"
)

func main() {
    commands.Run()
}<|endoftext|>


<commit_before>/*

Command httpfsx launches mobile-friendly HTTP file-system explorer (readonly)

Basic usage:
	httpfsx --addr=tcp://:1024 --root=$HOME/share

*/
package main

import (
	"flag"
	"html"
	"io/ioutil"
	"log"
	"net"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/dustin/go-humanize"
	"github.com/valyala/fasthttp"
)

// ----------------------------------------------------------------------------

// Config holds all the data, needed to bootstrap an application.
type Config struct {
	Addr string // address to serve on
	Root string // public root directory path

	FasthttpConcurrency          uint64 // the maximum number of concurrent connections the server may serve
	FasthttpReadBufferSize       uint64 // per-connection buffer size for requests' reading. This also limits the maximum header size; bytes
	FasthttpWriteBufferSize      uint64 // per-connection buffer size for responses' writing; bytes
	FasthttpReadTimeout          uint64 // maximum duration for full request reading (including body); milliseconds
	FasthttpWriteTimeout         uint64 // maximum duration for full response writing (including body); milliseconds
	FasthttpMaxConnsPerIP        uint64 // maximum number of concurrent client connections allowed per IP
	FasthttpMaxRequestsPerConn   uint64 // maximum number of requests served per connection
	FasthttpMaxKeepaliveDuration uint64 // maximum keep-alive connection lifetime; milliseconds
	FasthttpMaxRequestBodySize   uint64 // maximum request body size; bytes
	FasthttpReduceMemoryUsage    bool   // aggressively reduces memory usage at the cost of higher CPU usage if set to true
	FasthttpGetOnly              bool   // rejects all non-GET requests if set to true
}

// ----------------------------------------------------------------------------

func main() {

	var err error

	config := Config{
		Addr: "tcp://127.0.0.1:1024",
		Root: "./",

		FasthttpConcurrency:          0,
		FasthttpReadBufferSize:       0,
		FasthttpWriteBufferSize:      0,
		FasthttpReadTimeout:          0,
		FasthttpWriteTimeout:         0,
		FasthttpMaxConnsPerIP:        0,
		FasthttpMaxRequestsPerConn:   0,
		FasthttpMaxKeepaliveDuration: 0,
		FasthttpMaxRequestBodySize:   0,
		FasthttpReduceMemoryUsage:    false,
		FasthttpGetOnly:              true,
	}

	flagSet := flag.NewFlagSet("main", flag.ExitOnError)

	flagSet.StringVar(&config.Addr, "addr", config.Addr, "Address to serve on")
	flagSet.StringVar(&config.Root, "root", config.Root, "Public root directory path")

	flagSet.Uint64Var(&config.FasthttpConcurrency, "fasthttp-concurrency", config.FasthttpConcurrency, "The maximum number of concurrent connections the server may serve")
	flagSet.Uint64Var(&config.FasthttpReadBufferSize, "fasthttp-read-buffer-size", config.FasthttpReadBufferSize, "Per-connection buffer size for requests' reading. This also limits the maximum header size; bytes")
	flagSet.Uint64Var(&config.FasthttpWriteBufferSize, "fasthttp-write-buffer-size", config.FasthttpWriteBufferSize, "Per-connection buffer size for responses' writing; bytes")
	flagSet.Uint64Var(&config.FasthttpReadTimeout, "fasthttp-read-timeout", config.FasthttpReadTimeout, "Maximum duration for full request reading (including body); milliseconds")
	flagSet.Uint64Var(&config.FasthttpWriteTimeout, "fasthttp-write-timeout", config.FasthttpWriteTimeout, "Maximum duration for full response writing (including body); milliseconds")
	flagSet.Uint64Var(&config.FasthttpMaxConnsPerIP, "fasthttp-max-conns-per-ip", config.FasthttpMaxConnsPerIP, "Maximum number of concurrent client connections allowed per IP")
	flagSet.Uint64Var(&config.FasthttpMaxRequestsPerConn, "fasthttp-max-requests-per-conn", config.FasthttpMaxRequestsPerConn, "Maximum number of requests served per connection")
	flagSet.Uint64Var(&config.FasthttpMaxKeepaliveDuration, "fasthttp-max-keepalive-duration", config.FasthttpMaxKeepaliveDuration, "Maximum keep-alive connection lifetime; milliseconds")
	flagSet.Uint64Var(&config.FasthttpMaxRequestBodySize, "fasthttp-max-request-body-size", config.FasthttpMaxRequestBodySize, "Maximum request body size; bytes")
	flagSet.BoolVar(&config.FasthttpReduceMemoryUsage, "fasthttp-reduce-memory-usage", config.FasthttpReduceMemoryUsage, "Aggressively reduces memory usage at the cost of higher CPU usage if set to true")
	flagSet.BoolVar(&config.FasthttpGetOnly, "fasthttp-get-only", config.FasthttpGetOnly, "Rejects all non-GET requests if set to true")

	flagSet.Parse(os.Args[1:])

	// validating addr to provide more informative error message (http.Listen is not so detailed):
	if !regexp.MustCompile("^(?:(?:tcp[46]?)://(?:.*?):\\d{1,})|(?:unix(?:packet)?://.+?)$").MatchString(config.Addr) {
		os.Stderr.WriteString("Error: --addr should be provided in a form of PROTO://ADDR, where PROTO is tcp, tcp4, tcp6, unix or unixpacket and ADDR is a HOST:PORT combination or /path/to/socket.unix")
		flag.Usage()
		os.Exit(1)
		return
	}

	// determining absolute public root path (to be informative; also, this allows to provide non-empty header for root location):
	config.Root = filepath.Clean(config.Root)
	if !filepath.IsAbs(config.Root) {
		wd, err := os.Getwd()
		if err != nil {
			os.Stderr.WriteString(err.Error() + "\n")
			os.Exit(1)
			return
		}
		config.Root = filepath.Join(wd, config.Root)
	}

	// parsing address string (decompose it into schema + host):
	addrURL, err := url.Parse(config.Addr)
	if err != nil {
		os.Stderr.WriteString(err.Error() + "\n")
		os.Exit(1)
		return
	}

	// create listener (doing it right now helps to detect errors like "address already in use"):
	listener, err := net.Listen(addrURL.Scheme, addrURL.Host)
	if err != nil {
		os.Stderr.WriteString(err.Error() + "\n")
		os.Exit(1)
		return
	}

	logger := log.New(os.Stderr, "", log.LstdFlags)

	handler := makeHandler(config.Root)

	server := fasthttp.Server{
		Handler:              handler,
		Name:                 "httpfsx v0.0.5", // TODO: don't forget to change this before creating release!
		Concurrency:          int(config.FasthttpConcurrency),
		ReadBufferSize:       int(config.FasthttpReadBufferSize),
		WriteBufferSize:      int(config.FasthttpWriteBufferSize),
		ReadTimeout:          time.Millisecond * time.Duration(config.FasthttpReadTimeout),
		WriteTimeout:         time.Millisecond * time.Duration(config.FasthttpWriteTimeout),
		MaxConnsPerIP:        int(config.FasthttpMaxConnsPerIP),
		MaxRequestsPerConn:   int(config.FasthttpMaxRequestsPerConn),
		MaxKeepaliveDuration: time.Millisecond * time.Duration(config.FasthttpMaxKeepaliveDuration),
		MaxRequestBodySize:   int(config.FasthttpMaxRequestBodySize),
		ReduceMemoryUsage:    config.FasthttpReduceMemoryUsage,
		GetOnly:              config.FasthttpGetOnly,
		Logger:               logger,
	}

	// notify user about current settings:
	logger.Printf("Addr: %s", addrURL.String())
	logger.Printf("Root: %s", config.Root)

	err = server.Serve(listener)
	if err != nil {
		os.Stderr.WriteString(err.Error() + "\n")
		os.Exit(1)
		return
	}

}

// ----------------------------------------------------------------------------

// decomposeOsError tries to determine HTTP status, suitable for passed os.* error.
func decomposeOsError(err error) (string, int) {

	if os.IsNotExist(err) {
		return err.Error(), fasthttp.StatusNotFound
	}

	if os.IsPermission(err) {
		return err.Error(), fasthttp.StatusForbidden
	}

	return err.Error(), fasthttp.StatusInternalServerError
}

// ----------------------------------------------------------------------------

// makeHandler generates new fasthttp handler closure for given root path.
func makeHandler(root string) fasthttp.RequestHandler {
	return func(ctx *fasthttp.RequestCtx) {

		relPath := path.Join("/", string(ctx.Path())) // relative request item path
		absPath := filepath.Join(root, relPath)       // absolute file-system item path

		// check if requested item exists and what is it's type:
		stats, err := os.Stat(absPath)
		if err != nil {
			ctx.Error(decomposeOsError(err))
			return
		}

		// serve dir's listing:
		if stats.IsDir() {

			// get dir contents:
			fis, err := ioutil.ReadDir(absPath)
			if err != nil {
				ctx.Error(decomposeOsError(err))
				return
			}

			escapedDirName := html.EscapeString(stats.Name())

			page := `
				<!DOCTYPE html>

						<meta name="robots" content="none">
						<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, target-densityDpi=medium-dpi">
						<title>` + escapedDirName + `</title>

` + escapedDirName + `

			`

			for _, fi := range fis {

				name := fi.Name()

				// skip (don't list) dotted (hidden) files:
				if strings.HasPrefix(name, ".") {
					continue
				}

				itemType := "file" // dir child type (file/dir)
				meta := ""         // some human-readable metadata about current child

				if fi.IsDir() {
					itemType = "dir"
					// for dirs only human-readable mod time is available:
					meta = "Modified " + humanize.Time(fi.ModTime())
				} else {
					// for files both human-readable size and mod time are available:
					meta = humanize.Bytes(uint64(fi.Size())) + ", modified " + humanize.Time(fi.ModTime())
				}

				page += `
					<li class="item ` + itemType + `">
						★
						<a class="link" href="` + html.EscapeString(path.Join(relPath, name)) + `">
							` + html.EscapeString(name) + `
							` + html.EscapeString(meta) + `

				`
			}

			page += `

							Clear storage

			`

			ctx.SetContentType("text/html; charset=utf-8")
			ctx.SetBodyString(page)

			return
		}

		// serve file:
		ctx.SendFile(absPath)

	}
}

// ----------------------------------------------------------------------------

// httpfsx CSS:
var style string = `

	.httpfsx {
		font-family: sans-serif;
	}

	.httpfsx a {
		text-decoration: none;
	}

	.httpfsx .header {
		margin: 0;
		padding: 0.5cm 0;
		font-size: 1cm;
	}

	.httpfsx .list {
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.httpfsx .list .item .star {
		display: inline-block;
		width: 1cm;
		height: 1cm;
		font-size: 1cm;
		line-height: 1cm;
		text-align: center;
		vertical-align: middle;
		color: #808080;
	}

	.httpfsx .list .item .star.active {
		color: #8B0000;
	}

	.httpfsx .list .item .link {
		display: inline-block;
		width: calc(100% - 1cm - 0.5cm);
		min-height: 1cm;
		padding: 0;
		vertical-align: top;
	}

	.httpfsx .list .item .link .name {
		font-size: 0.5cm;
		line-height: 0.6cm;
		color: #101010;
	}

	.httpfsx .list .item .link .name::after {
		color: #E0E0E0;
	}

	.httpfsx .list .item.dir .link .name::after {
		content: " 📁";
	}

	.httpfsx .list .item .link .meta {
		display: block;
		font-size: 0.3cm;
		line-height: 0.6cm;
		color: #808080;
	}

	.httpfsx .footer {
		font-size: 0.3cm;
		line-height: 0.3cm;
		text-align: right;
		padding-top: 1cm;
	}

	.httpfsx .footer .clear-storage {
		color: #808080;
	}

`

// ----------------------------------------------------------------------------

// httpfsx JS:
var script string = `

(function() {
	'use strict'

	// root element:
	var httpfsx = document.querySelector('.httpfsx')

	// file-system item nodes:
	var items = httpfsx.querySelectorAll('.httpfsx .list .item')

	// paths, that are listed on current page:
	var existingPaths = []

	for( var i = 0; i < items.length; i++ ) {

		// item wrapper node:
		var item = items[i]

		var star = item.querySelector('.star') // starring element
		var link = item.querySelector('.link') // link element (for item's path detection)

		// item's path:
		var path = link.getAttribute('href')

		// what key is used for storing current item's starred state:
		var starKey = 'httpfsx:star:' + path

		// remember "starring" key to simplify "toggle starring" click handler:
		star.setAttribute('data-httpfsx-star-key', starKey)

		// change star's view, if item is starred:
		if( localStorage.getItem(starKey) ) {
			star.classList.add('active')
		}

		// remember this item's path to remove deleted items from localStorage down the code:
		existingPaths.push(path.replace(/\/{2,}|\/$/g, ''))

	}

	// current request (location) path:
	var currentPath = location.pathname.replace(/\/{2,}|\/$/g, '')

	// traversing localStorage items to clean up deleted ones:
	for( var key in localStorage ) {

		// ignoring any foreign keys:
		if( key.indexOf('httpfsx:') == -1 ) {
			continue
		}

		// extracting stored item path from key:
		var storedPath = key.replace(/httpfsx:[^:]+?:/, '')

		// got item from other path, cannot touch it:
		if( storedPath.indexOf(currentPath) != 0 ) {
			continue
		}

		// does current localStorage item exists (not deleted)?
		var exists = false

		// checking, if current localStorage item is present on current location (page):
		for( var i = 0; i < existingPaths.length; i++ ) {
			var existingPath = existingPaths[i]
			if( storedPath.indexOf(existingPath) == 0 ) {
				exists = true
				break
			}
		}

		// removing deleted file-system items from localStorage:
		if( !exists ) {
			localStorage.removeItem(key)
		}

	}

	// capturing "star" and "clear-storage" clicks:
	httpfsx.addEventListener('click', function(event) {

		if( event.target.classList.contains('star') ) {

			var star = event.target

			var starKey = star.getAttribute('data-httpfsx-star-key')

			// toggle starring status:
			if( localStorage.getItem(starKey) ) {
				localStorage.removeItem(starKey)
				star.classList.remove('active')
			} else {
				localStorage.setItem(starKey, 'T')
				star.classList.add('active')
			}

		} else if( event.target.classList.contains('clear-storage') ) {

			// confirm and clear localStorage:
			if( confirm('Clear storage?') ) {

				localStorage.clear()

				// apply loosing stars to UI:
				var stars = httpfsx.querySelectorAll('.star')
				for( var i = 0; i < stars.length; i++ ) {
					var star = stars[i]
					star.classList.remove('active')
				}

				alert('Storage cleared')
			}

		}

	})

})()

`
<commit_msg>Dropped advanced config options in favor of fasthttp defaults. Switched from ctx.SendFile to FS handler to disable gzip cache.<commit_after>/*

Command httpfsx launches mobile-friendly HTTP file-system explorer (readonly)

Basic usage:
	httpfsx --addr=tcp://:1024 --root=$HOME/share

*/
package main

import (
	"flag"
	"html"
	"io/ioutil"
	"log"
	"net"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/dustin/go-humanize"
	"github.com/valyala/fasthttp"
)

// ----------------------------------------------------------------------------

// Config holds all the data, needed to bootstrap an application.
type Config struct {
	Addr string // address to serve on
	Root string // public root directory path
}

// ----------------------------------------------------------------------------

func main() {

	var err error

	config := Config{
		Addr: "tcp://127.0.0.1:1024",
		Root: "./",
	}

	flagSet := flag.NewFlagSet("main", flag.ExitOnError)
	flagSet.StringVar(&config.Addr, "addr", config.Addr, "Address to serve on")
	flagSet.StringVar(&config.Root, "root", config.Root, "Public root directory path")
	flagSet.Parse(os.Args[1:])

	// validating addr to provide more informative error message (http.Listen is not so detailed):
	if !regexp.MustCompile("^(?:(?:tcp[46]?)://(?:.*?):\\d{1,})|(?:unix(?:packet)?://.+?)$").MatchString(config.Addr) {
		os.Stderr.WriteString("Error: --addr should be provided in a form of PROTO://ADDR, where PROTO is tcp, tcp4, tcp6, unix or unixpacket and ADDR is a HOST:PORT combination or /path/to/socket.unix")
		flag.Usage()
		os.Exit(1)
		return
	}

	// determining absolute public root path (to be informative; also, this allows to provide non-empty header for root location):
	config.Root = filepath.Clean(config.Root)
	if !filepath.IsAbs(config.Root) {
		wd, err := os.Getwd()
		if err != nil {
			os.Stderr.WriteString(err.Error() + "\n")
			os.Exit(1)
			return
		}
		config.Root = filepath.Join(wd, config.Root)
	}

	// parsing address string (decompose it into schema + host):
	addrURL, err := url.Parse(config.Addr)
	if err != nil {
		os.Stderr.WriteString(err.Error() + "\n")
		os.Exit(1)
		return
	}

	// create listener (doing it right now helps to detect errors like "address already in use"):
	listener, err := net.Listen(addrURL.Scheme, addrURL.Host)
	if err != nil {
		os.Stderr.WriteString(err.Error() + "\n")
		os.Exit(1)
		return
	}

	logger := log.New(os.Stderr, "", log.LstdFlags)

	handler := makeHandler(config.Root)

	server := fasthttp.Server{
		Handler: handler,
		Name:    "httpfsx v0.0.6", // TODO: don't forget to change this before creating release!
		GetOnly: true,
		Logger:  logger,
	}

	// notify user about current settings:
	logger.Printf("Addr: %s", addrURL.String())
	logger.Printf("Root: %s", config.Root)

	err = server.Serve(listener)
	if err != nil {
		os.Stderr.WriteString(err.Error() + "\n")
		os.Exit(1)
		return
	}
}

// ----------------------------------------------------------------------------

// decomposeOsError tries to determine HTTP status, suitable for passed os.* error.
func decomposeOsError(err error) (string, int) {

	if os.IsNotExist(err) {
		return err.Error(), fasthttp.StatusNotFound
	}

	if os.IsPermission(err) {
		return err.Error(), fasthttp.StatusForbidden
	}

	return err.Error(), fasthttp.StatusInternalServerError
}

// ----------------------------------------------------------------------------

// makeHandler generates new fasthttp handler closure for given root path.
func makeHandler(root string) fasthttp.RequestHandler {
	fs := &fasthttp.FS{
		Root:            root,
		Compress:        false,
		AcceptByteRange: true,
	}
	fsHandler := fs.NewRequestHandler()

	return func(ctx *fasthttp.RequestCtx) {

		relPath := path.Join("/", string(ctx.Path())) // relative request item path
		absPath := filepath.Join(root, relPath)       // absolute file-system item path

		// check if requested item exists and what is it's type:
		stats, err := os.Stat(absPath)
		if err != nil {
			ctx.Error(decomposeOsError(err))
			return
		}

		// serve dir's listing:
		if stats.IsDir() {

			// get dir contents:
			fis, err := ioutil.ReadDir(absPath)
			if err != nil {
				ctx.Error(decomposeOsError(err))
				return
			}

			escapedDirName := html.EscapeString(stats.Name())

			page := `
				<!DOCTYPE html>

						<meta name="robots" content="none">
						<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, target-densityDpi=medium-dpi">
						<title>` + escapedDirName + `</title>

` + escapedDirName + `

			`

			for _, fi := range fis {

				name := fi.Name()

				// skip (don't list) dotted (hidden) files:
				if strings.HasPrefix(name, ".") {
					continue
				}

				itemType := "file" // dir child type (file/dir)
				meta := ""         // some human-readable metadata about current child

				if fi.IsDir() {
					itemType = "dir"
					// for dirs only human-readable mod time is available:
					meta = "Modified " + humanize.Time(fi.ModTime())
				} else {
					// for files both human-readable size and mod time are available:
					meta = humanize.Bytes(uint64(fi.Size())) + ", modified " + humanize.Time(fi.ModTime())
				}

				page += `
					<li class="item ` + itemType + `">
						★
						<a class="link" href="` + html.EscapeString(path.Join(relPath, name)) + `">
							` + html.EscapeString(name) + `
							` + html.EscapeString(meta) + `

				`
			}

			page += `

							Clear storage

			`

			ctx.SetContentType("text/html; charset=utf-8")
			ctx.SetBodyString(page)

			return
		}

		// serve file:
		fsHandler(ctx)
	}
}

// ----------------------------------------------------------------------------

// httpfsx CSS:
var style string = `

	.httpfsx {
		font-family: sans-serif;
	}

	.httpfsx a {
		text-decoration: none;
	}

	.httpfsx .header {
		margin: 0;
		padding: 0.5cm 0;
		font-size: 1cm;
	}

	.httpfsx .list {
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.httpfsx .list .item .star {
		display: inline-block;
		width: 1cm;
		height: 1cm;
		font-size: 1cm;
		line-height: 1cm;
		text-align: center;
		vertical-align: middle;
		color: #808080;
	}

	.httpfsx .list .item .star.active {
		color: #8B0000;
	}

	.httpfsx .list .item .link {
		display: inline-block;
		width: calc(100% - 1cm - 0.5cm);
		min-height: 1cm;
		padding: 0;
		vertical-align: top;
	}

	.httpfsx .list .item .link .name {
		font-size: 0.5cm;
		line-height: 0.6cm;
		color: #101010;
	}

	.httpfsx .list .item .link .name::after {
		color: #E0E0E0;
	}

	.httpfsx .list .item.dir .link .name::after {
		content: " 📁";
	}

	.httpfsx .list .item .link .meta {
		display: block;
		font-size: 0.3cm;
		line-height: 0.6cm;
		color: #808080;
	}

	.httpfsx .footer {
		font-size: 0.3cm;
		line-height: 0.3cm;
		text-align: right;
		padding-top: 1cm;
	}

	.httpfsx .footer .clear-storage {
		color: #808080;
	}

`

// ----------------------------------------------------------------------------

// httpfsx JS:
var script string = `

(function() {
	'use strict'

	// root element:
	var httpfsx = document.querySelector('.httpfsx')

	// file-system item nodes:
	var items = httpfsx.querySelectorAll('.httpfsx .list .item')

	// paths, that are listed on current page:
	var existingPaths = []

	for( var i = 0; i < items.length; i++ ) {

		// item wrapper node:
		var item = items[i]

		var star = item.querySelector('.star') // starring element
		var link = item.querySelector('.link') // link element (for item's path detection)

		// item's path:
		var path = link.getAttribute('href')

		// what key is used for storing current item's starred state:
		var starKey = 'httpfsx:star:' + path

		// remember "starring" key to simplify "toggle starring" click handler:
		star.setAttribute('data-httpfsx-star-key', starKey)

		// change star's view, if item is starred:
		if( localStorage.getItem(starKey) ) {
			star.classList.add('active')
		}

		// remember this item's path to remove deleted items from localStorage down the code:
		existingPaths.push(path.replace(/\/{2,}|\/$/g, ''))

	}

	// current request (location) path:
	var currentPath = location.pathname.replace(/\/{2,}|\/$/g, '')

	// traversing localStorage items to clean up deleted ones:
	for( var key in localStorage ) {

		// ignoring any foreign keys:
		if( key.indexOf('httpfsx:') == -1 ) {
			continue
		}

		// extracting stored item path from key:
		var storedPath = key.replace(/httpfsx:[^:]+?:/, '')

		// got item from other path, cannot touch it:
		if( storedPath.indexOf(currentPath) != 0 ) {
			continue
		}

		// does current localStorage item exists (not deleted)?
		var exists = false

		// checking, if current localStorage item is present on current location (page):
		for( var i = 0; i < existingPaths.length; i++ ) {
			var existingPath = existingPaths[i]
			if( storedPath.indexOf(existingPath) == 0 ) {
				exists = true
				break
			}
		}

		// removing deleted file-system items from localStorage:
		if( !exists ) {
			localStorage.removeItem(key)
		}

	}

	// capturing "star" and "clear-storage" clicks:
	httpfsx.addEventListener('click', function(event) {

		if( event.target.classList.contains('star') ) {

			var star = event.target

			var starKey = star.getAttribute('data-httpfsx-star-key')

			// toggle starring status:
			if( localStorage.getItem(starKey) ) {
				localStorage.removeItem(starKey)
				star.classList.remove('active')
			} else {
				localStorage.setItem(starKey, 'T')
				star.classList.add('active')
			}

		} else if( event.target.classList.contains('clear-storage') ) {

			// confirm and clear localStorage:
			if( confirm('Clear storage?') ) {

				localStorage.clear()

				// apply loosing stars to UI:
				var stars = httpfsx.querySelectorAll('.star')
				for( var i = 0; i < stars.length; i++ ) {
					var star = stars[i]
					star.classList.remove('active')
				}

				alert('Storage cleared')
			}

		}

	})

})()

`
<|endoftext|>


<commit_before>// Copyright 2015 Eleme Inc. All rights reserved.

// Package detector implements a tcp server that detects whether incoming
// metrics are anomalies and send alertings on anomalies found.
package detector

import (
	"bufio"
	"fmt"
	"net"
	"path/filepath"
	"time"

	"github.com/eleme/banshee/config"
	"github.com/eleme/banshee/detector/cursor"
	"github.com/eleme/banshee/filter"
	"github.com/eleme/banshee/models"
	"github.com/eleme/banshee/storage"
	"github.com/eleme/banshee/storage/indexdb"
	"github.com/eleme/banshee/storage/statedb"
	"github.com/eleme/banshee/util/log"
)

// Detector is a tcp server to detect anomalies.
type Detector struct {
	// Config
	cfg *config.Config
	// Storage
	db *storage.DB
	// Filter
	filter *filter.Filter
	// Cursor
	cursor *cursor.Cursor
	// Output
	outs []chan *models.Metric
}

// New creates a detector.
func New(cfg *config.Config, db *storage.DB, filter *filter.Filter) *Detector {
	d := new(Detector)
	d.cfg = cfg
	d.db = db
	d.filter = filter
	d.cursor = cursor.New(cfg.Detector.Factor, cfg.LeastC())
	return d
}

// Out outputs detected metrics to given channel.
func (d *Detector) Out(ch chan *models.Metric) {
	d.outs = append(d.outs, ch)
}

// output detected metrics to outs.
func (d *Detector) output(m *models.Metric) {
	for _, ch := range d.outs {
		select {
		case ch <- m:
		default:
			log.Error("output channel is full, skipping..")
		}
	}
}

// Start detector.
func (d *Detector) Start() {
	addr := fmt.Sprintf("0.0.0.0:%d", d.cfg.Detector.Port)
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal("failed to bind tcp://%s: %v", addr, err)
	}
	log.Info("detector is listening on tcp://%s..", addr)
	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Fatal("accept conn: %v", err)
		}
		go d.handle(conn)
	}
}

// Handle a connection, it will filter the mertics by rules and detect whether
// the metrics are anomalies.
func (d *Detector) handle(conn net.Conn) {
	// New conn
	addr := conn.RemoteAddr()
	defer func() {
		conn.Close()
		log.Info("conn %s disconnected", addr)
	}()
	log.Info("conn %s established", addr)
	// Scan line by line.
	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		if err := scanner.Err(); err != nil {
			log.Info("read conn: %v, closing it..", err)
			break
		}
		startAt := time.Now()
		// Parse
		line := scanner.Text()
		m, err := parseMetric(line)
		if err != nil {
			if len(line) > 10 {
				line = line[:10]
			}
			log.Error("parse '%s': %v, skipping..", line, err)
			continue
		}
		// Filter
		if d.match(m) {
			// Detect
			err = d.detect(m)
			if err != nil {
				log.Error("failed to detect: %v, skipping..", err)
				continue
			}
			elapsed := time.Since(startAt)
			log.Debug("%dμs %s %.3f", elapsed.Nanoseconds()/1000, m.Name, m.Score)
			// Output
			d.output(m)
			// Store
			if err := d.store(m); err != nil {
				log.Error("store metric %s: %v, skiping..", m.Name, err)
			}
		}
	}
}

// Test whether a metric matches the rules.
func (d *Detector) match(m *models.Metric) bool {
	// Check rules.
	rules := d.filter.MatchedRules(m)
	if len(rules) == 0 {
		log.Debug("%s hit no rules", m.Name)
		return false
	}
	// Check blacklist.
	for _, pattern := range d.cfg.Detector.BlackList {
		ok, err := filepath.Match(pattern, m.Name)
		if err == nil && ok {
			log.Debug("%s hit black pattern %s", m.Name, pattern)
			return false
		}
	}
	return true
}

// Detect incoming metric with 3-sigma rule and fill the metric.Score.
func (d *Detector) detect(m *models.Metric) error {
	// Get pervious state.
	s, err := d.db.State.Get(m)
	if err != nil && err != statedb.ErrNotFound {
		return err
	}
	if err == statedb.ErrNotFound {
		s = nil
	}
	// Fill blank with zeros.
	for _, pattern := range d.cfg.Detector.FillBlankZeros {
		if ok, _ := filepath.Match(pattern, m.Name); ok {
			// Need to fill zeros.
			if s, err = d.fillBlankZeros(m, s); err != nil {
				return err
			}
			break
		}
	}
	// Move state next.
	s = d.cursor.Next(s, m)
	// Put the next state to db.
	return d.db.State.Put(m, s)
}

// store detected metrics.
func (d *Detector) store(m *models.Metric) error {
	// Metric.
	if err := d.db.Metric.Put(m); err != nil {
		return err
	}
	// Index.
	idx := &models.Index{}
	idx.WriteMetric(m)
	if err := d.db.Index.Put(idx); err != nil {
		return err
	}
	return nil
}

// fillBlankZeros moves the state with zero on blanks.
func (d *Detector) fillBlankZeros(m *models.Metric, s *models.State) (*models.State, error) {
	// Get index.
	idx, err := d.db.Index.Get(m.Name)
	if err != nil && err != indexdb.ErrNotFound {
		return nil, err
	}
	if err == indexdb.ErrNotFound {
		// Not found, the metric is the first time seen and s must be nil.
		// The metric will be trusted since its state is nil.
		return s, nil
	}
	numGrid := d.cfg.Period[0]
	gridLen := d.cfg.Period[1]
	period := numGrid * gridLen
	periodNo := m.Stamp / period
	gridNo := (m.Stamp % period) / gridLen
	gridStart := gridNo*gridLen + periodNo*period
	for gridStart-period >= idx.Stamp {
		gridStart -= period
	}
	for ; gridStart < m.Stamp; gridStart += period {
		for stamp := gridStart; stamp < gridStart+gridLen && stamp < m.Stamp; stamp += d.cfg.Interval {
			if stamp > idx.Stamp {
				// Move state with zero.
				s = d.cursor.Next(s, &models.Metric{Stamp: stamp})
			}
		}
	}
	return s, nil
}
<commit_msg>Add minor comments.<commit_after>// Copyright 2015 Eleme Inc. All rights reserved.

// Package detector implements a tcp server that detects whether incoming
// metrics are anomalies and send alertings on anomalies found.
package detector

import (
	"bufio"
	"fmt"
	"net"
	"path/filepath"
	"time"

	"github.com/eleme/banshee/config"
	"github.com/eleme/banshee/detector/cursor"
	"github.com/eleme/banshee/filter"
	"github.com/eleme/banshee/models"
	"github.com/eleme/banshee/storage"
	"github.com/eleme/banshee/storage/indexdb"
	"github.com/eleme/banshee/storage/statedb"
	"github.com/eleme/banshee/util/log"
)

// Detector is a tcp server to detect anomalies.
type Detector struct {
	// Config
	cfg *config.Config
	// Storage
	db *storage.DB
	// Filter
	filter *filter.Filter
	// Cursor
	cursor *cursor.Cursor
	// Output
	outs []chan *models.Metric
}

// New creates a detector.
func New(cfg *config.Config, db *storage.DB, filter *filter.Filter) *Detector {
	d := new(Detector)
	d.cfg = cfg
	d.db = db
	d.filter = filter
	d.cursor = cursor.New(cfg.Detector.Factor, cfg.LeastC())
	return d
}

// Out outputs detected metrics to given channel.
func (d *Detector) Out(ch chan *models.Metric) {
	d.outs = append(d.outs, ch)
}

// output detected metrics to outs.
func (d *Detector) output(m *models.Metric) {
	for _, ch := range d.outs {
		select {
		case ch <- m:
		default:
			log.Error("output channel is full, skipping..")
		}
	}
}

// Start detector.
func (d *Detector) Start() {
	addr := fmt.Sprintf("0.0.0.0:%d", d.cfg.Detector.Port)
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal("failed to bind tcp://%s: %v", addr, err)
	}
	log.Info("detector is listening on tcp://%s..", addr)
	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Fatal("accept conn: %v", err)
		}
		go d.handle(conn)
	}
}

// Handle a connection, it will filter the mertics by rules and detect whether
// the metrics are anomalies.
func (d *Detector) handle(conn net.Conn) {
	// New conn
	addr := conn.RemoteAddr()
	defer func() {
		conn.Close()
		log.Info("conn %s disconnected", addr)
	}()
	log.Info("conn %s established", addr)
	// Scan line by line.
	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		if err := scanner.Err(); err != nil {
			log.Info("read conn: %v, closing it..", err)
			break
		}
		startAt := time.Now()
		// Parse
		line := scanner.Text()
		m, err := parseMetric(line)
		if err != nil {
			if len(line) > 10 {
				line = line[:10]
			}
			log.Error("parse '%s': %v, skipping..", line, err)
			continue
		}
		// Filter
		if d.match(m) {
			// Detect
			err = d.detect(m)
			if err != nil {
				log.Error("failed to detect: %v, skipping..", err)
				continue
			}
			elapsed := time.Since(startAt)
			log.Debug("%dμs %s %.3f", elapsed.Nanoseconds()/1000, m.Name, m.Score)
			// Output
			d.output(m)
			// Store
			if err := d.store(m); err != nil {
				log.Error("store metric %s: %v, skiping..", m.Name, err)
			}
		}
	}
}

// Test whether a metric matches the rules.
func (d *Detector) match(m *models.Metric) bool {
	// Check rules.
	rules := d.filter.MatchedRules(m)
	if len(rules) == 0 {
		log.Debug("%s hit no rules", m.Name)
		return false
	}
	// Check blacklist.
	for _, pattern := range d.cfg.Detector.BlackList {
		ok, err := filepath.Match(pattern, m.Name)
		if err == nil && ok {
			log.Debug("%s hit black pattern %s", m.Name, pattern)
			return false
		}
	}
	return true
}

// Detect incoming metric with 3-sigma rule and fill the metric.Score.
func (d *Detector) detect(m *models.Metric) error {
	// Get pervious state.
	s, err := d.db.State.Get(m)
	if err != nil && err != statedb.ErrNotFound {
		return err
	}
	if err == statedb.ErrNotFound {
		s = nil
	}
	// Fill blank with zeros.
	for _, pattern := range d.cfg.Detector.FillBlankZeros {
		if ok, _ := filepath.Match(pattern, m.Name); ok {
			// Need to fill zeros.
			if s, err = d.fillBlankZeros(m, s); err != nil {
				return err
			}
			break
		}
	}
	// Move state next.
	s = d.cursor.Next(s, m)
	// Put the next state to db.
	return d.db.State.Put(m, s)
}

// store detected metrics.
func (d *Detector) store(m *models.Metric) error {
	// Metric.
	if err := d.db.Metric.Put(m); err != nil {
		return err
	}
	// Index.
	idx := &models.Index{}
	idx.WriteMetric(m)
	if err := d.db.Index.Put(idx); err != nil {
		return err
	}
	return nil
}

// fillBlankZeros moves the state with zero on blanks.
func (d *Detector) fillBlankZeros(m *models.Metric, s *models.State) (*models.State, error) {
	// Get index.
	idx, err := d.db.Index.Get(m.Name)
	if err != nil && err != indexdb.ErrNotFound {
		return nil, err
	}
	if err == indexdb.ErrNotFound {
		// Not found, the metric is the first time seen and s must be nil.
		// The metric will be trusted since its state is nil.
		return s, nil
	}
	// Fill history blanks with zeros.
	numGrid := d.cfg.Period[0]
	gridLen := d.cfg.Period[1]
	period := numGrid * gridLen
	periodNo := m.Stamp / period
	gridNo := (m.Stamp % period) / gridLen
	// Find the start grid stamp
	gridStart := gridNo*gridLen + periodNo*period
	for gridStart-period >= idx.Stamp {
		gridStart -= period
	}
	for ; gridStart < m.Stamp; gridStart += period {
		for stamp := gridStart; stamp < gridStart+gridLen && stamp < m.Stamp; stamp += d.cfg.Interval {
			if stamp > idx.Stamp {
				// Move state with zero.
				s = d.cursor.Next(s, &models.Metric{Stamp: stamp})
			}
		}
	}
	return s, nil
}
<|endoftext|>