<commit_before>package command

import (
	"fmt"
	"io"
	"sort"
	"strings"
	"unicode"

	"github.com/pkg/errors"
)

type ConstError string

func (e ConstError) Error() string {
	return string(e)
}

const (
	Default = ""

	ErrNotFound = ConstError("command not found")
)

type stringArgsCommand struct {
	command         string
	description     string
	args            Args
	commandFunc     interface{}
	stringArgsFunc  StringArgsFunc
	resultsHandlers []ResultsHandler
}

func checkCommandChars(command string) error {
	if strings.IndexFunc(command, unicode.IsSpace) != -1 {
		return errors.Errorf("Command contains space characters: '%s'", command)
	}
	if strings.IndexFunc(command, unicode.IsGraphic) == -1 {
		return errors.Errorf("Command contains non graphc characters: '%s'", command)
	}
	return nil
}

type StringArgsCommandLogger interface {
	LogStringArgsCommand(command string, args []string)
}

type StringArgsCommandLoggerFunc func(command string, args []string)

func (f StringArgsCommandLoggerFunc) LogStringArgsCommand(command string, args []string) {
	f(command, args)
}

type StringArgsDispatcher struct {
	comm    map[string]*stringArgsCommand
	loggers []StringArgsCommandLogger
}

func NewStringArgsDispatcher(loggers ...StringArgsCommandLogger) *StringArgsDispatcher {
	return &StringArgsDispatcher{
		comm:    make(map[string]*stringArgsCommand),
		loggers: loggers,
	}
}

func (disp StringArgsDispatcher) AddCommand(command, description string, commandFunc interface{}, args Args, resultsHandlers ...ResultsHandler) error {
	if err := checkCommandChars(command); err != nil {
		return err
	}
	stringArgsFunc, err := GetStringArgsFunc(commandFunc, args, resultsHandlers...)
	if err != nil {
		return err
	}
	disp.comm[command] = &stringArgsCommand{
		command:         command,
		description:     description,
		args:            args,
		commandFunc:     commandFunc,
		stringArgsFunc:  stringArgsFunc,
		resultsHandlers: resultsHandlers,
	}
	return nil
}

func (disp StringArgsDispatcher) MustAddCommand(command, description string, commandFunc interface{}, args Args, resultsHandlers ...ResultsHandler) {
	err := disp.AddCommand(command, description, commandFunc, args, resultsHandlers...)
	if err != nil {
		panic(err)
	}
}

func (disp StringArgsDispatcher) AddDefaultCommand(description string, commandFunc interface{}, args Args, resultsHandlers ...ResultsHandler) error {
	stringArgsFunc, err := GetStringArgsFunc(commandFunc, args, resultsHandlers...)
	if err != nil {
		return err
	}
	disp.comm[Default] = &stringArgsCommand{
		command:         Default,
		description:     description,
		args:            args,
		commandFunc:     commandFunc,
		stringArgsFunc:  stringArgsFunc,
		resultsHandlers: resultsHandlers,
	}
	return nil
}

func (disp StringArgsDispatcher) MustAddDefaultCommand(description string, commandFunc interface{}, args Args, resultsHandlers ...ResultsHandler) {
	err := disp.AddDefaultCommand(description, commandFunc, args, resultsHandlers...)
	if err != nil {
		panic(err)
	}
}

func (disp StringArgsDispatcher) HasCommnd(command string) bool {
	_, found := disp.comm[command]
	return found
}

func (disp StringArgsDispatcher) HasDefaultCommnd() bool {
	_, found := disp.comm[Default]
	return found
}

func (disp StringArgsDispatcher) Dispatch(command string, args ...string) error {
	cmd, found := disp.comm[command]
	if !found {
		return ErrNotFound
	}
	for _, logger := range disp.loggers {
		logger.LogStringArgsCommand(command, args)
	}
	return cmd.stringArgsFunc(args...)
}

func (disp StringArgsDispatcher) MustDispatch(command string, args ...string) {
	err := disp.Dispatch(command, args...)
	if err != nil {
		panic(err)
	}
}

func (disp StringArgsDispatcher) DispatchDefaultCommand() error {
	return disp.Dispatch(Default)
}

func (disp StringArgsDispatcher) MustDispatchDefaultCommand() {
	err := disp.DispatchDefaultCommand()
	if err != nil {
		panic(err)
	}
}

func (disp StringArgsDispatcher) DispatchCombined(commandAndArgs []string) (command string, err error) {
	if len(commandAndArgs) == 0 {
		return Default, disp.DispatchDefaultCommand()
	}
	command = commandAndArgs[0]
	args := commandAndArgs[1:]
	return command, disp.Dispatch(command, args...)
}

func (disp StringArgsDispatcher) MustDispatchCombined(commandAndArgs []string) (command string) {
	command, err := disp.DispatchCombined(commandAndArgs)
	if err != nil {
		panic(err)
	}
	return command
}

func (disp StringArgsDispatcher) PrintCommands(appName string) {
	list := make([]*stringArgsCommand, 0, len(disp.comm))
	for _, cmd := range disp.comm {
		list = append(list, cmd)
	}
	sort.Slice(list, func(i, j int) bool {
		return list[i].command < list[j].command
	})

	for _, cmd := range list {
		CommandUsageColor.Printf("  %s %s %s\n", appName, cmd.command, cmd.args)
		if len(cmd.description) == 0 {
			CommandDescriptionColor.Println()
		} else {
			CommandDescriptionColor.Printf("      %s\n", cmd.description)
		}
	}
}

func (disp StringArgsDispatcher) PrintCommandsUsageIntro(appName string, output io.Writer) {
	if len(disp.comm) > 0 {
		fmt.Fprint(output, "Commands:\n")
		disp.PrintCommands(appName)
		fmt.Fprint(output, "Flags:\n")
	}
}
<commit_msg>constError<commit_after>package command

import (
	"fmt"
	"io"
	"sort"
	"strings"
	"unicode"

	"github.com/pkg/errors"
)

type constError string

func (e constError) Error() string {
	return string(e)
}

const (
	Default = ""

	ErrNotFound = constError("command not found")
)

type stringArgsCommand struct {
	command         string
	description     string
	args            Args
	commandFunc     interface{}
	stringArgsFunc  StringArgsFunc
	resultsHandlers []ResultsHandler
}

func checkCommandChars(command string) error {
	if strings.IndexFunc(command, unicode.IsSpace) != -1 {
		return errors.Errorf("Command contains space characters: '%s'", command)
	}
	if strings.IndexFunc(command, unicode.IsGraphic) == -1 {
		return errors.Errorf("Command contains non graphc characters: '%s'", command)
	}
	return nil
}

type StringArgsCommandLogger interface {
	LogStringArgsCommand(command string, args []string)
}

type StringArgsCommandLoggerFunc func(command string, args []string)

func (f StringArgsCommandLoggerFunc) LogStringArgsCommand(command string, args []string) {
	f(command, args)
}

type StringArgsDispatcher struct {
	comm    map[string]*stringArgsCommand
	loggers []StringArgsCommandLogger
}

func NewStringArgsDispatcher(loggers ...StringArgsCommandLogger) *StringArgsDispatcher {
	return &StringArgsDispatcher{
		comm:    make(map[string]*stringArgsCommand),
		loggers: loggers,
	}
}

func (disp StringArgsDispatcher) AddCommand(command, description string, commandFunc interface{}, args Args, resultsHandlers ...ResultsHandler) error {
	if err := checkCommandChars(command); err != nil {
		return err
	}
	stringArgsFunc, err := GetStringArgsFunc(commandFunc, args, resultsHandlers...)
	if err != nil {
		return err
	}
	disp.comm[command] = &stringArgsCommand{
		command:         command,
		description:     description,
		args:            args,
		commandFunc:     commandFunc,
		stringArgsFunc:  stringArgsFunc,
		resultsHandlers: resultsHandlers,
	}
	return nil
}

func (disp StringArgsDispatcher) MustAddCommand(command, description string, commandFunc interface{}, args Args, resultsHandlers ...ResultsHandler) {
	err := disp.AddCommand(command, description, commandFunc, args, resultsHandlers...)
	if err != nil {
		panic(err)
	}
}

func (disp StringArgsDispatcher) AddDefaultCommand(description string, commandFunc interface{}, args Args, resultsHandlers ...ResultsHandler) error {
	stringArgsFunc, err := GetStringArgsFunc(commandFunc, args, resultsHandlers...)
	if err != nil {
		return err
	}
	disp.comm[Default] = &stringArgsCommand{
		command:         Default,
		description:     description,
		args:            args,
		commandFunc:     commandFunc,
		stringArgsFunc:  stringArgsFunc,
		resultsHandlers: resultsHandlers,
	}
	return nil
}

func (disp StringArgsDispatcher) MustAddDefaultCommand(description string, commandFunc interface{}, args Args, resultsHandlers ...ResultsHandler) {
	err := disp.AddDefaultCommand(description, commandFunc, args, resultsHandlers...)
	if err != nil {
		panic(err)
	}
}

func (disp StringArgsDispatcher) HasCommnd(command string) bool {
	_, found := disp.comm[command]
	return found
}

func (disp StringArgsDispatcher) HasDefaultCommnd() bool {
	_, found := disp.comm[Default]
	return found
}

func (disp StringArgsDispatcher) Dispatch(command string, args ...string) error {
	cmd, found := disp.comm[command]
	if !found {
		return ErrNotFound
	}
	for _, logger := range disp.loggers {
		logger.LogStringArgsCommand(command, args)
	}
	return cmd.stringArgsFunc(args...)
}

func (disp StringArgsDispatcher) MustDispatch(command string, args ...string) {
	err := disp.Dispatch(command, args...)
	if err != nil {
		panic(err)
	}
}

func (disp StringArgsDispatcher) DispatchDefaultCommand() error {
	return disp.Dispatch(Default)
}

func (disp StringArgsDispatcher) MustDispatchDefaultCommand() {
	err := disp.DispatchDefaultCommand()
	if err != nil {
		panic(err)
	}
}

func (disp StringArgsDispatcher) DispatchCombined(commandAndArgs []string) (command string, err error) {
	if len(commandAndArgs) == 0 {
		return Default, disp.DispatchDefaultCommand()
	}
	command = commandAndArgs[0]
	args := commandAndArgs[1:]
	return command, disp.Dispatch(command, args...)
}

func (disp StringArgsDispatcher) MustDispatchCombined(commandAndArgs []string) (command string) {
	command, err := disp.DispatchCombined(commandAndArgs)
	if err != nil {
		panic(err)
	}
	return command
}

func (disp StringArgsDispatcher) PrintCommands(appName string) {
	list := make([]*stringArgsCommand, 0, len(disp.comm))
	for _, cmd := range disp.comm {
		list = append(list, cmd)
	}
	sort.Slice(list, func(i, j int) bool {
		return list[i].command < list[j].command
	})

	for _, cmd := range list {
		CommandUsageColor.Printf("  %s %s %s\n", appName, cmd.command, cmd.args)
		if len(cmd.description) == 0 {
			CommandDescriptionColor.Println()
		} else {
			CommandDescriptionColor.Printf("      %s\n", cmd.description)
		}
	}
}

func (disp StringArgsDispatcher) PrintCommandsUsageIntro(appName string, output io.Writer) {
	if len(disp.comm) > 0 {
		fmt.Fprint(output, "Commands:\n")
		disp.PrintCommands(appName)
		fmt.Fprint(output, "Flags:\n")
	}
}
<|endoftext|>


<commit_before>package sarama

import (
	"reflect"
	"runtime"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/davecgh/go-spew/spew"
)

var recordBatchTestCases = []struct {
	name         string
	batch        RecordBatch
	encoded      []byte
	oldGoEncoded []byte // used in case of gzipped content for go versions prior to 1.8
}{
	{
		name: "empty record",
		batch: RecordBatch{
			Version:        2,
			FirstTimestamp: time.Unix(0, 0),
			MaxTimestamp:   time.Unix(0, 0),
			Records:        []*Record{},
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 49, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,                // Version
			89, 95, 183, 221, // CRC
			0, 0, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 0, 0, 0, 0, 0, 0, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 0, // Number of Records
		},
	},
	{
		name: "control batch",
		batch: RecordBatch{
			Version:        2,
			Control:        true,
			FirstTimestamp: time.Unix(0, 0),
			MaxTimestamp:   time.Unix(0, 0),
			Records:        []*Record{},
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 49, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,               // Version
			81, 46, 67, 217, // CRC
			0, 32, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 0, 0, 0, 0, 0, 0, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 0, // Number of Records
		},
	},
	{
		name: "uncompressed record",
		batch: RecordBatch{
			Version:         2,
			FirstTimestamp:  time.Unix(1479847795, 0),
			MaxTimestamp:    time.Unix(0, 0),
			LastOffsetDelta: 0,
			Records: []*Record{{
				TimestampDelta: 5 * time.Millisecond,
				Key:            []byte{1, 2, 3, 4},
				Value:          []byte{5, 6, 7},
				Headers: []*RecordHeader{{
					Key:   []byte{8, 9, 10},
					Value: []byte{11, 12},
				}},
			}},
			recordsLen: 21,
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 70, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,                // Version
			84, 121, 97, 253, // CRC
			0, 0, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 1, 88, 141, 205, 89, 56, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 1, // Number of Records
			40, // Record Length
			0,  // Attributes
			10, // Timestamp Delta
			0,  // Offset Delta
			8,  // Key Length
			1, 2, 3, 4,
			6, // Value Length
			5, 6, 7,
			2,        // Number of Headers
			6,        // Header Key Length
			8, 9, 10, // Header Key
			4,      // Header Value Length
			11, 12, // Header Value
		},
	},
	{
		name: "gzipped record",
		batch: RecordBatch{
			Version:         2,
			Codec:           CompressionGZIP,
			FirstTimestamp:  time.Unix(1479847795, 0),
			MaxTimestamp:    time.Unix(0, 0),
			LastOffsetDelta: 0,
			Records: []*Record{{
				TimestampDelta: 5 * time.Millisecond,
				Key:            []byte{1, 2, 3, 4},
				Value:          []byte{5, 6, 7},
				Headers: []*RecordHeader{{
					Key:   []byte{8, 9, 10},
					Value: []byte{11, 12},
				}},
			}},
			recordsLen: 21,
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 94, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,                  // Version
			159, 236, 182, 189, // CRC
			0, 1, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 1, 88, 141, 205, 89, 56, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 1, // Number of Records
			31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 210, 96, 224, 98, 224, 96, 100, 98, 102, 97, 99, 101,
			99, 103, 98, 227, 224, 228, 98, 225, 230, 1, 4, 0, 0, 255, 255, 173, 201, 88, 103, 21, 0, 0, 0,
		},
		oldGoEncoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 94, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,               // Version
			0, 216, 14, 210, // CRC
			0, 1, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 1, 88, 141, 205, 89, 56, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 1, // Number of Records
			31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 210, 96, 224, 98, 224, 96, 100, 98, 102, 97, 99, 101,
			99, 103, 98, 227, 224, 228, 98, 225, 230, 1, 4, 0, 0, 255, 255, 173, 201, 88, 103, 21, 0, 0, 0,
		},
	},
	{
		name: "snappy compressed record",
		batch: RecordBatch{
			Version:         2,
			Codec:           CompressionSnappy,
			FirstTimestamp:  time.Unix(1479847795, 0),
			MaxTimestamp:    time.Unix(0, 0),
			LastOffsetDelta: 0,
			Records: []*Record{{
				TimestampDelta: 5 * time.Millisecond,
				Key:            []byte{1, 2, 3, 4},
				Value:          []byte{5, 6, 7},
				Headers: []*RecordHeader{{
					Key:   []byte{8, 9, 10},
					Value: []byte{11, 12},
				}},
			}},
			recordsLen: 21,
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 72, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,              // Version
			21, 0, 159, 97, // CRC
			0, 2, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 1, 88, 141, 205, 89, 56, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 1, // Number of Records
			21, 80, 40, 0, 10, 0, 8, 1, 2, 3, 4, 6, 5, 6, 7, 2, 6, 8, 9, 10, 4, 11, 12,
		},
	},
	{
		name: "lz4 compressed record",
		batch: RecordBatch{
			Version:         2,
			Codec:           CompressionLZ4,
			FirstTimestamp:  time.Unix(1479847795, 0),
			MaxTimestamp:    time.Unix(0, 0),
			LastOffsetDelta: 0,
			Records: []*Record{{
				TimestampDelta: 5 * time.Millisecond,
				Key:            []byte{1, 2, 3, 4},
				Value:          []byte{5, 6, 7},
				Headers: []*RecordHeader{{
					Key:   []byte{8, 9, 10},
					Value: []byte{11, 12},
				}},
			}},
			recordsLen: 21,
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 89, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,                 // Version
			169, 74, 119, 197, // CRC
			0, 3, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 1, 88, 141, 205, 89, 56, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 1, // Number of Records
			4, 34, 77, 24, 100, 112, 185, 21, 0, 0, 128, 40, 0, 10, 0, 8, 1, 2, 3, 4, 6, 5, 6, 7, 2,
			6, 8, 9, 10, 4, 11, 12, 0, 0, 0, 0, 12, 59, 239, 146,
		},
	},
}

func isOldGo(t *testing.T) bool {
	v := strings.Split(runtime.Version()[2:], ".")
	if len(v) < 2 {
		t.Logf("Can't parse version: %s", runtime.Version())
		return false
	}
	maj, err := strconv.Atoi(v[0])
	if err != nil {
		t.Logf("Can't parse version: %s", runtime.Version())
		return false
	}
	min, err := strconv.Atoi(v[1])
	if err != nil {
		t.Logf("Can't parse version: %s", runtime.Version())
		return false
	}
	return maj < 1 || (maj == 1 && min < 8)
}

func TestRecordBatchEncoding(t *testing.T) {
	for _, tc := range recordBatchTestCases {
		if tc.oldGoEncoded != nil && isOldGo(t) {
			testEncodable(t, tc.name, &tc.batch, tc.oldGoEncoded)
		} else {
			testEncodable(t, tc.name, &tc.batch, tc.encoded)
		}
	}
}

func TestRecordBatchDecoding(t *testing.T) {
	for _, tc := range recordBatchTestCases {
		batch := RecordBatch{}
		testDecodable(t, tc.name, &batch, tc.encoded)
		for _, r := range batch.Records {
			r.length = varintLengthField{}
		}
		for _, r := range tc.batch.Records {
			r.length = varintLengthField{}
		}
		if !reflect.DeepEqual(batch, tc.batch) {
			t.Errorf(spew.Sprintf("invalid decode of %s\ngot %+v\nwanted %+v", tc.name, batch, tc.batch))
		}
	}
}
<commit_msg>Fix gzip compression encode/decode test<commit_after>package sarama

import (
	"reflect"
	"runtime"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/davecgh/go-spew/spew"
)

var recordBatchTestCases = []struct {
	name         string
	batch        RecordBatch
	encoded      []byte
	oldGoEncoded []byte // used in case of gzipped content for go versions prior to 1.8
}{
	{
		name: "empty record",
		batch: RecordBatch{
			Version:        2,
			FirstTimestamp: time.Unix(0, 0),
			MaxTimestamp:   time.Unix(0, 0),
			Records:        []*Record{},
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 49, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,                // Version
			89, 95, 183, 221, // CRC
			0, 0, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 0, 0, 0, 0, 0, 0, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 0, // Number of Records
		},
	},
	{
		name: "control batch",
		batch: RecordBatch{
			Version:        2,
			Control:        true,
			FirstTimestamp: time.Unix(0, 0),
			MaxTimestamp:   time.Unix(0, 0),
			Records:        []*Record{},
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 49, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,               // Version
			81, 46, 67, 217, // CRC
			0, 32, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 0, 0, 0, 0, 0, 0, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 0, // Number of Records
		},
	},
	{
		name: "uncompressed record",
		batch: RecordBatch{
			Version:         2,
			FirstTimestamp:  time.Unix(1479847795, 0),
			MaxTimestamp:    time.Unix(0, 0),
			LastOffsetDelta: 0,
			Records: []*Record{{
				TimestampDelta: 5 * time.Millisecond,
				Key:            []byte{1, 2, 3, 4},
				Value:          []byte{5, 6, 7},
				Headers: []*RecordHeader{{
					Key:   []byte{8, 9, 10},
					Value: []byte{11, 12},
				}},
			}},
			recordsLen: 21,
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 70, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,                // Version
			84, 121, 97, 253, // CRC
			0, 0, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 1, 88, 141, 205, 89, 56, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 1, // Number of Records
			40, // Record Length
			0,  // Attributes
			10, // Timestamp Delta
			0,  // Offset Delta
			8,  // Key Length
			1, 2, 3, 4,
			6, // Value Length
			5, 6, 7,
			2,        // Number of Headers
			6,        // Header Key Length
			8, 9, 10, // Header Key
			4,      // Header Value Length
			11, 12, // Header Value
		},
	},
	{
		name: "gzipped record",
		batch: RecordBatch{
			Version:          2,
			Codec:            CompressionGZIP,
			CompressionLevel: CompressionLevelDefault,
			FirstTimestamp:   time.Unix(1479847795, 0),
			MaxTimestamp:     time.Unix(0, 0),
			LastOffsetDelta:  0,
			Records: []*Record{{
				TimestampDelta: 5 * time.Millisecond,
				Key:            []byte{1, 2, 3, 4},
				Value:          []byte{5, 6, 7},
				Headers: []*RecordHeader{{
					Key:   []byte{8, 9, 10},
					Value: []byte{11, 12},
				}},
			}},
			recordsLen: 21,
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 94, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,                  // Version
			159, 236, 182, 189, // CRC
			0, 1, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 1, 88, 141, 205, 89, 56, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 1, // Number of Records
			31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 210, 96, 224, 98, 224, 96, 100, 98, 102, 97, 99, 101,
			99, 103, 98, 227, 224, 228, 98, 225, 230, 1, 4, 0, 0, 255, 255, 173, 201, 88, 103, 21, 0, 0, 0,
		},
		oldGoEncoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 94, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,               // Version
			0, 216, 14, 210, // CRC
			0, 1, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 1, 88, 141, 205, 89, 56, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 1, // Number of Records
			31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 210, 96, 224, 98, 224, 96, 100, 98, 102, 97, 99, 101,
			99, 103, 98, 227, 224, 228, 98, 225, 230, 1, 4, 0, 0, 255, 255, 173, 201, 88, 103, 21, 0, 0, 0,
		},
	},
	{
		name: "snappy compressed record",
		batch: RecordBatch{
			Version:         2,
			Codec:           CompressionSnappy,
			FirstTimestamp:  time.Unix(1479847795, 0),
			MaxTimestamp:    time.Unix(0, 0),
			LastOffsetDelta: 0,
			Records: []*Record{{
				TimestampDelta: 5 * time.Millisecond,
				Key:            []byte{1, 2, 3, 4},
				Value:          []byte{5, 6, 7},
				Headers: []*RecordHeader{{
					Key:   []byte{8, 9, 10},
					Value: []byte{11, 12},
				}},
			}},
			recordsLen: 21,
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 72, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,              // Version
			21, 0, 159, 97, // CRC
			0, 2, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 1, 88, 141, 205, 89, 56, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 1, // Number of Records
			21, 80, 40, 0, 10, 0, 8, 1, 2, 3, 4, 6, 5, 6, 7, 2, 6, 8, 9, 10, 4, 11, 12,
		},
	},
	{
		name: "lz4 compressed record",
		batch: RecordBatch{
			Version:         2,
			Codec:           CompressionLZ4,
			FirstTimestamp:  time.Unix(1479847795, 0),
			MaxTimestamp:    time.Unix(0, 0),
			LastOffsetDelta: 0,
			Records: []*Record{{
				TimestampDelta: 5 * time.Millisecond,
				Key:            []byte{1, 2, 3, 4},
				Value:          []byte{5, 6, 7},
				Headers: []*RecordHeader{{
					Key:   []byte{8, 9, 10},
					Value: []byte{11, 12},
				}},
			}},
			recordsLen: 21,
		},
		encoded: []byte{
			0, 0, 0, 0, 0, 0, 0, 0, // First Offset
			0, 0, 0, 89, // Length
			0, 0, 0, 0, // Partition Leader Epoch
			2,                 // Version
			169, 74, 119, 197, // CRC
			0, 3, // Attributes
			0, 0, 0, 0, // Last Offset Delta
			0, 0, 1, 88, 141, 205, 89, 56, // First Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Max Timestamp
			0, 0, 0, 0, 0, 0, 0, 0, // Producer ID
			0, 0, // Producer Epoch
			0, 0, 0, 0, // First Sequence
			0, 0, 0, 1, // Number of Records
			4, 34, 77, 24, 100, 112, 185, 21, 0, 0, 128, 40, 0, 10, 0, 8, 1, 2, 3, 4, 6, 5, 6, 7, 2,
			6, 8, 9, 10, 4, 11, 12, 0, 0, 0, 0, 12, 59, 239, 146,
		},
	},
}

func isOldGo(t *testing.T) bool {
	v := strings.Split(runtime.Version()[2:], ".")
	if len(v) < 2 {
		t.Logf("Can't parse version: %s", runtime.Version())
		return false
	}
	maj, err := strconv.Atoi(v[0])
	if err != nil {
		t.Logf("Can't parse version: %s", runtime.Version())
		return false
	}
	min, err := strconv.Atoi(v[1])
	if err != nil {
		t.Logf("Can't parse version: %s", runtime.Version())
		return false
	}
	return maj < 1 || (maj == 1 && min < 8)
}

func TestRecordBatchEncoding(t *testing.T) {
	for _, tc := range recordBatchTestCases {
		if tc.oldGoEncoded != nil && isOldGo(t) {
			testEncodable(t, tc.name, &tc.batch, tc.oldGoEncoded)
		} else {
			testEncodable(t, tc.name, &tc.batch, tc.encoded)
		}
	}
}

func TestRecordBatchDecoding(t *testing.T) {
	for _, tc := range recordBatchTestCases {
		batch := RecordBatch{}
		testDecodable(t, tc.name, &batch, tc.encoded)
		for _, r := range batch.Records {
			r.length = varintLengthField{}
		}
		for _, r := range tc.batch.Records {
			r.length = varintLengthField{}
		}
		// The compression level is not restored on decoding. It is not needed
		// anyway. We only set it here to ensure that comparision succeeds.
		batch.CompressionLevel = tc.batch.CompressionLevel
		if !reflect.DeepEqual(batch, tc.batch) {
			t.Errorf(spew.Sprintf("invalid decode of %s\ngot %+v\nwanted %+v", tc.name, batch, tc.batch))
		}
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"github.com/davecgh/go-spew/spew"
	"github.com/nsf/termbox-go"
	"github.com/simulatedsimian/neo"
	"os"
	"path/filepath"
	"unicode/utf8"
)

func printAt(x, y int, s string, fg, bg termbox.Attribute) {
	for len(s) > 0 {
		r, rlen := utf8.DecodeRuneInString(s)
		termbox.SetCell(x, y, r, fg, bg)
		s = s[rlen:]
		x++
	}
}

func printAtDef(x, y int, s string) {
	printAt(x, y, s, termbox.ColorDefault, termbox.ColorDefault)
}

type treeNode struct {
	path     string
	info     os.FileInfo
	children []*treeNode
	parent   *treeNode
	expanded bool
	last     bool
}

func createNodes(rootPath string, parent *treeNode) ([]*treeNode, error) {

	var res []*treeNode
	err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {

		if err == nil && path != rootPath {
			if len(res) > 0 {
				res[len(res)-1].last = false
			}
			res = append(res, &treeNode{path, info, nil, parent, true, true})
			if info.IsDir() {
				return filepath.SkipDir
			}
		}
		return nil
	})

	return res, err
}

func populateChildren(node *treeNode) error {

	if node.info.IsDir() {
		children, err := createNodes(node.path, node)
		neo.PanicOnError(err)
		node.children = children
	}
	return nil
}

func drawNode(node *treeNode) {

	preamble := ""
	for n := node; n.parent != nil; n = n.parent {
		if n.parent.last {
			preamble = "   " + preamble
		} else {
			preamble = "│  " + preamble
		}
	}
	fmt.Print(preamble)

	if node.last {
		fmt.Print("└─")
	} else {
		fmt.Print("├─")
	}

	if node.info.IsDir() {
		if node.expanded {
			fmt.Print("[-]")
		} else {
			fmt.Print("[+]")
		}
	} else {
		fmt.Print("── ")
	}

	fmt.Println(node.info.Name())
}

func drawNodes(nodes []*treeNode) {

	for _, node := range nodes {
		drawNode(node)
		if node.expanded && len(node.children) > 0 {
			drawNodes(node.children)
		}
	}
}

func filltree(nodes []*treeNode) {

	for _, node := range nodes {
		populateChildren(node)
		if len(node.children) > 0 {
			filltree(node.children)
		}
	}
}

func test() {
	rootpath := "."
	if len(os.Args) > 1 {
		rootpath = os.Args[1]
	}

	nodes, err := createNodes(rootpath, nil)
	neo.PanicOnError(err)

	filltree(nodes)

	nodes[0].expanded = false

	drawNodes(nodes)

	spew.Dump(err)
	fmt.Println(err)
}

func termtest() {
	err := termbox.Init()
	if err != nil {
		panic(err)
	}
	defer termbox.Close()

	termbox.HideCursor()

	for {
		switch ev := termbox.PollEvent(); ev.Type {
		case termbox.EventKey:
			printAt(0, 1, fmt.Sprint(ev), termbox.ColorDefault, termbox.ColorDefault)
			termbox.Flush()

			switch ev.Key {
			case termbox.KeyEsc:
				return
			case termbox.KeyArrowUp:
			case termbox.KeyArrowDown:
			}

			termbox.Flush()

		case termbox.EventResize:
			x, y := ev.Width, ev.Height
			printAt(0, 0, fmt.Sprintf("[%d, %d] ", x, y), termbox.ColorDefault, termbox.ColorDefault)
			termbox.Flush()
		}
	}

	termbox.Flush()
}

func main() {
	test()
}
<commit_msg>nodes are only expanded when children are added<commit_after>package main

import (
	"fmt"
	"github.com/davecgh/go-spew/spew"
	"github.com/nsf/termbox-go"
	"github.com/simulatedsimian/neo"
	"os"
	"path/filepath"
	"unicode/utf8"
)

func printAt(x, y int, s string, fg, bg termbox.Attribute) {
	for len(s) > 0 {
		r, rlen := utf8.DecodeRuneInString(s)
		termbox.SetCell(x, y, r, fg, bg)
		s = s[rlen:]
		x++
	}
}

func printAtDef(x, y int, s string) {
	printAt(x, y, s, termbox.ColorDefault, termbox.ColorDefault)
}

type treeNode struct {
	path     string
	info     os.FileInfo
	children []*treeNode
	parent   *treeNode
	expanded bool
	last     bool
}

func createNodes(rootPath string, parent *treeNode) ([]*treeNode, error) {

	var res []*treeNode
	err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {

		if err == nil && path != rootPath {
			if len(res) > 0 {
				res[len(res)-1].last = false
			}
			res = append(res, &treeNode{path, info, nil, parent, false, true})
			if info.IsDir() {
				return filepath.SkipDir
			}
		}
		return nil
	})

	return res, err
}

func populateChildren(node *treeNode) error {

	if node.info.IsDir() {
		children, err := createNodes(node.path, node)
		neo.PanicOnError(err)
		node.children = children
		node.expanded = true
	}
	return nil
}

func drawNode(node *treeNode) {

	preamble := ""
	for n := node; n.parent != nil; n = n.parent {
		if n.parent.last {
			preamble = "   " + preamble
		} else {
			preamble = "│  " + preamble
		}
	}
	fmt.Print(preamble)

	if node.last {
		fmt.Print("└─")
	} else {
		fmt.Print("├─")
	}

	if node.info.IsDir() {
		if node.expanded {
			fmt.Print("[-]")
		} else {
			fmt.Print("[+]")
		}
	} else {
		fmt.Print("── ")
	}

	fmt.Println(node.info.Name())
}

func drawNodes(nodes []*treeNode) {

	for _, node := range nodes {
		drawNode(node)
		if node.expanded && len(node.children) > 0 {
			drawNodes(node.children)
		}
	}
}

func filltree(nodes []*treeNode) {

	for _, node := range nodes {
		populateChildren(node)
		if len(node.children) > 0 {
			filltree(node.children)
		}
	}
}

func test() {
	rootpath := "."
	if len(os.Args) > 1 {
		rootpath = os.Args[1]
	}

	nodes, err := createNodes(rootpath, nil)
	neo.PanicOnError(err)

	filltree(nodes)

	nodes[0].expanded = false

	drawNodes(nodes)

	spew.Dump(err)
	fmt.Println(err)
}

func termtest() {
	err := termbox.Init()
	if err != nil {
		panic(err)
	}
	defer termbox.Close()

	termbox.HideCursor()

	for {
		switch ev := termbox.PollEvent(); ev.Type {
		case termbox.EventKey:
			printAt(0, 1, fmt.Sprint(ev), termbox.ColorDefault, termbox.ColorDefault)
			termbox.Flush()

			switch ev.Key {
			case termbox.KeyEsc:
				return
			case termbox.KeyArrowUp:
			case termbox.KeyArrowDown:
			}

			termbox.Flush()

		case termbox.EventResize:
			x, y := ev.Width, ev.Height
			printAt(0, 0, fmt.Sprintf("[%d, %d] ", x, y), termbox.ColorDefault, termbox.ColorDefault)
			termbox.Flush()
		}
	}

	termbox.Flush()
}

func main() {
	test()
}
<|endoftext|>


<commit_before>// Copyright 2012, Bryan Matsuo. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

/*  Filename:    gutterd.go
 *  Author:      Bryan Matsuo <bmatsuo@soe.ucsc.edu>
 *  Created:     2012-03-04 17:28:31.728667 -0800 PST
 *  Description: Main source file in gutterd
 */

import (
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"time"
)

var (
	config   *Config    // Deamon configuration.
	watching []string   // Base-level directories to watch for torrents.
	handlers []*Handler // The ordered set of torrent handlers.
	opt      Options    // Command line options.
)

// Return from a pollFunc type to stop poll().
var ErrPollStop = fmt.Errorf("STOP POLLING")

// A function that can be used polling.
type pollFunc func() (time.Duration, error)

// Repeatedly call fn until ErrPollStop is returned.
func poll(fn pollFunc) {
	for {
		d, err := fn()
		if err == ErrPollStop {
			break
		} else if err != nil {
			fmt.Printf("Error: %v\n", err)
		}
		time.Sleep(d)
	}
}

func HomeDirectory() (home string, err error) {
	if home = os.Getenv("HOME"); home == "" {
		err = errors.New("Environment variable HOME not set.")
	}
	return
}

// Read the config file and setup global variables.
func init() {
	var err error
	defconfig := &Config{
		PollFrequency: 60,
		LogPath:       "&2",
	}

	loggerMux = new(LoggerMux)
	DefaultLogger = loggerMux.NewSource("gutterd")
	initLogger := log.New(os.Stderr, "", 0)
	loggerMux.NewSink(initLogger, "gutterd")

	opt = parseFlags()

	// Read the deamon configuration.
	if opt.ConfigPath != "" {
		if config, err = LoadConfig(opt.ConfigPath, defconfig); err != nil {
			Fatalf("%s: %v", "Couldn't load configuration", err)
		}
	} else if home, err := HomeDirectory(); err != nil {
		Fatal(err)
	} else if config, err = LoadConfig(home+"/.config/gutterd.json", defconfig); err != nil {
		Fatalf("%s: %v", "Couldn't load configuration", err)
	}

	handlers = config.MakeHandlers()

	watching = config.Watch

	if opt.PollFrequency > 0 {
		config.PollFrequency = opt.PollFrequency
	}

	if opt.Watch != nil {
		config.Watch = opt.Watch
	}

	// Setup the logging destination.
	if opt.LogPath != "" {
		config.LogPath = opt.LogPath
	}
	var logfile io.Writer
	switch config.LogPath {
	case "":
		fallthrough
	case "&2":
		logfile = os.Stderr
	case "&1":
		logfile = os.Stdout
	default:
		logfile, err = os.OpenFile(config.LogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
		if err != nil {
			Fatalf("Couldn't open log file: %s", config.LogPath)
		}
	}
	loggerMux.NewSink(log.New(logfile, "", log.LstdFlags), "gutterd")
	loggerMux.RemoveSink(initLogger)
}

// Handle a .torrent file.
func handleFile(path string) {
	torrent, err := ReadMetadataFile(path)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	Printf("%-8s%s", "TORRENT", torrent.Info.Name)
	// Find the first handler matching the supplied torrent.
	for _, handler := range handlers {
		if handler.Match(torrent) {
			Printf("%-8s%-14s", "MATCH", handler.Name)
			Printf("%-8s%s\n\n", "MOVING", handler.Watch)
			mvpath := filepath.Join(handler.Watch, filepath.Base(path))
			if err := os.Rename(path, mvpath); err != nil {
				Printf("%-8s%v", "ERROR", err)
			}
			return
		}
	}
	Printf("%-8s%-14s%s\n\n", "NO MATCH", "", torrent.Info.Name)
}

func main() {
	if len(watching) == 0 {
		Fatalf("%s\t%s", "ERROR", "Not watching any directories")
	}

	// Poll watch directories, handling all torrents found.
	poll(pollFunc(func() (time.Duration, error) {
		for _, watch := range watching {
			torrents, err := filepath.Glob(filepath.Join(watch, "*.torrent"))
			if err != nil {
				Printf("Error polling %s:\n%v", watch, err)
				continue
			}
			for _, _torrent := range torrents {
				handleFile(_torrent)
				continue
			}
		}
		return (config.PollFrequency * 1e9), nil
	}))
}
<commit_msg>Switch all logging in gutterd.go to the new logger.<commit_after>// Copyright 2012, Bryan Matsuo. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

/*  Filename:    gutterd.go
 *  Author:      Bryan Matsuo <bmatsuo@soe.ucsc.edu>
 *  Created:     2012-03-04 17:28:31.728667 -0800 PST
 *  Description: Main source file in gutterd
 */

import (
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"time"
)

var (
	config   *Config    // Deamon configuration.
	watching []string   // Base-level directories to watch for torrents.
	handlers []*Handler // The ordered set of torrent handlers.
	opt      Options    // Command line options.
)

// Return from a pollFunc type to stop poll().
var ErrPollStop = fmt.Errorf("STOP POLLING")

// A function that can be used polling.
type pollFunc func() (time.Duration, error)

// Repeatedly call fn until ErrPollStop is returned.
func poll(fn pollFunc) {
	for cont := true; cont; {
		switch d, err := fn(); err {
		case nil:
			time.Sleep(d)
		case ErrPollStop:
			cont = false
		default:
			Error(err)
		}
	}
}

func HomeDirectory() (home string, err error) {
	if home = os.Getenv("HOME"); home == "" {
		err = errors.New("Environment variable HOME not set.")
	}
	return
}

// Read the config file and setup global variables.
func init() {
	var err error
	defconfig := &Config{
		PollFrequency: 60,
		LogPath:       "&2",
	}

	loggerMux = new(LoggerMux)
	DefaultLogger = loggerMux.NewSource("gutterd")
	initLogger := log.New(os.Stderr, "", 0)
	loggerMux.NewSink(initLogger, "gutterd")

	opt = parseFlags()

	// Read the deamon configuration.
	if opt.ConfigPath != "" {
		if config, err = LoadConfig(opt.ConfigPath, defconfig); err != nil {
			Fatal("Couldn't load configuration: ", err)
		}
	} else if home, err := HomeDirectory(); err != nil {
		Fatal(err)
	} else if config, err = LoadConfig(home+"/.config/gutterd.json", defconfig); err != nil {
		Fatal("Couldn't load configuration: ", err)
	}

	handlers = config.MakeHandlers()

	watching = config.Watch

	if opt.PollFrequency > 0 {
		config.PollFrequency = opt.PollFrequency
	}

	if opt.Watch != nil {
		config.Watch = opt.Watch
	}

	// Setup the logging destination.
	if opt.LogPath != "" {
		config.LogPath = opt.LogPath
	}
	var logfile io.Writer
	switch config.LogPath {
	case "":
		fallthrough
	case "&2":
		logfile = os.Stderr
	case "&1":
		logfile = os.Stdout
	default:
		logfile, err = os.OpenFile(config.LogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
		if err != nil {
			Fatalf("Couldn't open log file: %s", config.LogPath)
		}
	}
	loggerMux.NewSink(log.New(logfile, "", log.LstdFlags), "gutterd")
	loggerMux.RemoveSink(initLogger)
}

// Handle a .torrent file.
func handleFile(path string) {
	torrent, err := ReadMetadataFile(path)
	if err != nil {
		Error(err)
		return
	}
	// Find the first handler matching the supplied torrent.
	for _, handler := range handlers {
		if handler.Match(torrent) {
			Printf("MATCH\t%s\t%s\t%s", torrent.Info.Name, handler.Name, handler.Watch)
			mvpath := filepath.Join(handler.Watch, filepath.Base(path))
			if err := os.Rename(path, mvpath); err != nil {
				Error(err)
			}
			return
		}
	}
	Print("NO MATCH\t", torrent.Info.Name)
}

func main() {
	if len(watching) == 0 {
		Fatal("Not watching any directories")
	}

	// Poll watch directories, handling all torrents found.
	poll(pollFunc(func() (time.Duration, error) {
		for _, watch := range watching {
			torrents, err := filepath.Glob(filepath.Join(watch, "*.torrent"))
			if err != nil {
				Error(err)
				continue
			}
			for _, _torrent := range torrents {
				handleFile(_torrent)
				continue
			}
		}
		return (config.PollFrequency * 1e9), nil
	}))
}
<|endoftext|>


<commit_before>package hamaprs

// #cgo LDFLAGS: -lfap
/*
#include <fap.h>
#include <stdlib.h>

// type is a reserved keyword in Go, we need something to reach p->type
fap_packet_type_t getPacketType(fap_packet_t* p) {
	if (!p) return -1;
    if (p->type != NULL) return *p->type;
    return -1;
}
*/
import "C"
import (
	"errors"
	"reflect"
	"runtime"
	"strings"
	"time"
	"unsafe"
)

type PacketType int

const (
	LocationPacketType PacketType = iota
	ObjectPacketType
	ItemPacketType
	MicePacketType
	NMEAPacketType
	WXPacketType
	MessagePacketType
	CapabilitiesPacketType
	StatusPacketType
	TelemetryPacketType
	TelemetryMessagePacketType
	DXSpotPacketType
	ExperimentalPacketType
	InvalidPacketType
)

// InvalidCoordinate is a marker for an unset position
const InvalidCoordinate float64 = 360

// Packet describes an APRS packet
type Packet struct {
	PacketType
	Timestamp           int
	SourceCallsign      string
	DestinationCallsign string
	Path                []string
	Status              string
	Symbol              string
	Latitude            float64
	Longitude           float64
	Altitude            float64
	Speed               float64
	Course              uint8
	Weather             *WeatherReport
	Telemetry           *Telemetry
	RawMessage          string
	MicE                string
	Message             string
	Comment             string
}

// WeatherReport describes the weather related part of an APRS packet
type WeatherReport struct {
	Temperature       float64
	InsideTemperature float64
	Humidity          uint8
	InsideHumidity    uint8
	WindGust          float64
	WindDirection     uint8
	WindSpeed         float64
	Pressure          float64
}

// Telemetry describes the telemetry related part of an APRS packet
type Telemetry struct {
	Val1, Val2, Val3, Val4, Val5 float64
}

// Parser is an APRS Parser
type Parser struct{}

// Returns a new APRS Parser
func NewParser() *Parser {
	C.fap_init()
	p := &Parser{}
	runtime.SetFinalizer(p, func() {
		C.fap_cleanup()
	})
	return p
}

// ParsePacket parse raw packet string and return a new Packet
// if an error occurs it will return a packet with fill RawMessage only
func (p *Parser) ParsePacket(raw string, isAX25 bool) (*Packet, error) {
	packet := &Packet{Latitude: InvalidCoordinate, Longitude: InvalidCoordinate}
	return p.FillPacket(raw, isAX25, packet)
}

func (p *Parser) FillPacket(raw string, isAX25 bool, packet *Packet) (*Packet, error) {
	message_cstring := C.CString(raw)
	message_length := C.uint(len(raw))
	defer C.free(unsafe.Pointer(message_cstring))

	cpacket := C.fap_parseaprs(message_cstring, message_length, C.short(boolToInt(isAX25)))

	defer C.fap_free(cpacket)

	if cpacket.error_code != nil {
		ebuffer := (*C.char)(C.malloc(C.size_t(60)))
		defer C.free(unsafe.Pointer(ebuffer))

		C.fap_explain_error(*cpacket.error_code, ebuffer)
		return &Packet{RawMessage: raw}, errors.New(C.GoString(ebuffer))
	}

	packet.Timestamp = int(time.Now().Unix())
	packet.SourceCallsign = strings.ToUpper(C.GoString(cpacket.src_callsign))
	packet.DestinationCallsign = strings.ToUpper(C.GoString(cpacket.dst_callsign))
	packet.Latitude = parseNilableCoordinate(cpacket.latitude)
	packet.Longitude = parseNilableCoordinate(cpacket.longitude)
	packet.Speed = parseNilableFloat(cpacket.speed)
	packet.Course = parseNilableUInt(cpacket.course)
	packet.Altitude = parseNilableFloat(cpacket.altitude)
	packet.Message = C.GoString(cpacket.message)
	packet.Status = C.GoStringN(cpacket.status, C.int(cpacket.status_len))
	packet.Comment = C.GoStringN(cpacket.comment, C.int(cpacket.comment_len))
	packet.RawMessage = raw

	if C.int(cpacket.path_len) > 0 {
		var CPath **C.char = cpacket.path
		length := int(cpacket.path_len)
		hdr := reflect.SliceHeader{
			Data: uintptr(unsafe.Pointer(CPath)),
			Len:  length,
			Cap:  length,
		}
		ptrSlice := *(*[]*C.char)(unsafe.Pointer(&hdr))
		packet.Path = make([]string, int(cpacket.path_len))
		for i, v := range ptrSlice {
			packet.Path[i] = C.GoString(v)
		}
	}

	switch C.getPacketType(cpacket) {
	case C.fapLOCATION:
		packet.PacketType = LocationPacketType
	case C.fapOBJECT:
		packet.PacketType = ObjectPacketType
	case C.fapITEM:
		packet.PacketType = ItemPacketType
	case C.fapMICE:
		packet.PacketType = MicePacketType
	case C.fapNMEA:
		packet.PacketType = NMEAPacketType
	case C.fapWX:
		packet.PacketType = WXPacketType
	case C.fapMESSAGE:
		packet.PacketType = MessagePacketType
	case C.fapCAPABILITIES:
		packet.PacketType = CapabilitiesPacketType
	case C.fapSTATUS:
		packet.PacketType = StatusPacketType
	case C.fapTELEMETRY:
		packet.PacketType = TelemetryPacketType
	case C.fapTELEMETRY_MESSAGE:
		packet.PacketType = TelemetryMessagePacketType
	case C.fapDX_SPOT:
		packet.PacketType = DXSpotPacketType
	case C.fapEXPERIMENTAL:
		packet.PacketType = ExperimentalPacketType
	default:
		packet.PacketType = InvalidPacketType
	}

	if cpacket.wx_report != nil {
		w := WeatherReport{
			Temperature:       parseNilableFloat(cpacket.wx_report.temp),
			InsideTemperature: parseNilableFloat(cpacket.wx_report.temp_in),
			Humidity:          parseNilableUInt(cpacket.wx_report.humidity),
			InsideHumidity:    parseNilableUInt(cpacket.wx_report.humidity_in),
			WindGust:          parseNilableFloat(cpacket.wx_report.wind_gust),
			WindDirection:     parseNilableUInt(cpacket.wx_report.wind_dir),
			WindSpeed:         parseNilableFloat(cpacket.wx_report.wind_speed),
			Pressure:          parseNilableFloat(cpacket.wx_report.pressure),
		}
		packet.Weather = &w
	}

	if cpacket.telemetry != nil {
		t := Telemetry{
			Val1: parseNilableFloat(cpacket.telemetry.val1),
			Val2: parseNilableFloat(cpacket.telemetry.val2),
			Val3: parseNilableFloat(cpacket.telemetry.val3),
			Val4: parseNilableFloat(cpacket.telemetry.val4),
			Val5: parseNilableFloat(cpacket.telemetry.val5),
		}
		packet.Telemetry = &t
	}

	// MicE alloc a buffer of 20 bytes for fap_mice_mbits_to_message C func
	cbuffer := (*C.char)(C.malloc(C.size_t(20)))
	defer C.free(unsafe.Pointer(cbuffer))

	if cpacket.messagebits != nil {
		C.fap_mice_mbits_to_message(cpacket.messagebits, cbuffer)
		packet.MicE = C.GoString(cbuffer)
	}

	return packet, nil
}

// IncludePosition return true if the packet contains a Position
func (p *Packet) IncludePosition() bool {
	if p.Latitude == InvalidCoordinate || p.Longitude == InvalidCoordinate {
		return false
	}
	return true
}

// Device return the Device describing the transceiver used to send this packet
func (p *Packet) Device() *Device {

	// 1st test exact match
	if toCall, ok := toCalls[p.DestinationCallsign]; ok {
		return &toCall
	}

	// then look in the trie
	if d := trieRoot.match(p.DestinationCallsign); d != nil {
		return d
	}

	// then search for mices we need a least '_$ or '..._$
	if len(p.Comment) >= 3 {
		// last two chars
		ls := p.Comment[len(p.Comment)-2:]
		if toCall, ok := toCalls[ls]; ok {
			return &toCall
		}
	}
	return nil
}

// return a short version of the callsign as KK6NXK for KK6NXK-7
func ShortCallsign(c string) string {
	s := strings.Split(c, "-")
	return s[0]
}

func boolToInt(b bool) int {
	if b {
		return 1
	}
	return 0
}

func parseNilableFloat(d *C.double) float64 {
	if d != nil {
		return float64(C.double(*d))
	}
	return 0
}

func parseNilableCoordinate(d *C.double) float64 {
	if d != nil {
		return float64(C.double(*d))
	}
	return InvalidCoordinate
}

func parseNilableUInt(d *C.uint) uint8 {
	if d != nil {
		return uint8(C.uint(*d))
	}
	return 0
}
<commit_msg>get rid of message when used in place of packet<commit_after>package hamaprs

// #cgo LDFLAGS: -lfap
/*
#include <fap.h>
#include <stdlib.h>

// type is a reserved keyword in Go, we need something to reach p->type
fap_packet_type_t getPacketType(fap_packet_t* p) {
	if (!p) return -1;
    if (p->type != NULL) return *p->type;
    return -1;
}
*/
import "C"
import (
	"errors"
	"reflect"
	"runtime"
	"strings"
	"time"
	"unsafe"
)

type PacketType int

const (
	LocationPacketType PacketType = iota
	ObjectPacketType
	ItemPacketType
	MicePacketType
	NMEAPacketType
	WXPacketType
	MessagePacketType
	CapabilitiesPacketType
	StatusPacketType
	TelemetryPacketType
	TelemetryMessagePacketType
	DXSpotPacketType
	ExperimentalPacketType
	InvalidPacketType
)

// InvalidCoordinate is a marker for an unset position
const InvalidCoordinate float64 = 360

// Packet describes an APRS packet
type Packet struct {
	PacketType
	Timestamp           int
	SourceCallsign      string
	DestinationCallsign string
	Path                []string
	Status              string
	Symbol              string
	Latitude            float64
	Longitude           float64
	Altitude            float64
	Speed               float64
	Course              uint8
	Weather             *WeatherReport
	Telemetry           *Telemetry
	RawPacket           string
	MicE                string
	Message             string
	Comment             string
}

// WeatherReport describes the weather related part of an APRS packet
type WeatherReport struct {
	Temperature       float64
	InsideTemperature float64
	Humidity          uint8
	InsideHumidity    uint8
	WindGust          float64
	WindDirection     uint8
	WindSpeed         float64
	Pressure          float64
}

// Telemetry describes the telemetry related part of an APRS packet
type Telemetry struct {
	Val1, Val2, Val3, Val4, Val5 float64
}

// Parser is an APRS Parser
type Parser struct{}

// Returns a new APRS Parser
func NewParser() *Parser {
	C.fap_init()
	p := &Parser{}
	runtime.SetFinalizer(p, func() {
		C.fap_cleanup()
	})
	return p
}

// ParsePacket parse raw packet string and return a new Packet
// if an error occurs it will return a packet with fill RawMessage only
func (p *Parser) ParsePacket(raw string, isAX25 bool) (*Packet, error) {
	packet := &Packet{Latitude: InvalidCoordinate, Longitude: InvalidCoordinate}
	return p.FillPacket(raw, isAX25, packet)
}

func (p *Parser) FillPacket(raw string, isAX25 bool, packet *Packet) (*Packet, error) {
	packet_cstring := C.CString(raw)
	packet_clength := C.uint(len(raw))
	defer C.free(unsafe.Pointer(packet_cstring))

	cpacket := C.fap_parseaprs(packet_cstring, packet_clength, C.short(boolToInt(isAX25)))

	defer C.fap_free(cpacket)

	if cpacket.error_code != nil {
		ebuffer := (*C.char)(C.malloc(C.size_t(60)))
		defer C.free(unsafe.Pointer(ebuffer))

		C.fap_explain_error(*cpacket.error_code, ebuffer)
		return &Packet{RawPacket: raw}, errors.New(C.GoString(ebuffer))
	}

	packet.Timestamp = int(time.Now().Unix())
	packet.SourceCallsign = strings.ToUpper(C.GoString(cpacket.src_callsign))
	packet.DestinationCallsign = strings.ToUpper(C.GoString(cpacket.dst_callsign))
	packet.Latitude = parseNilableCoordinate(cpacket.latitude)
	packet.Longitude = parseNilableCoordinate(cpacket.longitude)
	packet.Speed = parseNilableFloat(cpacket.speed)
	packet.Course = parseNilableUInt(cpacket.course)
	packet.Altitude = parseNilableFloat(cpacket.altitude)
	packet.Message = C.GoString(cpacket.message)
	packet.Status = C.GoStringN(cpacket.status, C.int(cpacket.status_len))
	packet.Comment = C.GoStringN(cpacket.comment, C.int(cpacket.comment_len))
	packet.RawPacket = raw

	if C.int(cpacket.path_len) > 0 {
		var CPath **C.char = cpacket.path
		length := int(cpacket.path_len)
		hdr := reflect.SliceHeader{
			Data: uintptr(unsafe.Pointer(CPath)),
			Len:  length,
			Cap:  length,
		}
		ptrSlice := *(*[]*C.char)(unsafe.Pointer(&hdr))
		packet.Path = make([]string, int(cpacket.path_len))
		for i, v := range ptrSlice {
			packet.Path[i] = C.GoString(v)
		}
	}

	switch C.getPacketType(cpacket) {
	case C.fapLOCATION:
		packet.PacketType = LocationPacketType
	case C.fapOBJECT:
		packet.PacketType = ObjectPacketType
	case C.fapITEM:
		packet.PacketType = ItemPacketType
	case C.fapMICE:
		packet.PacketType = MicePacketType
	case C.fapNMEA:
		packet.PacketType = NMEAPacketType
	case C.fapWX:
		packet.PacketType = WXPacketType
	case C.fapMESSAGE:
		packet.PacketType = MessagePacketType
	case C.fapCAPABILITIES:
		packet.PacketType = CapabilitiesPacketType
	case C.fapSTATUS:
		packet.PacketType = StatusPacketType
	case C.fapTELEMETRY:
		packet.PacketType = TelemetryPacketType
	case C.fapTELEMETRY_MESSAGE:
		packet.PacketType = TelemetryMessagePacketType
	case C.fapDX_SPOT:
		packet.PacketType = DXSpotPacketType
	case C.fapEXPERIMENTAL:
		packet.PacketType = ExperimentalPacketType
	default:
		packet.PacketType = InvalidPacketType
	}

	if cpacket.wx_report != nil {
		w := WeatherReport{
			Temperature:       parseNilableFloat(cpacket.wx_report.temp),
			InsideTemperature: parseNilableFloat(cpacket.wx_report.temp_in),
			Humidity:          parseNilableUInt(cpacket.wx_report.humidity),
			InsideHumidity:    parseNilableUInt(cpacket.wx_report.humidity_in),
			WindGust:          parseNilableFloat(cpacket.wx_report.wind_gust),
			WindDirection:     parseNilableUInt(cpacket.wx_report.wind_dir),
			WindSpeed:         parseNilableFloat(cpacket.wx_report.wind_speed),
			Pressure:          parseNilableFloat(cpacket.wx_report.pressure),
		}
		packet.Weather = &w
	}

	if cpacket.telemetry != nil {
		t := Telemetry{
			Val1: parseNilableFloat(cpacket.telemetry.val1),
			Val2: parseNilableFloat(cpacket.telemetry.val2),
			Val3: parseNilableFloat(cpacket.telemetry.val3),
			Val4: parseNilableFloat(cpacket.telemetry.val4),
			Val5: parseNilableFloat(cpacket.telemetry.val5),
		}
		packet.Telemetry = &t
	}

	// MicE alloc a buffer of 20 bytes for fap_mice_mbits_to_message C func
	cbuffer := (*C.char)(C.malloc(C.size_t(20)))
	defer C.free(unsafe.Pointer(cbuffer))

	if cpacket.messagebits != nil {
		C.fap_mice_mbits_to_message(cpacket.messagebits, cbuffer)
		packet.MicE = C.GoString(cbuffer)
	}

	return packet, nil
}

// IncludePosition return true if the packet contains a Position
func (p *Packet) IncludePosition() bool {
	if p.Latitude == InvalidCoordinate || p.Longitude == InvalidCoordinate {
		return false
	}
	return true
}

// Device return the Device describing the transceiver used to send this packet
func (p *Packet) Device() *Device {

	// 1st test exact match
	if toCall, ok := toCalls[p.DestinationCallsign]; ok {
		return &toCall
	}

	// then look in the trie
	if d := trieRoot.match(p.DestinationCallsign); d != nil {
		return d
	}

	// then search for mices we need a least '_$ or '..._$
	if len(p.Comment) >= 3 {
		// last two chars
		ls := p.Comment[len(p.Comment)-2:]
		if toCall, ok := toCalls[ls]; ok {
			return &toCall
		}
	}
	return nil
}

// return a short version of the callsign as KK6NXK for KK6NXK-7
func ShortCallsign(c string) string {
	s := strings.Split(c, "-")
	return s[0]
}

func boolToInt(b bool) int {
	if b {
		return 1
	}
	return 0
}

func parseNilableFloat(d *C.double) float64 {
	if d != nil {
		return float64(C.double(*d))
	}
	return 0
}

func parseNilableCoordinate(d *C.double) float64 {
	if d != nil {
		return float64(C.double(*d))
	}
	return InvalidCoordinate
}

func parseNilableUInt(d *C.uint) uint8 {
	if d != nil {
		return uint8(C.uint(*d))
	}
	return 0
}
<|endoftext|>


<commit_before>package reddit

// Comment represents a comment on Reddit (Reddit type t1_).
// https://github.com/reddit/reddit/wiki/JSON#comment-implements-votable--created
type Comment struct {
	ID        string `mapstructure:"id"`
	Name      string `mapstructure:"name"`
	Permalink string `mapstructure:"permalink"`

	CreatedUTC uint64 `mapstructure:"created_utc"`
	Deleted    bool   `mapstructure:"deleted"`

	Ups   int32 `mapstructure:"ups"`
	Downs int32 `mapstructure:"downs"`
	Likes bool  `mapstructure:"likes"`

	Author              string `mapstructure:"author"`
	AuthorFlairCSSClass string `mapstructure:"author_flair_css_class"`
	AuthorFlairText     string `mapstructure:"author_flair_text"`

	LinkAuthor string `mapstructure:"link_author"`
	LinkURL    string `mapstructure:"link_url"`
	LinkTitle  string `mapstructure:"link_title"`

	Subreddit   string `mapstructure:"subreddit"`
	SubredditID string `mapstructure:"subreddit_id"`

	Body     string `mapstructure:"body"`
	BodyHTML string `mapstructure:"body_html"`

	ParentID string     `mapstructure:"parent_id"`
	Replies  []*Comment `mapstructure:"reply_tree"`

	Gilded        int32  `mapstructure:"gilded"`
	Distinguished string `mapstructure:"distinguished"`
}

// Post represents posts on Reddit (Reddit type t3_).
// https://github.com/reddit/reddit/wiki/JSON#link-implements-votable--created
type Post struct {
	ID        string `mapstructure:"id"`
	Name      string `mapstructure:"name"`
	Permalink string `mapstructure:"permalink"`

	CreatedUTC uint64 `mapstructure:"created_utc"`
	Deleted    bool   `mapstructure:"deleted"`

	Ups   int32 `mapstructure:"ups"`
	Downs int32 `mapstructure:"downs"`
	Likes bool  `mapstructure:"likes"`

	Author              string `mapstructure:"author"`
	AuthorFlairCSSClass string `mapstructure:"author_flair_css_class"`
	AuthorFlairText     string `mapstructure:"author_flair_text"`

	Title  string `mapstructure:"title"`
	Score  int32  `mapstructure:"score"`
	URL    string `mapstructure:"url"`
	Domain string `mapstructure:"domain"`
	NSFW   bool   `mapstructure:"over_18"`

	Subreddit   string `mapstructure:"subreddit"`
	SubredditID string `mapstructure:"subreddit_id"`

	IsSelf       bool   `mapstructure:"is_self"`
	SelfText     string `mapstructure:"selftext"`
	SelfTextHTML string `mapstructure:"selftext_html"`

	Replies []*Comment `mapstructure:"reply_tree"`

	Hidden            bool   `mapstructure:"hidden"`
	LinkFlairCSSClass string `mapstructure:"link_flair_css_class"`
	LinkFlairText     string `mapstructure:"link_flair_text"`

	NumComments int32  `mapstructure:"num_comments"`
	Locked      bool   `mapstructure:"locked"`
	Thumbnail   string `mapstructure:"thumbnail"`

	Gilded        int32  `mapstructure:"gilded"`
	Distinguished string `mapstructure:"distinguished"`
	Stickied      bool   `mapstructure:"stickied"`
}

// Message represents messages on Reddit (Reddit type t4_).
// https://github.com/reddit/reddit/wiki/JSON#message-implements-created
type Message struct {
	ID   string `mapstructure:"id"`
	Name string `mapstructure:"name"`

	CreatedUTC uint64 `mapstructure:"created_utc"`

	Author   string `mapstructure:"author"`
	Subject  string `mapstructure:"subject"`
	Body     string `mapstructure:"body"`
	BodyHTML string `mapstructure:"body_html"`

	Context          string `mapstructure:"context"`
	FirstMessageName string `mapstructure:"first_message_name"`
	Likes            bool   `mapstructure:"likes"`
	LinkTitle        string `mapstructure:"link_title"`

	New      bool   `mapstructure:"new"`
	ParentID string `mapstructure:"parent_id"`

	Subreddit  string `mapstructure:"subreddit"`
	WasComment bool   `mapstructure:"was_comment"`
}

// Harvest is a set of all possible elements that Reddit could return in a
// listing.
type Harvest struct {
	Comments []*Comment
	Posts    []*Post
	Messages []*Message
}
<commit_msg>add IsRoot method to comment<commit_after>package reddit

import "strings"

// Comment represents a comment on Reddit (Reddit type t1_).
// https://github.com/reddit/reddit/wiki/JSON#comment-implements-votable--created
type Comment struct {
	ID        string `mapstructure:"id"`
	Name      string `mapstructure:"name"`
	Permalink string `mapstructure:"permalink"`

	CreatedUTC uint64 `mapstructure:"created_utc"`
	Deleted    bool   `mapstructure:"deleted"`

	Ups   int32 `mapstructure:"ups"`
	Downs int32 `mapstructure:"downs"`
	Likes bool  `mapstructure:"likes"`

	Author              string `mapstructure:"author"`
	AuthorFlairCSSClass string `mapstructure:"author_flair_css_class"`
	AuthorFlairText     string `mapstructure:"author_flair_text"`

	LinkAuthor string `mapstructure:"link_author"`
	LinkURL    string `mapstructure:"link_url"`
	LinkTitle  string `mapstructure:"link_title"`

	Subreddit   string `mapstructure:"subreddit"`
	SubredditID string `mapstructure:"subreddit_id"`

	Body     string `mapstructure:"body"`
	BodyHTML string `mapstructure:"body_html"`

	ParentID string     `mapstructure:"parent_id"`
	Replies  []*Comment `mapstructure:"reply_tree"`

	Gilded        int32  `mapstructure:"gilded"`
	Distinguished string `mapstructure:"distinguished"`
}

// IsRoot is true when the comment is a top level comment.
func (c *Comment) IsRoot() bool {
	parentType := strings.Split(c.ParentID, "_")[0]
	return parentType == "t3"
}

// Post represents posts on Reddit (Reddit type t3_).
// https://github.com/reddit/reddit/wiki/JSON#link-implements-votable--created
type Post struct {
	ID        string `mapstructure:"id"`
	Name      string `mapstructure:"name"`
	Permalink string `mapstructure:"permalink"`

	CreatedUTC uint64 `mapstructure:"created_utc"`
	Deleted    bool   `mapstructure:"deleted"`

	Ups   int32 `mapstructure:"ups"`
	Downs int32 `mapstructure:"downs"`
	Likes bool  `mapstructure:"likes"`

	Author              string `mapstructure:"author"`
	AuthorFlairCSSClass string `mapstructure:"author_flair_css_class"`
	AuthorFlairText     string `mapstructure:"author_flair_text"`

	Title  string `mapstructure:"title"`
	Score  int32  `mapstructure:"score"`
	URL    string `mapstructure:"url"`
	Domain string `mapstructure:"domain"`
	NSFW   bool   `mapstructure:"over_18"`

	Subreddit   string `mapstructure:"subreddit"`
	SubredditID string `mapstructure:"subreddit_id"`

	IsSelf       bool   `mapstructure:"is_self"`
	SelfText     string `mapstructure:"selftext"`
	SelfTextHTML string `mapstructure:"selftext_html"`

	Replies []*Comment `mapstructure:"reply_tree"`

	Hidden            bool   `mapstructure:"hidden"`
	LinkFlairCSSClass string `mapstructure:"link_flair_css_class"`
	LinkFlairText     string `mapstructure:"link_flair_text"`

	NumComments int32  `mapstructure:"num_comments"`
	Locked      bool   `mapstructure:"locked"`
	Thumbnail   string `mapstructure:"thumbnail"`

	Gilded        int32  `mapstructure:"gilded"`
	Distinguished string `mapstructure:"distinguished"`
	Stickied      bool   `mapstructure:"stickied"`
}

// Message represents messages on Reddit (Reddit type t4_).
// https://github.com/reddit/reddit/wiki/JSON#message-implements-created
type Message struct {
	ID   string `mapstructure:"id"`
	Name string `mapstructure:"name"`

	CreatedUTC uint64 `mapstructure:"created_utc"`

	Author   string `mapstructure:"author"`
	Subject  string `mapstructure:"subject"`
	Body     string `mapstructure:"body"`
	BodyHTML string `mapstructure:"body_html"`

	Context          string `mapstructure:"context"`
	FirstMessageName string `mapstructure:"first_message_name"`
	Likes            bool   `mapstructure:"likes"`
	LinkTitle        string `mapstructure:"link_title"`

	New      bool   `mapstructure:"new"`
	ParentID string `mapstructure:"parent_id"`

	Subreddit  string `mapstructure:"subreddit"`
	WasComment bool   `mapstructure:"was_comment"`
}

// Harvest is a set of all possible elements that Reddit could return in a
// listing.
type Harvest struct {
	Comments []*Comment
	Posts    []*Post
	Messages []*Message
}
<|endoftext|>


<commit_before>/*
Package compress provides a clever gzip compressing handler.

It takes care to not handle small contents, or contents that are already compressed (like JPEG, MPEG or PDF).
Trying to gzip them not only wastes CPU but can potentially increase the response size.

Make sure to include this handler above any other handler that alter the response body.
*/
package compress

import (
	"compress/gzip"
	"net/http"
	"strconv"
	"strings"
	"sync"
)

var (
	gzipPool = sync.Pool{New: func() interface{} {
		return gzip.NewWriter(nil)
	}}

	gzippableMinSize = 150

	notGzippableTypes = map[string]struct{}{
		"application/font-woff": {},
		"application/gzip":      {},
		"application/pdf":       {},
		"application/zip":       {},
		"audio/mp4":             {},
		"audio/mpeg":            {},
		"audio/webm":            {},
		"image/gif":             {},
		"image/jpeg":            {},
		"image/png":             {},
		"image/webp":            {},
		"video/h264":            {},
		"video/mp4":             {},
		"video/mpeg":            {},
		"video/ogg":             {},
		"video/vp8":             {},
		"video/webm":            {},
	}
)

// An Handler provides a clever gzip compressing handler.
type Handler struct {
	Next http.Handler
}

// Handle returns a Handler wrapping another http.Handler.
func Handle(h http.Handler) *Handler {
	return &Handler{h}
}

// HandleFunc returns a Handler wrapping an http.HandlerFunc.
func HandleFunc(f http.HandlerFunc) *Handler {
	return Handle(f)
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") || r.Header.Get("Sec-WebSocket-Key") != "" {
		h.Next.ServeHTTP(w, r)
		return
	}

	cw := &compressWriter{
		ResponseWriter: w,
		gzipWriter:     gzipPool.Get().(*gzip.Writer),
	}
	defer gzipPool.Put(cw.gzipWriter)
	defer cw.close()

	h.Next.ServeHTTP(cw, r)
}

// compressWriter binds the downstream repsonse writing into gzipWriter if the first content is detected as gzippable.
// gzipUse keeps this detection result:
//	-1	detected but not used
// 	0	not detected yet
// 	1	detected and used
type compressWriter struct {
	http.ResponseWriter
	gzipWriter *gzip.Writer
	gzipUse    int
	status     int
}

// WriteHeader catches a downstream WriteHeader call and caches the status code.
// The header will be written later, on the first Write call and after all the headers has been correctly set.
func (cw *compressWriter) WriteHeader(status int) {
	cw.status = status
}

// writePostponedHeader writes the response header with the cached status code.
func (cw *compressWriter) writePostponedHeader() {
	if cw.status == 0 {
		cw.status = http.StatusOK
	}
	cw.ResponseWriter.WriteHeader(cw.status)
}

// Write sets the compressing headers and calls the gzip writer, but only if the Content-Type header defines a compressible content.
// Otherwise, it calls the original Write method.
func (cw *compressWriter) Write(b []byte) (int, error) {
	if cw.gzipUse == 0 {
		ct := cw.ResponseWriter.Header().Get("Content-Type")
		if ct == "" {
			ct = http.DetectContentType(b)
			cw.ResponseWriter.Header().Set("Content-Type", ct)
		}

		cl, _ := strconv.Atoi(cw.ResponseWriter.Header().Get("Content-Length"))
		if cl < 1 {
			cl = len(b) // If no Content-Length, take the length of this first chunk.
		}

		if isGzippable(ct, cl) {
			cw.gzipUse = 1
			cw.setGzipHeaders()
			cw.gzipWriter.Reset(cw.ResponseWriter)
		} else {
			cw.gzipUse = -1
		}

		cw.writePostponedHeader()
	}

	if cw.gzipUse == 1 {
		return cw.gzipWriter.Write(b)
	}
	return cw.ResponseWriter.Write(b)
}

// close closes the gzip writer if it has been used.
func (cw *compressWriter) close() {
	if cw.gzipUse == 1 {
		cw.gzipWriter.Close()
	}
}

// setGzipHeaders sets the Content-Encoding header for a gzip response.
// Because the compressed content will have a new size, it also removes the Content-Length header as it could have been set downstream by another handler.
func (cw *compressWriter) setGzipHeaders() {
	cw.ResponseWriter.Header().Del("Content-Length")
	cw.ResponseWriter.Header().Set("Content-Encoding", "gzip")
}

// isGzippable checks if a content must be compressed following its content length (cl) and content type (ct).
func isGzippable(ct string, cl int) bool {
	if cl < gzippableMinSize || ct == "" {
		return false
	}

	_, ok := notGzippableTypes[strings.ToLower(strings.SplitN(ct, ";", 2)[0])]
	return !ok
}
<commit_msg>Update doc<commit_after>/*
Package compress provides a clever gzip compressing handler.

It takes care to not handle small contents, or contents that are already compressed (like JPEG, MPEG or PDF).
Trying to gzip them not only wastes CPU but can potentially increase the response size.

Make sure to include this handler above any other handler that alter the response body.
*/
package compress

import (
	"compress/gzip"
	"net/http"
	"strconv"
	"strings"
	"sync"
)

var (
	gzipPool = sync.Pool{New: func() interface{} {
		return gzip.NewWriter(nil)
	}}

	gzippableMinSize = 150

	notGzippableTypes = map[string]struct{}{
		"application/font-woff": {},
		"application/gzip":      {},
		"application/pdf":       {},
		"application/zip":       {},
		"audio/mp4":             {},
		"audio/mpeg":            {},
		"audio/webm":            {},
		"image/gif":             {},
		"image/jpeg":            {},
		"image/png":             {},
		"image/webp":            {},
		"video/h264":            {},
		"video/mp4":             {},
		"video/mpeg":            {},
		"video/ogg":             {},
		"video/vp8":             {},
		"video/webm":            {},
	}
)

// An Handler provides a clever gzip compressing handler.
type Handler struct {
	Next http.Handler
}

// Handle returns a Handler wrapping another http.Handler.
func Handle(h http.Handler) *Handler {
	return &Handler{h}
}

// HandleFunc returns a Handler wrapping an http.HandlerFunc.
func HandleFunc(f http.HandlerFunc) *Handler {
	return Handle(f)
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") || r.Header.Get("Sec-WebSocket-Key") != "" {
		h.Next.ServeHTTP(w, r)
		return
	}

	cw := &compressWriter{
		ResponseWriter: w,
		gzipWriter:     gzipPool.Get().(*gzip.Writer),
	}
	defer gzipPool.Put(cw.gzipWriter)
	defer cw.close()

	h.Next.ServeHTTP(cw, r)
}

// compressWriter binds the downstream repsonse writing into gzipWriter if the first content is detected as gzippable.
// gzipUse keeps this detection result:
//	-1	detection done && gzip not used
// 	0	detection not done
// 	1	detection done && gzip used
type compressWriter struct {
	http.ResponseWriter
	gzipWriter *gzip.Writer
	gzipUse    int
	status     int
}

// WriteHeader catches a downstream WriteHeader call and caches the status code.
// The header will be written later, on the first Write call and after all the headers has been correctly set.
func (cw *compressWriter) WriteHeader(status int) {
	cw.status = status
}

// writePostponedHeader writes the response header with the cached status code.
func (cw *compressWriter) writePostponedHeader() {
	if cw.status == 0 {
		cw.status = http.StatusOK
	}
	cw.ResponseWriter.WriteHeader(cw.status)
}

// Write sets the compressing headers and calls the gzip writer, but only if the Content-Type header defines a compressible content.
// Otherwise, it calls the original Write method.
func (cw *compressWriter) Write(b []byte) (int, error) {
	if cw.gzipUse == 0 {
		ct := cw.ResponseWriter.Header().Get("Content-Type")
		if ct == "" {
			ct = http.DetectContentType(b)
			cw.ResponseWriter.Header().Set("Content-Type", ct)
		}

		cl, _ := strconv.Atoi(cw.ResponseWriter.Header().Get("Content-Length"))
		if cl < 1 {
			cl = len(b) // If no Content-Length, take the length of this first chunk.
		}

		if isGzippable(ct, cl) {
			cw.gzipUse = 1
			cw.setGzipHeaders()
			cw.gzipWriter.Reset(cw.ResponseWriter)
		} else {
			cw.gzipUse = -1
		}

		cw.writePostponedHeader()
	}

	if cw.gzipUse == 1 {
		return cw.gzipWriter.Write(b)
	}
	return cw.ResponseWriter.Write(b)
}

// close closes the gzip writer if it has been used.
func (cw *compressWriter) close() {
	if cw.gzipUse == 1 {
		cw.gzipWriter.Close()
	}
}

// setGzipHeaders sets the Content-Encoding header for a gzip response.
// Because the compressed content will have a new size, it also removes the Content-Length header as it could have been set downstream by another handler.
func (cw *compressWriter) setGzipHeaders() {
	cw.ResponseWriter.Header().Del("Content-Length")
	cw.ResponseWriter.Header().Set("Content-Encoding", "gzip")
}

// isGzippable checks if a content must be compressed following its content length (cl) and content type (ct).
func isGzippable(ct string, cl int) bool {
	if cl < gzippableMinSize || ct == "" {
		return false
	}

	_, ok := notGzippableTypes[strings.ToLower(strings.SplitN(ct, ";", 2)[0])]
	return !ok
}
<|endoftext|>


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

package redis

// Error represents an error returned in a command reply.
type Error string

func (err Error) Error() string { return string(err) }

// Conn represents a connection to a Redis server.
type Conn interface {
	// Close closes the connection.
	Close() error

	// Err returns a non-nil value when the connection is not usable.
	Err() error

	// Do sends a command to the server and returns the received reply.
	Do(commandName string, args ...interface{}) (reply interface{}, err error)

	// Send writes the command to the client's output buffer.
	Send(commandName string, args ...interface{}) error

	// Flush flushes the output buffer to the Redis server.
	Flush() error

	// Receive receives a single reply from the Redis server
	Receive() (reply interface{}, err error)
}

// Argument is the interface implemented by an object which wants to control how
// the object is converted to Redis bulk strings.
type Argument interface {
	// RedisArg returns a value to be encoded as a bulk string per the
	// conversions listed in the section 'Executing Commands'.
	// Implementations should typically return a []byte or string.
	RedisArg() interface{}
}

// Scanner is implemented by an object which wants to control it's value is
// interpreted when read from Redis.
type Scanner interface {
	// RedisScan assigns a value from a Redis value. The argument src is one of
	// the reply types listed in the section `Executing Commands`.
	//
	// An error should be returned if the value cannot be stored without
	// loss of information.
	RedisScan(src interface{}) error
}
<commit_msg>Fix grammar in comment (#283)<commit_after>// Copyright 2012 Gary Burd
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package redis

// Error represents an error returned in a command reply.
type Error string

func (err Error) Error() string { return string(err) }

// Conn represents a connection to a Redis server.
type Conn interface {
	// Close closes the connection.
	Close() error

	// Err returns a non-nil value when the connection is not usable.
	Err() error

	// Do sends a command to the server and returns the received reply.
	Do(commandName string, args ...interface{}) (reply interface{}, err error)

	// Send writes the command to the client's output buffer.
	Send(commandName string, args ...interface{}) error

	// Flush flushes the output buffer to the Redis server.
	Flush() error

	// Receive receives a single reply from the Redis server
	Receive() (reply interface{}, err error)
}

// Argument is the interface implemented by an object which wants to control how
// the object is converted to Redis bulk strings.
type Argument interface {
	// RedisArg returns a value to be encoded as a bulk string per the
	// conversions listed in the section 'Executing Commands'.
	// Implementations should typically return a []byte or string.
	RedisArg() interface{}
}

// Scanner is implemented by an object which wants to control its value is
// interpreted when read from Redis.
type Scanner interface {
	// RedisScan assigns a value from a Redis value. The argument src is one of
	// the reply types listed in the section `Executing Commands`.
	//
	// An error should be returned if the value cannot be stored without
	// loss of information.
	RedisScan(src interface{}) error
}
<|endoftext|>


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

package jtl

// inbound plugins, currently only supported plugins are webhook and stdin,
// other plugins could be provided for websocket, kafka, sqs etc.

import (
	"encoding/json"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	. "github.com/Comcast/eel/util"
)

type InboundPlugin interface {
	StartPlugin(Context)
	GetSettings() *PluginSettings
	StopPlugin(Context)
	IsActive() bool
}

type PluginSettings struct {
	Type       string
	Name       string
	Active     bool
	RestartOk  bool
	AutoStart  bool
	ExitOnErr  bool
	Parameters map[string]interface{}
	Stats      PluginStats
}

type PluginStats struct {
	MessageCount uint64
}

type NewInboundPlugin func(*PluginSettings) InboundPlugin

type PluginConfigList []*PluginSettings

// plugins by name
var inboundPluginMap = make(map[string]InboundPlugin, 0)

// plugins by type
var inboundPluginTypeMap = make(map[string]NewInboundPlugin, 0)

var pluginConfigList PluginConfigList

// RegisterInboundPlugin registers an (external) plugin implementation by plugin type
func RegisterInboundPluginType(newPlugin NewInboundPlugin, pluginType string) {
	inboundPluginTypeMap[pluginType] = newPlugin
}

func GetInboundPluginByType(pluginType string) InboundPlugin {
	// currently only one active plugin per type allowed!!!
	for _, v := range inboundPluginMap {
		if v.GetSettings().Type == pluginType {
			return v
		}
	}
	return nil
}

func GetInboundPluginByName(name string) InboundPlugin {
	return inboundPluginMap[name]
}

func PluginConfigHandler(w http.ResponseWriter, r *http.Request) {
	ctx := Gctx.SubContext()
	w.Header().Set("Content-Type", "application/json")
	state := make(map[string]interface{}, 0)
	state["Version"] = GetConfig(ctx).Version
	state["PluginConfigs"] = pluginConfigList
	buf, err := json.MarshalIndent(state, "", "\t")
	if err != nil {
		fmt.Fprintf(w, `{"error":"%s"}`, err.Error())
	} else {
		fmt.Fprintf(w, string(buf))
	}
}

func ManagePluginsUIHandler(w http.ResponseWriter, r *http.Request) {
	ctx := Gctx.SubContext()
	pluginsTemplate, err := template.ParseFiles("web/plugins.html")
	if err != nil {
		ctx.Log().Error("error_type", "manage_plugins", "cause", "template_parse_error", "error", err.Error())
	}
	operation := r.FormValue("operation")
	name := r.FormValue("name")
	if operation == "Start" && name != "" {
		p := GetInboundPluginByName(name)
		if p != nil && !p.IsActive() {
			p.StartPlugin(ctx)
			time.Sleep(1 * time.Second)
		}
	} else if operation == "Stop" && name != "" {
		p := GetInboundPluginByName(name)
		if p != nil && p.IsActive() {
			p.StopPlugin(ctx)
		}
	}
	psl := make([]*PluginSettings, 0)
	for _, p := range inboundPluginMap {
		psl = append(psl, p.GetSettings())
	}
	err = pluginsTemplate.Execute(w, psl)
	if err != nil {
		ctx.Log().Error("error_type", "manage_plugins", "cause", "template_exec_error", "error", err.Error())
	}
}

func ManagePluginsHandler(w http.ResponseWriter, r *http.Request) {
	ctx := Gctx.SubContext()
	path := strings.Split(r.URL.Path, "/")
	pName := path[len(path)-2]
	p := GetInboundPluginByName(pName)
	if p == nil {
		ctx.Log().Error("error_type", "manage_plugins", "cause", "unknown_plugin", "error", "unknown plugin "+pName)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, `{"error":"%s"}`, "unknown plugin "+pName)
		return
	}
	op := path[len(path)-1]
	switch op {
	case "start":
		if p.GetSettings().RestartOk == false {
			ctx.Log().Error("error_type", "manage_plugins", "cause", "plugin_cannot_restart", "error", "plugin cannot restart "+pName)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, "plugin cannot restart "+pName)
			return
		}
		if p.GetSettings().Active == true {
			ctx.Log().Error("error_type", "manage_plugins", "cause", "plugin_already_running", "error", "plugin already running "+pName)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, "plugin already running "+pName)
			return
		}
		p.StartPlugin(ctx)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, `{"plugin":"%s", "action":"started"}`, pName)
	case "stop":
		if p.GetSettings().RestartOk == false {
			ctx.Log().Error("error_type", "manage_plugins", "cause", "plugin_cannot_restart", "error", "plugin cannot restart "+pName)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, "plugin cannot restart "+pName)
			return
		}
		if p.GetSettings().Active == false {
			ctx.Log().Error("error_type", "manage_plugins", "cause", "plugin_stopped", "error", "plugin stopped "+pName)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, "plugin stopped "+pName)
			return
		}
		p.StopPlugin(ctx)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, `{"plugin":"%s", "action":"stopped"}`, pName)
	case "state":
		state := make(map[string]interface{}, 0)
		state["Version"] = GetConfig(ctx).Version
		state["PluginConfigs"] = p.GetSettings()
		buf, err := json.MarshalIndent(state, "", "\t")
		if err != nil {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, err.Error())
		} else {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			fmt.Fprintf(w, string(buf))
		}
	case "config":
		state := make(map[string]interface{}, 0)
		state["Version"] = GetConfig(ctx).Version
		state["PluginConfigs"] = p.GetSettings()
		buf, err := json.MarshalIndent(state, "", "\t")
		if err != nil {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, err.Error())
		} else {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			fmt.Fprintf(w, string(buf))
		}
	default:
		ctx.Log().Error("error_type", "manage_plugins", "cause", "unknown_op", "error", "unknown op "+op)
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, `{"error":"%s"}`, "unknown plugin "+pName)
		return
	}
}

func GetPluginConfigList(ctx Context) PluginConfigList {
	configFile, err := os.Open(filepath.Join(filepath.Dir(ConfigPath), "plugins.json"))
	if err != nil {
		// csv-context-go may not be ready yet for logging
		fmt.Printf("{ \"error\" : \"%s\" }", err.Error())
		ctx.Log().Error("error_type", "get_plugin_config", "cause", "open_config", "error", err.Error())
		os.Exit(1)
	}
	defer configFile.Close()
	configData, err := ioutil.ReadAll(configFile)
	if err != nil {
		fmt.Printf("{ \"error\" : \"%s\" }", err.Error())
		ctx.Log().Error("error_type", "get_plugin_config", "cause", "read_config", "error", err.Error())
		os.Exit(1)
	}
	var config PluginConfigList
	err = json.Unmarshal(configData, &config)
	if err != nil {
		fmt.Printf("{ \"error\" : \"%s\" }", err.Error())
		ctx.Log().Error("error_type", "get_plugin_config", "cause", "parse_config", "error", err.Error())
		os.Exit(1)
	}
	return config
}

func LoadInboundPlugins(ctx Context) {
	// load plugin configs
	pluginConfigList = GetPluginConfigList(ctx)
	for _, e := range pluginConfigList {
		// dependency injection
		np := inboundPluginTypeMap[e.Type]
		if np == nil {
			ctx.Log().Error("error_type", "bad_plugin_config", "cause", "unknown_plugin_type", "plugin_type", e.Type)
		} else {
			inboundPluginMap[e.Name] = np(e)
		}
	}
	// launch plugins
	for k, v := range inboundPluginMap {
		if v.GetSettings().AutoStart {
			ctx.Log().Info("action", "launching_inbound_plugin", "plugin_name", k, "pugin_type", v.GetSettings().Type)
			v.StartPlugin(ctx)
		} else {
			ctx.Log().Info("action", "skipping_inactive_plugin", "plugin_name", k, "pugin_type", v.GetSettings().Type)
		}
	}
	// need sync path in inproc.go
	if GetInboundPluginByType("WEBHOOK") != nil {
		syncPath := GetInboundPluginByType("WEBHOOK").GetSettings().Parameters["EventProcPath"]
		Gctx.AddConfigValue(EelSyncPath, syncPath)
	} else {
		Gctx.AddConfigValue(EelSyncPath, "")
	}
}
<commit_msg>more plugin stats<commit_after>/**
 * Copyright 2015 Comcast Cable Communications Management, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jtl

// inbound plugins, currently only supported plugins are webhook and stdin,
// other plugins could be provided for websocket, kafka, sqs etc.

import (
	"encoding/json"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	. "github.com/Comcast/eel/util"
)

type InboundPlugin interface {
	StartPlugin(Context)
	GetSettings() *PluginSettings
	StopPlugin(Context)
	IsActive() bool
}

type PluginSettings struct {
	Type       string
	Name       string
	Active     bool
	RestartOk  bool
	AutoStart  bool
	ExitOnErr  bool
	Parameters map[string]interface{}
	Stats      PluginStats
}

type PluginStats struct {
	MessageCount      uint64
	MessagesPerSecond uint64
	MessagesPerMinute uint64
}

type NewInboundPlugin func(*PluginSettings) InboundPlugin

type PluginConfigList []*PluginSettings

// plugins by name
var inboundPluginMap = make(map[string]InboundPlugin, 0)

// plugins by type
var inboundPluginTypeMap = make(map[string]NewInboundPlugin, 0)

var pluginConfigList PluginConfigList

// RegisterInboundPlugin registers an (external) plugin implementation by plugin type
func RegisterInboundPluginType(newPlugin NewInboundPlugin, pluginType string) {
	inboundPluginTypeMap[pluginType] = newPlugin
}

func GetInboundPluginByType(pluginType string) InboundPlugin {
	// currently only one active plugin per type allowed!!!
	for _, v := range inboundPluginMap {
		if v.GetSettings().Type == pluginType {
			return v
		}
	}
	return nil
}

func GetInboundPluginByName(name string) InboundPlugin {
	return inboundPluginMap[name]
}

func PluginConfigHandler(w http.ResponseWriter, r *http.Request) {
	ctx := Gctx.SubContext()
	w.Header().Set("Content-Type", "application/json")
	state := make(map[string]interface{}, 0)
	state["Version"] = GetConfig(ctx).Version
	state["PluginConfigs"] = pluginConfigList
	buf, err := json.MarshalIndent(state, "", "\t")
	if err != nil {
		fmt.Fprintf(w, `{"error":"%s"}`, err.Error())
	} else {
		fmt.Fprintf(w, string(buf))
	}
}

func ManagePluginsUIHandler(w http.ResponseWriter, r *http.Request) {
	ctx := Gctx.SubContext()
	pluginsTemplate, err := template.ParseFiles("web/plugins.html")
	if err != nil {
		ctx.Log().Error("error_type", "manage_plugins", "cause", "template_parse_error", "error", err.Error())
	}
	operation := r.FormValue("operation")
	name := r.FormValue("name")
	if operation == "Start" && name != "" {
		p := GetInboundPluginByName(name)
		if p != nil && !p.IsActive() {
			p.StartPlugin(ctx)
			time.Sleep(1 * time.Second)
		}
	} else if operation == "Stop" && name != "" {
		p := GetInboundPluginByName(name)
		if p != nil && p.IsActive() {
			p.StopPlugin(ctx)
		}
	}
	psl := make([]*PluginSettings, 0)
	for _, p := range inboundPluginMap {
		psl = append(psl, p.GetSettings())
	}
	err = pluginsTemplate.Execute(w, psl)
	if err != nil {
		ctx.Log().Error("error_type", "manage_plugins", "cause", "template_exec_error", "error", err.Error())
	}
}

func ManagePluginsHandler(w http.ResponseWriter, r *http.Request) {
	ctx := Gctx.SubContext()
	path := strings.Split(r.URL.Path, "/")
	pName := path[len(path)-2]
	p := GetInboundPluginByName(pName)
	if p == nil {
		ctx.Log().Error("error_type", "manage_plugins", "cause", "unknown_plugin", "error", "unknown plugin "+pName)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, `{"error":"%s"}`, "unknown plugin "+pName)
		return
	}
	op := path[len(path)-1]
	switch op {
	case "start":
		if p.GetSettings().RestartOk == false {
			ctx.Log().Error("error_type", "manage_plugins", "cause", "plugin_cannot_restart", "error", "plugin cannot restart "+pName)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, "plugin cannot restart "+pName)
			return
		}
		if p.GetSettings().Active == true {
			ctx.Log().Error("error_type", "manage_plugins", "cause", "plugin_already_running", "error", "plugin already running "+pName)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, "plugin already running "+pName)
			return
		}
		p.StartPlugin(ctx)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, `{"plugin":"%s", "action":"started"}`, pName)
	case "stop":
		if p.GetSettings().RestartOk == false {
			ctx.Log().Error("error_type", "manage_plugins", "cause", "plugin_cannot_restart", "error", "plugin cannot restart "+pName)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, "plugin cannot restart "+pName)
			return
		}
		if p.GetSettings().Active == false {
			ctx.Log().Error("error_type", "manage_plugins", "cause", "plugin_stopped", "error", "plugin stopped "+pName)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, "plugin stopped "+pName)
			return
		}
		p.StopPlugin(ctx)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, `{"plugin":"%s", "action":"stopped"}`, pName)
	case "state":
		state := make(map[string]interface{}, 0)
		state["Version"] = GetConfig(ctx).Version
		state["PluginConfigs"] = p.GetSettings()
		buf, err := json.MarshalIndent(state, "", "\t")
		if err != nil {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, err.Error())
		} else {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			fmt.Fprintf(w, string(buf))
		}
	case "config":
		state := make(map[string]interface{}, 0)
		state["Version"] = GetConfig(ctx).Version
		state["PluginConfigs"] = p.GetSettings()
		buf, err := json.MarshalIndent(state, "", "\t")
		if err != nil {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, `{"error":"%s"}`, err.Error())
		} else {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			fmt.Fprintf(w, string(buf))
		}
	default:
		ctx.Log().Error("error_type", "manage_plugins", "cause", "unknown_op", "error", "unknown op "+op)
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, `{"error":"%s"}`, "unknown plugin "+pName)
		return
	}
}

func GetPluginConfigList(ctx Context) PluginConfigList {
	configFile, err := os.Open(filepath.Join(filepath.Dir(ConfigPath), "plugins.json"))
	if err != nil {
		// csv-context-go may not be ready yet for logging
		fmt.Printf("{ \"error\" : \"%s\" }", err.Error())
		ctx.Log().Error("error_type", "get_plugin_config", "cause", "open_config", "error", err.Error())
		os.Exit(1)
	}
	defer configFile.Close()
	configData, err := ioutil.ReadAll(configFile)
	if err != nil {
		fmt.Printf("{ \"error\" : \"%s\" }", err.Error())
		ctx.Log().Error("error_type", "get_plugin_config", "cause", "read_config", "error", err.Error())
		os.Exit(1)
	}
	var config PluginConfigList
	err = json.Unmarshal(configData, &config)
	if err != nil {
		fmt.Printf("{ \"error\" : \"%s\" }", err.Error())
		ctx.Log().Error("error_type", "get_plugin_config", "cause", "parse_config", "error", err.Error())
		os.Exit(1)
	}
	return config
}

func LoadInboundPlugins(ctx Context) {
	// load plugin configs
	pluginConfigList = GetPluginConfigList(ctx)
	for _, e := range pluginConfigList {
		// dependency injection
		np := inboundPluginTypeMap[e.Type]
		if np == nil {
			ctx.Log().Error("error_type", "bad_plugin_config", "cause", "unknown_plugin_type", "plugin_type", e.Type)
		} else {
			inboundPluginMap[e.Name] = np(e)
		}
	}
	// launch plugins
	for k, v := range inboundPluginMap {
		if v.GetSettings().AutoStart {
			ctx.Log().Info("action", "launching_inbound_plugin", "plugin_name", k, "pugin_type", v.GetSettings().Type)
			v.StartPlugin(ctx)
		} else {
			ctx.Log().Info("action", "skipping_inactive_plugin", "plugin_name", k, "pugin_type", v.GetSettings().Type)
		}
	}
	// need sync path in inproc.go
	if GetInboundPluginByType("WEBHOOK") != nil {
		syncPath := GetInboundPluginByType("WEBHOOK").GetSettings().Parameters["EventProcPath"]
		Gctx.AddConfigValue(EelSyncPath, syncPath)
	} else {
		Gctx.AddConfigValue(EelSyncPath, "")
	}
}
<|endoftext|>