<commit_before>/*
Package flagtag provides support for creating command line flags by tagging appropriate struct fields with the 'flag' tag.
*/
package flagtag

import (
	"errors"
	"flag"
	"reflect"
	"strconv"
	"strings"
	"time"
	"unsafe"
)

// MustConfigureAndParse is like ConfigureAndParse, the only difference is that
// it will panic in case of an error.
func MustConfigureAndParse(config interface{}) {
	if err := ConfigureAndParse(config); err != nil {
		panic(err)
	}
}

// MustConfigure is like Configure, the only difference is that it will panic
// in case of an error.
func MustConfigure(config interface{}) {
	if err := Configure(config); err != nil {
		panic(err)
	}
}

// ConfigureAndParse will first attempt to configure the flags according to the
// provided config type. If any error occurs, this error will be returned and
// the command line arguments will not be parsed. If no error occurs, the
// command line arguments will be parsed and the config type will contain the
// result.
// Using this function may remove the need to even import the flag package at
// all.
func ConfigureAndParse(config interface{}) error {
	if err := Configure(config); err != nil {
		return err
	}
	flag.Parse()
	return nil
}

// Configure will configure the flag parameters according to the tags of the
// provided data type. It is allowed to call this method multiple times with
// different data types. (As long as flag's Parse() method has not been called
// yet.)
// Fields without a 'flag' tag or with an empty 'flag' tag will be ignored.
//
// The 'flag' tag consists of 3 parts, similar to the *Var-functions of the
// flag package. Parts are separated by a comma. The parts are:
//  - 1st: flag name
//  - 2nd: default value
//  - 3rd: usage description
//
// Example:
//  `flag:"verbose,false,Enable verbose output."`.
//
// This will create a flag 'verbose', which defaults to 'false' and shows usage
// information "Enable verbose output.".
//
// If an error occurs, this error will be returned and the configuration of
// other struct fields will be aborted.
func Configure(config interface{}) error {
	val, err := getStructValue(config)
	if err != nil {
		return err
	}
	return configure(val)
}

// configure (recursively) configures flags as they are discovered in the provided type and value.
// In case of an error, the error is returned. Possible errors are:
// - Invalid default values, error of type ErrInvalidDefault.
// - nil pointer provided.
// - nil interface provided.
// - interface to nil value provided.
// - Tagged variable uses unsupported data type.
func configure(structValue reflect.Value) error {
	var structType = structValue.Type()
	for i := 0; i < structType.NumField(); i++ {
		field := structType.Field(i)
		fieldType := field.Type
		fieldValue := structValue.Field(i)
		t := field.Tag.Get("flag")
		if t == "" {
			// if field is not tagged then we do not need to flag the type itself
			if fieldType.Kind() == reflect.Struct {
				// kind is a struct => recurse into inner struct
				if err := configure(fieldValue); err != nil {
					return err
				}
			}
		} else {
			// field is tagged, continue investigating what kind of flag to create
			tag := parseTag(t, field.Tag.Get("flagopt"))
			if tag.Name == "" {
				// tag is invalid, since there is no name
				return errors.New("field '" + field.Name + "': invalid flag name: empty string")
			}
			switch fieldType.Kind() {
			case reflect.Ptr:
				// unwrap pointer
				if fieldValue.IsNil() {
					return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "'): cannot use nil pointer")
				}
				fieldType = fieldType.Elem()
				fieldValue = fieldValue.Elem()
			case reflect.Interface:
				// check if interface is valid
				if fieldValue.IsNil() {
					return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "'): cannot use nil interface")
				}
				var value = reflect.ValueOf(fieldValue.Interface())
				switch value.Type().Kind() {
				case reflect.Ptr, reflect.Interface:
					if value.IsNil() {
						return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "'): cannot use nil interface value")
					}
				}
			}
			if !fieldValue.CanSet() {
				return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "') is unexported or unaddressable: cannot use this field")
			}
			// TODO create a tag hint for ignoring the ValueInterface check
			if !tag.Options.skipFlagValue && registerFlagByValueInterface(fieldValue, &tag) {
				// no error during registration => Var-flag registered => continue with next field
				continue
			}
			if err := registerFlagByPrimitive(field.Name, fieldValue, &tag); err != nil {
				return err
			}
		}
	}
	return nil
}

// registerFlagByValueInterface checks if the provided type can be treated as flag.Value.
// If so, a flag.Value flag is set and true is returned. If no flag is set, false is returned.
func registerFlagByValueInterface(fieldValue reflect.Value, tag *flagTag) bool {
	switch fieldValue.Type().Kind() {
	case reflect.Interface:
		if value, ok := fieldValue.Interface().(flag.Value); ok {
			// unknown interface implements flag.Value, register as such
			// TODO (how to) set default value? (i.e. ignore default value?)
			flag.Var(value, tag.Name, tag.Description)
			return true
		}
	default:
		if value, ok := fieldValue.Addr().Interface().(flag.Value); ok {
			// field type implements flag.Value interface, register as such
			// TODO (how to) set default value? (i.e. ignore default value?)
			flag.Var(value, tag.Name, tag.Description)
			return true
		}
	}
	return false
}

// registerFlagByPrimitive registers a single field as one of the primitive flag types. Types are matched by
// kind, so types derived from one of the basic types are still eligible for a flag.
//
// If it is not possible to register a flag because of an unknown data type, an error will be returned.
// If the specified default value is invalid, an error of type ErrInvalidDefault will be returned.
func registerFlagByPrimitive(fieldName string, fieldValue reflect.Value, tag *flagTag) error {
	var fieldType = fieldValue.Type()
	// Check time.Duration first, since it will also match one of the basic kinds.
	if durationVar, ok := fieldValue.Addr().Interface().(*time.Duration); ok {
		// field is a time.Duration
		defaultVal, err := time.ParseDuration(tag.DefaultValue)
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.DurationVar(durationVar, tag.Name, defaultVal, tag.Description)
		return nil
	}
	// Check basic kinds.
	var fieldPtr = unsafe.Pointer(fieldValue.UnsafeAddr())
	switch fieldType.Kind() {
	case reflect.String:
		flag.StringVar((*string)(fieldPtr), tag.Name, tag.DefaultValue, tag.Description)
	case reflect.Bool:
		defaultVal, err := strconv.ParseBool(tag.DefaultValue)
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.BoolVar((*bool)(fieldPtr), tag.Name, defaultVal, tag.Description)
	case reflect.Float64:
		defaultVal, err := strconv.ParseFloat(tag.DefaultValue, 64)
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.Float64Var((*float64)(fieldPtr), tag.Name, defaultVal, tag.Description)
	case reflect.Int:
		defaultVal, err := strconv.ParseInt(tag.DefaultValue, 0, fieldType.Bits())
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.IntVar((*int)(fieldPtr), tag.Name, int(defaultVal), tag.Description)
	case reflect.Int64:
		defaultVal, err := strconv.ParseInt(tag.DefaultValue, 0, 64)
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.Int64Var((*int64)(fieldPtr), tag.Name, defaultVal, tag.Description)
	case reflect.Uint:
		defaultVal, err := strconv.ParseUint(tag.DefaultValue, 0, fieldType.Bits())
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.UintVar((*uint)(fieldPtr), tag.Name, uint(defaultVal), tag.Description)
	case reflect.Uint64:
		defaultVal, err := strconv.ParseUint(tag.DefaultValue, 0, 64)
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.Uint64Var((*uint64)(fieldPtr), tag.Name, defaultVal, tag.Description)
	default:
		return errors.New("unsupported data type (kind '" + strconv.FormatUint(uint64(fieldType.Kind()), 10) + "') for field '" + fieldName + "' (tag '" + tag.Name + "')")
	}
	return nil
}

// getStructValue checks that the provided config instance is actually a struct not a nil value.
func getStructValue(config interface{}) (reflect.Value, error) {
	var zero reflect.Value
	if config == nil {
		return zero, errors.New("config cannot be nil")
	}
	ptr := reflect.ValueOf(config)
	if ptr.IsNil() {
		return zero, errors.New("config cannot point to nil")
	}
	val := reflect.Indirect(ptr)
	if val.Kind() != reflect.Struct {
		return zero, errors.New("config instance is not a struct")
	}
	return val, nil
}

// parseTag parses a string of text and separates the various sections of the 'flag'-tag.
func parseTag(value string, optvalue string) flagTag {
	parts := strings.SplitN(value, ",", 3)
	for len(parts) < 3 {
		parts = append(parts, "")
	}
	var flag = flagTag{Name: parts[0], DefaultValue: parts[1], Description: parts[2]}
	if optvalue != "" {
		if strings.Contains(optvalue, "skipFlagValue") {
			flag.Options.skipFlagValue = true
		}
	}
	return flag
}

// flagTag contains the parsed tag values
type flagTag struct {
	Name         string
	DefaultValue string
	Description  string
	Options      flagoptTag
}

type ErrInvalidDefault struct {
	field string
	tag   string
	err   error
}

func (e *ErrInvalidDefault) Error() string {
	return "invalid default value for field '" + e.field + "' (tag '" + e.tag + "'): " + e.err.Error()
}

// flagoptTag contains the parsed additional flag options
type flagoptTag struct {
	skipFlagValue bool
}
<commit_msg>Removed obsolete TODO<commit_after>/*
Package flagtag provides support for creating command line flags by tagging appropriate struct fields with the 'flag' tag.
*/
package flagtag

import (
	"errors"
	"flag"
	"reflect"
	"strconv"
	"strings"
	"time"
	"unsafe"
)

// MustConfigureAndParse is like ConfigureAndParse, the only difference is that
// it will panic in case of an error.
func MustConfigureAndParse(config interface{}) {
	if err := ConfigureAndParse(config); err != nil {
		panic(err)
	}
}

// MustConfigure is like Configure, the only difference is that it will panic
// in case of an error.
func MustConfigure(config interface{}) {
	if err := Configure(config); err != nil {
		panic(err)
	}
}

// ConfigureAndParse will first attempt to configure the flags according to the
// provided config type. If any error occurs, this error will be returned and
// the command line arguments will not be parsed. If no error occurs, the
// command line arguments will be parsed and the config type will contain the
// result.
// Using this function may remove the need to even import the flag package at
// all.
func ConfigureAndParse(config interface{}) error {
	if err := Configure(config); err != nil {
		return err
	}
	flag.Parse()
	return nil
}

// Configure will configure the flag parameters according to the tags of the
// provided data type. It is allowed to call this method multiple times with
// different data types. (As long as flag's Parse() method has not been called
// yet.)
// Fields without a 'flag' tag or with an empty 'flag' tag will be ignored.
//
// The 'flag' tag consists of 3 parts, similar to the *Var-functions of the
// flag package. Parts are separated by a comma. The parts are:
//  - 1st: flag name
//  - 2nd: default value
//  - 3rd: usage description
//
// Example:
//  `flag:"verbose,false,Enable verbose output."`.
//
// This will create a flag 'verbose', which defaults to 'false' and shows usage
// information "Enable verbose output.".
//
// If an error occurs, this error will be returned and the configuration of
// other struct fields will be aborted.
func Configure(config interface{}) error {
	val, err := getStructValue(config)
	if err != nil {
		return err
	}
	return configure(val)
}

// configure (recursively) configures flags as they are discovered in the provided type and value.
// In case of an error, the error is returned. Possible errors are:
// - Invalid default values, error of type ErrInvalidDefault.
// - nil pointer provided.
// - nil interface provided.
// - interface to nil value provided.
// - Tagged variable uses unsupported data type.
func configure(structValue reflect.Value) error {
	var structType = structValue.Type()
	for i := 0; i < structType.NumField(); i++ {
		field := structType.Field(i)
		fieldType := field.Type
		fieldValue := structValue.Field(i)
		t := field.Tag.Get("flag")
		if t == "" {
			// if field is not tagged then we do not need to flag the type itself
			if fieldType.Kind() == reflect.Struct {
				// kind is a struct => recurse into inner struct
				if err := configure(fieldValue); err != nil {
					return err
				}
			}
		} else {
			// field is tagged, continue investigating what kind of flag to create
			tag := parseTag(t, field.Tag.Get("flagopt"))
			if tag.Name == "" {
				// tag is invalid, since there is no name
				return errors.New("field '" + field.Name + "': invalid flag name: empty string")
			}
			switch fieldType.Kind() {
			case reflect.Ptr:
				// unwrap pointer
				if fieldValue.IsNil() {
					return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "'): cannot use nil pointer")
				}
				fieldType = fieldType.Elem()
				fieldValue = fieldValue.Elem()
			case reflect.Interface:
				// check if interface is valid
				if fieldValue.IsNil() {
					return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "'): cannot use nil interface")
				}
				var value = reflect.ValueOf(fieldValue.Interface())
				switch value.Type().Kind() {
				case reflect.Ptr, reflect.Interface:
					if value.IsNil() {
						return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "'): cannot use nil interface value")
					}
				}
			}
			if !fieldValue.CanSet() {
				return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "') is unexported or unaddressable: cannot use this field")
			}
			if !tag.Options.skipFlagValue && registerFlagByValueInterface(fieldValue, &tag) {
				// no error during registration => Var-flag registered => continue with next field
				continue
			}
			if err := registerFlagByPrimitive(field.Name, fieldValue, &tag); err != nil {
				return err
			}
		}
	}
	return nil
}

// registerFlagByValueInterface checks if the provided type can be treated as flag.Value.
// If so, a flag.Value flag is set and true is returned. If no flag is set, false is returned.
func registerFlagByValueInterface(fieldValue reflect.Value, tag *flagTag) bool {
	switch fieldValue.Type().Kind() {
	case reflect.Interface:
		if value, ok := fieldValue.Interface().(flag.Value); ok {
			// unknown interface implements flag.Value, register as such
			// TODO (how to) set default value? (i.e. ignore default value?)
			flag.Var(value, tag.Name, tag.Description)
			return true
		}
	default:
		if value, ok := fieldValue.Addr().Interface().(flag.Value); ok {
			// field type implements flag.Value interface, register as such
			// TODO (how to) set default value? (i.e. ignore default value?)
			flag.Var(value, tag.Name, tag.Description)
			return true
		}
	}
	return false
}

// registerFlagByPrimitive registers a single field as one of the primitive flag types. Types are matched by
// kind, so types derived from one of the basic types are still eligible for a flag.
//
// If it is not possible to register a flag because of an unknown data type, an error will be returned.
// If the specified default value is invalid, an error of type ErrInvalidDefault will be returned.
func registerFlagByPrimitive(fieldName string, fieldValue reflect.Value, tag *flagTag) error {
	var fieldType = fieldValue.Type()
	// Check time.Duration first, since it will also match one of the basic kinds.
	if durationVar, ok := fieldValue.Addr().Interface().(*time.Duration); ok {
		// field is a time.Duration
		defaultVal, err := time.ParseDuration(tag.DefaultValue)
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.DurationVar(durationVar, tag.Name, defaultVal, tag.Description)
		return nil
	}
	// Check basic kinds.
	var fieldPtr = unsafe.Pointer(fieldValue.UnsafeAddr())
	switch fieldType.Kind() {
	case reflect.String:
		flag.StringVar((*string)(fieldPtr), tag.Name, tag.DefaultValue, tag.Description)
	case reflect.Bool:
		defaultVal, err := strconv.ParseBool(tag.DefaultValue)
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.BoolVar((*bool)(fieldPtr), tag.Name, defaultVal, tag.Description)
	case reflect.Float64:
		defaultVal, err := strconv.ParseFloat(tag.DefaultValue, 64)
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.Float64Var((*float64)(fieldPtr), tag.Name, defaultVal, tag.Description)
	case reflect.Int:
		defaultVal, err := strconv.ParseInt(tag.DefaultValue, 0, fieldType.Bits())
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.IntVar((*int)(fieldPtr), tag.Name, int(defaultVal), tag.Description)
	case reflect.Int64:
		defaultVal, err := strconv.ParseInt(tag.DefaultValue, 0, 64)
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.Int64Var((*int64)(fieldPtr), tag.Name, defaultVal, tag.Description)
	case reflect.Uint:
		defaultVal, err := strconv.ParseUint(tag.DefaultValue, 0, fieldType.Bits())
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.UintVar((*uint)(fieldPtr), tag.Name, uint(defaultVal), tag.Description)
	case reflect.Uint64:
		defaultVal, err := strconv.ParseUint(tag.DefaultValue, 0, 64)
		if err != nil {
			return &ErrInvalidDefault{fieldName, tag.Name, err}
		}
		flag.Uint64Var((*uint64)(fieldPtr), tag.Name, defaultVal, tag.Description)
	default:
		return errors.New("unsupported data type (kind '" + strconv.FormatUint(uint64(fieldType.Kind()), 10) + "') for field '" + fieldName + "' (tag '" + tag.Name + "')")
	}
	return nil
}

// getStructValue checks that the provided config instance is actually a struct not a nil value.
func getStructValue(config interface{}) (reflect.Value, error) {
	var zero reflect.Value
	if config == nil {
		return zero, errors.New("config cannot be nil")
	}
	ptr := reflect.ValueOf(config)
	if ptr.IsNil() {
		return zero, errors.New("config cannot point to nil")
	}
	val := reflect.Indirect(ptr)
	if val.Kind() != reflect.Struct {
		return zero, errors.New("config instance is not a struct")
	}
	return val, nil
}

// parseTag parses a string of text and separates the various sections of the 'flag'-tag.
func parseTag(value string, optvalue string) flagTag {
	parts := strings.SplitN(value, ",", 3)
	for len(parts) < 3 {
		parts = append(parts, "")
	}
	var flag = flagTag{Name: parts[0], DefaultValue: parts[1], Description: parts[2]}
	if optvalue != "" {
		if strings.Contains(optvalue, "skipFlagValue") {
			flag.Options.skipFlagValue = true
		}
	}
	return flag
}

// flagTag contains the parsed tag values
type flagTag struct {
	Name         string
	DefaultValue string
	Description  string
	Options      flagoptTag
}

type ErrInvalidDefault struct {
	field string
	tag   string
	err   error
}

func (e *ErrInvalidDefault) Error() string {
	return "invalid default value for field '" + e.field + "' (tag '" + e.tag + "'): " + e.err.Error()
}

// flagoptTag contains the parsed additional flag options
type flagoptTag struct {
	skipFlagValue bool
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"io"
	"os"
	"strings"

	log "github.com/Sirupsen/logrus"
)

const (
	DEFAULT_SSH_COMMAND = "ssh"
	APPNAME             = "novassh"
	VERSION             = "0.2.3"
)

// Commands
const (
	CMD_HELP = iota + 1
	CMD_LIST
	CMD_CONNECT
	CMD_DEAUTH
)

type Command int

func (c *Command) String() string {
	switch *c {
	case CMD_HELP:
		return "HELP"
	case CMD_LIST:
		return "LIST"
	case CMD_CONNECT:
		return "SSH"
	case CMD_DEAUTH:
		return "DEAUTH"
	default:
		return "UNKNOWN"
	}
}

// Connection types
const (
	// Use SSH(default)
	CON_SSH = iota + 1

	// Use serial console via websocket
	CON_CONSOLE
)

type ConnType int

func (c *ConnType) String() string {
	switch *c {
	case CON_SSH:
		return "SSH"
	case CON_CONSOLE:
		return "CONSOLE"
	default:
		return "UNKNOWN"
	}
}

// Config
type Config struct {
	// Outputs
	Stdout io.Writer
	Stdin  io.Reader
	Stderr io.Writer

	// Arguments
	Args []string

	// Connection type
	ConnType ConnType

	// Executable name of SSH
	SshCommand string

	// Option flags for SSH command
	SshOptions []string

	// Hostname to connect to the instance
	SshHost string

	// Username of SSH
	SshUser string

	// Command-name to be run on the instance
	SshRemoteCommand string

	// Websocket URL (For TYPE_CONSOLE only)
	ConsoleUrl string

	// Authentication cache (Default: false)
	AuthCache bool
}

func (c *Config) ParseArgs() (command Command, err error) {
	// Environments
	if os.Getenv("NOVASSH_COMMAND") != "" {
		c.SshCommand = os.Getenv("NOVASSH_COMMAND")
	}

	// Defaults
	c.SshCommand = DEFAULT_SSH_COMMAND
	c.ConnType = CON_SSH
	c.AuthCache = false

	// Aeguments
	i := 0
	sshargs := []string{}
	for i < len(c.Args) {
		arg := c.Args[i]
		if arg == "--debug" {
			// Enable debug
			log.SetLevel(log.DebugLevel)
			enableDebugTransport()

		} else if arg == "--command" {
			// Detects SSH command
			i++
			c.SshCommand = c.Args[i]

		} else if arg == "--list" {
			// List instances
			command = CMD_LIST

		} else if arg == "--authcache" {
			// Authentication cache
			c.AuthCache = true

		} else if arg == "--deauth" {
			// Remove credential cache
			command = CMD_DEAUTH

		} else if arg == "--help" {
			command = CMD_HELP
			break

		} else if arg == "--console" {
			// Use serial console
			c.ConnType = CON_CONSOLE

		} else {
			command = CMD_CONNECT
			sshargs = append(sshargs, arg)
		}
		i++
	}

	// Display help if no arguments are given
	if command == 0 && len(sshargs) == 0 {
		command = CMD_HELP
	}

	log.Debugf("Command: %s", command.String())

	if command == CMD_CONNECT {
		return CMD_CONNECT, c.parseSshArgs(sshargs)
	} else {
		return command, nil
	}
}

func (c *Config) parseSshArgs(args []string) (err error) {
	nova := NewNova()
	if err := nova.Init(c.AuthCache); err != nil {
		return err
	}

	found := false
	pos := len(args) - 1 // position of machine name in arguments
	for pos >= 0 {
		arg := args[pos]
		found, err = c.resolveMachineName(nova, arg)
		if err != nil {
			return err

		} else if found {
			break
		}
		pos--
	}

	if found {
		if pos > 0 {
			c.SshOptions = args[:pos]
		}
		if len(args) > 1 {
			c.SshRemoteCommand = strings.Join(args[pos+1:], " ")
		}
		return nil

	} else {
		return fmt.Errorf("Could not found the server.")
	}
}

func (c *Config) resolveMachineName(nova *nova, arg string) (found bool, err error) {
	var user, instancename string

	if strings.Index(arg, "@") > 0 {
		ss := strings.Split(arg, "@")
		user = ss[0]
		instancename = ss[1]
	} else {
		instancename = arg
	}

	log.Debugf("Try to find the server: instance-name=%s", instancename)

	machine, err := nova.Find(instancename)
	if err != nil {
		// error
		return false, err

	} else if machine != nil {
		// Found
		c.SshHost = machine.Ipaddr
		c.SshUser = user

		// Set console url if type is CON_CONSOLE
		if c.ConnType == CON_CONSOLE {
			url, err := nova.GetConsoleUrl(machine)
			if err != nil {
				return true, err
			}
			c.ConsoleUrl = url
		}

		return true, nil

	} else {
		// Not found
		log.Debugf("No match: name=%s", instancename)
		return false, nil
	}
}
<commit_msg>Update version<commit_after>package main

import (
	"fmt"
	"io"
	"os"
	"strings"

	log "github.com/Sirupsen/logrus"
)

const (
	DEFAULT_SSH_COMMAND = "ssh"
	APPNAME             = "novassh"
	VERSION             = "0.2.4"
)

// Commands
const (
	CMD_HELP = iota + 1
	CMD_LIST
	CMD_CONNECT
	CMD_DEAUTH
)

type Command int

func (c *Command) String() string {
	switch *c {
	case CMD_HELP:
		return "HELP"
	case CMD_LIST:
		return "LIST"
	case CMD_CONNECT:
		return "SSH"
	case CMD_DEAUTH:
		return "DEAUTH"
	default:
		return "UNKNOWN"
	}
}

// Connection types
const (
	// Use SSH(default)
	CON_SSH = iota + 1

	// Use serial console via websocket
	CON_CONSOLE
)

type ConnType int

func (c *ConnType) String() string {
	switch *c {
	case CON_SSH:
		return "SSH"
	case CON_CONSOLE:
		return "CONSOLE"
	default:
		return "UNKNOWN"
	}
}

// Config
type Config struct {
	// Outputs
	Stdout io.Writer
	Stdin  io.Reader
	Stderr io.Writer

	// Arguments
	Args []string

	// Connection type
	ConnType ConnType

	// Executable name of SSH
	SshCommand string

	// Option flags for SSH command
	SshOptions []string

	// Hostname to connect to the instance
	SshHost string

	// Username of SSH
	SshUser string

	// Command-name to be run on the instance
	SshRemoteCommand string

	// Websocket URL (For TYPE_CONSOLE only)
	ConsoleUrl string

	// Authentication cache (Default: false)
	AuthCache bool
}

func (c *Config) ParseArgs() (command Command, err error) {
	// Environments
	if os.Getenv("NOVASSH_COMMAND") != "" {
		c.SshCommand = os.Getenv("NOVASSH_COMMAND")
	}

	// Defaults
	c.SshCommand = DEFAULT_SSH_COMMAND
	c.ConnType = CON_SSH
	c.AuthCache = false

	// Aeguments
	i := 0
	sshargs := []string{}
	for i < len(c.Args) {
		arg := c.Args[i]
		if arg == "--debug" {
			// Enable debug
			log.SetLevel(log.DebugLevel)
			enableDebugTransport()

		} else if arg == "--command" {
			// Detects SSH command
			i++
			c.SshCommand = c.Args[i]

		} else if arg == "--list" {
			// List instances
			command = CMD_LIST

		} else if arg == "--authcache" {
			// Authentication cache
			c.AuthCache = true

		} else if arg == "--deauth" {
			// Remove credential cache
			command = CMD_DEAUTH

		} else if arg == "--help" {
			command = CMD_HELP
			break

		} else if arg == "--console" {
			// Use serial console
			c.ConnType = CON_CONSOLE

		} else {
			command = CMD_CONNECT
			sshargs = append(sshargs, arg)
		}
		i++
	}

	// Display help if no arguments are given
	if command == 0 && len(sshargs) == 0 {
		command = CMD_HELP
	}

	log.Debugf("Command: %s", command.String())

	if command == CMD_CONNECT {
		return CMD_CONNECT, c.parseSshArgs(sshargs)
	} else {
		return command, nil
	}
}

func (c *Config) parseSshArgs(args []string) (err error) {
	nova := NewNova()
	if err := nova.Init(c.AuthCache); err != nil {
		return err
	}

	found := false
	pos := len(args) - 1 // position of machine name in arguments
	for pos >= 0 {
		arg := args[pos]
		found, err = c.resolveMachineName(nova, arg)
		if err != nil {
			return err

		} else if found {
			break
		}
		pos--
	}

	if found {
		if pos > 0 {
			c.SshOptions = args[:pos]
		}
		if len(args) > 1 {
			c.SshRemoteCommand = strings.Join(args[pos+1:], " ")
		}
		return nil

	} else {
		return fmt.Errorf("Could not found the server.")
	}
}

func (c *Config) resolveMachineName(nova *nova, arg string) (found bool, err error) {
	var user, instancename string

	if strings.Index(arg, "@") > 0 {
		ss := strings.Split(arg, "@")
		user = ss[0]
		instancename = ss[1]
	} else {
		instancename = arg
	}

	log.Debugf("Try to find the server: instance-name=%s", instancename)

	machine, err := nova.Find(instancename)
	if err != nil {
		// error
		return false, err

	} else if machine != nil {
		// Found
		c.SshHost = machine.Ipaddr
		c.SshUser = user

		// Set console url if type is CON_CONSOLE
		if c.ConnType == CON_CONSOLE {
			url, err := nova.GetConsoleUrl(machine)
			if err != nil {
				return true, err
			}
			c.ConsoleUrl = url
		}

		return true, nil

	} else {
		// Not found
		log.Debugf("No match: name=%s", instancename)
		return false, nil
	}
}
<|endoftext|>


<commit_before>package awsconfig

import (
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
	"github.com/aws/aws-sdk-go/aws/ec2metadata"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/mitchellh/go-homedir"
	"gopkg.in/ini.v1"
)

// Option is AWS Config options
type Option struct {
	// AWS Access Key ID
	AccessKey string
	// AWS Secret Access Key
	SecretKey string
	// AssumeRole Arn
	Arn string
	// AWS Profile
	Profile string
	// Path to the shared config file
	Config string
	// Path to the shared credentials file
	Credentials string
	// AWS Session Token
	Token string
	// AWS Region
	Region string
	// ExpiryWindow
	ExpiryWindow time.Duration
}

const (
	// DefaultProfile is default profile name
	DefaultProfile = "default"
)

func loadIni(filename string) (*ini.File, error) {
	return ini.Load(filename)
}

// GetRegionFromConfigFile returns the region from the shared config file
func GetRegionFromConfigFile(configFile, profile string) (string, error) {
	var cfg *ini.File
	var err error
	cfg, err = loadIni(configFile)

	if err != nil {
		return "", err
	}

	var s *ini.Section
	s, err = cfg.GetSection(profile)
	if err != nil {
		return "", err
	}

	var key *ini.Key
	key, err = s.GetKey("region")

	return key.String(), err
}

// GetProfiles returns the shared config file section names
func GetProfiles(configFile ...string) []string {
	var cfg *ini.File
	var err error
	var conf string

	if len(configFile) == 0 {
		conf = GetSharedCredentialsFile()
	} else {
		conf = configFile[0]
	}

	cfg, err = loadIni(conf)

	if err != nil {
		return []string{}
	}

	defaultSectionLen := len(cfg.Section("").Keys())
	sections := cfg.SectionStrings()

	if defaultSectionLen == 0 {
		return sections[1:]
	}

	return sections
}

func isLoadConfig() bool {
	return os.Getenv("AWS_SDK_LOAD_CONFIG") == "1" || strings.ToLower(os.Getenv("AWS_SDK_LOAD_CONFIG")) == "true"
}

// GetProfile returns the profile
func GetProfile() string {
	profile := os.Getenv("AWS_PROFILE")
	if isLoadConfig() && profile == "" {
		profile = os.Getenv("AWS_DEFAULT_PROFILE")
	}

	if profile == "" {
		return DefaultProfile
	}

	return profile
}

// GetRegionFromInstanceIdentityDocument returns the region from the Instance Metadata
func GetRegionFromInstanceIdentityDocument() string {
	region, err := ec2metadata.New(session.New()).Region()
	if err == nil {
		return region
	}

	return ""
}

// GetRegion returns the region
func GetRegion(profile string) string {
	region := os.Getenv("AWS_REGION")
	if isLoadConfig() && region == "" {
		region = os.Getenv("AWS_DEFAULT_REGION")
	}

	if region == "" {
		if profile == "" {
			profile = DefaultProfile
		}
		region, _ = GetRegionFromConfigFile(GetSharedConfigFile(), profile)
	}

	if region == "" && os.Getenv("USE_METADATA") != "0" {
		region = GetRegionFromInstanceIdentityDocument()
	}

	return region
}

func createConfigPath(filename string) string {
	dir, err := homedir.Dir()
	if err != nil {
		return ""
	}

	// /path/to/homedir/.aws/{config,credentials}
	return filepath.Join(dir, ".aws", filename)
}

// GetSharedCredentialsFile returns the path to the shared credentials file
func GetSharedCredentialsFile() string {
	creds := os.Getenv("AWS_SHARED_CREDENTIALS_FILE")
	if creds == "" {
		creds = createConfigPath("credentials")
	}

	return creds
}

// GetSharedConfigFile returns the path to the shared config file
func GetSharedConfigFile() string {
	config := os.Getenv("AWS_CONFIG_FILE")
	if config == "" {
		config = createConfigPath("config")
	}

	return config
}

// NewCredentials returns the *credentials.Credentials
func NewCredentials(opt Option) (*credentials.Credentials, error) {
	var creds *credentials.Credentials
	var profile string
	var accessKey string
	var secretKey string
	var token string
	var region string
	var credsPath string
	var err error

	if opt.Profile == "" {
		profile = GetProfile()
	} else {
		profile = opt.Profile
	}

	if opt.Region == "" {
		region = GetRegion(profile)
	} else {
		region = opt.Region
	}

	env, err := credentials.NewEnvCredentials().Get()
	if err != nil {
		return creds, err
	}

	if opt.AccessKey == "" {
		accessKey = env.AccessKeyID
	} else {
		accessKey = opt.AccessKey
	}

	if opt.SecretKey == "" {
		secretKey = env.SecretAccessKey
	} else {
		secretKey = opt.SecretKey
	}

	if opt.Token == "" {
		token = env.SessionToken
	} else {
		token = opt.Token
	}

	if accessKey == "" && secretKey == "" {
		sess := session.Must(session.NewSession(&aws.Config{
			Region: aws.String(region),
		}))

		p := &ec2rolecreds.EC2RoleProvider{
			Client: ec2metadata.New(sess, &aws.Config{
				HTTPClient: &http.Client{Timeout: 10 * time.Second},
			}),
			ExpiryWindow: opt.ExpiryWindow,
		}
		var val credentials.Value
		val, err = p.Retrieve()
		accessKey = val.AccessKeyID
		secretKey = val.SecretAccessKey
		token = val.SessionToken
	}

	sharedCreds := &credentials.SharedCredentialsProvider{
		Filename: credsPath,
		Profile:  profile,
	}

	staticCreds := &credentials.StaticProvider{
		Value: credentials.Value{
			AccessKeyID:     accessKey,
			SecretAccessKey: secretKey,
			SessionToken:    token,
		},
	}

	creds = credentials.NewChainCredentials(
		[]credentials.Provider{
			staticCreds,
			sharedCreds,
		})

	if opt.Arn != "" {
		c := &aws.Config{
			Credentials: creds,
			Region:      aws.String(region),
		}
		sess := session.Must(session.NewSession(c))
		creds = stscreds.NewCredentials(sess, opt.Arn)
	}

	return creds, err
}

// NewConfig returns the *aws.Config
func NewConfig(opt Option) (*aws.Config, error) {
	var c *aws.Config
	var creds *credentials.Credentials
	var err error
	var region string
	var profile string

	creds, err = NewCredentials(opt)
	if err != nil {
		return c, err
	}

	if opt.Profile == "" {
		profile = GetProfile()
	} else {
		profile = opt.Profile
	}

	if opt.Region != "" {
		region = opt.Region
	} else {
		region = GetRegion(profile)
	}

	c = &aws.Config{
		Credentials: creds,
		Region:      aws.String(region),
	}

	return c, err
}

/*
NewSession returns the *session.Session.

The argument arg Option or *aws.Config
*/
func NewSession(arg ...interface{}) (*session.Session, error) {
	var sess *session.Session
	var sessOpts session.Options
	var c *aws.Config
	var err error

	if len(arg) == 0 {
		return session.NewSession()
	}

	switch val := arg[0].(type) {
	case Option:
		c, err = NewConfig(val)
		if err != nil {
			return sess, err
		}

		sessOpts.Config = *c
		sessOpts.Profile = val.Profile
	case *aws.Config:
		sessOpts.Config = *val
	default:
		return sess, fmt.Errorf("Invalid arg: %T", val)
	}

	return session.NewSessionWithOptions(sessOpts)
}
<commit_msg>fix EnvCredentials<commit_after>package awsconfig

import (
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
	"github.com/aws/aws-sdk-go/aws/ec2metadata"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/mitchellh/go-homedir"
	"gopkg.in/ini.v1"
)

// Option is AWS Config options
type Option struct {
	// AWS Access Key ID
	AccessKey string
	// AWS Secret Access Key
	SecretKey string
	// AssumeRole Arn
	Arn string
	// AWS Profile
	Profile string
	// Path to the shared config file
	Config string
	// Path to the shared credentials file
	Credentials string
	// AWS Session Token
	Token string
	// AWS Region
	Region string
	// ExpiryWindow
	ExpiryWindow time.Duration
}

const (
	// DefaultProfile is default profile name
	DefaultProfile = "default"
)

func loadIni(filename string) (*ini.File, error) {
	return ini.Load(filename)
}

// GetRegionFromConfigFile returns the region from the shared config file
func GetRegionFromConfigFile(configFile, profile string) (string, error) {
	var cfg *ini.File
	var err error
	cfg, err = loadIni(configFile)

	if err != nil {
		return "", err
	}

	var s *ini.Section
	s, err = cfg.GetSection(profile)
	if err != nil {
		return "", err
	}

	var key *ini.Key
	key, err = s.GetKey("region")

	return key.String(), err
}

// GetProfiles returns the shared config file section names
func GetProfiles(configFile ...string) []string {
	var cfg *ini.File
	var err error
	var conf string

	if len(configFile) == 0 {
		conf = GetSharedCredentialsFile()
	} else {
		conf = configFile[0]
	}

	cfg, err = loadIni(conf)

	if err != nil {
		return []string{}
	}

	defaultSectionLen := len(cfg.Section("").Keys())
	sections := cfg.SectionStrings()

	if defaultSectionLen == 0 {
		return sections[1:]
	}

	return sections
}

func isLoadConfig() bool {
	return os.Getenv("AWS_SDK_LOAD_CONFIG") == "1" || strings.ToLower(os.Getenv("AWS_SDK_LOAD_CONFIG")) == "true"
}

// GetProfile returns the profile
func GetProfile() string {
	profile := os.Getenv("AWS_PROFILE")
	if isLoadConfig() && profile == "" {
		profile = os.Getenv("AWS_DEFAULT_PROFILE")
	}

	if profile == "" {
		return DefaultProfile
	}

	return profile
}

// GetRegionFromInstanceIdentityDocument returns the region from the Instance Metadata
func GetRegionFromInstanceIdentityDocument() string {
	region, err := ec2metadata.New(session.New()).Region()
	if err == nil {
		return region
	}

	return ""
}

// GetRegion returns the region
func GetRegion(profile string) string {
	region := os.Getenv("AWS_REGION")
	if isLoadConfig() && region == "" {
		region = os.Getenv("AWS_DEFAULT_REGION")
	}

	if region == "" {
		if profile == "" {
			profile = DefaultProfile
		}
		region, _ = GetRegionFromConfigFile(GetSharedConfigFile(), profile)
	}

	if region == "" && os.Getenv("USE_METADATA") != "0" {
		region = GetRegionFromInstanceIdentityDocument()
	}

	return region
}

func createConfigPath(filename string) string {
	dir, err := homedir.Dir()
	if err != nil {
		return ""
	}

	// /path/to/homedir/.aws/{config,credentials}
	return filepath.Join(dir, ".aws", filename)
}

// GetSharedCredentialsFile returns the path to the shared credentials file
func GetSharedCredentialsFile() string {
	creds := os.Getenv("AWS_SHARED_CREDENTIALS_FILE")
	if creds == "" {
		creds = createConfigPath("credentials")
	}

	return creds
}

// GetSharedConfigFile returns the path to the shared config file
func GetSharedConfigFile() string {
	config := os.Getenv("AWS_CONFIG_FILE")
	if config == "" {
		config = createConfigPath("config")
	}

	return config
}

// NewCredentials returns the *credentials.Credentials
func NewCredentials(opt Option) (*credentials.Credentials, error) {
	var creds *credentials.Credentials
	var profile string
	var accessKey string
	var secretKey string
	var token string
	var region string
	var credsPath string
	var err error

	if opt.Profile == "" {
		profile = GetProfile()
	} else {
		profile = opt.Profile
	}

	if opt.Region == "" {
		region = GetRegion(profile)
	} else {
		region = opt.Region
	}

	env, _ := credentials.NewEnvCredentials().Get()

	if env.AccessKeyID != "" {
		accessKey = env.AccessKeyID
	} else {
		accessKey = opt.AccessKey
	}

	if env.SecretAccessKey != "" {
		secretKey = env.SecretAccessKey
	} else {
		secretKey = opt.SecretKey
	}

	if opt.Token == "" {
		token = env.SessionToken
	} else {
		token = opt.Token
	}

	if accessKey == "" && secretKey == "" {
		sess := session.Must(session.NewSession(&aws.Config{
			Region: aws.String(region),
		}))

		p := &ec2rolecreds.EC2RoleProvider{
			Client: ec2metadata.New(sess, &aws.Config{
				HTTPClient: &http.Client{Timeout: 10 * time.Second},
			}),
			ExpiryWindow: opt.ExpiryWindow,
		}
		var val credentials.Value
		val, err = p.Retrieve()
		accessKey = val.AccessKeyID
		secretKey = val.SecretAccessKey
		token = val.SessionToken
	}

	sharedCreds := &credentials.SharedCredentialsProvider{
		Filename: credsPath,
		Profile:  profile,
	}

	staticCreds := &credentials.StaticProvider{
		Value: credentials.Value{
			AccessKeyID:     accessKey,
			SecretAccessKey: secretKey,
			SessionToken:    token,
		},
	}

	creds = credentials.NewChainCredentials(
		[]credentials.Provider{
			staticCreds,
			sharedCreds,
		})

	if opt.Arn != "" {
		c := &aws.Config{
			Credentials: creds,
			Region:      aws.String(region),
		}
		sess := session.Must(session.NewSession(c))
		creds = stscreds.NewCredentials(sess, opt.Arn)
	}

	return creds, err
}

// NewConfig returns the *aws.Config
func NewConfig(opt Option) (*aws.Config, error) {
	var c *aws.Config
	var creds *credentials.Credentials
	var err error
	var region string
	var profile string

	creds, err = NewCredentials(opt)
	if err != nil {
		return c, err
	}

	if opt.Profile == "" {
		profile = GetProfile()
	} else {
		profile = opt.Profile
	}

	if opt.Region != "" {
		region = opt.Region
	} else {
		region = GetRegion(profile)
	}

	c = &aws.Config{
		Credentials: creds,
		Region:      aws.String(region),
	}

	return c, err
}

/*
NewSession returns the *session.Session.

The argument arg Option or *aws.Config
*/
func NewSession(arg ...interface{}) (*session.Session, error) {
	var sess *session.Session
	var sessOpts session.Options
	var c *aws.Config
	var err error

	if len(arg) == 0 {
		return session.NewSession()
	}

	switch val := arg[0].(type) {
	case Option:
		c, err = NewConfig(val)
		if err != nil {
			return sess, err
		}

		sessOpts.Config = *c
		sessOpts.Profile = val.Profile
	case *aws.Config:
		sessOpts.Config = *val
	default:
		return sess, fmt.Errorf("Invalid arg: %T", val)
	}

	return session.NewSessionWithOptions(sessOpts)
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"os"
)

type Server struct {
	Name, Path string
}

type Config struct {
	filename   string
	ServerName string
	Port       uint16
	Servers    []Server
}

func loadConfig(filename string) (*Config, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	c := &Config{
		Port: 8080,
	}
	err = json.NewDecoder(f).Decode(c)
	if err != nil {
		return nil, err
	}
	c.filename = filename
	return c, nil
}

func (c *Config) save() error {
	f, err := os.Create(c.filename)
	if err != nil {
		return err
	}
	defer f.Close()
	return json.NewEncoder(f).Encode(c)
}

func (c *Config) Name(_ struct{}, serverName *string) error {
	*serverName = c.ServerName
	return nil
}
<commit_msg>Added config defaults and List method<commit_after>package main

import (
	"encoding/json"
	"os"
)

type Server struct {
	Name, Path string
}

type Config struct {
	filename   string
	ServerName string
	Port       uint16
	Servers    []Server
	selected   int
}

func loadConfig(filename string) (*Config, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	c := &Config{
		filename:   filename,
		ServerName: "Minecraft",
		Port:       8080,
		Servers:    make([]Server, 0),
		selected:   -1,
	}
	err = json.NewDecoder(f).Decode(c)
	if err != nil {
		return nil, err
	}
	return c, nil
}

func (c *Config) save() error {
	f, err := os.Create(c.filename)
	if err != nil {
		return err
	}
	defer f.Close()
	return json.NewEncoder(f).Encode(c)
}

func (c *Config) Name(_ struct{}, serverName *string) error {
	*serverName = c.ServerName
	return nil
}

func (c *Config) List(_ struct{}, list *[]Server) error {
	*list = c.Servers
	return nil
}
<|endoftext|>


<commit_before>// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>.  All rights reserved.

package log4go

import (
	"encoding/xml"
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"strings"
)

type xmlProperty struct {
	Name  string `xml:"name,attr"`
	Value string `xml:",chardata"`
}

type xmlFilter struct {
	Enabled  string        `xml:"enabled,attr"`
	Tag      string        `xml:"tag"`
	Level    string        `xml:"level"`
	Type     string        `xml:"type"`
	Property []xmlProperty `xml:"property"`
}

type xmlLoggerConfig struct {
	Filter []xmlFilter `xml:"filter"`
}

// Load XML configuration; see examples/example.xml for documentation
func (log Logger) LoadConfiguration(filename string) {
	log.Close()

	// Open the configuration file
	fd, err := os.Open(filename)
	if err != nil {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not open %q for reading: %s\n", filename, err)
		os.Exit(1)
	}

	contents, err := ioutil.ReadAll(fd)
	if err != nil {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not read %q: %s\n", filename, err)
		os.Exit(1)
	}

	xc := new(xmlLoggerConfig)
	if err := xml.Unmarshal(contents, xc); err != nil {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not parse XML configuration in %q: %s\n", filename, err)
		os.Exit(1)
	}

	for _, xmlfilt := range xc.Filter {
		var filt LogWriter
		var lvl Level
		bad, good, enabled := false, true, false

		// Check required children
		if len(xmlfilt.Enabled) == 0 {
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required attribute %s for filter missing in %s\n", "enabled", filename)
			bad = true
		} else {
			enabled = xmlfilt.Enabled != "false"
		}
		if len(xmlfilt.Tag) == 0 {
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "tag", filename)
			bad = true
		}
		if len(xmlfilt.Type) == 0 {
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "type", filename)
			bad = true
		}
		if len(xmlfilt.Level) == 0 {
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "level", filename)
			bad = true
		}

		switch xmlfilt.Level {
		case "FINEST":
			lvl = FINEST
		case "FINE":
			lvl = FINE
		case "DEBUG":
			lvl = DEBUG
		case "TRACE":
			lvl = TRACE
		case "INFO":
			lvl = INFO
		case "WARNING":
			lvl = WARNING
		case "ERROR":
			lvl = ERROR
		case "CRITICAL":
			lvl = CRITICAL
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter has unknown value in %s: %s\n", "level", filename, xmlfilt.Level)
			bad = true
		}

		// Just so all of the required attributes are errored at the same time if missing
		if bad {
			os.Exit(1)
		}

		switch xmlfilt.Type {
		case "console":
			filt, good = xmlToConsoleLogWriter(filename, xmlfilt.Property, enabled)
		case "file":
			filt, good = xmlToFileLogWriter(filename, xmlfilt.Property, enabled)
		case "xml":
			filt, good = xmlToXMLLogWriter(filename, xmlfilt.Property, enabled)
		case "socket":
			filt, good = xmlToSocketLogWriter(filename, xmlfilt.Property, enabled)
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not load XML configuration in %s: unknown filter type \"%s\"\n", filename, xmlfilt.Type)
			os.Exit(1)
		}

		// Just so all of the required params are errored at the same time if wrong
		if !good {
			os.Exit(1)
		}

		// If we're disabled (syntax and correctness checks only), don't add to logger
		if !enabled {
			continue
		}

		log[xmlfilt.Tag] = &Filter{lvl, filt}
	}
}

func xmlToConsoleLogWriter(filename string, props []xmlProperty, enabled bool) (*ConsoleLogWriter, bool) {
	// Parse properties
	for _, prop := range props {
		switch prop.Name {
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for console filter in %s\n", prop.Name, filename)
		}
	}

	// If it's disabled, we're just checking syntax
	if !enabled {
		return nil, true
	}

	return NewConsoleLogWriter(), true
}

// Parse a number with K/M/G suffixes based on thousands (1000) or 2^10 (1024)
func strToNumSuffix(str string, mult int) int {
	num := 1
	if len(str) > 1 {
		switch str[len(str)-1] {
		case 'G', 'g':
			num *= mult
			fallthrough
		case 'M', 'm':
			num *= mult
			fallthrough
		case 'K', 'k':
			num *= mult
			str = str[0 : len(str)-1]
		}
	}
	parsed, _ := strconv.Atoi(str)
	return parsed * num
}
func xmlToFileLogWriter(filename string, props []xmlProperty, enabled bool) (*FileLogWriter, bool) {
	file := ""
	format := "[%D %T] [%L] (%S) %M"
	maxlines := 0
	maxsize := 0
	daily := false
	rotate := false

	// Parse properties
	for _, prop := range props {
		switch prop.Name {
		case "filename":
			file = strings.Trim(prop.Value, " \r\n")
		case "format":
			format = strings.Trim(prop.Value, " \r\n")
		case "maxlines":
			maxlines = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1000)
		case "maxsize":
			maxsize = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1024)
		case "daily":
			daily = strings.Trim(prop.Value, " \r\n") != "false"
		case "rotate":
			rotate = strings.Trim(prop.Value, " \r\n") != "false"
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for file filter in %s\n", prop.Name, filename)
		}
	}

	// Check properties
	if len(file) == 0 {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for file filter missing in %s\n", "filename", filename)
		return nil, false
	}

	// If it's disabled, we're just checking syntax
	if !enabled {
		return nil, true
	}

	flw := NewFileLogWriter(file, rotate)
	flw.SetFormat(format)
	flw.SetRotateLines(maxlines)
	flw.SetRotateSize(maxsize)
	flw.SetRotateDaily(daily)
	return flw, true
}

func xmlToXMLLogWriter(filename string, props []xmlProperty, enabled bool) (*FileLogWriter, bool) {
	file := ""
	maxrecords := 0
	maxsize := 0
	daily := false
	rotate := false

	// Parse properties
	for _, prop := range props {
		switch prop.Name {
		case "filename":
			file = strings.Trim(prop.Value, " \r\n")
		case "maxrecords":
			maxrecords = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1000)
		case "maxsize":
			maxsize = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1024)
		case "daily":
			daily = strings.Trim(prop.Value, " \r\n") != "false"
		case "rotate":
			rotate = strings.Trim(prop.Value, " \r\n") != "false"
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for xml filter in %s\n", prop.Name, filename)
		}
	}

	// Check properties
	if len(file) == 0 {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for xml filter missing in %s\n", "filename", filename)
		return nil, false
	}

	// If it's disabled, we're just checking syntax
	if !enabled {
		return nil, true
	}

	xlw := NewXMLLogWriter(file, rotate)
	xlw.SetRotateLines(maxrecords)
	xlw.SetRotateSize(maxsize)
	xlw.SetRotateDaily(daily)
	return xlw, true
}

func xmlToSocketLogWriter(filename string, props []xmlProperty, enabled bool) (SocketLogWriter, bool) {
	endpoint := ""
	protocol := "udp"

	// Parse properties
	for _, prop := range props {
		switch prop.Name {
		case "endpoint":
			endpoint = strings.Trim(prop.Value, " \r\n")
		case "protocol":
			protocol = strings.Trim(prop.Value, " \r\n")
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for file filter in %s\n", prop.Name, filename)
		}
	}

	// Check properties
	if len(endpoint) == 0 {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for file filter missing in %s\n", "endpoint", filename)
		return nil, false
	}

	// If it's disabled, we're just checking syntax
	if !enabled {
		return nil, true
	}

	return NewSocketLogWriter(protocol, endpoint), true
}
<commit_msg>add ConsoleLogWriter format config<commit_after>// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>.  All rights reserved.

package log4go

import (
	"encoding/xml"
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"strings"
)

type xmlProperty struct {
	Name  string `xml:"name,attr"`
	Value string `xml:",chardata"`
}

type xmlFilter struct {
	Enabled  string        `xml:"enabled,attr"`
	Tag      string        `xml:"tag"`
	Level    string        `xml:"level"`
	Type     string        `xml:"type"`
	Property []xmlProperty `xml:"property"`
}

type xmlLoggerConfig struct {
	Filter []xmlFilter `xml:"filter"`
}

// Load XML configuration; see examples/example.xml for documentation
func (log Logger) LoadConfiguration(filename string) {
	log.Close()

	// Open the configuration file
	fd, err := os.Open(filename)
	if err != nil {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not open %q for reading: %s\n", filename, err)
		os.Exit(1)
	}

	contents, err := ioutil.ReadAll(fd)
	if err != nil {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not read %q: %s\n", filename, err)
		os.Exit(1)
	}

	xc := new(xmlLoggerConfig)
	if err := xml.Unmarshal(contents, xc); err != nil {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not parse XML configuration in %q: %s\n", filename, err)
		os.Exit(1)
	}

	for _, xmlfilt := range xc.Filter {
		var filt LogWriter
		var lvl Level
		bad, good, enabled := false, true, false

		// Check required children
		if len(xmlfilt.Enabled) == 0 {
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required attribute %s for filter missing in %s\n", "enabled", filename)
			bad = true
		} else {
			enabled = xmlfilt.Enabled != "false"
		}
		if len(xmlfilt.Tag) == 0 {
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "tag", filename)
			bad = true
		}
		if len(xmlfilt.Type) == 0 {
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "type", filename)
			bad = true
		}
		if len(xmlfilt.Level) == 0 {
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "level", filename)
			bad = true
		}

		switch xmlfilt.Level {
		case "FINEST":
			lvl = FINEST
		case "FINE":
			lvl = FINE
		case "DEBUG":
			lvl = DEBUG
		case "TRACE":
			lvl = TRACE
		case "INFO":
			lvl = INFO
		case "WARNING":
			lvl = WARNING
		case "ERROR":
			lvl = ERROR
		case "CRITICAL":
			lvl = CRITICAL
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter has unknown value in %s: %s\n", "level", filename, xmlfilt.Level)
			bad = true
		}

		// Just so all of the required attributes are errored at the same time if missing
		if bad {
			os.Exit(1)
		}

		switch xmlfilt.Type {
		case "console":
			filt, good = xmlToConsoleLogWriter(filename, xmlfilt.Property, enabled)
		case "file":
			filt, good = xmlToFileLogWriter(filename, xmlfilt.Property, enabled)
		case "xml":
			filt, good = xmlToXMLLogWriter(filename, xmlfilt.Property, enabled)
		case "socket":
			filt, good = xmlToSocketLogWriter(filename, xmlfilt.Property, enabled)
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not load XML configuration in %s: unknown filter type \"%s\"\n", filename, xmlfilt.Type)
			os.Exit(1)
		}

		// Just so all of the required params are errored at the same time if wrong
		if !good {
			os.Exit(1)
		}

		// If we're disabled (syntax and correctness checks only), don't add to logger
		if !enabled {
			continue
		}

		log[xmlfilt.Tag] = &Filter{lvl, filt}
	}
}

func xmlToConsoleLogWriter(filename string, props []xmlProperty, enabled bool) (*ConsoleLogWriter, bool) {

	format := "[%D %T] [%L] (%S) %M"

	// Parse properties
	for _, prop := range props {
		switch prop.Name {
		case "format":
			format = strings.Trim(prop.Value, " \r\n")
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for console filter in %s\n", prop.Name, filename)
		}
	}

	// If it's disabled, we're just checking syntax
	if !enabled {
		return nil, true
	}

	clw := NewConsoleLogWriter()
	clw.SetFormat(format)

	return clw, true
}

// Parse a number with K/M/G suffixes based on thousands (1000) or 2^10 (1024)
func strToNumSuffix(str string, mult int) int {
	num := 1
	if len(str) > 1 {
		switch str[len(str)-1] {
		case 'G', 'g':
			num *= mult
			fallthrough
		case 'M', 'm':
			num *= mult
			fallthrough
		case 'K', 'k':
			num *= mult
			str = str[0 : len(str)-1]
		}
	}
	parsed, _ := strconv.Atoi(str)
	return parsed * num
}
func xmlToFileLogWriter(filename string, props []xmlProperty, enabled bool) (*FileLogWriter, bool) {
	file := ""
	format := "[%D %T] [%L] (%S) %M"
	maxlines := 0
	maxsize := 0
	daily := false
	rotate := false

	// Parse properties
	for _, prop := range props {
		switch prop.Name {
		case "filename":
			file = strings.Trim(prop.Value, " \r\n")
		case "format":
			format = strings.Trim(prop.Value, " \r\n")
		case "maxlines":
			maxlines = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1000)
		case "maxsize":
			maxsize = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1024)
		case "daily":
			daily = strings.Trim(prop.Value, " \r\n") != "false"
		case "rotate":
			rotate = strings.Trim(prop.Value, " \r\n") != "false"
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for file filter in %s\n", prop.Name, filename)
		}
	}

	// Check properties
	if len(file) == 0 {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for file filter missing in %s\n", "filename", filename)
		return nil, false
	}

	// If it's disabled, we're just checking syntax
	if !enabled {
		return nil, true
	}

	flw := NewFileLogWriter(file, rotate)
	flw.SetFormat(format)
	flw.SetRotateLines(maxlines)
	flw.SetRotateSize(maxsize)
	flw.SetRotateDaily(daily)
	return flw, true
}

func xmlToXMLLogWriter(filename string, props []xmlProperty, enabled bool) (*FileLogWriter, bool) {
	file := ""
	maxrecords := 0
	maxsize := 0
	daily := false
	rotate := false

	// Parse properties
	for _, prop := range props {
		switch prop.Name {
		case "filename":
			file = strings.Trim(prop.Value, " \r\n")
		case "maxrecords":
			maxrecords = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1000)
		case "maxsize":
			maxsize = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1024)
		case "daily":
			daily = strings.Trim(prop.Value, " \r\n") != "false"
		case "rotate":
			rotate = strings.Trim(prop.Value, " \r\n") != "false"
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for xml filter in %s\n", prop.Name, filename)
		}
	}

	// Check properties
	if len(file) == 0 {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for xml filter missing in %s\n", "filename", filename)
		return nil, false
	}

	// If it's disabled, we're just checking syntax
	if !enabled {
		return nil, true
	}

	xlw := NewXMLLogWriter(file, rotate)
	xlw.SetRotateLines(maxrecords)
	xlw.SetRotateSize(maxsize)
	xlw.SetRotateDaily(daily)
	return xlw, true
}

func xmlToSocketLogWriter(filename string, props []xmlProperty, enabled bool) (SocketLogWriter, bool) {
	endpoint := ""
	protocol := "udp"

	// Parse properties
	for _, prop := range props {
		switch prop.Name {
		case "endpoint":
			endpoint = strings.Trim(prop.Value, " \r\n")
		case "protocol":
			protocol = strings.Trim(prop.Value, " \r\n")
		default:
			fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for file filter in %s\n", prop.Name, filename)
		}
	}

	// Check properties
	if len(endpoint) == 0 {
		fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for file filter missing in %s\n", "endpoint", filename)
		return nil, false
	}

	// If it's disabled, we're just checking syntax
	if !enabled {
		return nil, true
	}

	return NewSocketLogWriter(protocol, endpoint), true
}
<|endoftext|>


<commit_before>// Copyright (c) 2017 Jonathan Chappelow
// Copyright (c) 2016 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"

	flags "github.com/btcsuite/go-flags"
	"github.com/decred/dcrd/chaincfg"
	"github.com/decred/dcrutil"
	"github.com/decred/dcrwallet/netparams"
)

const (
	defaultConfigFilename = "dcrdata.conf"
	defaultLogLevel       = "info"
	defaultLogDirname     = "logs"
	defaultLogFilename    = "dcrdata.log"
)

var curDir, _ = os.Getwd()
var activeNet = &netparams.MainNetParams
var activeChain = &chaincfg.MainNetParams

var (
	dcrdHomeDir = dcrutil.AppDataDir("dcrd", false)
	//dcrdataapiHomeDir            = dcrutil.AppDataDir("dcrdataapi", false)
	//defaultDaemonRPCKeyFile  = filepath.Join(dcrdHomeDir, "rpc.key")
	defaultDaemonRPCCertFile = filepath.Join(dcrdHomeDir, "rpc.cert")
	defaultConfigFile        = filepath.Join(curDir, defaultConfigFilename)
	defaultLogDir            = filepath.Join(curDir, defaultLogDirname)
	defaultHost              = "localhost"
	//defaultEmailSubject      = "dcrdataapi transaction notification"

	defaultAPIProto   = "http"
	defaultAPIListen  = "127.0.0.1:6000"
	defaultIndentJSON = "   "

	defaultMonitorMempool     = true
	defaultMempoolMinInterval = 2
	defaultMempoolMaxInterval = 120
	defaultMPTriggerTickets   = 1

	defaultDBFileName = "dcrdata.sqlt.db"
)

type config struct {
	// General application behavior
	ConfigFile  string `short:"C" long:"configfile" description:"Path to configuration file"`
	ShowVersion bool   `short:"V" long:"version" description:"Display version information and exit"`
	TestNet     bool   `long:"testnet" description:"Use the test network (default mainnet)"`
	SimNet      bool   `long:"simnet" description:"Use the simulation test network (default mainnet)"`
	DebugLevel  string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"`
	Quiet       bool   `short:"q" long:"quiet" description:"Easy way to set debuglevel to error"`
	LogDir      string `long:"logdir" description:"Directory to log output"`
	CPUProfile  string `long:"cpuprofile" description:"File for CPU profiling."`

	// API
	APIProto   string `long:"apiproto" description:"Protocol for API (http or https)"`
	APIListen  string `long:"apilisten" description:"Listen address for API"`
	IndentJSON string `long:"indentjson" description:"String for JSON indentation (default is \"   \"), when indentation is requested via URL query."`
	UseRealIP  bool   `long:"userealip" description:"Use the RealIP middleware from the pressly/chi/middleware package to get the client's real IP from the X-Forwarded-For or X-Real-IP headers, in that order."`

	// Comamnd execution
	//CmdName string `short:"c" long:"cmdname" description:"Command name to run. Must be on %PATH%."`
	//CmdArgs string `short:"a" long:"cmdargs" description:"Comma-separated list of arguments for command to run. The specifier %n is substituted for block height at execution, and %h is substituted for block hash."`

	// Data I/O
	MonitorMempool     bool   `short:"m" long:"mempool" description:"Monitor mempool for new transactions, and report ticketfee info when new tickets are added."`
	MempoolMinInterval int    `long:"mp-min-interval" description:"The minimum time in seconds between mempool reports, regarless of number of new tickets seen."`
	MempoolMaxInterval int    `long:"mp-max-interval" description:"The maximum time in seconds between mempool reports (within a couple seconds), regarless of number of new tickets seen."`
	MPTriggerTickets   int    `long:"mp-ticket-trigger" description:"The number minimum number of new tickets that must be seen to trigger a new mempool report."`
	DumpAllMPTix       bool   `long:"dumpallmptix" description:"Dump to file the fees of all the tickets in mempool."`
	DBFileName         string `long:"dbfile" description:"SQLite DB file name (default is dcrdata.sqlt.db)."`

	//WatchAddresses []string `short:"w" long:"watchaddress" description:"Watched address (receiving). One per line."`
	//WatchOutpoints []string `short:"o" long:"watchout" description:"Watched outpoint (sending). One per line."`

	// SMTPUser     string `long:"smtpuser" description:"SMTP user name"`
	// SMTPPass     string `long:"smtppass" description:"SMTP password"`
	// SMTPServer   string `long:"smtpserver" description:"SMTP host name"`
	// EmailAddr    string `long:"emailaddr" description:"Destination email address for alerts"`
	// EmailSubject string `long:"emailsubj" description:"Email subject. (default \"dcrdataapi transaction notification\")"`

	OutFolder string `short:"f" long:"outfolder" description:"Folder for file outputs"`

	// RPC client options
	DcrdUser         string `long:"dcrduser" description:"Daemon RPC user name"`
	DcrdPass         string `long:"dcrdpass" description:"Daemon RPC password"`
	DcrdServ         string `long:"dcrdserv" description:"Hostname/IP and port of dcrd RPC server to connect to (default localhost:9109, testnet: localhost:19109, simnet: localhost:19556)"`
	DcrdCert         string `long:"dcrdcert" description:"File containing the dcrd certificate file"`
	DisableDaemonTLS bool   `long:"nodaemontls" description:"Disable TLS for the daemon RPC client -- NOTE: This is only allowed if the RPC client is connecting to localhost"`
}

var (
	defaultConfig = config{
		DebugLevel:         defaultLogLevel,
		ConfigFile:         defaultConfigFile,
		LogDir:             defaultLogDir,
		APIProto:           defaultAPIProto,
		APIListen:          defaultAPIListen,
		IndentJSON:         defaultIndentJSON,
		DcrdCert:           defaultDaemonRPCCertFile,
		MonitorMempool:     defaultMonitorMempool,
		MempoolMinInterval: defaultMempoolMinInterval,
		MempoolMaxInterval: defaultMempoolMaxInterval,
		MPTriggerTickets:   defaultMPTriggerTickets,
		DBFileName:         defaultDBFileName,
		//EmailSubject:       defaultEmailSubject,
	}
)

// cleanAndExpandPath expands environement variables and leading ~ in the
// passed path, cleans the result, and returns it.
func cleanAndExpandPath(path string) string {
	// Expand initial ~ to OS specific home directory.
	if strings.HasPrefix(path, "~") {
		homeDir := filepath.Dir(dcrdHomeDir)
		path = strings.Replace(path, "~", homeDir, 1)
	}

	// NOTE: The os.ExpandEnv doesn't work with Windows cmd.exe-style
	// %VARIABLE%, but they variables can still be expanded via POSIX-style
	// $VARIABLE.
	// So, replace any %VAR% with ${VAR}
	r := regexp.MustCompile(`%(?P<VAR>[^%/\\]*)%`)
	path = r.ReplaceAllString(path, "$${${VAR}}")
	return filepath.Clean(os.ExpandEnv(path))
}

// validLogLevel returns whether or not logLevel is a valid debug log level.
func validLogLevel(logLevel string) bool {
	switch logLevel {
	case "trace":
		fallthrough
	case "debug":
		fallthrough
	case "info":
		fallthrough
	case "warn":
		fallthrough
	case "error":
		fallthrough
	case "critical":
		return true
	}
	return false
}

// supportedSubsystems returns a sorted slice of the supported subsystems for
// logging purposes.
func supportedSubsystems() []string {
	// Convert the subsystemLoggers map keys to a slice.
	subsystems := make([]string, 0, len(subsystemLoggers))
	for subsysID := range subsystemLoggers {
		subsystems = append(subsystems, subsysID)
	}

	// Sort the subsytems for stable display.
	sort.Strings(subsystems)
	return subsystems
}

// parseAndSetDebugLevels attempts to parse the specified debug level and set
// the levels accordingly.  An appropriate error is returned if anything is
// invalid.
func parseAndSetDebugLevels(debugLevel string) error {
	// When the specified string doesn't have any delimters, treat it as
	// the log level for all subsystems.
	if !strings.Contains(debugLevel, ",") && !strings.Contains(debugLevel, "=") {
		// Validate debug log level.
		if !validLogLevel(debugLevel) {
			str := "The specified debug level [%v] is invalid"
			return fmt.Errorf(str, debugLevel)
		}

		// Change the logging level for all subsystems.
		setLogLevels(debugLevel)

		return nil
	}

	// Split the specified string into subsystem/level pairs while detecting
	// issues and update the log levels accordingly.
	for _, logLevelPair := range strings.Split(debugLevel, ",") {
		if !strings.Contains(logLevelPair, "=") {
			str := "The specified debug level contains an invalid " +
				"subsystem/level pair [%v]"
			return fmt.Errorf(str, logLevelPair)
		}

		// Extract the specified subsystem and log level.
		fields := strings.Split(logLevelPair, "=")
		subsysID, logLevel := fields[0], fields[1]

		// Validate subsystem.
		if _, exists := subsystemLoggers[subsysID]; !exists {
			str := "The specified subsystem [%v] is invalid -- " +
				"supported subsytems %v"
			return fmt.Errorf(str, subsysID, supportedSubsystems())
		}

		// Validate log level.
		if !validLogLevel(logLevel) {
			str := "The specified debug level [%v] is invalid"
			return fmt.Errorf(str, logLevel)
		}

		setLogLevel(subsysID, logLevel)
	}

	return nil
}

// loadConfig initializes and parses the config using a config file and command
// line options.
func loadConfig() (*config, error) {
	loadConfigError := func(err error) (*config, error) {
		return nil, err
	}

	// Default config.
	cfg := defaultConfig

	// A config file in the current directory takes precedence.
	if _, err := os.Stat(defaultConfigFilename); !os.IsNotExist(err) {
		cfg.ConfigFile = defaultConfigFile
	}

	// Pre-parse the command line options to see if an alternative config
	// file or the version flag was specified.
	preCfg := cfg
	preParser := flags.NewParser(&preCfg, flags.HelpFlag|flags.PassDoubleDash)
	_, err := preParser.Parse()
	if err != nil {
		e, ok := err.(*flags.Error)
		if !ok || e.Type != flags.ErrHelp {
			preParser.WriteHelp(os.Stderr)
		}
		if ok && e.Type == flags.ErrHelp {
			preParser.WriteHelp(os.Stdout)
			os.Exit(0)
		}
		return loadConfigError(err)
	}

	// Show the version and exit if the version flag was specified.
	appName := filepath.Base(os.Args[0])
	appName = strings.TrimSuffix(appName, filepath.Ext(appName))
	if preCfg.ShowVersion {
		fmt.Println(appName, "version", ver.String())
		os.Exit(0)
	}

	// Load additional config from file.
	var configFileError error
	parser := flags.NewParser(&cfg, flags.Default)
	err = flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile)
	if err != nil {
		if _, ok := err.(*os.PathError); !ok {
			fmt.Fprintln(os.Stderr, err)
			parser.WriteHelp(os.Stderr)
			return loadConfigError(err)
		}
		configFileError = err
	}

	// Parse command line options again to ensure they take precedence.
	_, err = parser.Parse()
	if err != nil {
		if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
			parser.WriteHelp(os.Stderr)
		}
		return loadConfigError(err)
	}

	// Warn about missing config file after the final command line parse
	// succeeds.  This prevents the warning on help messages and invalid
	// options.
	if configFileError != nil {
		log.Warnf("%v", configFileError)
		//fmt.Printf("%v\n",configFileError)
		return loadConfigError(configFileError)
	}

	// Choose the active network params based on the selected network.
	// Multiple networks can't be selected simultaneously.
	numNets := 0
	activeNet = &netparams.MainNetParams
	activeChain = &chaincfg.MainNetParams
	if cfg.TestNet {
		activeNet = &netparams.TestNet2Params
		activeChain = &chaincfg.TestNet2Params
		numNets++
	}
	if cfg.SimNet {
		activeNet = &netparams.SimNetParams
		activeChain = &chaincfg.SimNetParams
		numNets++
	}
	if numNets > 1 {
		str := "%s: The testnet and simnet params can't be used " +
			"together -- choose one"
		err := fmt.Errorf(str, "loadConfig")
		fmt.Fprintln(os.Stderr, err)
		parser.WriteHelp(os.Stderr)
		return loadConfigError(err)
	}

	// Set the host names and ports to the default if the
	// user does not specify them.
	if cfg.DcrdServ == "" {
		cfg.DcrdServ = defaultHost + ":" + activeNet.JSONRPCClientPort
	}

	// Put comma-separated comamnd line aguments into slice of strings
	//cfg.CmdArgs = strings.Split(cfg.CmdArgs[0], ",")

	// // Output folder
	cfg.OutFolder = cleanAndExpandPath(cfg.OutFolder)
	cfg.OutFolder = filepath.Join(cfg.OutFolder, activeNet.Name)

	// The HTTP server port can not be beyond a uint16's size in value.
	// if cfg.HttpSvrPort > 0xffff {
	// 	str := "%s: Invalid HTTP port number for HTTP server"
	// 	err := fmt.Errorf(str, "loadConfig")
	// 	fmt.Fprintln(os.Stderr, err)
	// 	parser.WriteHelp(os.Stderr)
	// 	return loadConfigError(err)
	// }

	// Append the network type to the log directory so it is "namespaced"
	// per network.
	cfg.LogDir = cleanAndExpandPath(cfg.LogDir)
	cfg.LogDir = filepath.Join(cfg.LogDir, activeNet.Name)

	// Special show command to list supported subsystems and exit.
	if cfg.DebugLevel == "show" {
		fmt.Println("Supported subsystems", supportedSubsystems())
		os.Exit(0)
	}

	// Initialize log rotation.  After log rotation has been initialized, the
	// logger variables may be used.
	initLogRotator(filepath.Join(cfg.LogDir, defaultLogFilename))

	// Parse, validate, and set debug log level(s).
	if cfg.Quiet {
		cfg.DebugLevel = "error"
	}
	if err := parseAndSetDebugLevels(cfg.DebugLevel); err != nil {
		err = fmt.Errorf("%s: %v", "loadConfig", err.Error())
		fmt.Fprintln(os.Stderr, err)
		parser.WriteHelp(os.Stderr)
		return loadConfigError(err)
	}

	return &cfg, nil
}
<commit_msg>Use btclog for determining valid log levels<commit_after>// Copyright (c) 2017 Jonathan Chappelow
// Copyright (c) 2016 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"

	"github.com/btcsuite/btclog"
	flags "github.com/btcsuite/go-flags"
	"github.com/decred/dcrd/chaincfg"
	"github.com/decred/dcrutil"
	"github.com/decred/dcrwallet/netparams"
)

const (
	defaultConfigFilename = "dcrdata.conf"
	defaultLogLevel       = "info"
	defaultLogDirname     = "logs"
	defaultLogFilename    = "dcrdata.log"
)

var curDir, _ = os.Getwd()
var activeNet = &netparams.MainNetParams
var activeChain = &chaincfg.MainNetParams

var (
	dcrdHomeDir = dcrutil.AppDataDir("dcrd", false)
	//dcrdataapiHomeDir            = dcrutil.AppDataDir("dcrdataapi", false)
	//defaultDaemonRPCKeyFile  = filepath.Join(dcrdHomeDir, "rpc.key")
	defaultDaemonRPCCertFile = filepath.Join(dcrdHomeDir, "rpc.cert")
	defaultConfigFile        = filepath.Join(curDir, defaultConfigFilename)
	defaultLogDir            = filepath.Join(curDir, defaultLogDirname)
	defaultHost              = "localhost"
	//defaultEmailSubject      = "dcrdataapi transaction notification"

	defaultAPIProto   = "http"
	defaultAPIListen  = "127.0.0.1:6000"
	defaultIndentJSON = "   "

	defaultMonitorMempool     = true
	defaultMempoolMinInterval = 2
	defaultMempoolMaxInterval = 120
	defaultMPTriggerTickets   = 1

	defaultDBFileName = "dcrdata.sqlt.db"
)

type config struct {
	// General application behavior
	ConfigFile  string `short:"C" long:"configfile" description:"Path to configuration file"`
	ShowVersion bool   `short:"V" long:"version" description:"Display version information and exit"`
	TestNet     bool   `long:"testnet" description:"Use the test network (default mainnet)"`
	SimNet      bool   `long:"simnet" description:"Use the simulation test network (default mainnet)"`
	DebugLevel  string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"`
	Quiet       bool   `short:"q" long:"quiet" description:"Easy way to set debuglevel to error"`
	LogDir      string `long:"logdir" description:"Directory to log output"`
	CPUProfile  string `long:"cpuprofile" description:"File for CPU profiling."`

	// API
	APIProto   string `long:"apiproto" description:"Protocol for API (http or https)"`
	APIListen  string `long:"apilisten" description:"Listen address for API"`
	IndentJSON string `long:"indentjson" description:"String for JSON indentation (default is \"   \"), when indentation is requested via URL query."`
	UseRealIP  bool   `long:"userealip" description:"Use the RealIP middleware from the pressly/chi/middleware package to get the client's real IP from the X-Forwarded-For or X-Real-IP headers, in that order."`

	// Comamnd execution
	//CmdName string `short:"c" long:"cmdname" description:"Command name to run. Must be on %PATH%."`
	//CmdArgs string `short:"a" long:"cmdargs" description:"Comma-separated list of arguments for command to run. The specifier %n is substituted for block height at execution, and %h is substituted for block hash."`

	// Data I/O
	MonitorMempool     bool   `short:"m" long:"mempool" description:"Monitor mempool for new transactions, and report ticketfee info when new tickets are added."`
	MempoolMinInterval int    `long:"mp-min-interval" description:"The minimum time in seconds between mempool reports, regarless of number of new tickets seen."`
	MempoolMaxInterval int    `long:"mp-max-interval" description:"The maximum time in seconds between mempool reports (within a couple seconds), regarless of number of new tickets seen."`
	MPTriggerTickets   int    `long:"mp-ticket-trigger" description:"The number minimum number of new tickets that must be seen to trigger a new mempool report."`
	DumpAllMPTix       bool   `long:"dumpallmptix" description:"Dump to file the fees of all the tickets in mempool."`
	DBFileName         string `long:"dbfile" description:"SQLite DB file name (default is dcrdata.sqlt.db)."`

	//WatchAddresses []string `short:"w" long:"watchaddress" description:"Watched address (receiving). One per line."`
	//WatchOutpoints []string `short:"o" long:"watchout" description:"Watched outpoint (sending). One per line."`

	// SMTPUser     string `long:"smtpuser" description:"SMTP user name"`
	// SMTPPass     string `long:"smtppass" description:"SMTP password"`
	// SMTPServer   string `long:"smtpserver" description:"SMTP host name"`
	// EmailAddr    string `long:"emailaddr" description:"Destination email address for alerts"`
	// EmailSubject string `long:"emailsubj" description:"Email subject. (default \"dcrdataapi transaction notification\")"`

	OutFolder string `short:"f" long:"outfolder" description:"Folder for file outputs"`

	// RPC client options
	DcrdUser         string `long:"dcrduser" description:"Daemon RPC user name"`
	DcrdPass         string `long:"dcrdpass" description:"Daemon RPC password"`
	DcrdServ         string `long:"dcrdserv" description:"Hostname/IP and port of dcrd RPC server to connect to (default localhost:9109, testnet: localhost:19109, simnet: localhost:19556)"`
	DcrdCert         string `long:"dcrdcert" description:"File containing the dcrd certificate file"`
	DisableDaemonTLS bool   `long:"nodaemontls" description:"Disable TLS for the daemon RPC client -- NOTE: This is only allowed if the RPC client is connecting to localhost"`
}

var (
	defaultConfig = config{
		DebugLevel:         defaultLogLevel,
		ConfigFile:         defaultConfigFile,
		LogDir:             defaultLogDir,
		APIProto:           defaultAPIProto,
		APIListen:          defaultAPIListen,
		IndentJSON:         defaultIndentJSON,
		DcrdCert:           defaultDaemonRPCCertFile,
		MonitorMempool:     defaultMonitorMempool,
		MempoolMinInterval: defaultMempoolMinInterval,
		MempoolMaxInterval: defaultMempoolMaxInterval,
		MPTriggerTickets:   defaultMPTriggerTickets,
		DBFileName:         defaultDBFileName,
		//EmailSubject:       defaultEmailSubject,
	}
)

// cleanAndExpandPath expands environement variables and leading ~ in the
// passed path, cleans the result, and returns it.
func cleanAndExpandPath(path string) string {
	// Expand initial ~ to OS specific home directory.
	if strings.HasPrefix(path, "~") {
		homeDir := filepath.Dir(dcrdHomeDir)
		path = strings.Replace(path, "~", homeDir, 1)
	}

	// NOTE: The os.ExpandEnv doesn't work with Windows cmd.exe-style
	// %VARIABLE%, but they variables can still be expanded via POSIX-style
	// $VARIABLE.
	// So, replace any %VAR% with ${VAR}
	r := regexp.MustCompile(`%(?P<VAR>[^%/\\]*)%`)
	path = r.ReplaceAllString(path, "$${${VAR}}")
	return filepath.Clean(os.ExpandEnv(path))
}

// validLogLevel returns whether or not logLevel is a valid debug log level.
func validLogLevel(logLevel string) bool {
	_, ok := btclog.LevelFromString(logLevel)
	return ok
}

// supportedSubsystems returns a sorted slice of the supported subsystems for
// logging purposes.
func supportedSubsystems() []string {
	// Convert the subsystemLoggers map keys to a slice.
	subsystems := make([]string, 0, len(subsystemLoggers))
	for subsysID := range subsystemLoggers {
		subsystems = append(subsystems, subsysID)
	}

	// Sort the subsytems for stable display.
	sort.Strings(subsystems)
	return subsystems
}

// parseAndSetDebugLevels attempts to parse the specified debug level and set
// the levels accordingly.  An appropriate error is returned if anything is
// invalid.
func parseAndSetDebugLevels(debugLevel string) error {
	// When the specified string doesn't have any delimters, treat it as
	// the log level for all subsystems.
	if !strings.Contains(debugLevel, ",") && !strings.Contains(debugLevel, "=") {
		// Validate debug log level.
		if !validLogLevel(debugLevel) {
			str := "The specified debug level [%v] is invalid"
			return fmt.Errorf(str, debugLevel)
		}

		// Change the logging level for all subsystems.
		setLogLevels(debugLevel)

		return nil
	}

	// Split the specified string into subsystem/level pairs while detecting
	// issues and update the log levels accordingly.
	for _, logLevelPair := range strings.Split(debugLevel, ",") {
		if !strings.Contains(logLevelPair, "=") {
			str := "The specified debug level contains an invalid " +
				"subsystem/level pair [%v]"
			return fmt.Errorf(str, logLevelPair)
		}

		// Extract the specified subsystem and log level.
		fields := strings.Split(logLevelPair, "=")
		subsysID, logLevel := fields[0], fields[1]

		// Validate subsystem.
		if _, exists := subsystemLoggers[subsysID]; !exists {
			str := "The specified subsystem [%v] is invalid -- " +
				"supported subsytems %v"
			return fmt.Errorf(str, subsysID, supportedSubsystems())
		}

		// Validate log level.
		if !validLogLevel(logLevel) {
			str := "The specified debug level [%v] is invalid"
			return fmt.Errorf(str, logLevel)
		}

		setLogLevel(subsysID, logLevel)
	}

	return nil
}

// loadConfig initializes and parses the config using a config file and command
// line options.
func loadConfig() (*config, error) {
	loadConfigError := func(err error) (*config, error) {
		return nil, err
	}

	// Default config.
	cfg := defaultConfig

	// A config file in the current directory takes precedence.
	if _, err := os.Stat(defaultConfigFilename); !os.IsNotExist(err) {
		cfg.ConfigFile = defaultConfigFile
	}

	// Pre-parse the command line options to see if an alternative config
	// file or the version flag was specified.
	preCfg := cfg
	preParser := flags.NewParser(&preCfg, flags.HelpFlag|flags.PassDoubleDash)
	_, err := preParser.Parse()
	if err != nil {
		e, ok := err.(*flags.Error)
		if !ok || e.Type != flags.ErrHelp {
			preParser.WriteHelp(os.Stderr)
		}
		if ok && e.Type == flags.ErrHelp {
			preParser.WriteHelp(os.Stdout)
			os.Exit(0)
		}
		return loadConfigError(err)
	}

	// Show the version and exit if the version flag was specified.
	appName := filepath.Base(os.Args[0])
	appName = strings.TrimSuffix(appName, filepath.Ext(appName))
	if preCfg.ShowVersion {
		fmt.Println(appName, "version", ver.String())
		os.Exit(0)
	}

	// Load additional config from file.
	var configFileError error
	parser := flags.NewParser(&cfg, flags.Default)
	err = flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile)
	if err != nil {
		if _, ok := err.(*os.PathError); !ok {
			fmt.Fprintln(os.Stderr, err)
			parser.WriteHelp(os.Stderr)
			return loadConfigError(err)
		}
		configFileError = err
	}

	// Parse command line options again to ensure they take precedence.
	_, err = parser.Parse()
	if err != nil {
		if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
			parser.WriteHelp(os.Stderr)
		}
		return loadConfigError(err)
	}

	// Warn about missing config file after the final command line parse
	// succeeds.  This prevents the warning on help messages and invalid
	// options.
	if configFileError != nil {
		log.Warnf("%v", configFileError)
		//fmt.Printf("%v\n",configFileError)
		return loadConfigError(configFileError)
	}

	// Choose the active network params based on the selected network.
	// Multiple networks can't be selected simultaneously.
	numNets := 0
	activeNet = &netparams.MainNetParams
	activeChain = &chaincfg.MainNetParams
	if cfg.TestNet {
		activeNet = &netparams.TestNet2Params
		activeChain = &chaincfg.TestNet2Params
		numNets++
	}
	if cfg.SimNet {
		activeNet = &netparams.SimNetParams
		activeChain = &chaincfg.SimNetParams
		numNets++
	}
	if numNets > 1 {
		str := "%s: The testnet and simnet params can't be used " +
			"together -- choose one"
		err := fmt.Errorf(str, "loadConfig")
		fmt.Fprintln(os.Stderr, err)
		parser.WriteHelp(os.Stderr)
		return loadConfigError(err)
	}

	// Set the host names and ports to the default if the
	// user does not specify them.
	if cfg.DcrdServ == "" {
		cfg.DcrdServ = defaultHost + ":" + activeNet.JSONRPCClientPort
	}

	// Put comma-separated comamnd line aguments into slice of strings
	//cfg.CmdArgs = strings.Split(cfg.CmdArgs[0], ",")

	// // Output folder
	cfg.OutFolder = cleanAndExpandPath(cfg.OutFolder)
	cfg.OutFolder = filepath.Join(cfg.OutFolder, activeNet.Name)

	// The HTTP server port can not be beyond a uint16's size in value.
	// if cfg.HttpSvrPort > 0xffff {
	// 	str := "%s: Invalid HTTP port number for HTTP server"
	// 	err := fmt.Errorf(str, "loadConfig")
	// 	fmt.Fprintln(os.Stderr, err)
	// 	parser.WriteHelp(os.Stderr)
	// 	return loadConfigError(err)
	// }

	// Append the network type to the log directory so it is "namespaced"
	// per network.
	cfg.LogDir = cleanAndExpandPath(cfg.LogDir)
	cfg.LogDir = filepath.Join(cfg.LogDir, activeNet.Name)

	// Special show command to list supported subsystems and exit.
	if cfg.DebugLevel == "show" {
		fmt.Println("Supported subsystems", supportedSubsystems())
		os.Exit(0)
	}

	// Initialize log rotation.  After log rotation has been initialized, the
	// logger variables may be used.
	initLogRotator(filepath.Join(cfg.LogDir, defaultLogFilename))

	// Parse, validate, and set debug log level(s).
	if cfg.Quiet {
		cfg.DebugLevel = "error"
	}
	if err := parseAndSetDebugLevels(cfg.DebugLevel); err != nil {
		err = fmt.Errorf("%s: %v", "loadConfig", err.Error())
		fmt.Fprintln(os.Stderr, err)
		parser.WriteHelp(os.Stderr)
		return loadConfigError(err)
	}

	return &cfg, nil
}
<|endoftext|>