<commit_before>package acmewrapper

import (
	"crypto"
	"crypto/tls"
	"log"
	"os"
	"sync"

	"github.com/xenolf/lego/acme"
)

type LoggerInterface interface {
	Printf(format string, v ...interface{})
}

// Allows to use a custom logger for logging purposes
var Logger LoggerInterface

func logf(s string, v ...interface{}) {
	if Logger == nil {
		log.Printf(s, v...)
	} else {
		Logger.Printf(s, v...)
	}
}

// AcmeWrapper is the main object which controls tls certificates and their renewals
type AcmeWrapper struct {
	sync.Mutex // configmutex ensures that settings for the ACME stuff don't happen in parallel
	Config     Config

	certmutex sync.RWMutex // certmutex is used to make sure that replacing certificates doesn't asplode

	// Our user's private key & registration. Both are needed in order to be able
	// to renew/generate new certs.
	privatekey   crypto.PrivateKey
	registration *acme.RegistrationResource

	// A map of custom certificates associated with special SNIs. The SNI request
	// passes through here
	certs map[string]*tls.Certificate

	// The current TLS cert used for SSL requests when the SNI doesn't match the map
	cert *tls.Certificate

	// The ACME client
	client *acme.Client
}

// GetEmail returns the user email (if any)
// NOTE: NOT threadsafe
func (w *AcmeWrapper) GetEmail() string {
	return w.Config.Email
}

// GetRegistration returns the registration currently being used
// NOTE: NOT threadsafe
func (w *AcmeWrapper) GetRegistration() *acme.RegistrationResource {
	return w.registration
}

// GetPrivateKey returns the private key for the given user.
// NOTE: NOT threadsafe
func (w *AcmeWrapper) GetPrivateKey() crypto.PrivateKey {
	return w.privatekey
}

// GetCertificate returns the current TLS certificate
func (w *AcmeWrapper) GetCertificate() *tls.Certificate {
	w.certmutex.RLock()
	defer w.certmutex.RUnlock()
	return w.cert
}

// AddSNI adds a domain name and certificate pair to the AcmeWrapper.
// Whenever a request is for the passed domain, its associated certifcate is returned.
func (w *AcmeWrapper) AddSNI(domain string, cert *tls.Certificate) {
	w.certmutex.Lock()
	defer w.certmutex.Unlock()

	w.certs[domain] = cert
}

// RemSNI removes a domain name and certificate pair from the AcmeWrapper. It is assumed that
// they were added using AddSNI.
func (w *AcmeWrapper) RemSNI(domain string) {
	w.certmutex.Lock()
	defer w.certmutex.Unlock()

	delete(w.certs, domain)
}

// TLSConfigGetCertificate is the main function used in the ACME wrapper. This is set in tls.Config to
// the GetCertificate property. Note that Certificates must be empty for it to be called
// correctly, so unless you know what you're doing, just use AcmeWrapper.TLSConfig()
func (w *AcmeWrapper) TLSConfigGetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
	w.certmutex.RLock()
	defer w.certmutex.RUnlock()

	// If the SNI is in the certs map, return that cert
	if _, ok := w.certs[clientHello.ServerName]; ok {
		return w.certs[clientHello.ServerName], nil
	}

	// Otherwise, return the default cert
	return w.cert, nil
}

// TLSConfig returns a TLS configuration that will automatically work with the golang ssl listener.
// This sets it up so that the server automatically uses a working cert, and updates the cert when
// necessary.
func (w *AcmeWrapper) TLSConfig() *tls.Config {
	return &tls.Config{
		//Go 1.6 "allows Listen to succeed when the Config has a nil Certificates, as long as the
		//GetCertificate callback is set" See https://golang.org/doc/go1.6#minor_library_changes.
		//So to add Go 1.5 support, we provide a blank certificate which will never be used.
		Certificates:   []tls.Certificate{tls.Certificate{}},
		GetCertificate: w.TLSConfigGetCertificate,
	}
}

// AcmeDisabled allows to enable/disable acme-based certificate. Note that it is assumed that
// this function is only called during server runtime (ie, your server is already listening).
// its main purpose is to enable live reload of acme configuration. Do NOT set AcmeDisabled
// in AcmeWrapper.Config, since it will panic.
func (w *AcmeWrapper) AcmeDisabled(set bool) error {
	w.Lock()
	w.Config.AcmeDisabled = set
	w.Unlock()
	if !set && w.client == nil {
		return w.initACME(true)
	}
	return nil
}

// SetNewCert loads a new TLS key/cert from the given files. Running it with the same
// filenames as existing cert will reload them
func (w *AcmeWrapper) SetNewCert(certfile, keyfile string) error {
	cert, err := tls.LoadX509KeyPair(certfile, keyfile)
	if err != nil {
		return err
	}
	w.certmutex.Lock()
	w.cert = &cert
	w.certmutex.Unlock()

	return nil
}

// New generates an AcmeWrapper given a configuration
func New(c Config) (*AcmeWrapper, error) {
	var err error
	// First, set up the default values for any settings that require
	// values
	if c.Server == "" {
		c.Server = DefaultServer
	}
	if c.PrivateKeyType == "" {
		c.PrivateKeyType = DefaultKeyType
	}
	if c.RenewTime == 0 {
		c.RenewTime = DefaultRenewTime
	}
	if c.RetryDelay == 0 {
		c.RetryDelay = DefaultRetryDelay
	}
	if c.RenewCheck == 0 {
		c.RenewCheck = DefaultRenewCheck
	}
	if c.Address == "" {
		c.Address = DefaultAddress
	}

	// Now set up the actual wrapper

	var w AcmeWrapper
	w.Config = c
	w.certs = make(map[string]*tls.Certificate)

	// Now load up the key and cert files for TLS if they are set
	if c.TLSKeyFile != "" || c.TLSCertFile != "" {
		err = w.SetNewCert(c.TLSCertFile, c.TLSKeyFile)
		if err != nil {
			if !os.IsNotExist(err) || c.AcmeDisabled {
				// The TLS key and cert file are only
				// allowed to not be there if ACME will generate them
				// TODO: We don't check here if both are missing vs 1 missing
				return nil, err
			}

		}
	}

	// If acme is enabled, initialize it!
	if !c.AcmeDisabled {
		// Initialize the ACME user
		// initUser succeeding initializes:
		//	- w.privatekey
		//	- w.registration
		//	- w.client
		if err = w.initACME(false); err != nil {
			return nil, err
		}
	}

	// Finally, start the background routine
	go backgroundExpirationChecker(&w)

	return &w, nil
}
<commit_msg>Fixed possible panic when used without SNI support<commit_after>package acmewrapper

import (
	"crypto"
	"crypto/tls"
	"log"
	"os"
	"sync"

	"github.com/xenolf/lego/acme"
)

type LoggerInterface interface {
	Printf(format string, v ...interface{})
}

// Allows to use a custom logger for logging purposes
var Logger LoggerInterface

func logf(s string, v ...interface{}) {
	if Logger == nil {
		log.Printf(s, v...)
	} else {
		Logger.Printf(s, v...)
	}
}

// AcmeWrapper is the main object which controls tls certificates and their renewals
type AcmeWrapper struct {
	sync.Mutex // configmutex ensures that settings for the ACME stuff don't happen in parallel
	Config     Config

	certmutex sync.RWMutex // certmutex is used to make sure that replacing certificates doesn't asplode

	// Our user's private key & registration. Both are needed in order to be able
	// to renew/generate new certs.
	privatekey   crypto.PrivateKey
	registration *acme.RegistrationResource

	// A map of custom certificates associated with special SNIs. The SNI request
	// passes through here
	certs map[string]*tls.Certificate

	// The current TLS cert used for SSL requests when the SNI doesn't match the map
	cert *tls.Certificate

	// The ACME client
	client *acme.Client
}

// GetEmail returns the user email (if any)
// NOTE: NOT threadsafe
func (w *AcmeWrapper) GetEmail() string {
	return w.Config.Email
}

// GetRegistration returns the registration currently being used
// NOTE: NOT threadsafe
func (w *AcmeWrapper) GetRegistration() *acme.RegistrationResource {
	return w.registration
}

// GetPrivateKey returns the private key for the given user.
// NOTE: NOT threadsafe
func (w *AcmeWrapper) GetPrivateKey() crypto.PrivateKey {
	return w.privatekey
}

// GetCertificate returns the current TLS certificate
func (w *AcmeWrapper) GetCertificate() *tls.Certificate {
	w.certmutex.RLock()
	defer w.certmutex.RUnlock()
	return w.cert
}

// AddSNI adds a domain name and certificate pair to the AcmeWrapper.
// Whenever a request is for the passed domain, its associated certifcate is returned.
func (w *AcmeWrapper) AddSNI(domain string, cert *tls.Certificate) {
	w.certmutex.Lock()
	defer w.certmutex.Unlock()

	w.certs[domain] = cert
}

// RemSNI removes a domain name and certificate pair from the AcmeWrapper. It is assumed that
// they were added using AddSNI.
func (w *AcmeWrapper) RemSNI(domain string) {
	w.certmutex.Lock()
	defer w.certmutex.Unlock()

	delete(w.certs, domain)
}

// TLSConfigGetCertificate is the main function used in the ACME wrapper. This is set in tls.Config to
// the GetCertificate property. Note that Certificates must be empty for it to be called
// correctly, so unless you know what you're doing, just use AcmeWrapper.TLSConfig()
func (w *AcmeWrapper) TLSConfigGetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
	w.certmutex.RLock()
	defer w.certmutex.RUnlock()

	// If the SNI is in the certs map, return that cert
	if _, ok := w.certs[clientHello.ServerName]; ok {
		return w.certs[clientHello.ServerName], nil
	}

	// Otherwise, return the default cert
	return w.cert, nil
}

// TLSConfig returns a TLS configuration that will automatically work with the golang ssl listener.
// This sets it up so that the server automatically uses a working cert, and updates the cert when
// necessary.
func (w *AcmeWrapper) TLSConfig() *tls.Config {
	return &tls.Config{
		//Go 1.6 "allows Listen to succeed when the Config has a nil Certificates, as long as the
		//GetCertificate callback is set" See https://golang.org/doc/go1.6#minor_library_changes.
		//So to add Go 1.5 support, we provide a certificate that will never be used.
		// This must be a valid cert, since if the client does not have SNI enabled in go1.4,
		// then there could be issues. AcmeWrapper does not support clients without SNI enabled at this time.
		Certificates:   []tls.Certificate{*w.cert},
		GetCertificate: w.TLSConfigGetCertificate,
	}
}

// AcmeDisabled allows to enable/disable acme-based certificate. Note that it is assumed that
// this function is only called during server runtime (ie, your server is already listening).
// its main purpose is to enable live reload of acme configuration. Do NOT set AcmeDisabled
// in AcmeWrapper.Config, since it will panic.
func (w *AcmeWrapper) AcmeDisabled(set bool) error {
	w.Lock()
	w.Config.AcmeDisabled = set
	w.Unlock()
	if !set && w.client == nil {
		return w.initACME(true)
	}
	return nil
}

// SetNewCert loads a new TLS key/cert from the given files. Running it with the same
// filenames as existing cert will reload them
func (w *AcmeWrapper) SetNewCert(certfile, keyfile string) error {
	cert, err := tls.LoadX509KeyPair(certfile, keyfile)
	if err != nil {
		return err
	}
	w.certmutex.Lock()
	w.cert = &cert
	w.certmutex.Unlock()

	return nil
}

// New generates an AcmeWrapper given a configuration
func New(c Config) (*AcmeWrapper, error) {
	var err error
	// First, set up the default values for any settings that require
	// values
	if c.Server == "" {
		c.Server = DefaultServer
	}
	if c.PrivateKeyType == "" {
		c.PrivateKeyType = DefaultKeyType
	}
	if c.RenewTime == 0 {
		c.RenewTime = DefaultRenewTime
	}
	if c.RetryDelay == 0 {
		c.RetryDelay = DefaultRetryDelay
	}
	if c.RenewCheck == 0 {
		c.RenewCheck = DefaultRenewCheck
	}
	if c.Address == "" {
		c.Address = DefaultAddress
	}

	// Now set up the actual wrapper

	var w AcmeWrapper
	w.Config = c
	w.certs = make(map[string]*tls.Certificate)

	// Now load up the key and cert files for TLS if they are set
	if c.TLSKeyFile != "" || c.TLSCertFile != "" {
		err = w.SetNewCert(c.TLSCertFile, c.TLSKeyFile)
		if err != nil {
			if !os.IsNotExist(err) || c.AcmeDisabled {
				// The TLS key and cert file are only
				// allowed to not be there if ACME will generate them
				// TODO: We don't check here if both are missing vs 1 missing
				return nil, err
			}

		}
	}

	// If acme is enabled, initialize it!
	if !c.AcmeDisabled {
		// Initialize the ACME user
		// initUser succeeding initializes:
		//	- w.privatekey
		//	- w.registration
		//	- w.client
		if err = w.initACME(false); err != nil {
			return nil, err
		}
	}

	// Finally, start the background routine
	go backgroundExpirationChecker(&w)

	return &w, nil
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"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 {
		setupNode()
	}
}

func setupNode() {
	PrintError(gode.Setup(), true)
}

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"]
		if !contains(PluginNames(), name) {
			panic(name + " is not installed")
		}
		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 && 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>Fix error when in plugins cache & not node_modules<commit_after>package main

import (
	"encoding/json"
	"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 {
		setupNode()
	}
}

func setupNode() {
	PrintError(gode.Setup(), true)
}

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"]
		if !contains(PluginNames(), name) {
			panic(name + " is not installed")
		}
		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))
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"syscall"

	"github.com/dickeyxxx/gode"
	"github.com/stvp/rollbar"
)

// Plugin represents a javascript plugin
type Plugin struct {
	Topics   TopicSet   `json:"topics"`
	Commands CommandSet `json:"commands"`
}

var node = gode.NewClient(AppDir)

// SetupNode sets up node and npm in ~/.heroku
func SetupNode() {
	node.Registry = "http://54.173.158.18"
	node.NpmVersion = "2.1.8"
	if !node.IsSetup() {
		Log("setting up plugins... ")
		must(node.Setup())
		Logln("done")
	}
}

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

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

var pluginsInstallCmd = &Command{
	Topic:       "plugins",
	Command:     "install",
	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["name"]
		Errf("Installing plugin %s... ", name)
		must(node.InstallPackage(name))
		plugin := getPlugin(name)
		if plugin == nil || len(plugin.Commands) == 0 {
			Err("This does not appear to be a Heroku plugin, uninstalling... ")
			must(node.RemovePackage(name))
		}
		Errln("done")
	},
}

var pluginsListCmd = &Command{
	Topic:       "plugins",
	Description: "Lists the installed plugins",
	Help: `Lists installed plugins

  Example:
  $ heroku plugins`,

	Run: func(ctx *Context) {
		packages, err := node.Packages()
		must(err)
		for _, pkg := range packages {
			Println(pkg.Name, pkg.Version)
		}
	},
}

func runFn(module, topic, command string) func(ctx *Context) {
	return func(ctx *Context) {
		ctxJSON, err := json.Marshal(ctx)
		must(err)
		script := fmt.Sprintf(`
		require('%s')
		.commands.filter(function (command) {
			return command.topic == '%s' && command.command == '%s'
		})[0]
		.run(%s)`, module, topic, command, ctxJSON)

		cmd := node.RunScript(script)
		cmd.Stdout = Stdout
		r, w, _ := os.Pipe()
		cmd.Stderr = w
		stderr := captureText(r)
		if err := cmd.Run(); err != nil {
			Errf("Error in %s\n", ctx.Command.Plugin)
			if Channel != "?" {
				nodeErr := fmt.Errorf("%s %s\n%s", ctx.Command.Plugin, ctx.Command, stderr.String())
				rollbar.ErrorWithStack(rollbar.ERR, nodeErr, rollbar.Stack{})
				rollbar.Wait()
			}
			// Exit with the same exit code
			Exit(getExitCode(err))
		}
	}
}

func captureText(r io.Reader) *bytes.Buffer {
	var b bytes.Buffer
	go func() {
		scanner := bufio.NewScanner(r)
		for scanner.Scan() {
			b.WriteString(scanner.Text() + "\n")
			Errln(scanner.Text())
		}
	}()
	return &b
}

func getExitCode(err error) int {
	exitErr, ok := err.(*exec.ExitError)
	if !ok {
		panic(err)
	}
	status, ok := exitErr.Sys().(syscall.WaitStatus)
	if !ok {
		panic(err)
	}
	return status.ExitStatus()
}

func getPlugin(name string) *Plugin {
	script := `console.log(JSON.stringify(require('` + name + `')))`
	cmd := node.RunScript(script)
	cmd.Stderr = Stderr
	output, err := cmd.StdoutPipe()
	must(err)
	must(cmd.Start())
	var plugin Plugin
	err = json.NewDecoder(output).Decode(&plugin)
	if err != nil {
		Errln("Error reading plugin:", name)
		return nil
	}
	must(cmd.Wait())
	for _, command := range plugin.Commands {
		command.Plugin = name
		command.Run = runFn(name, command.Topic, command.Command)
	}
	return &plugin
}

// GetPlugins goes through all the node plugins and returns them in Go stucts
func GetPlugins() []Plugin {
	names := PluginNames()
	plugins := make([]Plugin, 0, len(names))
	for _, name := range names {
		plugin := getPlugin(name)
		if plugin != nil {
			plugins = append(plugins, *plugin)
		}
	}
	return plugins
}

// PluginNames just lists the files in ~/.heroku/node_modules
func PluginNames() []string {
	files, _ := ioutil.ReadDir(filepath.Join(AppDir, "node_modules"))
	names := make([]string, 0, len(files))
	for _, f := range files {
		names = append(names, f.Name())
	}
	return names
}
<commit_msg>Require plugin name<commit_after>package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"syscall"

	"github.com/dickeyxxx/gode"
	"github.com/stvp/rollbar"
)

// Plugin represents a javascript plugin
type Plugin struct {
	Topics   TopicSet   `json:"topics"`
	Commands CommandSet `json:"commands"`
}

var node = gode.NewClient(AppDir)

// SetupNode sets up node and npm in ~/.heroku
func SetupNode() {
	node.Registry = "http://54.173.158.18"
	node.NpmVersion = "2.1.8"
	if !node.IsSetup() {
		Log("setting up plugins... ")
		must(node.Setup())
		Logln("done")
	}
}

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

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

var pluginsInstallCmd = &Command{
	Topic:       "plugins",
	Command:     "install",
	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["name"]
		if len(name) == 0 {
			Errln("Must specify a plugin name")
			return
		}
		Errf("Installing plugin %s... ", name)
		fmt.Println(len(name))

		must(node.InstallPackage(name))
		plugin := getPlugin(name)
		if plugin == nil || len(plugin.Commands) == 0 {
			Err("This does not appear to be a Heroku plugin, uninstalling... ")
			must(node.RemovePackage(name))
		}
		Errln("done")
	},
}

var pluginsListCmd = &Command{
	Topic:       "plugins",
	Description: "Lists the installed plugins",
	Help: `Lists installed plugins

  Example:
  $ heroku plugins`,

	Run: func(ctx *Context) {
		packages, err := node.Packages()
		must(err)
		for _, pkg := range packages {
			Println(pkg.Name, pkg.Version)
		}
	},
}

func runFn(module, topic, command string) func(ctx *Context) {
	return func(ctx *Context) {
		ctxJSON, err := json.Marshal(ctx)
		must(err)
		script := fmt.Sprintf(`
		require('%s')
		.commands.filter(function (command) {
			return command.topic == '%s' && command.command == '%s'
		})[0]
		.run(%s)`, module, topic, command, ctxJSON)

		cmd := node.RunScript(script)
		cmd.Stdout = Stdout
		r, w, _ := os.Pipe()
		cmd.Stderr = w
		stderr := captureText(r)
		if err := cmd.Run(); err != nil {
			Errf("Error in %s\n", ctx.Command.Plugin)
			if Channel != "?" {
				nodeErr := fmt.Errorf("%s %s\n%s", ctx.Command.Plugin, ctx.Command, stderr.String())
				rollbar.ErrorWithStack(rollbar.ERR, nodeErr, rollbar.Stack{})
				rollbar.Wait()
			}
			// Exit with the same exit code
			Exit(getExitCode(err))
		}
	}
}

func captureText(r io.Reader) *bytes.Buffer {
	var b bytes.Buffer
	go func() {
		scanner := bufio.NewScanner(r)
		for scanner.Scan() {
			b.WriteString(scanner.Text() + "\n")
			Errln(scanner.Text())
		}
	}()
	return &b
}

func getExitCode(err error) int {
	exitErr, ok := err.(*exec.ExitError)
	if !ok {
		panic(err)
	}
	status, ok := exitErr.Sys().(syscall.WaitStatus)
	if !ok {
		panic(err)
	}
	return status.ExitStatus()
}

func getPlugin(name string) *Plugin {
	script := `console.log(JSON.stringify(require('` + name + `')))`
	cmd := node.RunScript(script)
	cmd.Stderr = Stderr
	output, err := cmd.StdoutPipe()
	must(err)
	must(cmd.Start())
	var plugin Plugin
	err = json.NewDecoder(output).Decode(&plugin)
	if err != nil {
		Errln("Error reading plugin:", name)
		return nil
	}
	must(cmd.Wait())
	for _, command := range plugin.Commands {
		command.Plugin = name
		command.Run = runFn(name, command.Topic, command.Command)
	}
	return &plugin
}

// GetPlugins goes through all the node plugins and returns them in Go stucts
func GetPlugins() []Plugin {
	names := PluginNames()
	plugins := make([]Plugin, 0, len(names))
	for _, name := range names {
		plugin := getPlugin(name)
		if plugin != nil {
			plugins = append(plugins, *plugin)
		}
	}
	return plugins
}

// PluginNames just lists the files in ~/.heroku/node_modules
func PluginNames() []string {
	files, _ := ioutil.ReadDir(filepath.Join(AppDir, "node_modules"))
	names := make([]string, 0, len(files))
	for _, f := range files {
		names = append(names, f.Name())
	}
	return names
}
<|endoftext|>


<commit_before>package statemachine

import (
	"fmt"
	"time"

	"github.com/lytics/metafora"
)

type StateCode string

const (
	Runnable  StateCode = "runnable"  // Scheduled
	Sleeping            = "sleeping"  // Scheduled, not running until time has elapsed
	Completed           = "completed" // Terminal, not scheduled
	Killed              = "killed"    // Terminal, not scheduled
	Failed              = "failed"    // Terminal, not scheduled
	Fault               = "fault"     // Scheduled, in error handling / retry logic
	Paused              = "paused"    // Scheduled, not running
)

// Terminal states will never run and cannot transition to a non-terminal
// state.
func (s StateCode) Terminal() bool {
	switch s {
	case Runnable, Sleeping, Paused, Fault:
		return false
	case Completed, Killed, Failed:
		return true
	default:
		panic("unknown state " + s)
	}
}

// State represents the current state of a stateful handler. See StateCode for
// details. Until and Errors are extra state used by the Sleeping and Fault
// states respectively.
type State struct {
	Code   StateCode  `json:"state"`
	Until  *time.Time `json:"until,omitempty"`
	Errors []Err      `json:"errors,omitempty"`
}

func (s State) String() string {
	switch s.Code {
	case Sleeping:
		return fmt.Sprintf("%s until %s", s.Code, s.Until)
	default:
		return string(s.Code)
	}
}

// Messages are events that cause state transitions. Until and Err are used by
// the Sleep and Error messages respectively.
type Message struct {
	Code MessageCode `json:"message"`

	// Until is when the statemachine should transition from sleeping to runnable
	Until *time.Time `json:"until,omitempty"`

	// Err is the error that caused this Error message
	Err error `json:"error,omitempty"`
}

// MessageCode is the symbolic name of a state transition.
type MessageCode string

const (
	Run        MessageCode = "run"
	Sleep                  = "sleep"
	Pause                  = "pause"
	Kill                   = "kill"
	Error                  = "error"
	Complete               = "complete"
	Checkpoint             = "checkpoint"

	// Special event which triggers state machine to exit without transitioning
	// between states.
	Release MessageCode = "release"
)

// Transitions represent a state machine transition from one state to another
// given an event message.
type Transition struct {
	Event MessageCode
	From  StateCode
	To    StateCode
}

func (t Transition) String() string {
	return fmt.Sprintf("%v---%v--->%v", t.From, t.Event, t.To)
}

var (
	// Rules is the state transition table.
	Rules = []Transition{
		// Runnable can transition to anything
		{Event: Checkpoint, From: Runnable, To: Runnable},
		{Event: Release, From: Runnable, To: Runnable},
		{Event: Sleep, From: Runnable, To: Sleeping},
		{Event: Complete, From: Runnable, To: Completed},
		{Event: Kill, From: Runnable, To: Killed},
		{Event: Error, From: Runnable, To: Fault},
		{Event: Pause, From: Runnable, To: Paused},

		// Sleeping can return to Runnable or be Killed/Paused
		{Event: Checkpoint, From: Sleeping, To: Sleeping},
		{Event: Sleep, From: Sleeping, To: Sleeping},
		{Event: Run, From: Sleeping, To: Runnable},
		{Event: Kill, From: Sleeping, To: Killed},
		{Event: Pause, From: Sleeping, To: Paused},

		// The error state transitions to either sleeping, failed, or released (to
		// allow custom error handlers to workaround localitly related errors).
		{Event: Sleep, From: Fault, To: Sleeping},
		{Event: Error, From: Fault, To: Failed},

		// Paused can return to Runnable, be put to Sleep, or Killed
		{Event: Checkpoint, From: Paused, To: Paused},
		{Event: Release, From: Paused, To: Paused},
		{Event: Run, From: Paused, To: Runnable},
		{Event: Sleep, From: Paused, To: Sleeping},
		{Event: Kill, From: Paused, To: Killed},
		{Event: Pause, From: Paused, To: Paused},

		// Completed, Failed, and Killed are terminal states that cannot transition
		// to anything.
	}
)

// StatefulHandler is the function signature that the state machine is able to
// run. Instead of metafora.Handler's Stop method, StatefulHandlers receive
// Messages via the commands chan and return their exit status via a Message.
//
// Normally StatefulHandlers simply return a Message as soon as it's received
// on the commands chan. However, it's also acceptable for a handler to return
// a different Message. For example if it encounters an error during shutdown,
// it may choose to return that error as an Error Message as opposed to the
// original command.
type StatefulHandler func(taskID string, commands <-chan Message) Message

type stateMachine struct {
	taskID     string
	h          StatefulHandler
	ss         StateStore
	cl         CommandListener
	cmds       chan Message
	errHandler ErrHandler
}

// New handler that creates a state machine and exposes state transitions to
// the given handler by calling its Transition method. It should be created in
// the HandlerFunc you use with metafora's Consumer.
//
// If ErrHandler is nil DefaultErrHandler will be used.
func New(tid string, h StatefulHandler, ss StateStore, cl CommandListener, e ErrHandler) metafora.Handler {
	if e == nil {
		e = DefaultErrHandler
	}
	return &stateMachine{
		taskID:     tid,
		h:          h,
		ss:         ss,
		cl:         cl,
		errHandler: e,
	}
}

// Run the state machine enabled handler. Loads the initial state and passes
// control to the internal stateful handler passing commands from the command
// listener into the handler's commands chan.
func (s *stateMachine) Run() (done bool) {
	// Multiplex external (Stop) messages and internal ones
	stopped := make(chan struct{})
	s.cmds = make(chan Message)
	go func() {
		for {
			select {
			case m := <-s.cl.Receive():
				select {
				case s.cmds <- m:
				case <-stopped:
					return
				}
			case <-stopped:
				return
			}
		}
	}()

	// Stop the command listener and internal message multiplexer when Run exits
	defer func() {
		s.cl.Stop()
		close(stopped)
	}()

	// Load the initial state
	state, err := s.ss.Load(s.taskID)
	if err != nil {
		// A failure to load the state for a task is *fatal* - the task will be
		// unscheduled and requires operator intervention to reschedule.
		metafora.Errorf("task=%q could not load initial state. Marking done! Error: %v", s.taskID, err)
		return true
	}
	if state.Code.Terminal() {
		metafora.Warnf("task=%q in terminal state %s - exiting.", s.taskID, state.Code)
		return true
	}

	// Main Run loop
	done = false
	for {
		metafora.Debugf("task=%q in state %s", s.taskID, state.Code)
		msg := s.exec(state)

		// Enter State
		// Apply Message
		newstate, ok := apply(state, msg)
		if !ok {
			metafora.Warnf("task=%q Invalid state transition=%q returned by task. Old state=%q", s.taskID, msg.Code, state.Code)
			msg = Message{Code: Error, Err: err}
			if newstate, ok = apply(state, msg); !ok {
				metafora.Errorf("task=%q Unable to transition to error state! Exiting with state=%q", s.taskID, state.Code)
				return state.Code.Terminal()
			}
		}

		// Save state
		if err := s.ss.Store(s.taskID, newstate); err != nil {
			metafora.Errorf("task=%q Unable to persist state=%q. Unscheduling.", s.taskID, newstate.Code)
			return true
		}

		// Set next state and loop if non-terminal
		state = newstate

		// Exit and unschedule task on terminal state.
		if state.Code.Terminal() {
			return true
		}

		// Release messages indicate the task should exit but not unschedule.
		if msg.Code == Release {
			return false
		}
	}
}

// execute non-terminal states
func (s *stateMachine) exec(state *State) Message {
	switch state.Code {
	case Runnable:
		// Runnable passes control to the stateful handler
		return run(s.h, s.taskID, s.cmds)
	case Paused:
		// Paused until a message arrives
		return <-s.cmds
	case Sleeping:
		// Sleeping until the specified time (or a message)
		if state.Until == nil {
			metafora.Warnf("task=%q told to sleep without a time. Resuming.", s.taskID)
			return Message{Code: Run}
		}
		dur := state.Until.Sub(time.Now())
		metafora.Infof("task=%q sleeping for %s", s.taskID, dur)
		timer := time.NewTimer(dur)
		select {
		case <-timer.C:
			return Message{Code: Run}
		case msg := <-s.cmds:
			timer.Stop()
			// Checkpoint is a special case that shouldn't affect sleep time, so
			// maintain it across the state transition
			if msg.Code == Checkpoint {
				msg.Until = state.Until
			}
			return msg
		}
	case Fault:
		// Special case where we potentially trim the current state to keep
		// errors from growing without bound.
		var msg Message
		msg, state.Errors = s.errHandler(s.taskID, state.Errors)
		return msg
	default:
		panic("invalid state: " + state.String())
	}
}

func run(f StatefulHandler, tid string, cmd <-chan Message) (m Message) {
	defer func() {
		if r := recover(); r != nil {
			metafora.Errorf("task=%q Run method panic()d! Applying Error message. Panic: %v", tid, r)
			m = Message{Code: Error, Err: fmt.Errorf("panic: %v", r)}
		}
	}()
	m = f(tid, cmd)
	return m
}

// Stop sends a Release message to the state machine through the command chan.
func (s *stateMachine) Stop() {
	s.cmds <- Message{Code: Release}
}

// apply a message to cause a state transition. Returns false if the state
// transition is invalid.
func apply(cur *State, m Message) (*State, bool) {
	//XXX Is a linear scan of all rules really the best option here?
	for _, trans := range Rules {
		if trans.Event == m.Code && trans.From == cur.Code {
			metafora.Debugf("Transitioned %s", trans)
			return &State{Code: trans.To, Until: m.Until}, true
		}
	}
	return cur, false
}
<commit_msg>Log all state transitions<commit_after>package statemachine

import (
	"fmt"
	"time"

	"github.com/lytics/metafora"
)

type StateCode string

const (
	Runnable  StateCode = "runnable"  // Scheduled
	Sleeping            = "sleeping"  // Scheduled, not running until time has elapsed
	Completed           = "completed" // Terminal, not scheduled
	Killed              = "killed"    // Terminal, not scheduled
	Failed              = "failed"    // Terminal, not scheduled
	Fault               = "fault"     // Scheduled, in error handling / retry logic
	Paused              = "paused"    // Scheduled, not running
)

// Terminal states will never run and cannot transition to a non-terminal
// state.
func (s StateCode) Terminal() bool {
	switch s {
	case Runnable, Sleeping, Paused, Fault:
		return false
	case Completed, Killed, Failed:
		return true
	default:
		panic("unknown state " + s)
	}
}

// State represents the current state of a stateful handler. See StateCode for
// details. Until and Errors are extra state used by the Sleeping and Fault
// states respectively.
type State struct {
	Code   StateCode  `json:"state"`
	Until  *time.Time `json:"until,omitempty"`
	Errors []Err      `json:"errors,omitempty"`
}

func (s State) String() string {
	switch s.Code {
	case Sleeping:
		return fmt.Sprintf("%s until %s", s.Code, s.Until)
	case Fault:
		return fmt.Sprintf("%s (%d errors)", s.Code, len(s.Errors))
	default:
		return string(s.Code)
	}
}

// Messages are events that cause state transitions. Until and Err are used by
// the Sleep and Error messages respectively.
type Message struct {
	Code MessageCode `json:"message"`

	// Until is when the statemachine should transition from sleeping to runnable
	Until *time.Time `json:"until,omitempty"`

	// Err is the error that caused this Error message
	Err error `json:"error,omitempty"`
}

func (m Message) String() string {
	switch m.Code {
	case Sleep:
		if m.Until != nil {
			return fmt.Sprintf("%s until %s", m.Code, m.Until)
		}
	case Error:
		if m.Err != nil {
			return fmt.Sprintf("%s: %s", m.Code, m.Err.Error())
		}
	}
	return string(m.Code)
}

// MessageCode is the symbolic name of a state transition.
type MessageCode string

const (
	Run        MessageCode = "run"
	Sleep                  = "sleep"
	Pause                  = "pause"
	Kill                   = "kill"
	Error                  = "error"
	Complete               = "complete"
	Checkpoint             = "checkpoint"

	// Special event which triggers state machine to exit without transitioning
	// between states.
	Release MessageCode = "release"
)

// Transitions represent a state machine transition from one state to another
// given an event message.
type Transition struct {
	Event MessageCode
	From  StateCode
	To    StateCode
}

func (t Transition) String() string {
	return fmt.Sprintf("%v---%v--->%v", t.From, t.Event, t.To)
}

var (
	// Rules is the state transition table.
	Rules = []Transition{
		// Runnable can transition to anything
		{Event: Checkpoint, From: Runnable, To: Runnable},
		{Event: Release, From: Runnable, To: Runnable},
		{Event: Sleep, From: Runnable, To: Sleeping},
		{Event: Complete, From: Runnable, To: Completed},
		{Event: Kill, From: Runnable, To: Killed},
		{Event: Error, From: Runnable, To: Fault},
		{Event: Pause, From: Runnable, To: Paused},

		// Sleeping can return to Runnable or be Killed/Paused
		{Event: Checkpoint, From: Sleeping, To: Sleeping},
		{Event: Sleep, From: Sleeping, To: Sleeping},
		{Event: Run, From: Sleeping, To: Runnable},
		{Event: Kill, From: Sleeping, To: Killed},
		{Event: Pause, From: Sleeping, To: Paused},

		// The error state transitions to either sleeping, failed, or released (to
		// allow custom error handlers to workaround localitly related errors).
		{Event: Sleep, From: Fault, To: Sleeping},
		{Event: Error, From: Fault, To: Failed},

		// Paused can return to Runnable, be put to Sleep, or Killed
		{Event: Checkpoint, From: Paused, To: Paused},
		{Event: Release, From: Paused, To: Paused},
		{Event: Run, From: Paused, To: Runnable},
		{Event: Sleep, From: Paused, To: Sleeping},
		{Event: Kill, From: Paused, To: Killed},
		{Event: Pause, From: Paused, To: Paused},

		// Completed, Failed, and Killed are terminal states that cannot transition
		// to anything.
	}
)

// StatefulHandler is the function signature that the state machine is able to
// run. Instead of metafora.Handler's Stop method, StatefulHandlers receive
// Messages via the commands chan and return their exit status via a Message.
//
// Normally StatefulHandlers simply return a Message as soon as it's received
// on the commands chan. However, it's also acceptable for a handler to return
// a different Message. For example if it encounters an error during shutdown,
// it may choose to return that error as an Error Message as opposed to the
// original command.
type StatefulHandler func(taskID string, commands <-chan Message) Message

type stateMachine struct {
	taskID     string
	h          StatefulHandler
	ss         StateStore
	cl         CommandListener
	cmds       chan Message
	errHandler ErrHandler
}

// New handler that creates a state machine and exposes state transitions to
// the given handler by calling its Transition method. It should be created in
// the HandlerFunc you use with metafora's Consumer.
//
// If ErrHandler is nil DefaultErrHandler will be used.
func New(tid string, h StatefulHandler, ss StateStore, cl CommandListener, e ErrHandler) metafora.Handler {
	if e == nil {
		e = DefaultErrHandler
	}
	return &stateMachine{
		taskID:     tid,
		h:          h,
		ss:         ss,
		cl:         cl,
		errHandler: e,
	}
}

// Run the state machine enabled handler. Loads the initial state and passes
// control to the internal stateful handler passing commands from the command
// listener into the handler's commands chan.
func (s *stateMachine) Run() (done bool) {
	// Multiplex external (Stop) messages and internal ones
	stopped := make(chan struct{})
	s.cmds = make(chan Message)
	go func() {
		for {
			select {
			case m := <-s.cl.Receive():
				select {
				case s.cmds <- m:
				case <-stopped:
					return
				}
			case <-stopped:
				return
			}
		}
	}()

	// Stop the command listener and internal message multiplexer when Run exits
	defer func() {
		s.cl.Stop()
		close(stopped)
	}()

	// Load the initial state
	state, err := s.ss.Load(s.taskID)
	if err != nil {
		// A failure to load the state for a task is *fatal* - the task will be
		// unscheduled and requires operator intervention to reschedule.
		metafora.Errorf("task=%q could not load initial state. Marking done! Error: %v", s.taskID, err)
		return true
	}
	if state.Code.Terminal() {
		metafora.Warnf("task=%q in terminal state %s - exiting.", s.taskID, state.Code)
		return true
	}

	// Main Run loop
	done = false
	for {
		metafora.Debugf("task=%q in state %s", s.taskID, state.Code)
		msg := s.exec(state)

		// Enter State
		// Apply Message
		newstate, ok := apply(state, msg)
		if !ok {
			metafora.Warnf("task=%q Invalid state transition=%q returned by task. Old state=%q", s.taskID, msg.Code, state.Code)
			msg = Message{Code: Error, Err: err}
			if newstate, ok = apply(state, msg); !ok {
				metafora.Errorf("task=%q Unable to transition to error state! Exiting with state=%q", s.taskID, state.Code)
				return state.Code.Terminal()
			}
		}

		metafora.Infof("task=%q transitioning %s --> %s --> %s", s.taskID, state, msg, newstate)

		// Save state
		if err := s.ss.Store(s.taskID, newstate); err != nil {
			metafora.Errorf("task=%q Unable to persist state=%q. Unscheduling.", s.taskID, newstate.Code)
			return true
		}

		// Set next state and loop if non-terminal
		state = newstate

		// Exit and unschedule task on terminal state.
		if state.Code.Terminal() {
			return true
		}

		// Release messages indicate the task should exit but not unschedule.
		if msg.Code == Release {
			return false
		}
	}
}

// execute non-terminal states
func (s *stateMachine) exec(state *State) Message {
	switch state.Code {
	case Runnable:
		// Runnable passes control to the stateful handler
		return run(s.h, s.taskID, s.cmds)
	case Paused:
		// Paused until a message arrives
		return <-s.cmds
	case Sleeping:
		// Sleeping until the specified time (or a message)
		if state.Until == nil {
			metafora.Warnf("task=%q told to sleep without a time. Resuming.", s.taskID)
			return Message{Code: Run}
		}
		dur := state.Until.Sub(time.Now())
		metafora.Infof("task=%q sleeping for %s", s.taskID, dur)
		timer := time.NewTimer(dur)
		select {
		case <-timer.C:
			return Message{Code: Run}
		case msg := <-s.cmds:
			timer.Stop()
			// Checkpoint is a special case that shouldn't affect sleep time, so
			// maintain it across the state transition
			if msg.Code == Checkpoint {
				msg.Until = state.Until
			}
			return msg
		}
	case Fault:
		// Special case where we potentially trim the current state to keep
		// errors from growing without bound.
		var msg Message
		msg, state.Errors = s.errHandler(s.taskID, state.Errors)
		return msg
	default:
		panic("invalid state: " + state.String())
	}
}

func run(f StatefulHandler, tid string, cmd <-chan Message) (m Message) {
	defer func() {
		if r := recover(); r != nil {
			metafora.Errorf("task=%q Run method panic()d! Applying Error message. Panic: %v", tid, r)
			m = Message{Code: Error, Err: fmt.Errorf("panic: %v", r)}
		}
	}()
	m = f(tid, cmd)
	return m
}

// Stop sends a Release message to the state machine through the command chan.
func (s *stateMachine) Stop() {
	s.cmds <- Message{Code: Release}
}

// apply a message to cause a state transition. Returns false if the state
// transition is invalid.
func apply(cur *State, m Message) (*State, bool) {
	//XXX Is a linear scan of all rules really the best option here?
	for _, trans := range Rules {
		if trans.Event == m.Code && trans.From == cur.Code {
			metafora.Debugf("Transitioned %s", trans)
			return &State{Code: trans.To, Until: m.Until}, true
		}
	}
	return cur, false
}
<|endoftext|>


<commit_before>package share_test

import (
	"os"
	"os/exec"
	"path/filepath"
	"testing"

	"github.com/nanobox-io/nanobox/models"
	"github.com/nanobox-io/nanobox/util/config"

	"github.com/nanobox-io/nanobox/util/provider/share"
)

func TestMain(m *testing.M) {
	// dont modify the actual exports
	// now we shouldnt need root :)
	provider := models.Provider{
		HostIP:  "192.168.1.2",
		MountIP: "192.168.1.4",
	}

	provider.Save()

	exec.Command("touch", "/tmp/exports").Run()
	share.EXPORTSFILE = "/tmp/exports"
	exitCode := m.Run()
	os.Remove("/tmp/exports")
	provider.Delete()
	os.Exit(exitCode)
}

func TestShare(t *testing.T) {

	path := config.LocalDir()
	if share.Exists(path) {
		t.Errorf("the path shouldnt exist but it did")
	}

	if err := share.Add(path); err != nil {
		t.Fatal("error adding", err)
	}

	if !share.Exists(path) {
		t.Fatal("the path didnt exist when it should")
	}

	subPath := filepath.Join(path, "util")
	if share.Exists(subPath) {
		t.Fatal("subpath appears to exist when it shouldnt")
	}

	if err := share.Add(subPath); err != nil {
		t.Fatal("error adding subpath", err)
	}

	if !share.Exists(subPath) {
		t.Fatal("subpath didnt exist")
	}

	if !share.Exists(path) {
		t.Fatal("path didnt exist")
	}

	if err := share.Remove(path); err != nil {
		t.Errorf("failed to remove the path %s", err)
	}

	if share.Exists(path) {
		t.Errorf("failed to remove path")
	}

	if !share.Exists(subPath) {
		t.Errorf("removing path also removed subpath")
	}

	if err := share.Remove(subPath); err != nil {
		t.Errorf("failed to remove the subPath %s", err)
	}

	if share.Exists(path) {
		t.Errorf("path exists after it shouldnt")
	}

	if share.Exists(subPath) {
		t.Errorf("subpath exists after it shouldnt")
	}

}
<commit_msg>update tests to confirm the etc/exports works<commit_after>package share_test

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/nanobox-io/nanobox/models"
	"github.com/nanobox-io/nanobox/util/config"

	"github.com/nanobox-io/nanobox/util/provider/share"
)

func TestMain(m *testing.M) {
	// dont modify the actual exports
	// now we shouldnt need root :)
	provider := models.Provider{
		HostIP:  "192.168.1.2",
		MountIP: "192.168.1.4",
	}

	provider.Save()

	share.EXPORTSFILE = "/tmp/exports"
	exitCode := m.Run()
	os.Remove("/tmp/exports")
	provider.Delete()
	os.Exit(exitCode)
}

func TestShare(t *testing.T) {

	path := config.LocalDir()
	if share.Exists(path) {
		t.Errorf("the path shouldnt exist but it did")
	}

	if err := share.Add(path); err != nil {
		t.Fatal("error adding", err)
	}

	if !share.Exists(path) {
		t.Fatal("the path didnt exist when it should")
	}

	subPath := filepath.Join(path, "util")
	if share.Exists(subPath) {
		t.Fatal("subpath appears to exist when it shouldnt")
	}

	if err := share.Add(subPath); err != nil {
		t.Fatal("error adding subpath", err)
	}

	if !share.Exists(subPath) {
		t.Fatal("subpath didnt exist")
	}

	if !share.Exists(path) {
		t.Fatal("path didnt exist")
	}

	if err := share.Remove(path); err != nil {
		t.Errorf("failed to remove the path %s", err)
	}

	if share.Exists(path) {
		t.Errorf("failed to remove path")
	}

	if !share.Exists(subPath) {
		t.Errorf("removing path also removed subpath")
	}

	if err := share.Remove(subPath); err != nil {
		t.Errorf("failed to remove the subPath %s", err)
	}

	if share.Exists(path) {
		t.Errorf("path exists after it shouldnt")
	}

	if share.Exists(subPath) {
		t.Errorf("subpath exists after it shouldnt")
	}

}
<|endoftext|>


<commit_before>package paths

import (
	"bytes"
	"fmt"
	"io"
	"testing"

	. "github.com/VonC/godbg"
	"github.com/VonC/senvgo/prgs"
	. "github.com/smartystreets/goconvey/convey"
)

func TestPath(t *testing.T) {

	FocusConvey("Tests for Path", t, func() {
		Convey("An empty path remains empty", func() {
			SetBuffers(nil)
			p := NewPath("")
			So(p.path, ShouldEqual, "")
			So(NoOutput(), ShouldBeTrue)
		})

		Convey("An http path remains unchanged", func() {
			SetBuffers(nil)
			p := NewPath(`http://a\b/../c`)
			So(p.path, ShouldEqual, `http://a\b/../c`)
			So(NoOutput(), ShouldBeTrue)
		})
		Convey("A path without trailing / must have one if it is an existing folder", func() {
			SetBuffers(nil)
			p := NewPath(`../paths`)
			So(p.path, ShouldEqual, `..\paths\`)
			So(NoOutput(), ShouldBeTrue)
		})
		Convey("A path without trailing / must keep it even if it is not an existing folder", func() {
			SetBuffers(nil)
			p := NewPath(`xxx\`)
			p = NewPath(`xxx/`)
			So(p.path, ShouldEqual, `xxx\`)
			So(NoOutput(), ShouldBeTrue)
		})

		FocusConvey("A Path can test if it is a Dir", func() {
			SetBuffers(nil)
			p := NewPath("")
			So(p.IsDir(), ShouldBeFalse)
			So(OutString(), ShouldBeEmpty)
			So(ErrString(), ShouldEqual, `open : The system cannot find the file specified.
`)
		})
	})
}

type testPrg struct{ name string }

func (tp *testPrg) Name() string { return tp.name }

type testPathWriter struct{ b *bytes.Buffer }

type testWriter struct{ w io.Writer }

func (tw *testWriter) Write(p []byte) (n int, err error) {
	s := string(p)
	if s == "prg2" {
		return 0, fmt.Errorf("Error writing '%s'", s)
	}
	return tw.w.Write(p)
}

func (tpw *testPathWriter) WritePath(prgs []prgs.Prg, w io.Writer) error {
	if err := pw.WritePath(prgs, w); err != nil {
		return err
	}
	return nil
}

func TestMain(t *testing.T) {
	tpw := &testPathWriter{b: bytes.NewBuffer(nil)}
	prgs := []prgs.Prg{&testPrg{name: "prg1"}, &testPrg{name: "prg2"}}
	Convey("Tests for Path Writer", t, func() {

		Convey("A Path writer writes any empty path if no prgs", func() {
			SetBuffers(nil)
			err := tpw.WritePath(prgs, tpw.b)
			So(err, ShouldBeNil)
			So(tpw.b.String(), ShouldEqual, "prg1prg2")
			So(NoOutput(), ShouldBeTrue)
		})

		Convey("A Path writer can report error during writing", func() {
			SetBuffers(nil)
			tpw.b = bytes.NewBuffer(nil)
			tw := &testWriter{w: tpw.b}
			err := tpw.WritePath(prgs, tw)
			So(err.Error(), ShouldEqual, "Error writing 'prg2'")
			So(tpw.b.String(), ShouldEqual, "prg1")
			So(NoOutput(), ShouldBeTrue)
		})
	})
}
<commit_msg>Test IsDir() on existing paths<commit_after>package paths

import (
	"bytes"
	"fmt"
	"io"
	"testing"

	. "github.com/VonC/godbg"
	"github.com/VonC/senvgo/prgs"
	. "github.com/smartystreets/goconvey/convey"
)

func TestPath(t *testing.T) {

	FocusConvey("Tests for Path", t, func() {
		Convey("An empty path remains empty", func() {
			SetBuffers(nil)
			p := NewPath("")
			So(p.path, ShouldEqual, "")
			So(NoOutput(), ShouldBeTrue)
		})

		Convey("An http path remains unchanged", func() {
			SetBuffers(nil)
			p := NewPath(`http://a\b/../c`)
			So(p.path, ShouldEqual, `http://a\b/../c`)
			So(NoOutput(), ShouldBeTrue)
		})
		Convey("A path without trailing / must have one if it is an existing folder", func() {
			SetBuffers(nil)
			p := NewPath(`../paths`)
			So(p.path, ShouldEqual, `..\paths\`)
			So(NoOutput(), ShouldBeTrue)
		})
		Convey("A path without trailing / must keep it even if it is not an existing folder", func() {
			SetBuffers(nil)
			p := NewPath(`xxx\`)
			p = NewPath(`xxx/`)
			So(p.path, ShouldEqual, `xxx\`)
			So(NoOutput(), ShouldBeTrue)
		})

		FocusConvey("A Path can test if it is a Dir", func() {
			SetBuffers(nil)
			p := NewPath("")
			So(p.IsDir(), ShouldBeFalse)
			So(OutString(), ShouldBeEmpty)
			So(ErrString(), ShouldEqual, `open : The system cannot find the file specified.
`)
			SetBuffers(nil)
			p = NewPath("..")
			So(p.IsDir(), ShouldBeTrue)
			So(NoOutput(), ShouldBeTrue)
			So(p.path, ShouldEqual, `..\`)
			p = NewPath("../paths")
			So(p.IsDir(), ShouldBeTrue)
			So(NoOutput(), ShouldBeTrue)
			So(p.path, ShouldEqual, `..\paths\`)
		})
	})
}

type testPrg struct{ name string }

func (tp *testPrg) Name() string { return tp.name }

type testPathWriter struct{ b *bytes.Buffer }

type testWriter struct{ w io.Writer }

func (tw *testWriter) Write(p []byte) (n int, err error) {
	s := string(p)
	if s == "prg2" {
		return 0, fmt.Errorf("Error writing '%s'", s)
	}
	return tw.w.Write(p)
}

func (tpw *testPathWriter) WritePath(prgs []prgs.Prg, w io.Writer) error {
	if err := pw.WritePath(prgs, w); err != nil {
		return err
	}
	return nil
}

func TestMain(t *testing.T) {
	tpw := &testPathWriter{b: bytes.NewBuffer(nil)}
	prgs := []prgs.Prg{&testPrg{name: "prg1"}, &testPrg{name: "prg2"}}
	Convey("Tests for Path Writer", t, func() {

		Convey("A Path writer writes any empty path if no prgs", func() {
			SetBuffers(nil)
			err := tpw.WritePath(prgs, tpw.b)
			So(err, ShouldBeNil)
			So(tpw.b.String(), ShouldEqual, "prg1prg2")
			So(NoOutput(), ShouldBeTrue)
		})

		Convey("A Path writer can report error during writing", func() {
			SetBuffers(nil)
			tpw.b = bytes.NewBuffer(nil)
			tw := &testWriter{w: tpw.b}
			err := tpw.WritePath(prgs, tw)
			So(err.Error(), ShouldEqual, "Error writing 'prg2'")
			So(tpw.b.String(), ShouldEqual, "prg1")
			So(NoOutput(), ShouldBeTrue)
		})
	})
}
<|endoftext|>


<commit_before>package agent

import (
	"strings"
	"unicode/utf8"

	"github.com/mattn/go-runewidth"
	"github.com/nsf/termbox-go"
)

var help = `
Usage:

  list        List all the todo s.
  add <task>  Add a new todo task.
  help|h      Show help info.

Press Ctr+C to quit.`

// type Agent present the togo terminate's all attributes.
type Agent struct {
	// Now, it contain togo's prompt strings.
	Chars string

	// alert string, it is "Press Ctr+Q to quit.".
	AlertString string

	// foreground color.
	Fg termbox.Attribute

	// background color.
	Bg termbox.Attribute

	// version
	Version string
}

// return a default Agent.
func New() *Agent {
	return &Agent{
		AlertString: "Press what you want. Press Ctr+C to quit.",
		Fg:          termbox.ColorWhite,
		Bg:          termbox.ColorBlack,
		Version:     "0.0.2",
	}
}

// given a string, this func print them to the terminal,
// return the total width of the string.
func (agent *Agent) PringLine(str string, x int, y int) int {
	// strWidth is the total width of the string
	strWidth := 0

	width, _ := termbox.Size()

	for len(str) > 0 {
		c, w := utf8.DecodeRuneInString(str)
		if c == utf8.RuneError {
			c = '?'
			w = 1
		}

		str = str[w:]

		termbox.SetCell(x, y, c, agent.Fg, agent.Bg)

		x += runewidth.RuneWidth(c)
		strWidth += runewidth.RuneWidth(c)
	}

	blankPosition := strWidth

	// print blank into the rest of line.
	for blankPosition < width {
		termbox.SetCell(blankPosition, y, ' ', agent.Fg, agent.Bg)
		blankPosition++
	}

	return strWidth
}

// a function to print N lines to the screen.
func (agent *Agent) PringLines(str string, x, y int) {
	lines := strings.Split(str, "\n")

	for _, line := range lines {
		agent.PringLine(line, x, y)
		x = 0
		y++
	}
}

// backspace key support
func (agent *Agent) DeletePromp() {
	_, size := utf8.DecodeLastRuneInString(agent.Chars)
	agent.Chars = agent.Chars[:len(agent.Chars)-size]
	agent.DrawPromp("")
}

// display help info
func (agent *Agent) ShowHelp() {
	termbox.Clear(agent.Fg, agent.Bg)

	agent.Chars = ""
	agent.DrawPromp("")
	agent.PringLines(help, 0, 1)
	termbox.Flush()
}

// parse command. Now, just support help command to show the help info.
func (agent *Agent) ParseCmd() {
	command := strings.Split(agent.Chars, " ")[0]

	switch command {

	// help command.
	case "help", "h":
		agent.ShowHelp()

	// default, print what you press.
	default:
		termbox.Clear(agent.Fg, agent.Bg)
		chars := agent.Chars
		agent.Chars = ""
		agent.DrawPromp("")

		agent.PringLine(chars, 0, 2)
		termbox.Flush()
	}
}

// draw the promp line.
func (agent *Agent) DrawPromp(str string) {
	agent.Chars = agent.Chars + string(str)

	prompStr := "TOGO> " + agent.Chars

	x := 0
	y := 0

	width := agent.PringLine(prompStr, x, y)
	termbox.SetCursor(width, 0)
	termbox.Flush()
}

// the public api of the Agent package.
func (agent *Agent) Run() {
	err := termbox.Init()
	if err != nil {
		panic(err)
	}

	defer termbox.Close()
	termbox.Clear(agent.Fg, agent.Bg)
	agent.DrawPromp("")

	agent.PringLine(agent.AlertString, 0, 2)
	agent.PringLine("Latest Version: "+agent.Version, 0, 3)
	termbox.Flush()

loop:

	for {
		switch ev := termbox.PollEvent(); ev.Type {
		case termbox.EventKey:
			switch ev.Key {

			// quit the program.
			case termbox.KeyCtrlC:
				break loop

			// puts " " to the screen, when we press space.
			case termbox.KeySpace:
				str := " "
				agent.DrawPromp(str)

			// support back space key
			case termbox.KeyBackspace, termbox.KeyBackspace2:
				agent.DeletePromp()

			// when we press Enter, parse the command.
			case termbox.KeyEnter:
				agent.ParseCmd()

			// convert rune to string, then draw the promp.
			default:
				str := string(ev.Ch)
				agent.DrawPromp(str)
			}
		}
	}
}
<commit_msg>v0.0.3<commit_after>package agent

import (
	"strings"
	"unicode/utf8"

	"github.com/mattn/go-runewidth"
	"github.com/nsf/termbox-go"
)

var help = `
Usage:

  list        List all the todo s.
  add <task>  Add a new todo task.
  help|h      Show help info.

Press Ctr+C to quit.`

// type Agent present the togo terminate's all attributes.
type Agent struct {
	// Now, it contain togo's prompt strings.
	Chars string

	// alert string, it is "Press Ctr+Q to quit.".
	AlertString string

	// foreground color.
	Fg termbox.Attribute

	// background color.
	Bg termbox.Attribute

	// version
	Version string
}

// return a default Agent.
func New() *Agent {
	return &Agent{
		AlertString: "Press what you want. Press Ctr+C to quit.",
		Fg:          termbox.ColorWhite,
		Bg:          termbox.ColorBlack,
		Version:     "0.0.3",
	}
}

// given a string, this func print them to the terminal,
// return the total width of the string.
func (agent *Agent) PringLine(str string, x int, y int) int {
	// strWidth is the total width of the string
	strWidth := 0

	width, _ := termbox.Size()

	for len(str) > 0 {
		c, w := utf8.DecodeRuneInString(str)
		if c == utf8.RuneError {
			c = '?'
			w = 1
		}

		str = str[w:]

		termbox.SetCell(x, y, c, agent.Fg, agent.Bg)

		x += runewidth.RuneWidth(c)
		strWidth += runewidth.RuneWidth(c)
	}

	blankPosition := strWidth

	// print blank into the rest of line.
	for blankPosition < width {
		termbox.SetCell(blankPosition, y, ' ', agent.Fg, agent.Bg)
		blankPosition++
	}

	return strWidth
}

// a function to print N lines to the screen.
func (agent *Agent) PringLines(str string, x, y int) {
	lines := strings.Split(str, "\n")

	for _, line := range lines {
		agent.PringLine(line, x, y)
		x = 0
		y++
	}
}

// backspace key support
func (agent *Agent) DeletePromp() {
	_, size := utf8.DecodeLastRuneInString(agent.Chars)
	agent.Chars = agent.Chars[:len(agent.Chars)-size]
	agent.DrawPromp("")
}

// display help info
func (agent *Agent) ShowHelp() {
	termbox.Clear(agent.Fg, agent.Bg)

	agent.Chars = ""
	agent.DrawPromp("")
	agent.PringLines(help, 0, 1)
	termbox.Flush()
}

// parse command. Now, just support help command to show the help info.
func (agent *Agent) ParseCmd() {
	command := strings.Split(agent.Chars, " ")[0]

	switch command {

	// help command.
	case "help", "h":
		agent.ShowHelp()

	// default, print what you press.
	default:
		termbox.Clear(agent.Fg, agent.Bg)
		chars := agent.Chars
		agent.Chars = ""
		agent.DrawPromp("")

		agent.PringLine(chars, 0, 2)
		termbox.Flush()
	}
}

// draw the promp line.
func (agent *Agent) DrawPromp(str string) {
	agent.Chars = agent.Chars + string(str)

	prompStr := "TOGO> " + agent.Chars

	x := 0
	y := 0

	width := agent.PringLine(prompStr, x, y)
	termbox.SetCursor(width, 0)
	termbox.Flush()
}

// the public api of the Agent package.
func (agent *Agent) Run() {
	err := termbox.Init()
	if err != nil {
		panic(err)
	}

	defer termbox.Close()
	termbox.Clear(agent.Fg, agent.Bg)
	agent.DrawPromp("")

	agent.PringLine(agent.AlertString, 0, 2)
	agent.PringLine("Latest Version: "+agent.Version, 0, 3)
	termbox.Flush()

loop:

	for {
		switch ev := termbox.PollEvent(); ev.Type {
		case termbox.EventKey:
			switch ev.Key {

			// quit the program.
			case termbox.KeyCtrlC:
				break loop

			// puts " " to the screen, when we press space.
			case termbox.KeySpace:
				str := " "
				agent.DrawPromp(str)

			// support back space key
			case termbox.KeyBackspace, termbox.KeyBackspace2:
				agent.DeletePromp()

			// when we press Enter, parse the command.
			case termbox.KeyEnter:
				agent.ParseCmd()

			// convert rune to string, then draw the promp.
			default:
				str := string(ev.Ch)
				agent.DrawPromp(str)
			}
		}
	}
}
<|endoftext|>


<commit_before>package agent

import (
	"fmt"
	"net"
	"net/url"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/drone/drone/build"
	"github.com/drone/drone/model"
	"github.com/drone/drone/queue"
	"github.com/drone/drone/version"
	"github.com/drone/drone/yaml"
	"github.com/drone/drone/yaml/expander"
	"github.com/drone/drone/yaml/transform"
)

type Logger interface {
	Write(*build.Line)
}

type Agent struct {
	Update    UpdateFunc
	Logger    LoggerFunc
	Engine    build.Engine
	Timeout   time.Duration
	Platform  string
	Namespace string
	Disable   []string
	Escalate  []string
	Netrc     []string
	Local     string
	Pull      bool
}

func (a *Agent) Poll() error {

	// logrus.Infof("Starting build %s/%s#%d.%d",
	// 	payload.Repo.Owner, payload.Repo.Name, payload.Build.Number, payload.Job.Number)
	//
	//
	// 	logrus.Infof("Finished build %s/%s#%d.%d",
	// 		payload.Repo.Owner, payload.Repo.Name, payload.Build.Number, payload.Job.Number)

	return nil
}

func (a *Agent) Run(payload *queue.Work, cancel <-chan bool) error {

	payload.Job.Status = model.StatusRunning
	payload.Job.Started = time.Now().Unix()

	spec, err := a.prep(payload)
	if err != nil {
		payload.Job.Error = err.Error()
		payload.Job.ExitCode = 255
		payload.Job.Finished = payload.Job.Started
		payload.Job.Status = model.StatusError
		a.Update(payload)
		return err
	}
	a.Update(payload)
	err = a.exec(spec, payload, cancel)

	if err != nil {
		payload.Job.ExitCode = 255
		payload.Job.Error = err.Error()
	}
	if exitErr, ok := err.(*build.ExitError); ok {
		payload.Job.ExitCode = exitErr.Code
		payload.Job.Error = "" // exit errors are already written to the log
	}

	payload.Job.Finished = time.Now().Unix()

	switch payload.Job.ExitCode {
	case 128, 130, 137:
		payload.Job.Status = model.StatusKilled
	case 0:
		payload.Job.Status = model.StatusSuccess
	default:
		payload.Job.Status = model.StatusFailure
	}

	a.Update(payload)

	return err
}

func (a *Agent) prep(w *queue.Work) (*yaml.Config, error) {

	envs := toEnv(w)
	w.Yaml = expander.ExpandString(w.Yaml, envs)

	// inject the netrc file into the clone plugin if the repository is
	// private and requires authentication.
	var secrets []*model.Secret
	if w.Verified {
		secrets = append(secrets, w.Secrets...)
	}

	if w.Repo.IsPrivate {
		secrets = append(secrets, &model.Secret{
			Name:   "DRONE_NETRC_USERNAME",
			Value:  w.Netrc.Login,
			Images: []string{"*"},
			Events: []string{"*"},
		})
		secrets = append(secrets, &model.Secret{
			Name:   "DRONE_NETRC_PASSWORD",
			Value:  w.Netrc.Password,
			Images: []string{"*"},
			Events: []string{"*"},
		})
		secrets = append(secrets, &model.Secret{
			Name:   "DRONE_NETRC_MACHINE",
			Value:  w.Netrc.Machine,
			Images: []string{"*"},
			Events: []string{"*"},
		})
	}

	conf, err := yaml.ParseString(w.Yaml)
	if err != nil {
		return nil, err
	}

	src := "src"
	if url, _ := url.Parse(w.Repo.Link); url != nil {
		host, _, err := net.SplitHostPort(url.Host)
		if err == nil {
			url.Host = host
		}
		src = filepath.Join(src, url.Host, url.Path)
	}

	transform.Clone(conf, w.Repo.Kind)
	transform.Environ(conf, envs)
	transform.DefaultFilter(conf)
	if w.BuildLast != nil {
		transform.ChangeFilter(conf, w.BuildLast.Status)
	}

	transform.ImageSecrets(conf, secrets, w.Build.Event)
	transform.Identifier(conf)
	transform.WorkspaceTransform(conf, "/drone", src)

	if err := transform.Check(conf, w.Repo.IsTrusted); err != nil {
		return nil, err
	}

	transform.CommandTransform(conf)
	transform.ImagePull(conf, a.Pull)
	transform.ImageTag(conf)
	transform.ImageName(conf)
	transform.ImageNamespace(conf, a.Namespace)
	if err := transform.ImageEscalate(conf, a.Escalate); err != nil {
		return nil, err
	}
	transform.PluginParams(conf)

	if a.Local != "" {
		transform.PluginDisable(conf, a.Disable)
		transform.ImageVolume(conf, []string{a.Local + ":" + conf.Workspace.Path})
	}

	transform.Pod(conf)

	return conf, nil
}

func (a *Agent) exec(spec *yaml.Config, payload *queue.Work, cancel <-chan bool) error {

	conf := build.Config{
		Engine: a.Engine,
		Buffer: 500,
	}

	pipeline := conf.Pipeline(spec)
	defer pipeline.Teardown()

	// setup the build environment
	if err := pipeline.Setup(); err != nil {
		return err
	}

	timeout := time.After(time.Duration(payload.Repo.Timeout) * time.Minute)

	for {
		select {
		case <-pipeline.Done():
			return pipeline.Err()
		case <-cancel:
			pipeline.Stop()
			return fmt.Errorf("termination request received, build cancelled")
		case <-timeout:
			pipeline.Stop()
			return fmt.Errorf("maximum time limit exceeded, build cancelled")
		case <-time.After(a.Timeout):
			pipeline.Stop()
			return fmt.Errorf("terminal inactive for %v, build cancelled", a.Timeout)
		case <-pipeline.Next():

			// TODO(bradrydzewski) this entire block of code should probably get
			// encapsulated in the pipeline.
			status := model.StatusSuccess
			if pipeline.Err() != nil {
				status = model.StatusFailure
			}
			// updates the build status passed into each container. I realize this is
			// a bit out of place and will work to resolve.
			pipeline.Head().Environment["DRONE_BUILD_STATUS"] = status

			if !pipeline.Head().Constraints.Match(
				a.Platform,
				payload.Build.Deploy,
				payload.Build.Event,
				payload.Build.Branch,
				status, payload.Job.Environment) { // TODO: fix this whole section

				pipeline.Skip()
			} else {
				pipeline.Exec()
			}
		case line := <-pipeline.Pipe():
			a.Logger(line)
		}
	}
}

func toEnv(w *queue.Work) map[string]string {
	envs := map[string]string{
		"CI":                         "drone",
		"DRONE":                      "true",
		"DRONE_ARCH":                 "linux/amd64",
		"DRONE_REPO":                 w.Repo.FullName,
		"DRONE_REPO_SCM":             w.Repo.Kind,
		"DRONE_REPO_OWNER":           w.Repo.Owner,
		"DRONE_REPO_NAME":            w.Repo.Name,
		"DRONE_REPO_LINK":            w.Repo.Link,
		"DRONE_REPO_AVATAR":          w.Repo.Avatar,
		"DRONE_REPO_BRANCH":          w.Repo.Branch,
		"DRONE_REPO_PRIVATE":         fmt.Sprintf("%v", w.Repo.IsPrivate),
		"DRONE_REPO_TRUSTED":         fmt.Sprintf("%v", w.Repo.IsTrusted),
		"DRONE_REMOTE_URL":           w.Repo.Clone,
		"DRONE_COMMIT_SHA":           w.Build.Commit,
		"DRONE_COMMIT_REF":           w.Build.Ref,
		"DRONE_COMMIT_BRANCH":        w.Build.Branch,
		"DRONE_COMMIT_LINK":          w.Build.Link,
		"DRONE_COMMIT_MESSAGE":       w.Build.Message,
		"DRONE_COMMIT_AUTHOR":        w.Build.Author,
		"DRONE_COMMIT_AUTHOR_EMAIL":  w.Build.Email,
		"DRONE_COMMIT_AUTHOR_AVATAR": w.Build.Avatar,
		"DRONE_BUILD_NUMBER":         fmt.Sprintf("%d", w.Build.Number),
		"DRONE_BUILD_EVENT":          w.Build.Event,
		"DRONE_BUILD_STATUS":         w.Build.Status,
		"DRONE_BUILD_LINK":           fmt.Sprintf("%s/%s/%d", w.System.Link, w.Repo.FullName, w.Build.Number),
		"DRONE_BUILD_CREATED":        fmt.Sprintf("%d", w.Build.Created),
		"DRONE_BUILD_STARTED":        fmt.Sprintf("%d", w.Build.Started),
		"DRONE_BUILD_FINISHED":       fmt.Sprintf("%d", w.Build.Finished),
		"DRONE_YAML_VERIFIED":        fmt.Sprintf("%v", w.Verified),
		"DRONE_YAML_SIGNED":          fmt.Sprintf("%v", w.Signed),
		"DRONE_BRANCH":               w.Build.Branch,
		"DRONE_COMMIT":               w.Build.Commit,
		"DRONE_VERSION":              version.Version,
	}

	if w.Build.Event == model.EventTag {
		envs["DRONE_TAG"] = strings.TrimPrefix(w.Build.Ref, "refs/tags/")
	}
	if w.Build.Event == model.EventPull {
		envs["DRONE_PULL_REQUEST"] = pullRegexp.FindString(w.Build.Ref)
	}
	if w.Build.Event == model.EventDeploy {
		envs["DRONE_DEPLOY_TO"] = w.Build.Deploy
	}

	if w.BuildLast != nil {
		envs["DRONE_PREV_BUILD_STATUS"] = w.BuildLast.Status
		envs["DRONE_PREV_BUILD_NUMBER"] = fmt.Sprintf("%v", w.BuildLast.Number)
		envs["DRONE_PREV_COMMIT_SHA"] = w.BuildLast.Commit
	}

	// inject matrix values as environment variables
	for key, val := range w.Job.Environment {
		envs[key] = val
	}
	return envs
}

var pullRegexp = regexp.MustCompile("\\d+")
<commit_msg>Expose job properties in environment<commit_after>package agent

import (
	"fmt"
	"net"
	"net/url"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/drone/drone/build"
	"github.com/drone/drone/model"
	"github.com/drone/drone/queue"
	"github.com/drone/drone/version"
	"github.com/drone/drone/yaml"
	"github.com/drone/drone/yaml/expander"
	"github.com/drone/drone/yaml/transform"
)

type Logger interface {
	Write(*build.Line)
}

type Agent struct {
	Update    UpdateFunc
	Logger    LoggerFunc
	Engine    build.Engine
	Timeout   time.Duration
	Platform  string
	Namespace string
	Disable   []string
	Escalate  []string
	Netrc     []string
	Local     string
	Pull      bool
}

func (a *Agent) Poll() error {

	// logrus.Infof("Starting build %s/%s#%d.%d",
	// 	payload.Repo.Owner, payload.Repo.Name, payload.Build.Number, payload.Job.Number)
	//
	//
	// 	logrus.Infof("Finished build %s/%s#%d.%d",
	// 		payload.Repo.Owner, payload.Repo.Name, payload.Build.Number, payload.Job.Number)

	return nil
}

func (a *Agent) Run(payload *queue.Work, cancel <-chan bool) error {

	payload.Job.Status = model.StatusRunning
	payload.Job.Started = time.Now().Unix()

	spec, err := a.prep(payload)
	if err != nil {
		payload.Job.Error = err.Error()
		payload.Job.ExitCode = 255
		payload.Job.Finished = payload.Job.Started
		payload.Job.Status = model.StatusError
		a.Update(payload)
		return err
	}
	a.Update(payload)
	err = a.exec(spec, payload, cancel)

	if err != nil {
		payload.Job.ExitCode = 255
		payload.Job.Error = err.Error()
	}
	if exitErr, ok := err.(*build.ExitError); ok {
		payload.Job.ExitCode = exitErr.Code
		payload.Job.Error = "" // exit errors are already written to the log
	}

	payload.Job.Finished = time.Now().Unix()

	switch payload.Job.ExitCode {
	case 128, 130, 137:
		payload.Job.Status = model.StatusKilled
	case 0:
		payload.Job.Status = model.StatusSuccess
	default:
		payload.Job.Status = model.StatusFailure
	}

	a.Update(payload)

	return err
}

func (a *Agent) prep(w *queue.Work) (*yaml.Config, error) {

	envs := toEnv(w)
	w.Yaml = expander.ExpandString(w.Yaml, envs)

	// inject the netrc file into the clone plugin if the repository is
	// private and requires authentication.
	var secrets []*model.Secret
	if w.Verified {
		secrets = append(secrets, w.Secrets...)
	}

	if w.Repo.IsPrivate {
		secrets = append(secrets, &model.Secret{
			Name:   "DRONE_NETRC_USERNAME",
			Value:  w.Netrc.Login,
			Images: []string{"*"},
			Events: []string{"*"},
		})
		secrets = append(secrets, &model.Secret{
			Name:   "DRONE_NETRC_PASSWORD",
			Value:  w.Netrc.Password,
			Images: []string{"*"},
			Events: []string{"*"},
		})
		secrets = append(secrets, &model.Secret{
			Name:   "DRONE_NETRC_MACHINE",
			Value:  w.Netrc.Machine,
			Images: []string{"*"},
			Events: []string{"*"},
		})
	}

	conf, err := yaml.ParseString(w.Yaml)
	if err != nil {
		return nil, err
	}

	src := "src"
	if url, _ := url.Parse(w.Repo.Link); url != nil {
		host, _, err := net.SplitHostPort(url.Host)
		if err == nil {
			url.Host = host
		}
		src = filepath.Join(src, url.Host, url.Path)
	}

	transform.Clone(conf, w.Repo.Kind)
	transform.Environ(conf, envs)
	transform.DefaultFilter(conf)
	if w.BuildLast != nil {
		transform.ChangeFilter(conf, w.BuildLast.Status)
	}

	transform.ImageSecrets(conf, secrets, w.Build.Event)
	transform.Identifier(conf)
	transform.WorkspaceTransform(conf, "/drone", src)

	if err := transform.Check(conf, w.Repo.IsTrusted); err != nil {
		return nil, err
	}

	transform.CommandTransform(conf)
	transform.ImagePull(conf, a.Pull)
	transform.ImageTag(conf)
	transform.ImageName(conf)
	transform.ImageNamespace(conf, a.Namespace)
	if err := transform.ImageEscalate(conf, a.Escalate); err != nil {
		return nil, err
	}
	transform.PluginParams(conf)

	if a.Local != "" {
		transform.PluginDisable(conf, a.Disable)
		transform.ImageVolume(conf, []string{a.Local + ":" + conf.Workspace.Path})
	}

	transform.Pod(conf)

	return conf, nil
}

func (a *Agent) exec(spec *yaml.Config, payload *queue.Work, cancel <-chan bool) error {

	conf := build.Config{
		Engine: a.Engine,
		Buffer: 500,
	}

	pipeline := conf.Pipeline(spec)
	defer pipeline.Teardown()

	// setup the build environment
	if err := pipeline.Setup(); err != nil {
		return err
	}

	timeout := time.After(time.Duration(payload.Repo.Timeout) * time.Minute)

	for {
		select {
		case <-pipeline.Done():
			return pipeline.Err()
		case <-cancel:
			pipeline.Stop()
			return fmt.Errorf("termination request received, build cancelled")
		case <-timeout:
			pipeline.Stop()
			return fmt.Errorf("maximum time limit exceeded, build cancelled")
		case <-time.After(a.Timeout):
			pipeline.Stop()
			return fmt.Errorf("terminal inactive for %v, build cancelled", a.Timeout)
		case <-pipeline.Next():

			// TODO(bradrydzewski) this entire block of code should probably get
			// encapsulated in the pipeline.
			status := model.StatusSuccess
			if pipeline.Err() != nil {
				status = model.StatusFailure
			}
			// updates the build status passed into each container. I realize this is
			// a bit out of place and will work to resolve.
			pipeline.Head().Environment["DRONE_BUILD_STATUS"] = status

			if !pipeline.Head().Constraints.Match(
				a.Platform,
				payload.Build.Deploy,
				payload.Build.Event,
				payload.Build.Branch,
				status, payload.Job.Environment) { // TODO: fix this whole section

				pipeline.Skip()
			} else {
				pipeline.Exec()
			}
		case line := <-pipeline.Pipe():
			a.Logger(line)
		}
	}
}

func toEnv(w *queue.Work) map[string]string {
	envs := map[string]string{
		"CI":                         "drone",
		"DRONE":                      "true",
		"DRONE_ARCH":                 "linux/amd64",
		"DRONE_REPO":                 w.Repo.FullName,
		"DRONE_REPO_SCM":             w.Repo.Kind,
		"DRONE_REPO_OWNER":           w.Repo.Owner,
		"DRONE_REPO_NAME":            w.Repo.Name,
		"DRONE_REPO_LINK":            w.Repo.Link,
		"DRONE_REPO_AVATAR":          w.Repo.Avatar,
		"DRONE_REPO_BRANCH":          w.Repo.Branch,
		"DRONE_REPO_PRIVATE":         fmt.Sprintf("%v", w.Repo.IsPrivate),
		"DRONE_REPO_TRUSTED":         fmt.Sprintf("%v", w.Repo.IsTrusted),
		"DRONE_REMOTE_URL":           w.Repo.Clone,
		"DRONE_COMMIT_SHA":           w.Build.Commit,
		"DRONE_COMMIT_REF":           w.Build.Ref,
		"DRONE_COMMIT_BRANCH":        w.Build.Branch,
		"DRONE_COMMIT_LINK":          w.Build.Link,
		"DRONE_COMMIT_MESSAGE":       w.Build.Message,
		"DRONE_COMMIT_AUTHOR":        w.Build.Author,
		"DRONE_COMMIT_AUTHOR_EMAIL":  w.Build.Email,
		"DRONE_COMMIT_AUTHOR_AVATAR": w.Build.Avatar,
		"DRONE_BUILD_NUMBER":         fmt.Sprintf("%d", w.Build.Number),
		"DRONE_BUILD_EVENT":          w.Build.Event,
		"DRONE_BUILD_STATUS":         w.Build.Status,
		"DRONE_BUILD_LINK":           fmt.Sprintf("%s/%s/%d", w.System.Link, w.Repo.FullName, w.Build.Number),
		"DRONE_BUILD_CREATED":        fmt.Sprintf("%d", w.Build.Created),
		"DRONE_BUILD_STARTED":        fmt.Sprintf("%d", w.Build.Started),
		"DRONE_BUILD_FINISHED":       fmt.Sprintf("%d", w.Build.Finished),
		"DRONE_JOB_NUMBER":           fmt.Sprintf("%d", w.Job.Number),
		"DRONE_JOB_STATUS":           w.Job.Status,
		"DRONE_JOB_ERROR":            w.Job.Error,
		"DRONE_JOB_EXIT_CODE":        fmt.Sprintf("%d", w.Job.ExitCode),
		"DRONE_JOB_STARTED":          fmt.Sprintf("%d", w.Job.Started),
		"DRONE_JOB_FINISHED":         fmt.Sprintf("%d", w.Job.Finished),
		"DRONE_YAML_VERIFIED":        fmt.Sprintf("%v", w.Verified),
		"DRONE_YAML_SIGNED":          fmt.Sprintf("%v", w.Signed),
		"DRONE_BRANCH":               w.Build.Branch,
		"DRONE_COMMIT":               w.Build.Commit,
		"DRONE_VERSION":              version.Version,
	}

	if w.Build.Event == model.EventTag {
		envs["DRONE_TAG"] = strings.TrimPrefix(w.Build.Ref, "refs/tags/")
	}
	if w.Build.Event == model.EventPull {
		envs["DRONE_PULL_REQUEST"] = pullRegexp.FindString(w.Build.Ref)
	}
	if w.Build.Event == model.EventDeploy {
		envs["DRONE_DEPLOY_TO"] = w.Build.Deploy
	}

	if w.BuildLast != nil {
		envs["DRONE_PREV_BUILD_STATUS"] = w.BuildLast.Status
		envs["DRONE_PREV_BUILD_NUMBER"] = fmt.Sprintf("%v", w.BuildLast.Number)
		envs["DRONE_PREV_COMMIT_SHA"] = w.BuildLast.Commit
	}

	// inject matrix values as environment variables
	for key, val := range w.Job.Environment {
		envs[key] = val
	}
	return envs
}

var pullRegexp = regexp.MustCompile("\\d+")
<|endoftext|>