<commit_before>package cli

import (
	"errors"
	"fmt"
	"os"

	log "github.com/Sirupsen/logrus"
	"github.com/bitrise-io/bitrise-cli/bitrise"
	models "github.com/bitrise-io/bitrise-cli/models/models_1_0_0"
	"github.com/bitrise-io/go-pathutil/pathutil"
	stepmanModels "github.com/bitrise-io/stepman/models"
	"github.com/codegangsta/cli"
)

const (
	// DefaultBitriseConfigFileName ...
	DefaultBitriseConfigFileName = "bitrise.yml"
	// DefaultSecretsFileName ...
	DefaultSecretsFileName = ".bitrise.secrets.yml"
)

var (
	failedSteps   []string
	inventoryPath string
)

func isBuildFailed() bool {
	if len(failedSteps) > 0 {
		return true
	}
	return false
}

func exportEnvironmentsList(envsList []stepmanModels.EnvironmentItemModel) error {
	log.Debugln("[BITRISE_CLI] - Exporting environments:", envsList)

	for _, env := range envsList {
		key, value, err := env.GetKeyValuePair()
		if err != nil {
			return err
		}

		opts, err := env.GetOptions()
		if err != nil {
			return err
		}

		if value != "" {
			if err := bitrise.RunEnvmanAdd(key, value, *opts.IsExpand); err != nil {
				log.Errorln("[BITRISE_CLI] - Failed to run envman add")
				return err
			}
		}
	}
	return nil
}

func cleanupStepWorkDir() error {
	stepYMLPth := bitrise.BitriseWorkDirPath + "/current_step.yml"
	if err := bitrise.RemoveFile(stepYMLPth); err != nil {
		return errors.New(fmt.Sprint("Failed to remove step yml: ", err))
	}

	stepDir := bitrise.BitriseWorkStepsDirPath
	if err := bitrise.RemoveDir(stepDir); err != nil {
		return errors.New(fmt.Sprint("Failed to remove step work dir: ", err))
	}
	return nil
}

func activateAndRunSteps(workflow models.WorkflowModel, defaultStepLibSource string) error {
	log.Debugln("[BITRISE_CLI] - Activating and running steps")

	for idx, stepListItm := range workflow.Steps {
		compositeStepIDStr, workflowStep, err := models.GetStepIDStepDataPair(stepListItm)
		if err != nil {
			return err
		}
		stepIDData, err := models.CreateStepIDDataFromString(compositeStepIDStr, defaultStepLibSource)
		if err != nil {
			return err
		}
		log.Debugf("[BITRISE_CLI] - Running Step: %#v", workflowStep)

		stepDir := bitrise.BitriseWorkStepsDirPath

		if err := bitrise.RunStepmanSetup(stepIDData.SteplibSource); err != nil {
			log.Error("Failed to setup stepman:", err)
		}

		if err := cleanupStepWorkDir(); err != nil {
			return err
		}

		stepYMLPth := bitrise.BitriseWorkDirPath + "/current_step.yml"
		if err := bitrise.RunStepmanActivate(stepIDData.SteplibSource, stepIDData.ID, stepIDData.Version, stepDir, stepYMLPth); err != nil {
			log.Errorln("[BITRISE_CLI] - Failed to run stepman activate")
			failedSteps = append(failedSteps, compositeStepIDStr)
		} else {
			log.Debugf("[BITRISE_CLI] - Step activated: %s (%s)", stepIDData.ID, stepIDData.Version)

			specStep, err := bitrise.ReadSpecStep(stepYMLPth)
			log.Debugf("Spec read from YML: %#v\n", specStep)
			if err != nil {
				return err
			}

			if err := models.MergeStepWith(specStep, workflowStep); err != nil {
				return err
			}

			fmt.Println()
			log.Infof("========== (%d) %s ==========", idx, *specStep.Title)
			fmt.Println()

			if isBuildFailed() && !*specStep.IsAlwaysRun {
				log.Infof("A previous step failed and this step was not marked to IsAlwaysRun - skipping %s (%s)", stepIDData.ID, stepIDData.Version)
			} else {
				if err := runStep(specStep, stepIDData); err != nil {
					log.Errorln("[BITRISE_CLI] - Failed to run step:", err)
					failedSteps = append(failedSteps, compositeStepIDStr)
				}
			}
		}
	}
	return nil
}

func runStep(step stepmanModels.StepModel, stepIDData models.StepIDData) error {
	log.Debugf("[BITRISE_CLI] - Try running step: %s (%s)", stepIDData.ID, stepIDData.Version)

	// Add step envs
	for _, input := range step.Inputs {
		key, value, err := input.GetKeyValuePair()
		if err != nil {
			return err
		}

		opts, err := input.GetOptions()
		if err != nil {
			return err
		}

		if value != "" {
			log.Debugf("Input: %#v\n", input)
			if err := bitrise.RunEnvmanAdd(key, value, *opts.IsExpand); err != nil {
				log.Errorln("[BITRISE_CLI] - Failed to run envman add")
				return err
			}
		}
	}

	stepDir := bitrise.BitriseWorkStepsDirPath
	stepCmd := stepDir + "/" + "step.sh"
	cmd := []string{"bash", stepCmd}
	if err := bitrise.RunEnvmanRunInDir(bitrise.CurrentDir, cmd); err != nil {
		log.Errorln("[BITRISE_CLI] - Failed to run envman run")
		return err
	}

	log.Debugf("[BITRISE_CLI] - Step executed: %s (%s)", stepIDData.ID, stepIDData.Version)
	return nil
}

func doRun(c *cli.Context) {
	log.Debugln("[BITRISE_CLI] - Run")

	// Cleanup
	if err := bitrise.CleanupBitriseWorkPath(); err != nil {
		log.Fatal("Failed to cleanup bitrise work dir:", err)
	}
	failedSteps = []string{}

	// Input validation
	bitriseConfigPath := c.String(PathKey)
	if bitriseConfigPath == "" {
		log.Debugln("[BITRISE_CLI] - Workflow path not defined, searching for " + DefaultBitriseConfigFileName + " in current folder...")

		if exist, err := pathutil.IsPathExists("./" + DefaultBitriseConfigFileName); err != nil {
			log.Fatalln("[BITRISE_CLI] - Failed to check path:", err)
		} else if !exist {
			log.Fatalln("[BITRISE_CLI] - No workflow yml found")
		}
		bitriseConfigPath = "./" + DefaultBitriseConfigFileName
	}

	inventoryPath = c.String(InventoryKey)
	if inventoryPath == "" {
		log.Debugln("[BITRISE_CLI] - Inventory path not defined, searching for " + DefaultSecretsFileName + " in current folder...")
		inventoryPath = bitrise.CurrentDir + "/" + DefaultSecretsFileName

		if exist, err := pathutil.IsPathExists(inventoryPath); err != nil {
			log.Fatalln("[BITRISE_CLI] - Failed to check path:", err)
		} else if !exist {
			log.Debugln("[BITRISE_CLI] - No inventory yml found")
			inventoryPath = ""
		}
	} else {
		if exist, err := pathutil.IsPathExists(inventoryPath); err != nil {
			log.Fatalln("[BITRISE_CLI] - Failed to check path:", err)
		} else if !exist {
			log.Fatalln("[BITRISE_CLI] - No inventory yml found")
		}
	}
	if inventoryPath != "" {
		if err := bitrise.RunEnvmanEnvstoreTest(inventoryPath); err != nil {
			log.Fatal("Invalid invetory format:", err)
		}

		if err := bitrise.RunCopy(inventoryPath, bitrise.EnvstorePath); err != nil {
			log.Fatal("Failed to copy inventory:", err)
		}
	}

	// Workflow selection
	workflowToRunName := ""
	if len(c.Args()) < 1 {
		log.Infoln("No workfow specified!")
	} else {
		workflowToRunName = c.Args()[0]
	}

	// Envman setup
	if err := os.Setenv(bitrise.EnvstorePathEnvKey, bitrise.EnvstorePath); err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to add env:", err)
	}

	if err := os.Setenv(bitrise.FormattedOutputPathEnvKey, bitrise.FormattedOutputPath); err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to add env:", err)
	}

	if inventoryPath == "" {
		if err := bitrise.RunEnvmanInit(); err != nil {
			log.Fatalln("[BITRISE_CLI] - Failed to run envman init")
		}
	}

	// Run work flow
	bitriseConfig, err := bitrise.ReadBitriseConfig(bitriseConfigPath)
	if err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to read Workflow:", err)
	}

	// check workflow
	if workflowToRunName == "" {
		// no workflow specified
		//  list all the available ones and then exit
		log.Infoln("The following workflows are available:")
		for wfName := range bitriseConfig.Workflows {
			log.Infoln(" * " + wfName)
		}
		os.Exit(1)
	}

	workflowToRun, exist := bitriseConfig.Workflows[workflowToRunName]
	if !exist {
		log.Fatalln("[BITRISE_CLI] - Specified Workflow (" + workflowToRunName + ") does not exist!")
	}
	log.Infoln("[BITRISE_CLI] - Running Workflow:", workflowToRunName)

	// App level environment
	if err := exportEnvironmentsList(bitriseConfig.App.Environments); err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to export App environments:", err)
	}

	// Workflow level environments
	if err := exportEnvironmentsList(workflowToRun.Environments); err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to export Workflow environments:", err)
	}

	// Run the Workflow
	if err := activateAndRunSteps(workflowToRun, bitriseConfig.DefaultStepLibSource); err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to activate steps:", err)
	}

	log.Debugln("Failed steps:", failedSteps)
	log.Infoln("")
	log.Infoln("DONE - Congrats!!")
}
<commit_msg>log fixes<commit_after>package cli

import (
	"errors"
	"fmt"
	"os"

	log "github.com/Sirupsen/logrus"
	"github.com/bitrise-io/bitrise-cli/bitrise"
	models "github.com/bitrise-io/bitrise-cli/models/models_1_0_0"
	"github.com/bitrise-io/go-pathutil/pathutil"
	stepmanModels "github.com/bitrise-io/stepman/models"
	"github.com/codegangsta/cli"
)

const (
	// DefaultBitriseConfigFileName ...
	DefaultBitriseConfigFileName = "bitrise.yml"
	// DefaultSecretsFileName ...
	DefaultSecretsFileName = ".bitrise.secrets.yml"
)

var (
	failedSteps   []string
	inventoryPath string
)

func isBuildFailed() bool {
	if len(failedSteps) > 0 {
		return true
	}
	return false
}

func exportEnvironmentsList(envsList []stepmanModels.EnvironmentItemModel) error {
	log.Debugln("[BITRISE_CLI] - Exporting environments:", envsList)

	for _, env := range envsList {
		key, value, err := env.GetKeyValuePair()
		if err != nil {
			return err
		}

		opts, err := env.GetOptions()
		if err != nil {
			return err
		}

		if value != "" {
			if err := bitrise.RunEnvmanAdd(key, value, *opts.IsExpand); err != nil {
				log.Errorln("[BITRISE_CLI] - Failed to run envman add")
				return err
			}
		}
	}
	return nil
}

func cleanupStepWorkDir() error {
	stepYMLPth := bitrise.BitriseWorkDirPath + "/current_step.yml"
	if err := bitrise.RemoveFile(stepYMLPth); err != nil {
		return errors.New(fmt.Sprint("Failed to remove step yml: ", err))
	}

	stepDir := bitrise.BitriseWorkStepsDirPath
	if err := bitrise.RemoveDir(stepDir); err != nil {
		return errors.New(fmt.Sprint("Failed to remove step work dir: ", err))
	}
	return nil
}

func activateAndRunSteps(workflow models.WorkflowModel, defaultStepLibSource string) error {
	log.Debugln("[BITRISE_CLI] - Activating and running steps")

	for idx, stepListItm := range workflow.Steps {
		compositeStepIDStr, workflowStep, err := models.GetStepIDStepDataPair(stepListItm)
		if err != nil {
			return err
		}
		stepIDData, err := models.CreateStepIDDataFromString(compositeStepIDStr, defaultStepLibSource)
		if err != nil {
			return err
		}
		log.Debugf("[BITRISE_CLI] - Running Step: %#v", workflowStep)

		stepDir := bitrise.BitriseWorkStepsDirPath

		if err := bitrise.RunStepmanSetup(stepIDData.SteplibSource); err != nil {
			log.Error("Failed to setup stepman:", err)
		}

		if err := cleanupStepWorkDir(); err != nil {
			return err
		}

		stepYMLPth := bitrise.BitriseWorkDirPath + "/current_step.yml"
		if err := bitrise.RunStepmanActivate(stepIDData.SteplibSource, stepIDData.ID, stepIDData.Version, stepDir, stepYMLPth); err != nil {
			log.Errorln("[BITRISE_CLI] - Failed to run stepman activate")
			failedSteps = append(failedSteps, compositeStepIDStr)
		} else {
			log.Debugf("[BITRISE_CLI] - Step activated: %s (%s)", stepIDData.ID, stepIDData.Version)

			specStep, err := bitrise.ReadSpecStep(stepYMLPth)
			log.Debugf("Spec read from YML: %#v\n", specStep)
			if err != nil {
				return err
			}

			if err := models.MergeStepWith(specStep, workflowStep); err != nil {
				return err
			}

			fmt.Println()
			log.Infof("========== (%d) %s ==========", idx, *specStep.Title)
			fmt.Println()

			if isBuildFailed() && !*specStep.IsAlwaysRun {
				log.Infof("A previous step failed and this step was not marked to IsAlwaysRun - skipping (%s (%s))", stepIDData.ID, stepIDData.Version)
			} else {
				if err := runStep(specStep, stepIDData); err != nil {
					log.Errorln("[BITRISE_CLI] - Failed to run step:", err)
					failedSteps = append(failedSteps, compositeStepIDStr)
				}
			}
		}
	}
	return nil
}

func runStep(step stepmanModels.StepModel, stepIDData models.StepIDData) error {
	log.Debugf("[BITRISE_CLI] - Try running step: %s (%s)", stepIDData.ID, stepIDData.Version)

	// Add step envs
	for _, input := range step.Inputs {
		key, value, err := input.GetKeyValuePair()
		if err != nil {
			return err
		}

		opts, err := input.GetOptions()
		if err != nil {
			return err
		}

		if value != "" {
			log.Debugf("Input: %#v\n", input)
			if err := bitrise.RunEnvmanAdd(key, value, *opts.IsExpand); err != nil {
				log.Errorln("[BITRISE_CLI] - Failed to run envman add")
				return err
			}
		}
	}

	stepDir := bitrise.BitriseWorkStepsDirPath
	stepCmd := stepDir + "/" + "step.sh"
	cmd := []string{"bash", stepCmd}
	if err := bitrise.RunEnvmanRunInDir(bitrise.CurrentDir, cmd); err != nil {
		log.Errorln("[BITRISE_CLI] - Failed to run envman run")
		return err
	}

	log.Debugf("[BITRISE_CLI] - Step executed: %s (%s)", stepIDData.ID, stepIDData.Version)
	return nil
}

func doRun(c *cli.Context) {
	log.Debugln("[BITRISE_CLI] - Run")

	// Cleanup
	if err := bitrise.CleanupBitriseWorkPath(); err != nil {
		log.Fatal("Failed to cleanup bitrise work dir:", err)
	}
	failedSteps = []string{}

	// Input validation
	bitriseConfigPath := c.String(PathKey)
	if bitriseConfigPath == "" {
		log.Debugln("[BITRISE_CLI] - Workflow path not defined, searching for " + DefaultBitriseConfigFileName + " in current folder...")

		if exist, err := pathutil.IsPathExists("./" + DefaultBitriseConfigFileName); err != nil {
			log.Fatalln("[BITRISE_CLI] - Failed to check path:", err)
		} else if !exist {
			log.Fatalln("[BITRISE_CLI] - No workflow yml found")
		}
		bitriseConfigPath = "./" + DefaultBitriseConfigFileName
	}

	inventoryPath = c.String(InventoryKey)
	if inventoryPath == "" {
		log.Debugln("[BITRISE_CLI] - Inventory path not defined, searching for " + DefaultSecretsFileName + " in current folder...")
		inventoryPath = bitrise.CurrentDir + "/" + DefaultSecretsFileName

		if exist, err := pathutil.IsPathExists(inventoryPath); err != nil {
			log.Fatalln("[BITRISE_CLI] - Failed to check path:", err)
		} else if !exist {
			log.Debugln("[BITRISE_CLI] - No inventory yml found")
			inventoryPath = ""
		}
	} else {
		if exist, err := pathutil.IsPathExists(inventoryPath); err != nil {
			log.Fatalln("[BITRISE_CLI] - Failed to check path:", err)
		} else if !exist {
			log.Fatalln("[BITRISE_CLI] - No inventory yml found")
		}
	}
	if inventoryPath != "" {
		if err := bitrise.RunEnvmanEnvstoreTest(inventoryPath); err != nil {
			log.Fatal("Invalid invetory format:", err)
		}

		if err := bitrise.RunCopy(inventoryPath, bitrise.EnvstorePath); err != nil {
			log.Fatal("Failed to copy inventory:", err)
		}
	}

	// Workflow selection
	workflowToRunName := ""
	if len(c.Args()) < 1 {
		log.Infoln("No workfow specified!")
	} else {
		workflowToRunName = c.Args()[0]
	}

	// Envman setup
	if err := os.Setenv(bitrise.EnvstorePathEnvKey, bitrise.EnvstorePath); err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to add env:", err)
	}

	if err := os.Setenv(bitrise.FormattedOutputPathEnvKey, bitrise.FormattedOutputPath); err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to add env:", err)
	}

	if inventoryPath == "" {
		if err := bitrise.RunEnvmanInit(); err != nil {
			log.Fatalln("[BITRISE_CLI] - Failed to run envman init")
		}
	}

	// Run work flow
	bitriseConfig, err := bitrise.ReadBitriseConfig(bitriseConfigPath)
	if err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to read Workflow:", err)
	}

	// check workflow
	if workflowToRunName == "" {
		// no workflow specified
		//  list all the available ones and then exit
		log.Infoln("The following workflows are available:")
		for wfName := range bitriseConfig.Workflows {
			log.Infoln(" * " + wfName)
		}
		os.Exit(1)
	}

	workflowToRun, exist := bitriseConfig.Workflows[workflowToRunName]
	if !exist {
		log.Fatalln("[BITRISE_CLI] - Specified Workflow (" + workflowToRunName + ") does not exist!")
	}
	log.Infoln("[BITRISE_CLI] - Running Workflow:", workflowToRunName)

	// App level environment
	if err := exportEnvironmentsList(bitriseConfig.App.Environments); err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to export App environments:", err)
	}

	// Workflow level environments
	if err := exportEnvironmentsList(workflowToRun.Environments); err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to export Workflow environments:", err)
	}

	// Run the Workflow
	if err := activateAndRunSteps(workflowToRun, bitriseConfig.DefaultStepLibSource); err != nil {
		log.Fatalln("[BITRISE_CLI] - Failed to activate steps:", err)
	}

	log.Infoln("")
	if len(failedSteps) > 0 {
		log.Info("Failed steps:", failedSteps)
		log.Info("FINISHED but a couple of steps failed - Ouch")
	} else {
		log.Infoln("DONE - Congrats!!")
	}
}
<|endoftext|>


<commit_before>package client

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"sync"
	"time"

	"github.com/Focinfi/oncekv/config"
	"github.com/Focinfi/oncekv/log"
	"github.com/Focinfi/oncekv/utils/urlutil"
)

const (
	logPrefix      = "oncekv/client:"
	dbPutURLFormat = "%s/key"
)

var (
	requestTimeout       = config.Config().ClientRequestTimeout
	idealReponseDuration = config.Config().IdealResponseDuration
	// ErrDataNotFound for data not found response
	ErrDataNotFound = fmt.Errorf("%s data not found", logPrefix)

	// ErrTimeout for timeout
	ErrTimeout = fmt.Errorf("%s timeout", logPrefix)
)

type httpGetter interface {
	Get(url string) (resp *http.Response, err error)
}

type httpGetterFunc func(url string) (resp *http.Response, err error)

func (f httpGetterFunc) Get(url string) (resp *http.Response, err error) {
	return f(url)
}

type httpPoster interface {
	Post(url string, contentType string, body io.Reader) (resp *http.Response, err error)
}

type httpPosterFunc func(url string, contentType string, body io.Reader) (resp *http.Response, err error)

func (f httpPosterFunc) Post(url string, contentType string, body io.Reader) (resp *http.Response, err error) {
	return f(url, contentType, body)
}

var defaultGetter = httpGetter(httpGetterFunc(http.Get))
var defaultPoster = httpPoster(httpPosterFunc(http.Post))

// Option for Client option
type Option struct {
	RequestTimeout        time.Duration
	IdealResponseDuration time.Duration
}

// KV for kv storage
type KV struct {
	cli    *Client
	option *Option
}

type kvParams struct {
	Key     string `json:"key,omitempty"`
	Value   string `json:"value,omitempty"`
	Code    int    `json:"code,omitempty"`
	Message string `json:"message,omitempty"`
}

// DefaultKV returns a new KV with default option
// RequestTimeout: 100ms
// IdealResponseDuration: 50ms
func DefaultKV() (*KV, error) {
	return NewKV(nil)
}

// NewKV returns a new KV
func NewKV(option *Option) (*KV, error) {
	cli, err := New()
	if err != nil {
		return nil, err
	}

	if option == nil {
		option = &Option{
			RequestTimeout:        requestTimeout,
			IdealResponseDuration: idealReponseDuration,
		}
	}

	return &KV{cli: cli, option: option}, nil
}

// Get get the value of the key
func (kv *KV) Get(key string) (string, error) {
	val, err := kv.cache(key)
	log.DB.Infoln(logPrefix, val, err)

	// believe cache, if cache alive, it can always right
	if err == ErrDataNotFound {
		return "", err
	}

	if err == nil {
		return val, nil
	}

	val, err = kv.get(key)
	if err != nil {
		log.DB.Error(logPrefix, err)
		return "", err
	}

	return val, nil
}

// Put put key/value pair
func (kv *KV) Put(key string, value string) error {
	if kv.cli.fastDB == "" {
		return kv.tryAllDBSet(key, value)
	}

	duration, err := kv.set(key, value, kv.cli.fastDB)
	if err != nil {
		log.DB.Error(logPrefix, err)
		return kv.tryAllDBSet(key, value)
	}

	if duration > idealReponseDuration {
		// remove fastDB
		go func() { kv.cli.setFastDB("") }()
	}

	return nil
}

func (kv *KV) cache(key string) (string, error) {
	url := kv.cli.fastCache
	if url == "" {
		return kv.tryAllCaches(key)
	}

	val, _, err := kv.find(key, url, idealReponseDuration)
	if err == ErrDataNotFound {
		return "", err
	}

	if err != nil {
		return kv.tryAllCaches(key)
	}

	return val, err
}

func (kv *KV) get(key string) (string, error) {
	if kv.cli.fastDB == "" {
		return kv.tryAllDBfind(key)
	}

	val, duration, err := kv.find(key, kv.cli.fastDB, requestTimeout)
	if err == ErrDataNotFound {
		return "", err
	}

	if err != nil {
		log.DB.Error(logPrefix, err)
		return kv.tryAllDBfind(key)
	}

	if duration > idealReponseDuration {
		go func() { kv.cli.setFastDB("") }()
	}

	return val, nil
}

func (kv *KV) tryAllDBfind(key string) (string, error) {
	dbs := make([]string, len(kv.cli.dbs))
	copy(dbs, kv.cli.dbs)
	log.Biz.Infoln(logPrefix, "start get:", time.Now(), dbs)
	if len(dbs) == 0 {
		return "", fmt.Errorf("%s databases are not available\n", logPrefix)
	}

	var got bool
	var mux sync.Mutex
	var data = make(chan string)
	var completeCount int
	var fastURL string
	var resErr error

	for i, db := range dbs {
		go func(index int, url string) {
			val, _, err := kv.find(key, url, requestTimeout)
			if err != nil {
				log.DB.Error(logPrefix, err)
			}

			mux.Lock()
			defer mux.Unlock()
			if val != "" || err == ErrDataNotFound || completeCount == len(dbs) {
				if !got {
					got = true
					fastURL = url
					resErr = err

					go func() { data <- val }()
				}
			}
		}(i, db)
	}

	select {
	case <-time.After(requestTimeout):
		go kv.cli.setFastDB("")
		return "", ErrTimeout

	case value := <-data:
		log.Biz.Infoln(logPrefix, "end get:", time.Now())

		if value != "" || resErr == ErrDataNotFound {
			go kv.cli.setFastDB(fastURL)
		}

		return value, resErr
	}
}

func (kv *KV) tryAllDBSet(key string, value string) error {
	dbs := make([]string, len(kv.cli.dbs))
	copy(dbs, kv.cli.dbs)
	log.Biz.Infoln(logPrefix, "start tryAllDBSet:", time.Now(), dbs)
	if len(dbs) == 0 {
		return fmt.Errorf("%s db unavailable", logPrefix)
	}

	var mux sync.Mutex
	var fetched bool
	var fastURL string
	var completeCount int
	var err error

	var result = make(chan error)

	for i, db := range dbs {
		go func(index int, url string) {
			_, err = kv.set(key, value, url)

			if err != nil {
				log.DB.Error(logPrefix, err)
			}

			mux.Lock()
			defer mux.Unlock()

			completeCount++
			if err == nil || completeCount >= len(dbs) {
				if !fetched {
					fetched = true
					fastURL = url
					go func() { result <- err }()
				}
			}
		}(i, db)
	}

	select {
	case <-time.After(requestTimeout):
		go func() { kv.cli.setFastDB("") }()
		return ErrTimeout
	case res := <-result:
		log.Biz.Infoln(logPrefix, "end tryAllDBSet:", time.Now())

		if res == nil {
			go kv.cli.setFastDB(fastURL)
		}

		return res
	}
}

func (kv *KV) set(key string, value string, url string) (time.Duration, error) {
	log.Biz.Debugln(logPrefix, "put: ", key, value, url)
	begin := time.Now()
	b, err := json.Marshal(&kvParams{Key: key, Value: value})
	if err != nil {
		return requestTimeout, err
	}

	res, err := defaultPoster.Post(fmt.Sprintf(dbPutURLFormat, urlutil.MakeURL(url)), "application-type/json", bytes.NewReader(b))
	if err != nil {
		return requestTimeout, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return requestTimeout, fmt.Errorf("%s failed to set kv(url: %s), key: %s, value: %v\n", logPrefix, url, key, value)
	}

	return time.Now().Sub(begin), nil
}

func (kv *KV) parseData(readCloser io.ReadCloser, key string) (string, error) {
	b, err := ioutil.ReadAll(readCloser)
	if err != nil {
		return "", err
	}

	log.DB.Infoln("Message Resp:", string(b))

	param := &kvParams{}
	if err := json.Unmarshal(b, param); err != nil {
		return "", err
	}

	if param.Key != key {
		return "", fmt.Errorf("%s wrong response for key='%s'\n", logPrefix, key)
	}

	if param.Value == "" {
		return "", fmt.Errorf("%s empty value response for key = '%s'\n", logPrefix, key)
	}

	return param.Value, nil
}

func (kv *KV) find(key string, url string, timeout time.Duration) (value string, duration time.Duration, err error) {
	begin := time.Now()
	resChan := make(chan *http.Response)
	errChan := make(chan error)

	go func() {
		res, err := defaultGetter.Get(fmt.Sprintf("%s/key/%s", urlutil.MakeURL(url), key))
		if err != nil {
			errChan <- err
			return
		}

		resChan <- res
	}()

	select {
	case <-time.After(timeout):
		return "", requestTimeout, ErrTimeout

	case err := <-errChan:
		log.DB.Errorln(logPrefix, "find:", err)
		return "", requestTimeout, err

	case res := <-resChan:
		defer res.Body.Close()
		duration = time.Now().Sub(begin)

		if res.StatusCode == http.StatusNoContent {
			return "", duration, ErrDataNotFound
		}

		if res.StatusCode == http.StatusOK {
			val, err := kv.parseData(res.Body, key)
			if err == nil {
				return val, duration, nil
			}

			log.Biz.Errorln(logPrefix, "find/parseData error:", err)
			return "", requestTimeout, err
		}

		return "", requestTimeout, ErrTimeout
	}
}

// try all caching urls, set the fastCache
func (kv *KV) tryAllCaches(key string) (string, error) {
	caches := make([]string, len(kv.cli.caches))
	copy(caches, kv.cli.caches)
	log.Biz.Infoln(logPrefix, "start tryAllCaches:", time.Now(), caches)
	if len(caches) == 0 {
		return "", fmt.Errorf("%s caches are unavailable ", logPrefix)
	}

	var fetched bool
	var mux sync.Mutex
	var data = make(chan string)
	var completeCount int
	var fastURL string
	var minDuration = requestTimeout
	var completed = make(chan bool)
	var resErr error

	for i, cache := range caches {
		go func(index int, url string) {
			val, duration, err := kv.find(key, url, requestTimeout)
			log.DB.Infoln(logPrefix, key, url, val, duration, err)
			if err != nil {
				log.DB.Error(err)
			}

			mux.Lock()
			defer mux.Unlock()

			if duration <= minDuration {
				minDuration = duration
				fastURL = url
				log.DB.Infoln(fastURL, duration)
			}

			completeCount++
			log.DB.Infoln(completeCount, len(caches))
			if completeCount == len(caches) {
				go func() { completed <- true }()

				if !fetched {
					fetched = true
					resErr = err
					go func() { data <- val }()
				}
				return
			}

			if val != "" || err == ErrDataNotFound {
				if !fetched {
					fetched = true
					resErr = err
					go func() { data <- val }()
				}
			}
		}(i, cache)
	}

	go func() {
		<-completed
		if fastURL != "" {
			go kv.cli.setFastCache(fastURL)
		}
	}()

	select {
	case <-time.After(requestTimeout):
		return "", ErrTimeout

	case value := <-data:
		log.Biz.Println(logPrefix, "end tryAllCaches:", time.Now())
		return value, resErr
	}
}
<commit_msg>Fix should return http.StatusNotFound<commit_after>package client

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"sync"
	"time"

	"github.com/Focinfi/oncekv/config"
	"github.com/Focinfi/oncekv/log"
	"github.com/Focinfi/oncekv/utils/urlutil"
)

const (
	logPrefix      = "oncekv/client:"
	dbGetURLFormat = "%s/key/%s"
	dbPutURLFormat = "%s/key"
)

var (
	requestTimeout       = config.Config().ClientRequestTimeout
	idealReponseDuration = config.Config().IdealResponseDuration
	// ErrDataNotFound for data not found response
	ErrDataNotFound = fmt.Errorf("%s data not found", logPrefix)

	// ErrTimeout for timeout
	ErrTimeout = fmt.Errorf("%s timeout", logPrefix)
)

type httpGetter interface {
	Get(url string) (resp *http.Response, err error)
}

type httpGetterFunc func(url string) (resp *http.Response, err error)

func (f httpGetterFunc) Get(url string) (resp *http.Response, err error) {
	return f(url)
}

type httpPoster interface {
	Post(url string, contentType string, body io.Reader) (resp *http.Response, err error)
}

type httpPosterFunc func(url string, contentType string, body io.Reader) (resp *http.Response, err error)

func (f httpPosterFunc) Post(url string, contentType string, body io.Reader) (resp *http.Response, err error) {
	return f(url, contentType, body)
}

var defaultGetter = httpGetter(httpGetterFunc(http.Get))
var defaultPoster = httpPoster(httpPosterFunc(http.Post))

// Option for Client option
type Option struct {
	RequestTimeout        time.Duration
	IdealResponseDuration time.Duration
}

// KV for kv storage
type KV struct {
	cli    *Client
	option *Option
}

type kvParams struct {
	Key     string `json:"key,omitempty"`
	Value   string `json:"value,omitempty"`
	Code    int    `json:"code,omitempty"`
	Message string `json:"message,omitempty"`
}

// DefaultKV returns a new KV with default option
// RequestTimeout: 100ms
// IdealResponseDuration: 50ms
func DefaultKV() (*KV, error) {
	return NewKV(nil)
}

// NewKV returns a new KV
func NewKV(option *Option) (*KV, error) {
	cli, err := New()
	if err != nil {
		return nil, err
	}

	if option == nil {
		option = &Option{
			RequestTimeout:        requestTimeout,
			IdealResponseDuration: idealReponseDuration,
		}
	}

	return &KV{cli: cli, option: option}, nil
}

// Get get the value of the key
func (kv *KV) Get(key string) (string, error) {
	val, err := kv.cache(key)
	log.DB.Infoln(logPrefix, val, err)

	// believe cache, if cache alive, it can always right
	if err == ErrDataNotFound {
		return "", err
	}

	if err == nil {
		return val, nil
	}

	val, err = kv.get(key)
	if err != nil {
		log.DB.Error(logPrefix, err)
		return "", err
	}

	return val, nil
}

// Put put key/value pair
func (kv *KV) Put(key string, value string) error {
	if kv.cli.fastDB == "" {
		return kv.tryAllDBSet(key, value)
	}

	duration, err := kv.set(key, value, kv.cli.fastDB)
	if err != nil {
		log.DB.Error(logPrefix, err)
		return kv.tryAllDBSet(key, value)
	}

	if duration > idealReponseDuration {
		// remove fastDB
		go func() { kv.cli.setFastDB("") }()
	}

	return nil
}

func (kv *KV) cache(key string) (string, error) {
	url := kv.cli.fastCache
	if url == "" {
		return kv.tryAllCaches(key)
	}

	val, _, err := kv.find(key, url, idealReponseDuration)
	if err == ErrDataNotFound {
		return "", err
	}

	if err != nil {
		return kv.tryAllCaches(key)
	}

	return val, err
}

func (kv *KV) get(key string) (string, error) {
	if kv.cli.fastDB == "" {
		return kv.tryAllDBfind(key)
	}

	val, duration, err := kv.find(key, kv.cli.fastDB, requestTimeout)
	if err == ErrDataNotFound {
		return "", err
	}

	if err != nil {
		log.DB.Error(logPrefix, err)
		return kv.tryAllDBfind(key)
	}

	if duration > idealReponseDuration {
		go func() { kv.cli.setFastDB("") }()
	}

	return val, nil
}

func (kv *KV) tryAllDBfind(key string) (string, error) {
	dbs := make([]string, len(kv.cli.dbs))
	copy(dbs, kv.cli.dbs)
	log.Biz.Infoln(logPrefix, "start get:", time.Now(), dbs)
	if len(dbs) == 0 {
		return "", fmt.Errorf("%s databases are not available\n", logPrefix)
	}

	var got bool
	var mux sync.Mutex
	var data = make(chan string)
	var completeCount int
	var fastURL string
	var resErr error

	for i, db := range dbs {
		go func(index int, url string) {
			val, _, err := kv.find(key, url, requestTimeout)
			if err != nil {
				log.DB.Error(logPrefix, err)
			}

			mux.Lock()
			defer mux.Unlock()
			if val != "" || err == ErrDataNotFound || completeCount == len(dbs) {
				if !got {
					got = true
					fastURL = url
					resErr = err

					go func() { data <- val }()
				}
			}
		}(i, db)
	}

	select {
	case <-time.After(requestTimeout):
		go kv.cli.setFastDB("")
		return "", ErrTimeout

	case value := <-data:
		log.Biz.Infoln(logPrefix, "end get:", time.Now())

		if value != "" || resErr == ErrDataNotFound {
			go kv.cli.setFastDB(fastURL)
		}

		return value, resErr
	}
}

func (kv *KV) tryAllDBSet(key string, value string) error {
	dbs := make([]string, len(kv.cli.dbs))
	copy(dbs, kv.cli.dbs)
	log.Biz.Infoln(logPrefix, "start tryAllDBSet:", time.Now(), dbs)
	if len(dbs) == 0 {
		return fmt.Errorf("%s db unavailable", logPrefix)
	}

	var mux sync.Mutex
	var fetched bool
	var fastURL string
	var completeCount int
	var err error

	var result = make(chan error)

	for i, db := range dbs {
		go func(index int, url string) {
			_, err = kv.set(key, value, url)

			if err != nil {
				log.DB.Error(logPrefix, err)
			}

			mux.Lock()
			defer mux.Unlock()

			completeCount++
			if err == nil || completeCount >= len(dbs) {
				if !fetched {
					fetched = true
					fastURL = url
					go func() { result <- err }()
				}
			}
		}(i, db)
	}

	select {
	case <-time.After(requestTimeout):
		go func() { kv.cli.setFastDB("") }()
		return ErrTimeout
	case res := <-result:
		log.Biz.Infoln(logPrefix, "end tryAllDBSet:", time.Now())

		if res == nil {
			go kv.cli.setFastDB(fastURL)
		}

		return res
	}
}

func (kv *KV) set(key string, value string, url string) (time.Duration, error) {
	log.Biz.Debugln(logPrefix, "put: ", key, value, url)
	begin := time.Now()
	b, err := json.Marshal(&kvParams{Key: key, Value: value})
	if err != nil {
		return requestTimeout, err
	}

	res, err := defaultPoster.Post(fmt.Sprintf(dbPutURLFormat, urlutil.MakeURL(url)), "application-type/json", bytes.NewReader(b))
	if err != nil {
		return requestTimeout, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return requestTimeout, fmt.Errorf("%s failed to set kv(url: %s), key: %s, value: %v\n", logPrefix, url, key, value)
	}

	return time.Now().Sub(begin), nil
}

func (kv *KV) parseData(readCloser io.ReadCloser, key string) (string, error) {
	b, err := ioutil.ReadAll(readCloser)
	if err != nil {
		return "", err
	}

	log.DB.Infoln("Message Resp:", string(b))

	param := &kvParams{}
	if err := json.Unmarshal(b, param); err != nil {
		return "", err
	}

	if param.Key != key {
		return "", fmt.Errorf("%s wrong response for key='%s'\n", logPrefix, key)
	}

	if param.Value == "" {
		return "", fmt.Errorf("%s empty value response for key = '%s'\n", logPrefix, key)
	}

	return param.Value, nil
}

func (kv *KV) find(key string, url string, timeout time.Duration) (value string, duration time.Duration, err error) {
	begin := time.Now()
	resChan := make(chan *http.Response)
	errChan := make(chan error)

	go func() {
		res, err := defaultGetter.Get(fmt.Sprintf(dbGetURLFormat, urlutil.MakeURL(url), key))
		if err != nil {
			errChan <- err
			return
		}

		resChan <- res
	}()

	select {
	case <-time.After(timeout):
		return "", requestTimeout, ErrTimeout

	case err := <-errChan:
		log.DB.Errorln(logPrefix, "find:", err)
		return "", requestTimeout, err

	case res := <-resChan:
		defer res.Body.Close()
		duration = time.Now().Sub(begin)

		if res.StatusCode == http.StatusNotFound {
			return "", duration, ErrDataNotFound
		}

		if res.StatusCode == http.StatusOK {
			val, err := kv.parseData(res.Body, key)
			if err == nil {
				return val, duration, nil
			}

			log.Biz.Errorln(logPrefix, "find/parseData error:", err)
			return "", requestTimeout, err
		}

		return "", requestTimeout, ErrTimeout
	}
}

// try all caching urls, set the fastCache
func (kv *KV) tryAllCaches(key string) (string, error) {
	caches := make([]string, len(kv.cli.caches))
	copy(caches, kv.cli.caches)
	log.Biz.Infoln(logPrefix, "start tryAllCaches:", time.Now(), caches)
	if len(caches) == 0 {
		return "", fmt.Errorf("%s caches are unavailable ", logPrefix)
	}

	var fetched bool
	var mux sync.Mutex
	var data = make(chan string)
	var completeCount int
	var fastURL string
	var minDuration = requestTimeout
	var completed = make(chan bool)
	var resErr error

	for i, cache := range caches {
		go func(index int, url string) {
			val, duration, err := kv.find(key, url, requestTimeout)
			log.DB.Infoln(logPrefix, key, url, val, duration, err)
			if err != nil {
				log.DB.Error(err)
			}

			mux.Lock()
			defer mux.Unlock()

			if duration <= minDuration {
				minDuration = duration
				fastURL = url
				log.DB.Infoln(fastURL, duration)
			}

			completeCount++
			log.DB.Infoln(completeCount, len(caches))
			if completeCount == len(caches) {
				go func() { completed <- true }()

				if !fetched {
					fetched = true
					resErr = err
					go func() { data <- val }()
				}
				return
			}

			if val != "" || err == ErrDataNotFound {
				if !fetched {
					fetched = true
					resErr = err
					go func() { data <- val }()
				}
			}
		}(i, cache)
	}

	go func() {
		<-completed
		if fastURL != "" {
			go kv.cli.setFastCache(fastURL)
		}
	}()

	select {
	case <-time.After(requestTimeout):
		return "", ErrTimeout

	case value := <-data:
		log.Biz.Println(logPrefix, "end tryAllCaches:", time.Now())
		return value, resErr
	}
}
<|endoftext|>


<commit_before>package herd

import (
	"bufio"
	"fmt"
	"github.com/Symantec/Dominator/lib/constants"
	"github.com/Symantec/Dominator/lib/format"
	"io"
	"net/http"
	"strings"
	"time"
)

func (herd *Herd) showAliveSubsHandler(w http.ResponseWriter,
	req *http.Request) {
	herd.showSubs(w, "alive ", selectAliveSub)
}

func (herd *Herd) showAllSubsHandler(w http.ResponseWriter, req *http.Request) {
	herd.showSubs(w, "", nil)
}

func (herd *Herd) showCompliantSubsHandler(w http.ResponseWriter,
	req *http.Request) {
	herd.showSubs(w, "compliant ", selectCompliantSub)
}

func (herd *Herd) showDeviantSubsHandler(w http.ResponseWriter,
	req *http.Request) {
	herd.showSubs(w, "deviant ", selectDeviantSub)
}

func (herd *Herd) showReachableSubsHandler(w http.ResponseWriter,
	req *http.Request) {
	selector, err := herd.getReachableSelector(req.URL.RawQuery)
	if err != nil {
		fmt.Fprintln(w, err)
		return
	}
	herd.showSubs(w, "reachable ", selector)
}

func (herd *Herd) showSubs(w io.Writer, subType string,
	selectFunc func(*Sub) bool) {
	writer := bufio.NewWriter(w)
	defer writer.Flush()
	fmt.Fprintf(writer, "<title>Dominator %s subs</title>", subType)
	fmt.Fprintln(writer, ``)
	fmt.Fprintln(writer, "")
	fmt.Fprintln(writer, "
")
	fmt.Fprintln(writer, `<table border="1" style="width:100%">`)
	fmt.Fprintln(writer, "
")
	fmt.Fprintln(writer, "    Name	")
	fmt.Fprintln(writer, "    Required Image	")
	fmt.Fprintln(writer, "    Planned Image	")
	fmt.Fprintln(writer, "    Busy	")
	fmt.Fprintln(writer, "    Status	")
	fmt.Fprintln(writer, "    Uptime	")
	fmt.Fprintln(writer, "    Staleness	")
	fmt.Fprintln(writer, "    Last Update	")
	fmt.Fprintln(writer, "    Last Sync	")
	fmt.Fprintln(writer, "    Connect	")
	fmt.Fprintln(writer, "    Short Poll	")
	fmt.Fprintln(writer, "    Full Poll	")
	fmt.Fprintln(writer, "    Update Compute	")
	fmt.Fprintln(writer, "
")
	subs := herd.getSelectedSubs(selectFunc)
	for _, sub := range subs {
		showSub(writer, sub)
	}
	fmt.Fprintln(writer, "
")
	fmt.Fprintln(writer, "")
}

func showSub(writer io.Writer, sub *Sub) {
	if sub.isInsecure {
		fmt.Fprintln(writer, "
")
	} else {
		fmt.Fprintf(writer, "
\n")
	}
	subURL := fmt.Sprintf("http://%s:%d/",
		strings.SplitN(sub.String(), "*", 2)[0], constants.SubPortNumber)
	fmt.Fprintf(writer, "    %s	\n", subURL, sub)
	sub.herd.showImage(writer, sub.mdb.RequiredImage)
	sub.herd.showImage(writer, sub.mdb.PlannedImage)
	sub.showBusy(writer)
	fmt.Fprintf(writer, "    %s	\n", sub.status)
	timeNow := time.Now()
	showSince(writer, sub.pollTime, sub.startTime)
	showSince(writer, timeNow, sub.lastPollSucceededTime)
	showSince(writer, timeNow, sub.lastUpdateTime)
	showSince(writer, timeNow, sub.lastSyncTime)
	showDuration(writer, sub.lastConnectDuration)
	showDuration(writer, sub.lastShortPollDuration)
	showDuration(writer, sub.lastFullPollDuration)
	showDuration(writer, sub.lastComputeUpdateCpuDuration)
	fmt.Fprintf(writer, "
\n")
}

func (herd *Herd) showImage(writer io.Writer, name string) {
	if name == "" {
		fmt.Fprintln(writer, "    	")
	} else if image, err := herd.getImage(name); err != nil {
		fmt.Fprintf(writer, "    %s	\n", err)
	} else if image != nil {
		fmt.Fprintf(writer,
			"    %s	\n",
			herd.imageServerAddress, name, name)
	} else {
		fmt.Fprintf(writer, "    %s	\n",
			name)
	}
}

func (sub *Sub) showBusy(writer io.Writer) {
	if sub.busy {
		if sub.busyStartTime.IsZero() {
			fmt.Fprintln(writer, "    busy	")
		} else {
			fmt.Fprintf(writer, "    %s	\n",
				format.Duration(time.Since(sub.busyStartTime)))
		}
	} else {
		if sub.busyStartTime.IsZero() {
			fmt.Fprintln(writer, "    	")
		} else {
			fmt.Fprintf(writer, "    %s	\n",
				format.Duration(sub.busyStopTime.Sub(sub.busyStartTime)))
		}
	}
}

func showSince(writer io.Writer, now time.Time, since time.Time) {
	if now.IsZero() || since.IsZero() {
		fmt.Fprintf(writer, "    	\n")
	} else {
		showDuration(writer, now.Sub(since))
	}
}

func showDuration(writer io.Writer, duration time.Duration) {
	if duration < 1 {
		fmt.Fprintf(writer, "    	\n")
	} else {
		fmt.Fprintf(writer, "    %s	\n", format.Duration(duration))
	}
}
<commit_msg>Change some fmt.Fprintf() calls to fmt.Fprintln().<commit_after>package herd

import (
	"bufio"
	"fmt"
	"github.com/Symantec/Dominator/lib/constants"
	"github.com/Symantec/Dominator/lib/format"
	"io"
	"net/http"
	"strings"
	"time"
)

func (herd *Herd) showAliveSubsHandler(w http.ResponseWriter,
	req *http.Request) {
	herd.showSubs(w, "alive ", selectAliveSub)
}

func (herd *Herd) showAllSubsHandler(w http.ResponseWriter, req *http.Request) {
	herd.showSubs(w, "", nil)
}

func (herd *Herd) showCompliantSubsHandler(w http.ResponseWriter,
	req *http.Request) {
	herd.showSubs(w, "compliant ", selectCompliantSub)
}

func (herd *Herd) showDeviantSubsHandler(w http.ResponseWriter,
	req *http.Request) {
	herd.showSubs(w, "deviant ", selectDeviantSub)
}

func (herd *Herd) showReachableSubsHandler(w http.ResponseWriter,
	req *http.Request) {
	selector, err := herd.getReachableSelector(req.URL.RawQuery)
	if err != nil {
		fmt.Fprintln(w, err)
		return
	}
	herd.showSubs(w, "reachable ", selector)
}

func (herd *Herd) showSubs(w io.Writer, subType string,
	selectFunc func(*Sub) bool) {
	writer := bufio.NewWriter(w)
	defer writer.Flush()
	fmt.Fprintf(writer, "<title>Dominator %s subs</title>", subType)
	fmt.Fprintln(writer, ``)
	fmt.Fprintln(writer, "")
	fmt.Fprintln(writer, "
")
	fmt.Fprintln(writer, `<table border="1" style="width:100%">`)
	fmt.Fprintln(writer, "
")
	fmt.Fprintln(writer, "    Name	")
	fmt.Fprintln(writer, "    Required Image	")
	fmt.Fprintln(writer, "    Planned Image	")
	fmt.Fprintln(writer, "    Busy	")
	fmt.Fprintln(writer, "    Status	")
	fmt.Fprintln(writer, "    Uptime	")
	fmt.Fprintln(writer, "    Staleness	")
	fmt.Fprintln(writer, "    Last Update	")
	fmt.Fprintln(writer, "    Last Sync	")
	fmt.Fprintln(writer, "    Connect	")
	fmt.Fprintln(writer, "    Short Poll	")
	fmt.Fprintln(writer, "    Full Poll	")
	fmt.Fprintln(writer, "    Update Compute	")
	fmt.Fprintln(writer, "
")
	subs := herd.getSelectedSubs(selectFunc)
	for _, sub := range subs {
		showSub(writer, sub)
	}
	fmt.Fprintln(writer, "
")
	fmt.Fprintln(writer, "")
}

func showSub(writer io.Writer, sub *Sub) {
	if sub.isInsecure {
		fmt.Fprintln(writer, "
")
	} else {
		fmt.Fprintln(writer, "
")
	}
	subURL := fmt.Sprintf("http://%s:%d/",
		strings.SplitN(sub.String(), "*", 2)[0], constants.SubPortNumber)
	fmt.Fprintf(writer, "    %s	\n", subURL, sub)
	sub.herd.showImage(writer, sub.mdb.RequiredImage)
	sub.herd.showImage(writer, sub.mdb.PlannedImage)
	sub.showBusy(writer)
	fmt.Fprintf(writer, "    %s	\n", sub.status)
	timeNow := time.Now()
	showSince(writer, sub.pollTime, sub.startTime)
	showSince(writer, timeNow, sub.lastPollSucceededTime)
	showSince(writer, timeNow, sub.lastUpdateTime)
	showSince(writer, timeNow, sub.lastSyncTime)
	showDuration(writer, sub.lastConnectDuration)
	showDuration(writer, sub.lastShortPollDuration)
	showDuration(writer, sub.lastFullPollDuration)
	showDuration(writer, sub.lastComputeUpdateCpuDuration)
	fmt.Fprintln(writer, "
")
}

func (herd *Herd) showImage(writer io.Writer, name string) {
	if name == "" {
		fmt.Fprintln(writer, "    	")
	} else if image, err := herd.getImage(name); err != nil {
		fmt.Fprintf(writer, "    %s	\n", err)
	} else if image != nil {
		fmt.Fprintf(writer,
			"    %s	\n",
			herd.imageServerAddress, name, name)
	} else {
		fmt.Fprintf(writer, "    %s	\n",
			name)
	}
}

func (sub *Sub) showBusy(writer io.Writer) {
	if sub.busy {
		if sub.busyStartTime.IsZero() {
			fmt.Fprintln(writer, "    busy	")
		} else {
			fmt.Fprintf(writer, "    %s	\n",
				format.Duration(time.Since(sub.busyStartTime)))
		}
	} else {
		if sub.busyStartTime.IsZero() {
			fmt.Fprintln(writer, "    	")
		} else {
			fmt.Fprintf(writer, "    %s	\n",
				format.Duration(sub.busyStopTime.Sub(sub.busyStartTime)))
		}
	}
}

func showSince(writer io.Writer, now time.Time, since time.Time) {
	if now.IsZero() || since.IsZero() {
		fmt.Fprintln(writer, "    	")
	} else {
		showDuration(writer, now.Sub(since))
	}
}

func showDuration(writer io.Writer, duration time.Duration) {
	if duration < 1 {
		fmt.Fprintf(writer, "    	\n")
	} else {
		fmt.Fprintf(writer, "    %s	\n", format.Duration(duration))
	}
}
<|endoftext|>


<commit_before>/*
 * Minimalist Object Storage, (C) 2015 Minio, Inc.
 *
 * 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 main

import (
	"github.com/minio-io/cli"
)

var makeDonutCmd = cli.Command{
	Name:        "make",
	Usage:       "make",
	Description: "",
	Action:      doMakeDonutCmd,
}

var attachDiskCmd = cli.Command{
	Name:        "attach",
	Usage:       "attach disk",
	Description: "",
	Action:      doAttachDiskCmd,
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "name",
			Usage: "Donut name",
		},
	},
}

var detachDiskCmd = cli.Command{
	Name:        "detach",
	Usage:       "detach disk",
	Description: "",
	Action:      doDetachDiskCmd,
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "name",
			Usage: "Donut name",
		},
	},
}

var healDonutCmd = cli.Command{
	Name:        "heal",
	Usage:       "heal donut",
	Description: "",
	Action:      doHealDonutCmd,
}

var rebalanceDonutCmd = cli.Command{
	Name:        "rebalance",
	Usage:       "rebalance ",
	Description: "",
	Action:      doRebalanceDonutCmd,
}

var cpDonutCmd = cli.Command{
	Name:        "cp",
	Usage:       "cp",
	Description: "",
	Action:      doDonutCPCmd,
}

var mbDonutCmd = cli.Command{
	Name:        "mb",
	Usage:       "mb",
	Description: "",
	Action:      doMakeDonutBucketCmd,
}

var donutOptions = []cli.Command{
	makeDonutCmd,
	attachDiskCmd,
	detachDiskCmd,
	healDonutCmd,
	rebalanceDonutCmd,
	mbDonutCmd,
	cpDonutCmd,
}

func doHealDonutCmd(c *cli.Context) {
}

func doRebalanceDonutCmd(c *cli.Context) {
}
<commit_msg>Add some descriptions on donut commands<commit_after>/*
 * Minimalist Object Storage, (C) 2015 Minio, Inc.
 *
 * 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 main

import (
	"github.com/minio-io/cli"
)

var makeDonutCmd = cli.Command{
	Name:        "make",
	Usage:       "make donut",
	Description: "Make a new donut",
	Action:      doMakeDonutCmd,
}

var attachDiskCmd = cli.Command{
	Name:        "attach",
	Usage:       "attach disk",
	Description: "Attach disk to an existing donut",
	Action:      doAttachDiskCmd,
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "name",
			Usage: "Donut name",
		},
	},
}

var detachDiskCmd = cli.Command{
	Name:        "detach",
	Usage:       "detach disk",
	Description: "Detach disk from an existing donut",
	Action:      doDetachDiskCmd,
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "name",
			Usage: "Donut name",
		},
	},
}

var healDonutCmd = cli.Command{
	Name:        "heal",
	Usage:       "heal donut",
	Description: "Heal donut with any errors",
	Action:      doHealDonutCmd,
}

var rebalanceDonutCmd = cli.Command{
	Name:        "rebalance",
	Usage:       "rebalance donut",
	Description: "Rebalance data on donut after adding disks",
	Action:      doRebalanceDonutCmd,
}

var cpDonutCmd = cli.Command{
	Name:        "cp",
	Usage:       "cp",
	Description: "Copies a local file or dir or object or bucket to another location locally or to Donut or to S3.",
	Action:      doDonutCPCmd,
}

var mbDonutCmd = cli.Command{
	Name:        "mb",
	Usage:       "make bucket",
	Description: "Make a new bucket",
	Action:      doMakeDonutBucketCmd,
}

var donutOptions = []cli.Command{
	makeDonutCmd,
	attachDiskCmd,
	detachDiskCmd,
	healDonutCmd,
	rebalanceDonutCmd,
	mbDonutCmd,
	cpDonutCmd,
}

func doHealDonutCmd(c *cli.Context) {
}

func doRebalanceDonutCmd(c *cli.Context) {
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"syscall"

	"github.com/heroku/heroku-cli/Godeps/_workspace/src/github.com/dickeyxxx/golock"
	"github.com/heroku/heroku-cli/gode"
)

// Plugin represents a javascript plugin
type Plugin struct {
	Name     string     `json:"name"`
	Version  string     `json:"version"`
	Topics   TopicSet   `json:"topics"`
	Topic    *Topic     `json:"topic"`
	Commands CommandSet `json:"commands"`
}

// SetupNode sets up node and npm in ~/.heroku
func SetupNode() {
	gode.SetRootPath(AppDir())
	setup, err := gode.IsSetup()
	PrintError(err, false)
	if !setup {
		setupNode()
	}
}

func setupNode() {
	Err("heroku-cli: Adding dependencies...")
	PrintError(gode.Setup(), true)
	Errln(" done")
}

func updateNode() {
	gode.SetRootPath(AppDir())
	needsUpdate, err := gode.NeedsUpdate()
	PrintError(err, true)
	if needsUpdate {
		setupNode()
	}
}

// LoadPlugins loads the topics and commands from the JavaScript plugins into the CLI
func (cli *Cli) LoadPlugins(plugins map[string]*Plugin) {
	for _, plugin := range plugins {
		for _, topic := range plugin.Topics {
			cli.AddTopic(topic)
		}
		if plugin.Topic != nil {
			cli.AddTopic(plugin.Topic)
		}
		for _, command := range plugin.Commands {
			if !cli.AddCommand(command) {
				Errf("WARNING: command %s has already been defined\n", command)
			}
		}
	}
	sort.Sort(cli.Topics)
	sort.Sort(cli.Commands)
}

var pluginsTopic = &Topic{
	Name:        "plugins",
	Description: "manage plugins",
}

var pluginsInstallCmd = &Command{
	Topic:       "plugins",
	Command:     "install",
	Hidden:      true,
	Args:        []Arg{{Name: "name"}},
	Description: "Installs a plugin into the CLI",
	Help: `Install a Heroku plugin

  Example:
  $ heroku plugins:install dickeyxxx/heroku-production-status`,

	Run: func(ctx *Context) {
		name := ctx.Args.(map[string]string)["name"]
		if len(name) == 0 {
			Errln("Must specify a plugin name")
			return
		}
		Errf("Installing plugin %s... ", name)
		ExitIfError(installPlugins(name), true)
		Errln("done")
	},
}

var pluginsLinkCmd = &Command{
	Topic:       "plugins",
	Command:     "link",
	Description: "Links a local plugin into CLI",
	Args:        []Arg{{Name: "path", Optional: true}},
	Help: `Links a local plugin into CLI.
	This is useful when developing plugins locally.
	It simply symlinks the specified path into ~/.heroku/node_modules

  Example:
	$ heroku plugins:link .`,

	Run: func(ctx *Context) {
		path := ctx.Args.(map[string]string)["path"]
		if path == "" {
			path = "."
		}
		path, err := filepath.Abs(path)
		if err != nil {
			panic(err)
		}
		if _, err := os.Stat(path); err != nil {
			panic(err)
		}
		name := filepath.Base(path)
		newPath := pluginPath(name)
		os.Remove(newPath)
		os.RemoveAll(newPath)
		err = os.Symlink(path, newPath)
		if err != nil {
			panic(err)
		}
		plugin, err := ParsePlugin(name)
		ExitIfError(err, false)
		if name != plugin.Name {
			path = newPath
			newPath = pluginPath(plugin.Name)
			os.Remove(newPath)
			os.RemoveAll(newPath)
			os.Rename(path, newPath)
		}
		Println("Symlinked", plugin.Name)
		AddPluginsToCache(plugin)
	},
}

var pluginsUninstallCmd = &Command{
	Topic:       "plugins",
	Command:     "uninstall",
	Hidden:      true,
	Args:        []Arg{{Name: "name"}},
	Description: "Uninstalls a plugin from the CLI",
	Help: `Uninstalls a Heroku plugin

  Example:
  $ heroku plugins:uninstall heroku-production-status`,

	Run: func(ctx *Context) {
		name := ctx.Args.(map[string]string)["name"]
		Errf("Uninstalling plugin %s... ", name)
		ExitIfError(gode.RemovePackages(name), true)
		RemovePluginFromCache(name)
		Errln("done")
	},
}

var pluginsListCmd = &Command{
	Topic:       "plugins",
	Hidden:      true,
	Description: "Lists installed plugins",
	Help: `
Example:
  $ heroku plugins`,

	Run: func(ctx *Context) {
		SetupBuiltinPlugins()
		var plugins []string
		for _, plugin := range GetPlugins() {
			if plugin != nil && len(plugin.Commands) > 0 {
				symlinked := ""
				if isPluginSymlinked(plugin.Name) {
					symlinked = " (symlinked)"
				}
				plugins = append(plugins, fmt.Sprintf("%s %s %s", plugin.Name, plugin.Version, symlinked))
			}
		}
		sort.Strings(plugins)
		for _, plugin := range plugins {
			Println(plugin)
		}
	},
}

func runFn(plugin *Plugin, topic, command string) func(ctx *Context) {
	return func(ctx *Context) {
		readLockPlugin(plugin.Name)
		ctx.Dev = isPluginSymlinked(plugin.Name)
		ctxJSON, err := json.Marshal(ctx)
		if err != nil {
			panic(err)
		}
		title, _ := json.Marshal(processTitle(ctx))
		script := fmt.Sprintf(`
		'use strict';
		var moduleName = '%s';
		var moduleVersion = '%s';
		var topic = '%s';
		var command = '%s';
		process.title = %s;
		var ctx = %s;
		ctx.version = ctx.version + ' ' + moduleName + '/' + moduleVersion + ' node-' + process.version;
		var logPath = %s;
		process.chdir(ctx.cwd);
		if (!ctx.dev) {
			process.on('uncaughtException', function (err) {
				// ignore EPIPE errors (usually from piping to head)
				if (err.code === "EPIPE") return;
				console.error(' !   Error in ' + moduleName + ':')
				console.error(' !   ' + err.message || err);
				if (err.stack) {
					var fs = require('fs');
					var log = function (line) {
						var d = new Date().toISOString()
						.replace(/T/, ' ')
						.replace(/-/g, '/')
						.replace(/\..+/, '');
						fs.appendFileSync(logPath, d + ' ' + line + '\n');
					}
					log('Error during ' + topic + ':' + command);
					log(err.stack);
					console.error(' !   See ' + logPath + ' for more info.');
				}
				process.exit(1);
			});
		}
		if (command === '') { command = null }
		var module = require(moduleName);
		var cmd = module.commands.filter(function (c) {
			return c.topic === topic && c.command == command;
		})[0];
		cmd.run(ctx);`, plugin.Name, plugin.Version, topic, command, string(title), ctxJSON, strconv.Quote(ErrLogPath))

		// swallow sigint since the plugin will handle it
		swallowSignal(os.Interrupt)

		cmd := gode.RunScript(script)
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		if ctx.Flags["debugger"] == true {
			cmd = gode.DebugScript(script)
		}
		if err := cmd.Run(); err != nil {
			os.Exit(getExitCode(err))
		}
	}
}

func swallowSignal(s os.Signal) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, s)
	go func() {
		<-c
	}()
}

func getExitCode(err error) int {
	switch e := err.(type) {
	case nil:
		return 0
	case *exec.ExitError:
		status, ok := e.Sys().(syscall.WaitStatus)
		if !ok {
			panic(err)
		}
		return status.ExitStatus()
	default:
		panic(err)
	}
}

// ParsePlugin requires the plugin's node module
// to get the commands and metadata
func ParsePlugin(name string) (*Plugin, error) {
	script := `
	var plugin = require('` + name + `');
	if (!plugin.commands) throw new Error('Contains no commands. Is this a real plugin?');
	var pjson  = require('` + name + `/package.json');

	plugin.name    = pjson.name;
	plugin.version = pjson.version;

	console.log(JSON.stringify(plugin))`
	cmd := gode.RunScript(script)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("Error reading plugin: %s\n%s\n%s", name, err, string(output))
	}
	var plugin Plugin
	json.Unmarshal([]byte(output), &plugin)
	for _, command := range plugin.Commands {
		command.Plugin = plugin.Name
		command.Help = strings.TrimSpace(command.Help)
	}
	return &plugin, nil
}

// GetPlugins goes through all the node plugins and returns them in Go stucts
func GetPlugins() map[string]*Plugin {
	plugins := FetchPluginCache()
	for name, plugin := range plugins {
		if plugin == nil || !pluginExists(name) {
			delete(plugins, name)
		} else {
			for _, command := range plugin.Commands {
				command.Run = runFn(plugin, command.Topic, command.Command)
			}
		}
	}
	return plugins
}

// PluginNames lists all the plugin names
func PluginNames() []string {
	plugins := FetchPluginCache()
	names := make([]string, 0, len(plugins))
	for _, plugin := range plugins {
		if plugin != nil {
			names = append(names, plugin.Name)
		}
	}
	return names
}

// PluginNamesNotSymlinked returns all the plugins that are not symlinked
func PluginNamesNotSymlinked() []string {
	a := PluginNames()
	b := make([]string, 0, len(a))
	for _, plugin := range a {
		if !isPluginSymlinked(plugin) {
			b = append(b, plugin)
		}
	}
	return b
}

func isPluginSymlinked(plugin string) bool {
	path := filepath.Join(AppDir(), "node_modules", plugin)
	fi, err := os.Lstat(path)
	if err != nil {
		return false
	}
	return fi.Mode()&os.ModeSymlink != 0
}

// SetupBuiltinPlugins ensures all the builtinPlugins are installed
func SetupBuiltinPlugins() {
	pluginNames := difference(BuiltinPlugins, PluginNames())
	if len(pluginNames) == 0 {
		return
	}
	Err("heroku-cli: Installing core plugins...")
	if err := installPlugins(pluginNames...); err != nil {
		// retry once
		PrintError(gode.RemovePackages(pluginNames...), true)
		PrintError(gode.ClearCache(), true)
		Err("\rheroku-cli: Installing core plugins (retrying)...")
		ExitIfError(installPlugins(pluginNames...), true)
	}
	Errln(" done")
}

func difference(a, b []string) []string {
	res := make([]string, 0, len(a))
	for _, aa := range a {
		if !contains(b, aa) {
			res = append(res, aa)
		}
	}
	return res
}

func contains(arr []string, s string) bool {
	for _, a := range arr {
		if a == s {
			return true
		}
	}
	return false
}

func installPlugins(names ...string) error {
	for _, name := range names {
		lockPlugin(name)
	}
	defer func() {
		for _, name := range names {
			unlockPlugin(name)
		}
	}()
	err := gode.InstallPackages(names...)
	if err != nil {
		return err
	}
	plugins := make([]*Plugin, 0, len(names))
	for _, name := range names {
		plugin, err := ParsePlugin(name)
		if err != nil {
			return err
		}
		plugins = append(plugins, plugin)
	}
	AddPluginsToCache(plugins...)
	return nil
}

func pluginExists(plugin string) bool {
	exists, _ := fileExists(pluginPath(plugin))
	return exists
}

// directory location of plugin
func pluginPath(plugin string) string {
	return filepath.Join(AppDir(), "node_modules", plugin)
}

// lock a plugin for reading
func readLockPlugin(name string) {
	lockfile := updateLockPath + "." + name
	if exists, _ := fileExists(lockfile); exists {
		lockPlugin(name)
		unlockPlugin(name)
	}
}

// lock a plugin for writing
func lockPlugin(name string) {
	LogIfError(golock.Lock(updateLockPath + "." + name))
}

// unlock a plugin
func unlockPlugin(name string) {
	LogIfError(golock.Unlock(updateLockPath + "." + name))
}
<commit_msg>make sure plugins have commands<commit_after>package main

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"syscall"

	"github.com/heroku/heroku-cli/Godeps/_workspace/src/github.com/dickeyxxx/golock"
	"github.com/heroku/heroku-cli/gode"
)

// Plugin represents a javascript plugin
type Plugin struct {
	Name     string     `json:"name"`
	Version  string     `json:"version"`
	Topics   TopicSet   `json:"topics"`
	Topic    *Topic     `json:"topic"`
	Commands CommandSet `json:"commands"`
}

// SetupNode sets up node and npm in ~/.heroku
func SetupNode() {
	gode.SetRootPath(AppDir())
	setup, err := gode.IsSetup()
	PrintError(err, false)
	if !setup {
		setupNode()
	}
}

func setupNode() {
	Err("heroku-cli: Adding dependencies...")
	PrintError(gode.Setup(), true)
	Errln(" done")
}

func updateNode() {
	gode.SetRootPath(AppDir())
	needsUpdate, err := gode.NeedsUpdate()
	PrintError(err, true)
	if needsUpdate {
		setupNode()
	}
}

// LoadPlugins loads the topics and commands from the JavaScript plugins into the CLI
func (cli *Cli) LoadPlugins(plugins map[string]*Plugin) {
	for _, plugin := range plugins {
		for _, topic := range plugin.Topics {
			cli.AddTopic(topic)
		}
		if plugin.Topic != nil {
			cli.AddTopic(plugin.Topic)
		}
		for _, command := range plugin.Commands {
			if !cli.AddCommand(command) {
				Errf("WARNING: command %s has already been defined\n", command)
			}
		}
	}
	sort.Sort(cli.Topics)
	sort.Sort(cli.Commands)
}

var pluginsTopic = &Topic{
	Name:        "plugins",
	Description: "manage plugins",
}

var pluginsInstallCmd = &Command{
	Topic:       "plugins",
	Command:     "install",
	Hidden:      true,
	Args:        []Arg{{Name: "name"}},
	Description: "Installs a plugin into the CLI",
	Help: `Install a Heroku plugin

  Example:
  $ heroku plugins:install dickeyxxx/heroku-production-status`,

	Run: func(ctx *Context) {
		name := ctx.Args.(map[string]string)["name"]
		if len(name) == 0 {
			Errln("Must specify a plugin name")
			return
		}
		Errf("Installing plugin %s... ", name)
		ExitIfError(installPlugins(name), true)
		Errln("done")
	},
}

var pluginsLinkCmd = &Command{
	Topic:       "plugins",
	Command:     "link",
	Description: "Links a local plugin into CLI",
	Args:        []Arg{{Name: "path", Optional: true}},
	Help: `Links a local plugin into CLI.
	This is useful when developing plugins locally.
	It simply symlinks the specified path into ~/.heroku/node_modules

  Example:
	$ heroku plugins:link .`,

	Run: func(ctx *Context) {
		path := ctx.Args.(map[string]string)["path"]
		if path == "" {
			path = "."
		}
		path, err := filepath.Abs(path)
		if err != nil {
			panic(err)
		}
		if _, err := os.Stat(path); err != nil {
			panic(err)
		}
		name := filepath.Base(path)
		newPath := pluginPath(name)
		os.Remove(newPath)
		os.RemoveAll(newPath)
		err = os.Symlink(path, newPath)
		if err != nil {
			panic(err)
		}
		plugin, err := ParsePlugin(name)
		ExitIfError(err, false)
		if name != plugin.Name {
			path = newPath
			newPath = pluginPath(plugin.Name)
			os.Remove(newPath)
			os.RemoveAll(newPath)
			os.Rename(path, newPath)
		}
		Println("Symlinked", plugin.Name)
		AddPluginsToCache(plugin)
	},
}

var pluginsUninstallCmd = &Command{
	Topic:       "plugins",
	Command:     "uninstall",
	Hidden:      true,
	Args:        []Arg{{Name: "name"}},
	Description: "Uninstalls a plugin from the CLI",
	Help: `Uninstalls a Heroku plugin

  Example:
  $ heroku plugins:uninstall heroku-production-status`,

	Run: func(ctx *Context) {
		name := ctx.Args.(map[string]string)["name"]
		Errf("Uninstalling plugin %s... ", name)
		ExitIfError(gode.RemovePackages(name), true)
		RemovePluginFromCache(name)
		Errln("done")
	},
}

var pluginsListCmd = &Command{
	Topic:       "plugins",
	Hidden:      true,
	Description: "Lists installed plugins",
	Help: `
Example:
  $ heroku plugins`,

	Run: func(ctx *Context) {
		SetupBuiltinPlugins()
		var plugins []string
		for _, plugin := range GetPlugins() {
			if plugin != nil && len(plugin.Commands) > 0 {
				symlinked := ""
				if isPluginSymlinked(plugin.Name) {
					symlinked = " (symlinked)"
				}
				plugins = append(plugins, fmt.Sprintf("%s %s %s", plugin.Name, plugin.Version, symlinked))
			}
		}
		sort.Strings(plugins)
		for _, plugin := range plugins {
			Println(plugin)
		}
	},
}

func runFn(plugin *Plugin, topic, command string) func(ctx *Context) {
	return func(ctx *Context) {
		readLockPlugin(plugin.Name)
		ctx.Dev = isPluginSymlinked(plugin.Name)
		ctxJSON, err := json.Marshal(ctx)
		if err != nil {
			panic(err)
		}
		title, _ := json.Marshal(processTitle(ctx))
		script := fmt.Sprintf(`
		'use strict';
		var moduleName = '%s';
		var moduleVersion = '%s';
		var topic = '%s';
		var command = '%s';
		process.title = %s;
		var ctx = %s;
		ctx.version = ctx.version + ' ' + moduleName + '/' + moduleVersion + ' node-' + process.version;
		var logPath = %s;
		process.chdir(ctx.cwd);
		if (!ctx.dev) {
			process.on('uncaughtException', function (err) {
				// ignore EPIPE errors (usually from piping to head)
				if (err.code === "EPIPE") return;
				console.error(' !   Error in ' + moduleName + ':')
				console.error(' !   ' + err.message || err);
				if (err.stack) {
					var fs = require('fs');
					var log = function (line) {
						var d = new Date().toISOString()
						.replace(/T/, ' ')
						.replace(/-/g, '/')
						.replace(/\..+/, '');
						fs.appendFileSync(logPath, d + ' ' + line + '\n');
					}
					log('Error during ' + topic + ':' + command);
					log(err.stack);
					console.error(' !   See ' + logPath + ' for more info.');
				}
				process.exit(1);
			});
		}
		if (command === '') { command = null }
		var module = require(moduleName);
		var cmd = module.commands.filter(function (c) {
			return c.topic === topic && c.command == command;
		})[0];
		cmd.run(ctx);`, plugin.Name, plugin.Version, topic, command, string(title), ctxJSON, strconv.Quote(ErrLogPath))

		// swallow sigint since the plugin will handle it
		swallowSignal(os.Interrupt)

		cmd := gode.RunScript(script)
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		if ctx.Flags["debugger"] == true {
			cmd = gode.DebugScript(script)
		}
		if err := cmd.Run(); err != nil {
			os.Exit(getExitCode(err))
		}
	}
}

func swallowSignal(s os.Signal) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, s)
	go func() {
		<-c
	}()
}

func getExitCode(err error) int {
	switch e := err.(type) {
	case nil:
		return 0
	case *exec.ExitError:
		status, ok := e.Sys().(syscall.WaitStatus)
		if !ok {
			panic(err)
		}
		return status.ExitStatus()
	default:
		panic(err)
	}
}

// ParsePlugin requires the plugin's node module
// to get the commands and metadata
func ParsePlugin(name string) (*Plugin, error) {
	script := `
	var plugin = require('` + name + `');
	if (!plugin.commands) throw new Error('Contains no commands. Is this a real plugin?');
	var pjson  = require('` + name + `/package.json');

	plugin.name    = pjson.name;
	plugin.version = pjson.version;

	console.log(JSON.stringify(plugin))`
	cmd := gode.RunScript(script)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("Error reading plugin: %s\n%s\n%s", name, err, string(output))
	}
	var plugin Plugin
	json.Unmarshal([]byte(output), &plugin)
	for _, command := range plugin.Commands {
		command.Plugin = plugin.Name
		command.Help = strings.TrimSpace(command.Help)
	}
	return &plugin, nil
}

// GetPlugins goes through all the node plugins and returns them in Go stucts
func GetPlugins() map[string]*Plugin {
	plugins := FetchPluginCache()
	for name, plugin := range plugins {
		if plugin == nil || !pluginExists(name) {
			delete(plugins, name)
		} else {
			for _, command := range plugin.Commands {
				command.Run = runFn(plugin, command.Topic, command.Command)
			}
		}
	}
	return plugins
}

// PluginNames lists all the plugin names
func PluginNames() []string {
	plugins := FetchPluginCache()
	names := make([]string, 0, len(plugins))
	for _, plugin := range plugins {
		if plugin != nil && len(plugin.Commands) > 0 {
			names = append(names, plugin.Name)
		}
	}
	return names
}

// PluginNamesNotSymlinked returns all the plugins that are not symlinked
func PluginNamesNotSymlinked() []string {
	a := PluginNames()
	b := make([]string, 0, len(a))
	for _, plugin := range a {
		if !isPluginSymlinked(plugin) {
			b = append(b, plugin)
		}
	}
	return b
}

func isPluginSymlinked(plugin string) bool {
	path := filepath.Join(AppDir(), "node_modules", plugin)
	fi, err := os.Lstat(path)
	if err != nil {
		return false
	}
	return fi.Mode()&os.ModeSymlink != 0
}

// SetupBuiltinPlugins ensures all the builtinPlugins are installed
func SetupBuiltinPlugins() {
	pluginNames := difference(BuiltinPlugins, PluginNames())
	if len(pluginNames) == 0 {
		return
	}
	Err("heroku-cli: Installing core plugins...")
	if err := installPlugins(pluginNames...); err != nil {
		// retry once
		PrintError(gode.RemovePackages(pluginNames...), true)
		PrintError(gode.ClearCache(), true)
		Err("\rheroku-cli: Installing core plugins (retrying)...")
		ExitIfError(installPlugins(pluginNames...), true)
	}
	Errln(" done")
}

func difference(a, b []string) []string {
	res := make([]string, 0, len(a))
	for _, aa := range a {
		if !contains(b, aa) {
			res = append(res, aa)
		}
	}
	return res
}

func contains(arr []string, s string) bool {
	for _, a := range arr {
		if a == s {
			return true
		}
	}
	return false
}

func installPlugins(names ...string) error {
	for _, name := range names {
		lockPlugin(name)
	}
	defer func() {
		for _, name := range names {
			unlockPlugin(name)
		}
	}()
	err := gode.InstallPackages(names...)
	if err != nil {
		return err
	}
	plugins := make([]*Plugin, 0, len(names))
	for _, name := range names {
		plugin, err := ParsePlugin(name)
		if err != nil {
			return err
		}
		plugins = append(plugins, plugin)
	}
	AddPluginsToCache(plugins...)
	return nil
}

func pluginExists(plugin string) bool {
	exists, _ := fileExists(pluginPath(plugin))
	return exists
}

// directory location of plugin
func pluginPath(plugin string) string {
	return filepath.Join(AppDir(), "node_modules", plugin)
}

// lock a plugin for reading
func readLockPlugin(name string) {
	lockfile := updateLockPath + "." + name
	if exists, _ := fileExists(lockfile); exists {
		lockPlugin(name)
		unlockPlugin(name)
	}
}

// lock a plugin for writing
func lockPlugin(name string) {
	LogIfError(golock.Lock(updateLockPath + "." + name))
}

// unlock a plugin
func unlockPlugin(name string) {
	LogIfError(golock.Unlock(updateLockPath + "." + name))
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"syscall"

	"github.com/dickeyxxx/golock"
	"github.com/heroku/heroku-cli/gode"
)

// Plugin represents a javascript plugin
type Plugin struct {
	Name     string     `json:"name"`
	Version  string     `json:"version"`
	Topics   TopicSet   `json:"topics"`
	Topic    *Topic     `json:"topic"`
	Commands CommandSet `json:"commands"`
}

// SetupNode sets up node and npm in ~/.heroku
func SetupNode() {
	gode.SetRootPath(AppDir())
	setup, err := gode.IsSetup()
	PrintError(err, false)
	if !setup {
		PrintError(gode.Setup(), true)
	}
}

// LoadPlugins loads the topics and commands from the JavaScript plugins into the CLI
func (cli *Cli) LoadPlugins(plugins map[string]*Plugin) {
	for _, plugin := range plugins {
		for _, topic := range plugin.Topics {
			cli.AddTopic(topic)
		}
		if plugin.Topic != nil {
			cli.AddTopic(plugin.Topic)
		}
		for _, command := range plugin.Commands {
			if !cli.AddCommand(command) {
				Errf("WARNING: command %s has already been defined\n", command)
			}
		}
	}
	sort.Sort(cli.Topics)
	sort.Sort(cli.Commands)
}

var pluginsTopic = &Topic{
	Name:        "plugins",
	Description: "manage plugins",
}

var pluginsInstallCmd = &Command{
	Topic:       "plugins",
	Command:     "install",
	Hidden:      true,
	Args:        []Arg{{Name: "name"}},
	Description: "Installs a plugin into the CLI",
	Help: `Install a Heroku plugin

  Example:
  $ heroku plugins:install dickeyxxx/heroku-production-status`,

	Run: func(ctx *Context) {
		name := ctx.Args.(map[string]string)["name"]
		if len(name) == 0 {
			Errln("Must specify a plugin name")
			return
		}
		Errf("Installing plugin %s...", name)
		ExitIfError(installPlugins(name), true)
		Errln(" done")
	},
}

var pluginsLinkCmd = &Command{
	Topic:       "plugins",
	Command:     "link",
	Description: "Links a local plugin into CLI",
	Args:        []Arg{{Name: "path", Optional: true}},
	Help: `Links a local plugin into CLI.
	This is useful when developing plugins locally.
	It simply symlinks the specified path into ~/.heroku/node_modules

  Example:
	$ heroku plugins:link .`,

	Run: func(ctx *Context) {
		path := ctx.Args.(map[string]string)["path"]
		if path == "" {
			path = "."
		}
		path, err := filepath.Abs(path)
		if err != nil {
			panic(err)
		}
		if _, err := os.Stat(path); err != nil {
			panic(err)
		}
		name := filepath.Base(path)
		newPath := pluginPath(name)
		os.Remove(newPath)
		os.RemoveAll(newPath)
		err = os.Symlink(path, newPath)
		if err != nil {
			panic(err)
		}
		plugin, err := ParsePlugin(name)
		ExitIfError(err, false)
		if name != plugin.Name {
			path = newPath
			newPath = pluginPath(plugin.Name)
			os.Remove(newPath)
			os.RemoveAll(newPath)
			os.Rename(path, newPath)
		}
		Println("Symlinked", plugin.Name)
		AddPluginsToCache(plugin)
	},
}

var pluginsUninstallCmd = &Command{
	Topic:       "plugins",
	Command:     "uninstall",
	Hidden:      true,
	Args:        []Arg{{Name: "name"}},
	Description: "Uninstalls a plugin from the CLI",
	Help: `Uninstalls a Heroku plugin

  Example:
  $ heroku plugins:uninstall heroku-production-status`,

	Run: func(ctx *Context) {
		name := ctx.Args.(map[string]string)["name"]
		if !contains(PluginNames(), name) {
			ExitIfError(errors.New(name+" is not installed"), false)
		}
		Errf("Uninstalling plugin %s...", name)
		ExitIfError(gode.RemovePackages(name), true)
		RemovePluginFromCache(name)
		Errln(" done")
	},
}

var pluginsListCmd = &Command{
	Topic:       "plugins",
	Hidden:      true,
	Description: "Lists installed plugins",
	Help: `
Example:
  $ heroku plugins`,

	Run: func(ctx *Context) {
		SetupBuiltinPlugins()
		var plugins []string
		for _, plugin := range GetPlugins() {
			if plugin != nil && len(plugin.Commands) > 0 {
				symlinked := ""
				if isPluginSymlinked(plugin.Name) {
					symlinked = " (symlinked)"
				}
				plugins = append(plugins, fmt.Sprintf("%s %s %s", plugin.Name, plugin.Version, symlinked))
			}
		}
		sort.Strings(plugins)
		for _, plugin := range plugins {
			Println(plugin)
		}
	},
}

func runFn(plugin *Plugin, topic, command string) func(ctx *Context) {
	return func(ctx *Context) {
		readLockPlugin(plugin.Name)
		ctx.Dev = isPluginSymlinked(plugin.Name)
		ctxJSON, err := json.Marshal(ctx)
		if err != nil {
			panic(err)
		}
		title, _ := json.Marshal(processTitle(ctx))
		script := fmt.Sprintf(`
		'use strict';
		var moduleName = '%s';
		var moduleVersion = '%s';
		var topic = '%s';
		var command = '%s';
		process.title = %s;
		var ctx = %s;
		ctx.version = ctx.version + ' ' + moduleName + '/' + moduleVersion + ' node-' + process.version;
		var logPath = %s;
		process.chdir(ctx.cwd);
		if (!ctx.dev) {
			process.on('uncaughtException', function (err) {
				// ignore EPIPE errors (usually from piping to head)
				if (err.code === "EPIPE") return;
				console.error(' !   Error in ' + moduleName + ':')
				console.error(' !   ' + err.message || err);
				if (err.stack) {
					var fs = require('fs');
					var log = function (line) {
						var d = new Date().toISOString()
						.replace(/T/, ' ')
						.replace(/-/g, '/')
						.replace(/\..+/, '');
						fs.appendFileSync(logPath, d + ' ' + line + '\n');
					}
					log('Error during ' + topic + ':' + command);
					log(err.stack);
					console.error(' !   See ' + logPath + ' for more info.');
				}
				process.exit(1);
			});
		}
		if (command === '') { command = null }
		var module = require(moduleName);
		var cmd = module.commands.filter(function (c) {
			return c.topic === topic && c.command == command;
		})[0];
		cmd.run(ctx);`, plugin.Name, plugin.Version, topic, command, string(title), ctxJSON, strconv.Quote(ErrLogPath))

		// swallow sigint since the plugin will handle it
		swallowSignal(os.Interrupt)

		cmd := gode.RunScript(script)
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		if ctx.Flags["debugger"] == true {
			cmd = gode.DebugScript(script)
		}
		if err := cmd.Run(); err != nil {
			os.Exit(getExitCode(err))
		}
	}
}

func swallowSignal(s os.Signal) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, s)
	go func() {
		<-c
	}()
}

func getExitCode(err error) int {
	switch e := err.(type) {
	case nil:
		return 0
	case *exec.ExitError:
		status, ok := e.Sys().(syscall.WaitStatus)
		if !ok {
			panic(err)
		}
		return status.ExitStatus()
	default:
		panic(err)
	}
}

// ParsePlugin requires the plugin's node module
// to get the commands and metadata
func ParsePlugin(name string) (*Plugin, error) {
	script := `
	var plugin = require('` + name + `');
	if (!plugin.commands) throw new Error('Contains no commands. Is this a real plugin?');
	var pjson  = require('` + name + `/package.json');

	plugin.name    = pjson.name;
	plugin.version = pjson.version;

	console.log(JSON.stringify(plugin))`
	cmd := gode.RunScript(script)
	cmd.Stderr = Stderr
	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("Error reading plugin: %s\n%s", name, err)
	}
	var plugin Plugin
	err = json.Unmarshal([]byte(output), &plugin)
	if err != nil {
		return nil, fmt.Errorf("Error parsing plugin: %s\n%s\n%s", name, err, string(output))
	}
	for _, command := range plugin.Commands {
		command.Plugin = plugin.Name
		command.Help = strings.TrimSpace(command.Help)
	}
	return &plugin, nil
}

// GetPlugins goes through all the node plugins and returns them in Go stucts
func GetPlugins() map[string]*Plugin {
	plugins := FetchPluginCache()
	for name, plugin := range plugins {
		if plugin == nil || !pluginExists(name) {
			delete(plugins, name)
		} else {
			for _, command := range plugin.Commands {
				command.Run = runFn(plugin, command.Topic, command.Command)
			}
		}
	}
	return plugins
}

// PluginNames lists all the plugin names
func PluginNames() []string {
	plugins := FetchPluginCache()
	names := make([]string, 0, len(plugins))
	for _, plugin := range plugins {
		if plugin != nil && pluginExists(plugin.Name) && len(plugin.Commands) > 0 {
			names = append(names, plugin.Name)
		}
	}
	return names
}

// PluginNamesNotSymlinked returns all the plugins that are not symlinked
func PluginNamesNotSymlinked() []string {
	a := PluginNames()
	b := make([]string, 0, len(a))
	for _, plugin := range a {
		if !isPluginSymlinked(plugin) {
			b = append(b, plugin)
		}
	}
	return b
}

func isPluginSymlinked(plugin string) bool {
	path := filepath.Join(AppDir(), "node_modules", plugin)
	fi, err := os.Lstat(path)
	if err != nil {
		return false
	}
	return fi.Mode()&os.ModeSymlink != 0
}

// SetupBuiltinPlugins ensures all the builtinPlugins are installed
func SetupBuiltinPlugins() {
	pluginNames := difference(BuiltinPlugins, PluginNames())
	if len(pluginNames) == 0 {
		return
	}
	Err("heroku-cli: Installing core plugins...")
	if err := installPlugins(pluginNames...); err != nil {
		// retry once
		PrintError(gode.RemovePackages(pluginNames...), true)
		PrintError(gode.ClearCache(), true)
		Err("\rheroku-cli: Installing core plugins (retrying)...")
		ExitIfError(installPlugins(pluginNames...), true)
	}
	Errln(" done")
}

func difference(a, b []string) []string {
	res := make([]string, 0, len(a))
	for _, aa := range a {
		if !contains(b, aa) {
			res = append(res, aa)
		}
	}
	return res
}

func contains(arr []string, s string) bool {
	for _, a := range arr {
		if a == s {
			return true
		}
	}
	return false
}

func installPlugins(names ...string) error {
	for _, name := range names {
		lockPlugin(name)
	}
	defer func() {
		for _, name := range names {
			unlockPlugin(name)
		}
	}()
	err := gode.InstallPackages(names...)
	if err != nil {
		return err
	}
	plugins := make([]*Plugin, 0, len(names))
	for _, name := range names {
		plugin, err := ParsePlugin(name)
		if err != nil {
			return err
		}
		plugins = append(plugins, plugin)
	}
	AddPluginsToCache(plugins...)
	return nil
}

func pluginExists(plugin string) bool {
	exists, _ := fileExists(pluginPath(plugin))
	return exists
}

// directory location of plugin
func pluginPath(plugin string) string {
	return filepath.Join(AppDir(), "node_modules", plugin)
}

// lock a plugin for reading
func readLockPlugin(name string) {
	lockfile := updateLockPath + "." + name
	if exists, _ := fileExists(lockfile); exists {
		lockPlugin(name)
		unlockPlugin(name)
	}
}

// lock a plugin for writing
func lockPlugin(name string) {
	LogIfError(golock.Lock(updateLockPath + "." + name))
}

// unlock a plugin
func unlockPlugin(name string) {
	LogIfError(golock.Unlock(updateLockPath + "." + name))
}
<commit_msg>skip empty commands<commit_after>package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"syscall"

	"github.com/dickeyxxx/golock"
	"github.com/heroku/heroku-cli/gode"
)

// Plugin represents a javascript plugin
type Plugin struct {
	Name     string     `json:"name"`
	Version  string     `json:"version"`
	Topics   TopicSet   `json:"topics"`
	Topic    *Topic     `json:"topic"`
	Commands CommandSet `json:"commands"`
}

// SetupNode sets up node and npm in ~/.heroku
func SetupNode() {
	gode.SetRootPath(AppDir())
	setup, err := gode.IsSetup()
	PrintError(err, false)
	if !setup {
		PrintError(gode.Setup(), true)
	}
}

// LoadPlugins loads the topics and commands from the JavaScript plugins into the CLI
func (cli *Cli) LoadPlugins(plugins map[string]*Plugin) {
	for _, plugin := range plugins {
		for _, topic := range plugin.Topics {
			cli.AddTopic(topic)
		}
		if plugin.Topic != nil {
			cli.AddTopic(plugin.Topic)
		}
		for _, command := range plugin.Commands {
			if !cli.AddCommand(command) {
				Errf("WARNING: command %s has already been defined\n", command)
			}
		}
	}
	sort.Sort(cli.Topics)
	sort.Sort(cli.Commands)
}

var pluginsTopic = &Topic{
	Name:        "plugins",
	Description: "manage plugins",
}

var pluginsInstallCmd = &Command{
	Topic:       "plugins",
	Command:     "install",
	Hidden:      true,
	Args:        []Arg{{Name: "name"}},
	Description: "Installs a plugin into the CLI",
	Help: `Install a Heroku plugin

  Example:
  $ heroku plugins:install dickeyxxx/heroku-production-status`,

	Run: func(ctx *Context) {
		name := ctx.Args.(map[string]string)["name"]
		if len(name) == 0 {
			Errln("Must specify a plugin name")
			return
		}
		Errf("Installing plugin %s...", name)
		ExitIfError(installPlugins(name), true)
		Errln(" done")
	},
}

var pluginsLinkCmd = &Command{
	Topic:       "plugins",
	Command:     "link",
	Description: "Links a local plugin into CLI",
	Args:        []Arg{{Name: "path", Optional: true}},
	Help: `Links a local plugin into CLI.
	This is useful when developing plugins locally.
	It simply symlinks the specified path into ~/.heroku/node_modules

  Example:
	$ heroku plugins:link .`,

	Run: func(ctx *Context) {
		path := ctx.Args.(map[string]string)["path"]
		if path == "" {
			path = "."
		}
		path, err := filepath.Abs(path)
		if err != nil {
			panic(err)
		}
		if _, err := os.Stat(path); err != nil {
			panic(err)
		}
		name := filepath.Base(path)
		newPath := pluginPath(name)
		os.Remove(newPath)
		os.RemoveAll(newPath)
		err = os.Symlink(path, newPath)
		if err != nil {
			panic(err)
		}
		plugin, err := ParsePlugin(name)
		ExitIfError(err, false)
		if name != plugin.Name {
			path = newPath
			newPath = pluginPath(plugin.Name)
			os.Remove(newPath)
			os.RemoveAll(newPath)
			os.Rename(path, newPath)
		}
		Println("Symlinked", plugin.Name)
		AddPluginsToCache(plugin)
	},
}

var pluginsUninstallCmd = &Command{
	Topic:       "plugins",
	Command:     "uninstall",
	Hidden:      true,
	Args:        []Arg{{Name: "name"}},
	Description: "Uninstalls a plugin from the CLI",
	Help: `Uninstalls a Heroku plugin

  Example:
  $ heroku plugins:uninstall heroku-production-status`,

	Run: func(ctx *Context) {
		name := ctx.Args.(map[string]string)["name"]
		if !contains(PluginNames(), name) {
			ExitIfError(errors.New(name+" is not installed"), false)
		}
		Errf("Uninstalling plugin %s...", name)
		ExitIfError(gode.RemovePackages(name), true)
		RemovePluginFromCache(name)
		Errln(" done")
	},
}

var pluginsListCmd = &Command{
	Topic:       "plugins",
	Hidden:      true,
	Description: "Lists installed plugins",
	Help: `
Example:
  $ heroku plugins`,

	Run: func(ctx *Context) {
		SetupBuiltinPlugins()
		var plugins []string
		for _, plugin := range GetPlugins() {
			if plugin != nil && len(plugin.Commands) > 0 {
				symlinked := ""
				if isPluginSymlinked(plugin.Name) {
					symlinked = " (symlinked)"
				}
				plugins = append(plugins, fmt.Sprintf("%s %s %s", plugin.Name, plugin.Version, symlinked))
			}
		}
		sort.Strings(plugins)
		for _, plugin := range plugins {
			Println(plugin)
		}
	},
}

func runFn(plugin *Plugin, topic, command string) func(ctx *Context) {
	return func(ctx *Context) {
		readLockPlugin(plugin.Name)
		ctx.Dev = isPluginSymlinked(plugin.Name)
		ctxJSON, err := json.Marshal(ctx)
		if err != nil {
			panic(err)
		}
		title, _ := json.Marshal(processTitle(ctx))
		script := fmt.Sprintf(`
		'use strict';
		var moduleName = '%s';
		var moduleVersion = '%s';
		var topic = '%s';
		var command = '%s';
		process.title = %s;
		var ctx = %s;
		ctx.version = ctx.version + ' ' + moduleName + '/' + moduleVersion + ' node-' + process.version;
		var logPath = %s;
		process.chdir(ctx.cwd);
		if (!ctx.dev) {
			process.on('uncaughtException', function (err) {
				// ignore EPIPE errors (usually from piping to head)
				if (err.code === "EPIPE") return;
				console.error(' !   Error in ' + moduleName + ':')
				console.error(' !   ' + err.message || err);
				if (err.stack) {
					var fs = require('fs');
					var log = function (line) {
						var d = new Date().toISOString()
						.replace(/T/, ' ')
						.replace(/-/g, '/')
						.replace(/\..+/, '');
						fs.appendFileSync(logPath, d + ' ' + line + '\n');
					}
					log('Error during ' + topic + ':' + command);
					log(err.stack);
					console.error(' !   See ' + logPath + ' for more info.');
				}
				process.exit(1);
			});
		}
		if (command === '') { command = null }
		var module = require(moduleName);
		var cmd = module.commands.filter(function (c) {
			return c.topic === topic && c.command == command;
		})[0];
		cmd.run(ctx);`, plugin.Name, plugin.Version, topic, command, string(title), ctxJSON, strconv.Quote(ErrLogPath))

		// swallow sigint since the plugin will handle it
		swallowSignal(os.Interrupt)

		cmd := gode.RunScript(script)
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		if ctx.Flags["debugger"] == true {
			cmd = gode.DebugScript(script)
		}
		if err := cmd.Run(); err != nil {
			os.Exit(getExitCode(err))
		}
	}
}

func swallowSignal(s os.Signal) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, s)
	go func() {
		<-c
	}()
}

func getExitCode(err error) int {
	switch e := err.(type) {
	case nil:
		return 0
	case *exec.ExitError:
		status, ok := e.Sys().(syscall.WaitStatus)
		if !ok {
			panic(err)
		}
		return status.ExitStatus()
	default:
		panic(err)
	}
}

// ParsePlugin requires the plugin's node module
// to get the commands and metadata
func ParsePlugin(name string) (*Plugin, error) {
	script := `
	var plugin = require('` + name + `');
	if (!plugin.commands) throw new Error('Contains no commands. Is this a real plugin?');
	var pjson  = require('` + name + `/package.json');

	plugin.name    = pjson.name;
	plugin.version = pjson.version;

	console.log(JSON.stringify(plugin))`
	cmd := gode.RunScript(script)
	cmd.Stderr = Stderr
	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("Error reading plugin: %s\n%s", name, err)
	}
	var plugin Plugin
	err = json.Unmarshal([]byte(output), &plugin)
	if err != nil {
		return nil, fmt.Errorf("Error parsing plugin: %s\n%s\n%s", name, err, string(output))
	}
	for _, command := range plugin.Commands {
		if command == nil {
			continue
		}
		command.Plugin = plugin.Name
		command.Help = strings.TrimSpace(command.Help)
	}
	return &plugin, nil
}

// GetPlugins goes through all the node plugins and returns them in Go stucts
func GetPlugins() map[string]*Plugin {
	plugins := FetchPluginCache()
	for name, plugin := range plugins {
		if plugin == nil || !pluginExists(name) {
			delete(plugins, name)
		} else {
			for _, command := range plugin.Commands {
				command.Run = runFn(plugin, command.Topic, command.Command)
			}
		}
	}
	return plugins
}

// PluginNames lists all the plugin names
func PluginNames() []string {
	plugins := FetchPluginCache()
	names := make([]string, 0, len(plugins))
	for _, plugin := range plugins {
		if plugin != nil && pluginExists(plugin.Name) && len(plugin.Commands) > 0 {
			names = append(names, plugin.Name)
		}
	}
	return names
}

// PluginNamesNotSymlinked returns all the plugins that are not symlinked
func PluginNamesNotSymlinked() []string {
	a := PluginNames()
	b := make([]string, 0, len(a))
	for _, plugin := range a {
		if !isPluginSymlinked(plugin) {
			b = append(b, plugin)
		}
	}
	return b
}

func isPluginSymlinked(plugin string) bool {
	path := filepath.Join(AppDir(), "node_modules", plugin)
	fi, err := os.Lstat(path)
	if err != nil {
		return false
	}
	return fi.Mode()&os.ModeSymlink != 0
}

// SetupBuiltinPlugins ensures all the builtinPlugins are installed
func SetupBuiltinPlugins() {
	pluginNames := difference(BuiltinPlugins, PluginNames())
	if len(pluginNames) == 0 {
		return
	}
	Err("heroku-cli: Installing core plugins...")
	if err := installPlugins(pluginNames...); err != nil {
		// retry once
		PrintError(gode.RemovePackages(pluginNames...), true)
		PrintError(gode.ClearCache(), true)
		Err("\rheroku-cli: Installing core plugins (retrying)...")
		ExitIfError(installPlugins(pluginNames...), true)
	}
	Errln(" done")
}

func difference(a, b []string) []string {
	res := make([]string, 0, len(a))
	for _, aa := range a {
		if !contains(b, aa) {
			res = append(res, aa)
		}
	}
	return res
}

func contains(arr []string, s string) bool {
	for _, a := range arr {
		if a == s {
			return true
		}
	}
	return false
}

func installPlugins(names ...string) error {
	for _, name := range names {
		lockPlugin(name)
	}
	defer func() {
		for _, name := range names {
			unlockPlugin(name)
		}
	}()
	err := gode.InstallPackages(names...)
	if err != nil {
		return err
	}
	plugins := make([]*Plugin, 0, len(names))
	for _, name := range names {
		plugin, err := ParsePlugin(name)
		if err != nil {
			return err
		}
		plugins = append(plugins, plugin)
	}
	AddPluginsToCache(plugins...)
	return nil
}

func pluginExists(plugin string) bool {
	exists, _ := fileExists(pluginPath(plugin))
	return exists
}

// directory location of plugin
func pluginPath(plugin string) string {
	return filepath.Join(AppDir(), "node_modules", plugin)
}

// lock a plugin for reading
func readLockPlugin(name string) {
	lockfile := updateLockPath + "." + name
	if exists, _ := fileExists(lockfile); exists {
		lockPlugin(name)
		unlockPlugin(name)
	}
}

// lock a plugin for writing
func lockPlugin(name string) {
	LogIfError(golock.Lock(updateLockPath + "." + name))
}

// unlock a plugin
func unlockPlugin(name string) {
	LogIfError(golock.Unlock(updateLockPath + "." + name))
}
<|endoftext|>


<commit_before>package arn

import (
	"errors"
	"sort"

	"github.com/aerogo/nano"
)

// GetUser fetches the user with the given ID from the database.
func GetUser(id string) (*User, error) {
	obj, err := DB.Get("User", id)

	if err != nil {
		return nil, err
	}

	return obj.(*User), nil
}

// GetUserByNick fetches the user with the given nick from the database.
func GetUserByNick(nick string) (*User, error) {
	obj, err := DB.Get("NickToUser", nick)

	if err != nil {
		return nil, err
	}

	userID := obj.(*NickToUser).UserID
	user, err := GetUser(userID)

	return user, err
}

// GetUserByEmail fetches the user with the given email from the database.
func GetUserByEmail(email string) (*User, error) {
	if email == "" {
		return nil, errors.New("Email is empty")
	}

	obj, err := DB.Get("EmailToUser", email)

	if err != nil {
		return nil, err
	}

	userID := obj.(*EmailToUser).UserID
	user, err := GetUser(userID)

	return user, err
}

// GetUserByFacebookID fetches the user with the given Facebook ID from the database.
func GetUserByFacebookID(facebookID string) (*User, error) {
	obj, err := DB.Get("FacebookToUser", facebookID)

	if err != nil {
		return nil, err
	}

	userID := obj.(*FacebookToUser).UserID
	user, err := GetUser(userID)

	return user, err
}

// GetUserByGoogleID fetches the user with the given Google ID from the database.
func GetUserByGoogleID(googleID string) (*User, error) {
	obj, err := DB.Get("GoogleToUser", googleID)

	if err != nil {
		return nil, err
	}

	userID := obj.(*GoogleToUser).UserID
	user, err := GetUser(userID)

	return user, err
}

// StreamUsers returns a stream of all users.
func StreamUsers() chan *User {
	channel := make(chan *User, nano.ChannelBufferSize)

	go func() {
		for obj := range DB.All("User") {
			channel <- obj.(*User)
		}

		close(channel)
	}()

	return channel
}

// AllUsers returns a slice of all users.
func AllUsers() ([]*User, error) {
	var all []*User

	for obj := range StreamUsers() {
		all = append(all, obj)
	}

	return all, nil
}

// FilterUsers filters all users by a custom function.
func FilterUsers(filter func(*User) bool) []*User {
	var filtered []*User

	for obj := range StreamUsers() {
		if filter(obj) {
			filtered = append(filtered, obj)
		}
	}

	return filtered
}

// SortUsersLastSeen sorts a list of users by their last seen date.
func SortUsersLastSeen(users []*User) {
	sort.Slice(users, func(i, j int) bool {
		return users[i].LastSeen > users[j].LastSeen
	})
}

// SortUsersFollowers sorts a list of users by their number of followers.
func SortUsersFollowers(users []*User) {
	followCount := UserFollowerCountMap()

	sort.Slice(users, func(i, j int) bool {
		if users[i].HasAvatar() != users[j].HasAvatar() {
			return users[i].HasAvatar()
		}

		followersA := followCount[users[i].ID]
		followersB := followCount[users[j].ID]

		if followersA == followersB {
			return users[i].Nick < users[j].Nick
		}

		return followersA > followersB
	})
}
<commit_msg>Updated sort function name<commit_after>package arn

import (
	"errors"
	"sort"

	"github.com/aerogo/nano"
)

// GetUser fetches the user with the given ID from the database.
func GetUser(id string) (*User, error) {
	obj, err := DB.Get("User", id)

	if err != nil {
		return nil, err
	}

	return obj.(*User), nil
}

// GetUserByNick fetches the user with the given nick from the database.
func GetUserByNick(nick string) (*User, error) {
	obj, err := DB.Get("NickToUser", nick)

	if err != nil {
		return nil, err
	}

	userID := obj.(*NickToUser).UserID
	user, err := GetUser(userID)

	return user, err
}

// GetUserByEmail fetches the user with the given email from the database.
func GetUserByEmail(email string) (*User, error) {
	if email == "" {
		return nil, errors.New("Email is empty")
	}

	obj, err := DB.Get("EmailToUser", email)

	if err != nil {
		return nil, err
	}

	userID := obj.(*EmailToUser).UserID
	user, err := GetUser(userID)

	return user, err
}

// GetUserByFacebookID fetches the user with the given Facebook ID from the database.
func GetUserByFacebookID(facebookID string) (*User, error) {
	obj, err := DB.Get("FacebookToUser", facebookID)

	if err != nil {
		return nil, err
	}

	userID := obj.(*FacebookToUser).UserID
	user, err := GetUser(userID)

	return user, err
}

// GetUserByGoogleID fetches the user with the given Google ID from the database.
func GetUserByGoogleID(googleID string) (*User, error) {
	obj, err := DB.Get("GoogleToUser", googleID)

	if err != nil {
		return nil, err
	}

	userID := obj.(*GoogleToUser).UserID
	user, err := GetUser(userID)

	return user, err
}

// StreamUsers returns a stream of all users.
func StreamUsers() chan *User {
	channel := make(chan *User, nano.ChannelBufferSize)

	go func() {
		for obj := range DB.All("User") {
			channel <- obj.(*User)
		}

		close(channel)
	}()

	return channel
}

// AllUsers returns a slice of all users.
func AllUsers() ([]*User, error) {
	var all []*User

	for obj := range StreamUsers() {
		all = append(all, obj)
	}

	return all, nil
}

// FilterUsers filters all users by a custom function.
func FilterUsers(filter func(*User) bool) []*User {
	var filtered []*User

	for obj := range StreamUsers() {
		if filter(obj) {
			filtered = append(filtered, obj)
		}
	}

	return filtered
}

// SortUsersLastSeenFirst sorts a list of users by their last seen date.
func SortUsersLastSeenFirst(users []*User) {
	sort.Slice(users, func(i, j int) bool {
		return users[i].LastSeen > users[j].LastSeen
	})
}

// SortUsersLastSeenLast sorts a list of users by their last seen date.
func SortUsersLastSeenLast(users []*User) {
	sort.Slice(users, func(i, j int) bool {
		return users[i].LastSeen < users[j].LastSeen
	})
}

// SortUsersFollowers sorts a list of users by their number of followers.
func SortUsersFollowers(users []*User) {
	followCount := UserFollowerCountMap()

	sort.Slice(users, func(i, j int) bool {
		if users[i].HasAvatar() != users[j].HasAvatar() {
			return users[i].HasAvatar()
		}

		followersA := followCount[users[i].ID]
		followersB := followCount[users[j].ID]

		if followersA == followersB {
			return users[i].Nick < users[j].Nick
		}

		return followersA > followersB
	})
}
<|endoftext|>