~ach/hermes

48ca118d567b42fc989cd0ee091c2cd03841c62d — Andrew Chambers 3 years ago 88b3a0b signalsocks
Rework signal handling code.

Reroute signal forwarding via a unix socket,
the goal is to free up stdin to make way for
build debugging.
M src/cmd/builtins/runcmds.go => src/cmd/builtins/runcmds.go +4 -4
@@ 23,13 23,13 @@ func runcmdsMain() {

	ctx, cancelCtx := context.WithCancel(context.Background())

	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, os.Interrupt, unix.SIGHUP)
	go func() {
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt)
		<-c
		<-sigs
		_, _ = fmt.Fprintf(os.Stderr, "got interrupt, cancelling build.\n")
		cancelCtx()
		<-c
		<-sigs
		die("Second interrupt, aborting.")
	}()


M src/cmd/hermes-pkgstore/build.go => src/cmd/hermes-pkgstore/build.go +62 -16
@@ 4,23 4,38 @@ import (
	"bufio"
	"context"
	"fmt"
	"io"
	"net"
	"os"
	"os/signal"
	"path/filepath"
	"time"

	"github.com/andrewchambers/hermes/pkgs"
	"github.com/andrewchambers/hermes/store"
	"github.com/cenkalti/backoff"
	flag "github.com/spf13/pflag"
	"golang.org/x/sys/unix"
)

func waitForFileToExist(ctx context.Context, delay, timeout time.Duration, fpath string) error {
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()
	bo := backoff.WithContext(backoff.NewConstantBackOff(delay), ctx)
	return backoff.Retry(func() error {
		_, err := os.Stat(fpath)
		if err != nil && !os.IsNotExist(err) {
			return backoff.Permanent(err)
		}
		return err
	}, bo)
}

func buildMain() {

	normalizedPackage := flag.String("normalized-package", "", "Package file to open.")
	link := flag.String("link", "", "Create a link and add it as a gc root.")
	fetchSocket := flag.String("fetch-socket", "", "Unix socket of fetch server.")
	stdinSignals := flag.Bool("stdin-signals", false, "Read stdin for signals.")
	fetchSocket := flag.String("fetch-socket", "./fetch.sock", "Unix socket of fetch server.")
	signalSocket := flag.String("signal-socket", "", "Unix socket to open and read for signals.")

	flag.Parse()



@@ 30,35 45,66 @@ func buildMain() {
	buildCtx, cancelBuild := context.WithCancel(context.Background())
	defer cancelBuild()

	sigs := make(chan os.Signal, 1)
	// We can still print errors without being stopped.
	signal.Ignore(unix.SIGTTOU)
	signal.Notify(sigs, os.Interrupt, unix.SIGTERM, unix.SIGHUP)
	go func() {
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt, unix.SIGTERM, unix.SIGHUP)
		<-c
		<-sigs
		_, _ = fmt.Fprintf(os.Stderr, "build cancelling\n")
		cancelBuild()
		<-c
		// Don't treat SIGHUP as a reason to abort.
		// This is to avoid a case we send a TERM exit
		// and then we get a SIGHUP on disconnect causing a
		// hard abort.
		for <-sigs == unix.SIGHUP {
		}
		die("build aborting.")
	}()

	if *stdinSignals {
		// Reading signals passed from stdin lets the non setuid
	// For these two cases, we assume the socket already exists,
	// or will very soon. The main reason for this delay is when
	// a remote build happens, ssh has no way to signal to us
	// the forwarding channel is up, but it is likely to come up
	// before or very close to when this command reached the ssh
	// server.
	const socketWaitDelay = 1 * time.Millisecond
	const socketWaitTimeout = 20 * time.Second

	err := waitForFileToExist(buildCtx, socketWaitDelay, socketWaitTimeout, *fetchSocket)
	if err != nil {
		die("Error while waiting for fetch socket: %s\n", err)
	}

	if *signalSocket != "" {
		// Reading signals passed via a socket lets the non setuid
		// hermes notify the setuid pkgstore binary of events.
		err = waitForFileToExist(buildCtx, socketWaitDelay, socketWaitTimeout, *signalSocket)
		if err != nil {
			die("Error while waiting for signal socket: %s\n", err)
		}

		go func() {
			rdr := bufio.NewReader(os.Stdin)
			signals, err := net.Dial("unix", *signalSocket)
			if err != nil {
				_, _ = fmt.Fprintf(os.Stderr, "hermes-pkgstore unable to open signal socket: %s\n", err)
				cancelBuild()
			}
			defer signals.Close()

			rdr := bufio.NewReader(signals)
			for {
				ln, err := rdr.ReadString('\n')
				if ln != "" {
					switch ln {
					case "Interrupt\n":
						fallthrough
					case "Term\n":
					case "Interrupt\n", "Term\n":
						_, _ = fmt.Fprintf(os.Stderr, "hermes-pkgstore got shutdown signal\n")
						cancelBuild()
					default:
						_, _ = fmt.Fprintf(os.Stderr, "hermes-pkgstore got an unknown signal: %q\n", ln)
						cancelBuild()
					}
				}
				if err == io.EOF {
					break
				}
				if err != nil {
					break
				}

M src/cmd/hermes/build.go => src/cmd/hermes/build.go +250 -208
@@ 7,6 7,7 @@ import (
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"os"


@@ 15,12 16,16 @@ import (
	"path"
	"path/filepath"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/andrewchambers/hermes/extrasqlite"
	"github.com/andrewchambers/hermes/hscript/hscript"
	"github.com/andrewchambers/hermes/pkgs"
	"github.com/andrewchambers/hermes/proctools"
	"github.com/bvinc/go-sqlite-lite/sqlite3"
	"github.com/cenkalti/backoff"
	"github.com/kballard/go-shellquote"
	"github.com/pkg/errors"
	"golang.org/x/sync/errgroup"


@@ 31,12 36,13 @@ import (
)

type buildState struct {
	tmpDir          string
	fetchSocketPath string
	mirrorDBPath    string
	mirrorDB        *sqlite3.Conn
	nextGCRootPath  func() string
	nextPkgEdnPath  func() string
	tmpDir               string
	fetchSocketPath      string
	mirrorDBPath         string
	mirrorDB             *sqlite3.Conn
	nextGCRootPath       func() string
	nextPkgEdnPath       func() string
	nextSignalSocketPath func() string
}

type remoteBuildState struct {


@@ 83,103 89,72 @@ func cleanupTemporaryGCRoots(cfg *Config, dir string) error {
	return nil
}

// Start a hermes fetch server and return a closure which
// stops it and cleans up any resources/temporary directories
// it may have created.
//
// The reason fetch is an external process is so it can reliably
// be cancelled with a context, even when the fetch apis we use
// don't necessarily support them reliably.
func startFetchurlServer(ctx context.Context, state *buildState) (cleanup func(), err error) {
	cleanup = func() {}
	defer func() {
		if err != nil {
			cleanup()
		}
	}()

	self, err := os.Executable()
	if err != nil {
		return cleanup, errors.Wrap(err, "unable to determine path to hermes binary")
	}

	a, b, err := os.Pipe()
	if err != nil {
		return cleanup, errors.Wrap(err, "unable to create a temporary directory")
	}

	cleanup = wrapCleanup(cleanup, func() { _ = a.Close(); _ = b.Close() })

	args := []string{
		"fetch-server",
		"--socket", state.fetchSocketPath,
		"--mirrors", state.mirrorDBPath,
		"--work-dir", state.tmpDir,
	}
	cmd := exec.Command(self, args...)
	cmd.Stdout = b
	cmd.Stderr = os.Stdout
	err = cmd.Start()
func startBackgroundCommandInErrGroup(ctx context.Context, g *errgroup.Group, cmd *exec.Cmd) (func(), error) {
	err := cmd.Start()
	if err != nil {
		return cleanup, errors.Wrap(err, "error starting fetch server")
		return func() {}, errors.Wrapf(err, "error starting %#v", cmd.Args)
	}
	cleanup = wrapCleanup(cleanup, func() {
		_ = cmd.Process.Signal(unix.SIGTERM)
		_ = cmd.Wait()
	})

	var g errgroup.Group
	up := make(chan struct{}, 1)
	shutdownOnce := &sync.Once{}
	shutdown := make(chan struct{}, 1)

	g.Go(func() error {
		defer close(up)

		var upbuf [3]byte
		_, err = io.ReadFull(a, upbuf[:])
		if err != nil {
			return errors.Wrap(err, "error waiting for server up notification")
		}
		if string(upbuf[:]) != "up\n" {
			return errors.New("expected 'up' from fetch server.")
		select {
		case <-ctx.Done():
			_ = cmd.Process.Signal(unix.SIGTERM)
			return ctx.Err()
		case <-shutdown:
			_ = cmd.Process.Signal(unix.SIGTERM)
			return nil
		}
		return nil
	})

	g.Go(func() error {
		err := cmd.Wait()
		select {
		case <-ctx.Done():
			_ = a.Close()
			_ = b.Close()
			_ = cmd.Process.Signal(unix.SIGTERM)
			return ctx.Err()
		case <-up:
		case <-shutdown:
			return nil
		default:
			ctxErr := ctx.Err()
			if ctxErr != nil {
				return ctxErr
			}
			return errors.Wrapf(err, "child background process %#v died unexpectedly", cmd.Args)
		}
	})

	err = g.Wait()
	if err != nil {
		return cleanup, err
	cleanup := func() {
		shutdownOnce.Do(func() { close(shutdown) })
	}

	cleanup = wrapCleanup(cleanup, func() {
		_ = cmd.Process.Signal(unix.SIGTERM)
		_ = cmd.Wait()
	})
	return cleanup, nil

	err = os.Chmod(state.fetchSocketPath, 0770)
}

// Start a hermes fetch server and return a closure which
// stops it and cleans up any resources/temporary directories
// it may have created.
//
// The reason fetch is an external process is so it can reliably
// be cancelled.
func startFetchurlServer(ctx context.Context, g *errgroup.Group, state *buildState) (cleanup func(), err error) {
	self, err := os.Executable()
	if err != nil {
		return cleanup, err
		return cleanup, errors.Wrap(err, "unable to determine path to hermes binary")
	}

	cleanup = wrapCleanup(cleanup, func() {
		_ = os.Remove(state.fetchSocketPath)
	})

	return cleanup, nil
	args := []string{
		"fetch-server",
		"--socket", state.fetchSocketPath,
		"--mirrors", state.mirrorDBPath,
		"--work-dir", state.tmpDir,
	}
	cmd := exec.Command(self, args...)
	cmd.Stderr = os.Stderr
	return startBackgroundCommandInErrGroup(ctx, g, cmd)
}

func startRemoteMasterConnection(ctx context.Context, remoteState *remoteBuildState) (cleanup func(), err error) {
func startRemoteMasterConnection(ctx context.Context, g *errgroup.Group, remoteState *remoteBuildState) (cleanup func(), err error) {
	cleanup = func() {}
	defer func() {
		if err != nil {


@@ 193,20 168,32 @@ func startRemoteMasterConnection(ctx context.Context, remoteState *remoteBuildSt

	cmd := exec.Command(sshCmd[0], sshCmd[1:]...)
	cmd.Stderr = os.Stderr
	err = cmd.Start()

	stopMaster, err := startBackgroundCommandInErrGroup(ctx, g, cmd)
	if err != nil {
		return cleanup, errors.Wrap(err, "error starting fetch proxy")
		return cleanup, err
	}
	cleanup = wrapCleanup(cleanup, stopMaster)

	ctx, cancelTimeout := context.WithTimeout(ctx, 5*time.Second)
	defer cancelTimeout()
	bo := backoff.WithContext(backoff.NewConstantBackOff(1*time.Millisecond), ctx)
	err = backoff.Retry(func() error {
		_, err := os.Stat(remoteState.localSSHControlPath)
		if err != nil && !os.IsNotExist(err) {
			return backoff.Permanent(err)
		}
		return err
	}, bo)
	if err != nil {
		return cleanup, err
	}
	cleanup = wrapCleanup(cleanup, func() {
		_ = cmd.Process.Signal(unix.SIGTERM)
		_ = cmd.Wait()
	})

	return cleanup, nil
}

// Use SSH to proxy a our local fetchurl socket to the remote server.
func startRemoteFetchurlServer(ctx context.Context, remoteState *remoteBuildState, state *buildState) (cleanup func(), err error) {
func startRemoteFetchurlServer(ctx context.Context, g *errgroup.Group, remoteState *remoteBuildState, state *buildState) (cleanup func(), err error) {
	cleanup = func() {}
	defer func() {
		if err != nil {


@@ 223,20 210,25 @@ func startRemoteFetchurlServer(ctx context.Context, remoteState *remoteBuildStat

	cmd := exec.Command(sshCmd[0], sshCmd[1:]...)
	cmd.Stderr = os.Stderr
	err = cmd.Start()
	if err != nil {
		return cleanup, errors.Wrap(err, "error starting fetch proxy")
	}
	cleanup = wrapCleanup(cleanup, func() {
		_ = cmd.Process.Signal(unix.SIGTERM)
		_ = cmd.Wait()
	})
	return startBackgroundCommandInErrGroup(ctx, g, cmd)
}

	return cleanup, nil
func proxySignalSocket(ctx context.Context, g *errgroup.Group, remoteState *remoteBuildState, localSignals, remoteSignals string) (cleanup func(), err error) {

	sshCmd := sshCommandFromUrl(
		remoteState.remote,
		"-N",
		"-R", fmt.Sprintf("%s:%s", remoteSignals, localSignals),
		"-S", remoteState.localSSHControlPath,
	)

	cmd := exec.Command(sshCmd[0], sshCmd[1:]...)
	cmd.Stderr = os.Stderr
	return startBackgroundCommandInErrGroup(ctx, g, cmd)
}

// Use SSH to proxy a our local fetchurl socket to the remote server.
func createRemoteTempdir(ctx context.Context, remoteState *remoteBuildState) (remotePath string, cleanup func(), err error) {
func createRemoteTempdir(ctx context.Context, g *errgroup.Group, remoteState *remoteBuildState) (remotePath string, cleanup func(), err error) {
	cleanup = func() {}
	defer func() {
		if err != nil {


@@ 264,10 256,12 @@ func createRemoteTempdir(ctx context.Context, remoteState *remoteBuildState) (re
	cmd.Stderr = os.Stderr
	cmd.Stdout = w
	cmd.Stdin = r2
	err = cmd.Start()

	stopCmd, err := startBackgroundCommandInErrGroup(ctx, g, cmd)
	if err != nil {
		return "", cleanup, errors.Wrap(err, "error starting fetch proxy")
		return "", cleanup, err
	}
	cleanup = wrapCleanup(cleanup, stopCmd)

	brdr := bufio.NewReader(r)
	tmpDir, err := brdr.ReadString('\n')


@@ 275,29 269,27 @@ func createRemoteTempdir(ctx context.Context, remoteState *remoteBuildState) (re
		return "", cleanup, err
	}

	cleanup = wrapCleanup(cleanup, func() {
		_ = cmd.Process.Signal(unix.SIGTERM)
		_ = cmd.Wait()
	})

	return strings.TrimSpace(tmpDir), cleanup, nil
}

func buildPackageOnRemote(ctx context.Context, remote *url.URL, extraSSHFlags []string, state *buildState, cfg *Config, nbuildProcs int, pkg *pkgs.Package, link string) (string, error) {

	remoteBuildGroup, ctx := errgroup.WithContext(ctx)
	defer remoteBuildGroup.Wait()

	remoteState := &remoteBuildState{
		remote:              remote,
		localSSHControlPath: filepath.Join(state.tmpDir, "sshctl"),
	}
	remoteState.extraSSHFlags = append([]string{"-S", remoteState.localSSHControlPath}, extraSSHFlags...)

	stopMasterConnection, err := startRemoteMasterConnection(ctx, remoteState)
	stopMasterConnection, err := startRemoteMasterConnection(ctx, remoteBuildGroup, remoteState)
	if err != nil {
		return "", err
	}
	defer stopMasterConnection()

	tmpDir, cleanupTmpDir, err := createRemoteTempdir(ctx, remoteState)
	tmpDir, cleanupTmpDir, err := createRemoteTempdir(ctx, remoteBuildGroup, remoteState)
	if err != nil {
		return "", err
	}


@@ 308,7 300,7 @@ func buildPackageOnRemote(ctx context.Context, remote *url.URL, extraSSHFlags []
	remoteNormalizedPackagePath := filepath.Join(remoteState.remoteTmpDir, "pkg.edn")
	remoteOutLink := filepath.Join(remoteState.remoteTmpDir, "result")

	stopFetchurlServer, err := startRemoteFetchurlServer(ctx, remoteState, state)
	stopFetchurlServer, err := startRemoteFetchurlServer(ctx, remoteBuildGroup, remoteState, state)
	if err != nil {
		return "", err
	}


@@ 329,82 321,105 @@ func buildPackageOnRemote(ctx context.Context, remote *url.URL, extraSSHFlags []
	}
	normalizedPackageEdn = nil

	buildProcErrGroup, buildProcCtx := errgroup.WithContext(ctx)
	pkgstoreErrGroup, pkgStoreCtx := errgroup.WithContext(ctx)

	for i := 0; i < nbuildProcs; i++ {
		// Closure forces a copy of i, instead of
		// capturing a reference.
		func(ctx context.Context, i int) {
			buildProcErrGroup.Go(func() error {

				var cleanupErrGroup errgroup.Group

				args := remoteCommand(remote, remoteState.extraSSHFlags,
					"hermes",
					"pkgstore",
					"build",
				)
			pkgstoreErrGroup.Go(func() error {

				if remote.Path != "" {
					args = append(args, "--store", remote.Path)
				}

				args = append(args,
					"--normalized-package", remoteNormalizedPackagePath,
					"--fetch-socket", remoteState.remoteFetchSocketPath,
					"--stdin-signals",
				)

				if i == 0 {
					args = append(args, "--link", remoteOutLink)
				}

				cmd := exec.Command(args[0], args[1:]...)
				buildErrGroup, ctx := errgroup.WithContext(ctx)

				a, b, err := os.Pipe()
				signalSocketPath := state.nextSignalSocketPath()
				signalSocketListener, err := net.Listen("unix", signalSocketPath)
				if err != nil {
					return err
					return errors.Wrap(err, "unable to create signal forwarding socket")
				}
				defer a.Close()
				defer b.Close()

				cmd.Stdin = b
				cmd.Stderr = os.Stderr
				remoteSignalSocketPath := filepath.Join(remoteState.remoteTmpDir, filepath.Base(signalSocketPath))

				err = cmd.Start()
				stopSignalSocketProxy, err := proxySignalSocket(ctx, buildErrGroup, remoteState, signalSocketPath, remoteSignalSocketPath)
				if err != nil {
					return err
				}
				defer stopSignalSocketProxy()

				exited := make(chan struct{}, 1)
				cleanupErrGroup.Go(func() error {

				buildErrGroup.Go(func() error {
					defer signalSocketListener.Close()
					select {
					case <-ctx.Done():
						return ctx.Err()
					case <-exited:
						return nil
					}
				})

				buildErrGroup.Go(func() error {
					c, err := signalSocketListener.Accept()
					if err != nil {
						select {
						case <-exited:
							return nil
						default:
							return err
						}
					}
					defer c.Close()

					select {
					case <-ctx.Done():
						// Write the close signal via stdin so we can signal
						// even when it is a setuid binary.
						_, _ = a.Write([]byte("Term\n"))
						_ = a.Close()
						_, _ = c.Write([]byte("Term\n"))
						return nil
					case <-exited:
						return nil
					}
					return nil
				})

				cleanupErrGroup.Go(func() error {
				buildErrGroup.Go(func() error {
					defer close(exited)
					return cmd.Wait()
					defer stopSignalSocketProxy()

					args := remoteCommand(remote, remoteState.extraSSHFlags,
						"hermes",
						"pkgstore",
						"build",
					)

					if remote.Path != "" {
						args = append(args, "--store", remote.Path)
					}

					args = append(args,
						"--normalized-package", remoteNormalizedPackagePath,
						"--fetch-socket", remoteState.remoteFetchSocketPath,
						"--signal-socket", remoteSignalSocketPath,
					)

					if i == 0 {
						args = append(args, "--link", remoteOutLink)
					}

					cmd := exec.Command(args[0], args[1:]...)
					cmd.Stderr = os.Stderr
					return proctools.RunCmd(ctx, cmd, func() {})
				})

				err = cleanupErrGroup.Wait()
				err = buildErrGroup.Wait()
				if err != nil {
					return err
				}

				return nil
			})
		}(buildProcCtx, i)
		}(pkgStoreCtx, i)
	}

	err = buildProcErrGroup.Wait()
	err = pkgstoreErrGroup.Wait()
	if err != nil {
		return "", err
	}


@@ 480,66 495,82 @@ func buildPackage(ctx context.Context, state *buildState, cfg *Config, nbuildPro

	var packagePath string

	buildProcErrGroup, buildProcCtx := errgroup.WithContext(ctx)
	pkgstoreErrGroup, pkgStoreCtx := errgroup.WithContext(ctx)

	for i := 0; i < nbuildProcs; i++ {
		// Closure forces a copy of i, instead of
		// capturing a reference.
		// Closure forces a copy of i, instead of capturing a reference.
		func(ctx context.Context, i int) {
			buildProcErrGroup.Go(func() error {

				var cleanupErrGroup errgroup.Group

				args := []string{
					"build",
					"--store", cfg.StorePath,
					"--normalized-package", pkgFile.Name(),
					"--fetch-socket", state.fetchSocketPath,
					"--stdin-signals",
				}

				if i == 0 {
					args = append(args, "--link", link)
				}
			pkgstoreErrGroup.Go(func() error {

				cmd := exec.Command(cfg.PkgStoreBin, args...)
				buildErrGroup, ctx := errgroup.WithContext(ctx)

				a, b, err := os.Pipe()
				signalSocketPath := state.nextSignalSocketPath()
				signalSocketListener, err := net.Listen("unix", signalSocketPath)
				if err != nil {
					return err
					return errors.Wrap(err, "unable to create signal forwarding socket")
				}
				defer a.Close()
				defer b.Close()

				var output bytes.Buffer
				cmd.Stdin = b
				cmd.Stdout = &output
				cmd.Stderr = os.Stderr

				err = cmd.Start()
				if err != nil {
					return err
				}

				exited := make(chan struct{}, 1)
				cleanupErrGroup.Go(func() error {

				buildErrGroup.Go(func() error {
					defer signalSocketListener.Close()
					select {
					case <-ctx.Done():
						// Write the close signal via stdin so we can signal
						// even when it is a setuid binary.
						_, _ = a.Write([]byte("Term\n"))
						_ = a.Close()
						return ctx.Err()
					case <-exited:
						return nil
					}
					return nil
				})

				cleanupErrGroup.Go(func() error {
				buildErrGroup.Go(func() error {
					defer close(exited)
					return cmd.Wait()

					args := []string{
						"build",
						"--store", cfg.StorePath,
						"--normalized-package", pkgFile.Name(),
						"--fetch-socket", state.fetchSocketPath,
						"--signal-socket", signalSocketPath,
					}

					if i == 0 {
						args = append(args, "--link", link)
					}

					cmd := exec.Command(cfg.PkgStoreBin, args...)
					if i == 0 {
						cmd.Stdout = &output
					}
					cmd.Stderr = os.Stderr
					return proctools.RunCmd(ctx, cmd, func() {})
				})

				buildErrGroup.Go(func() error {
					c, err := signalSocketListener.Accept()
					if err != nil {
						select {
						case <-exited:
							return nil
						default:
							return err
						}
					}
					defer c.Close()

					select {
					case <-ctx.Done():
						// Write the close signal via stdin so we can signal
						// even when it is a setuid binary.
						_, _ = c.Write([]byte("Term\n"))
						return nil
					case <-exited:
						return nil
					}
				})

				err = cleanupErrGroup.Wait()
				err = buildErrGroup.Wait()
				if err != nil {
					return err
				}


@@ 550,10 581,10 @@ func buildPackage(ctx context.Context, state *buildState, cfg *Config, nbuildPro

				return nil
			})
		}(buildProcCtx, i)
		}(pkgStoreCtx, i)
	}

	err = buildProcErrGroup.Wait()
	err = pkgstoreErrGroup.Wait()
	if err != nil {
		return "", err
	}


@@ 717,13 748,14 @@ func buildMain() {
	ctx, cancelBuild := context.WithCancel(context.Background())
	defer cancelBuild()

	sigs := make(chan os.Signal, 1)
	signal.Ignore(unix.SIGTTOU)
	signal.Notify(sigs, os.Interrupt, unix.SIGTERM, unix.SIGHUP)
	go func() {
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt, unix.SIGTERM, unix.SIGHUP)
		<-c
		<-sigs
		_, _ = fmt.Fprintf(os.Stderr, "Got interrupt, cancelling build.\n")
		cancelBuild()
		<-c
		<-sigs
		die("Second interrupt, hard aborting build.\n")
	}()



@@ 736,7 768,9 @@ func buildMain() {
		die("Expected a single hermes url to build.\n")
	}

	err = func() error {
	buildErrGroup, ctx := errgroup.WithContext(ctx)

	buildErrGroup.Go(func() error {
		tmpDir, err := ioutil.TempDir("", "")
		if err != nil {
			return err


@@ 761,8 795,8 @@ func buildMain() {
		gcRootCount := int64(0)

		nextGCRootPath := func() string {
			gcRootCount += 1
			return filepath.Join(tmpDir, fmt.Sprintf("root-%d.gcroot", gcRootCount))
			v := atomic.AddInt64(&gcRootCount, 1)
			return filepath.Join(tmpDir, fmt.Sprintf("root-%d.gcroot", v))
		}
		defer func() {
			err := cleanupTemporaryGCRoots(cfg, tmpDir)


@@ 774,20 808,27 @@ func buildMain() {
		pkgEdnCount := int64(0)

		nextPkgEdnPath := func() string {
			pkgEdnCount += 1
			return filepath.Join(tmpDir, fmt.Sprintf("pkg-%d.edn", pkgEdnCount))
			v := atomic.AddInt64(&pkgEdnCount, 1)
			return filepath.Join(tmpDir, fmt.Sprintf("pkg-%d.edn", v))
		}

		signalSocketCount := int64(0)
		nextSignalSocketPath := func() string {
			v := atomic.AddInt64(&signalSocketCount, 1)
			return filepath.Join(tmpDir, fmt.Sprintf("signals-%d.socket", v))
		}

		state := &buildState{
			tmpDir:          tmpDir,
			fetchSocketPath: filepath.Join(tmpDir, "fetch.sock"),
			mirrorDBPath:    mirrorDBPath,
			mirrorDB:        mirrorDB,
			nextGCRootPath:  nextGCRootPath,
			nextPkgEdnPath:  nextPkgEdnPath,
			tmpDir:               tmpDir,
			fetchSocketPath:      filepath.Join(tmpDir, "fetch.sock"),
			mirrorDBPath:         mirrorDBPath,
			mirrorDB:             mirrorDB,
			nextGCRootPath:       nextGCRootPath,
			nextPkgEdnPath:       nextPkgEdnPath,
			nextSignalSocketPath: nextSignalSocketPath,
		}

		stopFetchurlServer, err := startFetchurlServer(ctx, state)
		stopFetchurlServer, err := startFetchurlServer(ctx, buildErrGroup, state)
		if err != nil {
			return err
		}


@@ 850,8 891,9 @@ func buildMain() {
		}

		return nil
	}()
	})

	err = buildErrGroup.Wait()
	if err != nil {
		if evalError, ok := errors.Cause(err).(*hscript.EvalError); ok {
			die("%s\n", evalError.Backtrace())

M src/cmd/hermes/copy.go => src/cmd/hermes/copy.go +4 -4
@@ 124,13 124,13 @@ func copyMain() {
	ctx, cancelBuild := context.WithCancel(context.Background())
	defer cancelBuild()

	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, os.Interrupt, unix.SIGTERM, unix.SIGHUP)
	go func() {
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt, unix.SIGTERM)
		<-c
		<-sigs
		_, _ = fmt.Fprintf(os.Stderr, "Got interrupt, cancelling copy.\n")
		cancelBuild()
		<-c
		<-sigs
		die("Second interrupt, aborting copy.\n")
	}()


M src/cmd/hermes/shell.go => src/cmd/hermes/shell.go +4 -3
@@ 147,12 147,13 @@ func shellMain() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, os.Interrupt, unix.SIGTERM, unix.SIGHUP)
	go func() {
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt, unix.SIGTERM)
		<-c
		<-sigs
		_, _ = fmt.Fprintf(os.Stderr, "Got interrupt.\n")
		cancel()
		<-sigs
		die("Aborting.\n")
	}()


M src/cmd/hermes/tmpdir.go => src/cmd/hermes/tmpdir.go +3 -3
@@ 17,10 17,10 @@ func tmpdirMain() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, os.Interrupt, unix.SIGTERM, unix.SIGHUP)
	go func() {
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt, unix.SIGTERM, unix.SIGHUP)
		<-c
		<-sigs
		cancel()
	}()


M src/go.mod => src/go.mod +1 -0
@@ 4,6 4,7 @@ go 1.12

require (
	github.com/bvinc/go-sqlite-lite v0.6.1
	github.com/cenkalti/backoff v2.2.1+incompatible
	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
	github.com/gofrs/flock v0.7.1
	github.com/jlaffaye/ftp v0.0.0-20190721194432-7cd8b0bcf3fc

M src/go.sum => src/go.sum +2 -0
@@ 2,6 2,8 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/bvinc/go-sqlite-lite v0.6.1 h1:JU8Rz5YAOZQiU3WEulKF084wfXpytRiqD2IaW2QjPz4=
github.com/bvinc/go-sqlite-lite v0.6.1/go.mod h1:2GiE60NUdb0aNhDdY+LXgrqAVDpi2Ijc6dB6ZMp9x6s=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc=

A src/vendor/github.com/cenkalti/backoff/.gitignore => src/vendor/github.com/cenkalti/backoff/.gitignore +22 -0
@@ 0,0 1,22 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe

A src/vendor/github.com/cenkalti/backoff/.travis.yml => src/vendor/github.com/cenkalti/backoff/.travis.yml +10 -0
@@ 0,0 1,10 @@
language: go
go:
  - 1.7
  - 1.x
  - tip
before_install:
  - go get github.com/mattn/goveralls
  - go get golang.org/x/tools/cmd/cover
script:
  - $HOME/gopath/bin/goveralls -service=travis-ci

A src/vendor/github.com/cenkalti/backoff/LICENSE => src/vendor/github.com/cenkalti/backoff/LICENSE +20 -0
@@ 0,0 1,20 @@
The MIT License (MIT)

Copyright (c) 2014 Cenk Altı

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

A src/vendor/github.com/cenkalti/backoff/README.md => src/vendor/github.com/cenkalti/backoff/README.md +30 -0
@@ 0,0 1,30 @@
# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Build Status][travis image]][travis] [![Coverage Status][coveralls image]][coveralls]

This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client].

[Exponential backoff][exponential backoff wiki]
is an algorithm that uses feedback to multiplicatively decrease the rate of some process,
in order to gradually find an acceptable rate.
The retries exponentially increase and stop increasing when a certain threshold is met.

## Usage

See https://godoc.org/github.com/cenkalti/backoff#pkg-examples

## Contributing

* I would like to keep this library as small as possible.
* Please don't send a PR without opening an issue and discussing it first.
* If proposed change is not a common use case, I will probably not accept it.

[godoc]: https://godoc.org/github.com/cenkalti/backoff
[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png
[travis]: https://travis-ci.org/cenkalti/backoff
[travis image]: https://travis-ci.org/cenkalti/backoff.png?branch=master
[coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master
[coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master

[google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java
[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff

[advanced example]: https://godoc.org/github.com/cenkalti/backoff#example_

A src/vendor/github.com/cenkalti/backoff/backoff.go => src/vendor/github.com/cenkalti/backoff/backoff.go +66 -0
@@ 0,0 1,66 @@
// Package backoff implements backoff algorithms for retrying operations.
//
// Use Retry function for retrying operations that may fail.
// If Retry does not meet your needs,
// copy/paste the function into your project and modify as you wish.
//
// There is also Ticker type similar to time.Ticker.
// You can use it if you need to work with channels.
//
// See Examples section below for usage examples.
package backoff

import "time"

// BackOff is a backoff policy for retrying an operation.
type BackOff interface {
	// NextBackOff returns the duration to wait before retrying the operation,
	// or backoff. Stop to indicate that no more retries should be made.
	//
	// Example usage:
	//
	// 	duration := backoff.NextBackOff();
	// 	if (duration == backoff.Stop) {
	// 		// Do not retry operation.
	// 	} else {
	// 		// Sleep for duration and retry operation.
	// 	}
	//
	NextBackOff() time.Duration

	// Reset to initial state.
	Reset()
}

// Stop indicates that no more retries should be made for use in NextBackOff().
const Stop time.Duration = -1

// ZeroBackOff is a fixed backoff policy whose backoff time is always zero,
// meaning that the operation is retried immediately without waiting, indefinitely.
type ZeroBackOff struct{}

func (b *ZeroBackOff) Reset() {}

func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 }

// StopBackOff is a fixed backoff policy that always returns backoff.Stop for
// NextBackOff(), meaning that the operation should never be retried.
type StopBackOff struct{}

func (b *StopBackOff) Reset() {}

func (b *StopBackOff) NextBackOff() time.Duration { return Stop }

// ConstantBackOff is a backoff policy that always returns the same backoff delay.
// This is in contrast to an exponential backoff policy,
// which returns a delay that grows longer as you call NextBackOff() over and over again.
type ConstantBackOff struct {
	Interval time.Duration
}

func (b *ConstantBackOff) Reset()                     {}
func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval }

func NewConstantBackOff(d time.Duration) *ConstantBackOff {
	return &ConstantBackOff{Interval: d}
}

A src/vendor/github.com/cenkalti/backoff/context.go => src/vendor/github.com/cenkalti/backoff/context.go +63 -0
@@ 0,0 1,63 @@
package backoff

import (
	"context"
	"time"
)

// BackOffContext is a backoff policy that stops retrying after the context
// is canceled.
type BackOffContext interface {
	BackOff
	Context() context.Context
}

type backOffContext struct {
	BackOff
	ctx context.Context
}

// WithContext returns a BackOffContext with context ctx
//
// ctx must not be nil
func WithContext(b BackOff, ctx context.Context) BackOffContext {
	if ctx == nil {
		panic("nil context")
	}

	if b, ok := b.(*backOffContext); ok {
		return &backOffContext{
			BackOff: b.BackOff,
			ctx:     ctx,
		}
	}

	return &backOffContext{
		BackOff: b,
		ctx:     ctx,
	}
}

func ensureContext(b BackOff) BackOffContext {
	if cb, ok := b.(BackOffContext); ok {
		return cb
	}
	return WithContext(b, context.Background())
}

func (b *backOffContext) Context() context.Context {
	return b.ctx
}

func (b *backOffContext) NextBackOff() time.Duration {
	select {
	case <-b.ctx.Done():
		return Stop
	default:
	}
	next := b.BackOff.NextBackOff()
	if deadline, ok := b.ctx.Deadline(); ok && deadline.Sub(time.Now()) < next {
		return Stop
	}
	return next
}

A src/vendor/github.com/cenkalti/backoff/exponential.go => src/vendor/github.com/cenkalti/backoff/exponential.go +153 -0
@@ 0,0 1,153 @@
package backoff

import (
	"math/rand"
	"time"
)

/*
ExponentialBackOff is a backoff implementation that increases the backoff
period for each retry attempt using a randomization function that grows exponentially.

NextBackOff() is calculated using the following formula:

 randomized interval =
     RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor])

In other words NextBackOff() will range between the randomization factor
percentage below and above the retry interval.

For example, given the following parameters:

 RetryInterval = 2
 RandomizationFactor = 0.5
 Multiplier = 2

the actual backoff period used in the next retry attempt will range between 1 and 3 seconds,
multiplied by the exponential, that is, between 2 and 6 seconds.

Note: MaxInterval caps the RetryInterval and not the randomized interval.

If the time elapsed since an ExponentialBackOff instance is created goes past the
MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop.

The elapsed time can be reset by calling Reset().

Example: Given the following default arguments, for 10 tries the sequence will be,
and assuming we go over the MaxElapsedTime on the 10th try:

 Request #  RetryInterval (seconds)  Randomized Interval (seconds)

  1          0.5                     [0.25,   0.75]
  2          0.75                    [0.375,  1.125]
  3          1.125                   [0.562,  1.687]
  4          1.687                   [0.8435, 2.53]
  5          2.53                    [1.265,  3.795]
  6          3.795                   [1.897,  5.692]
  7          5.692                   [2.846,  8.538]
  8          8.538                   [4.269, 12.807]
  9         12.807                   [6.403, 19.210]
 10         19.210                   backoff.Stop

Note: Implementation is not thread-safe.
*/
type ExponentialBackOff struct {
	InitialInterval     time.Duration
	RandomizationFactor float64
	Multiplier          float64
	MaxInterval         time.Duration
	// After MaxElapsedTime the ExponentialBackOff stops.
	// It never stops if MaxElapsedTime == 0.
	MaxElapsedTime time.Duration
	Clock          Clock

	currentInterval time.Duration
	startTime       time.Time
}

// Clock is an interface that returns current time for BackOff.
type Clock interface {
	Now() time.Time
}

// Default values for ExponentialBackOff.
const (
	DefaultInitialInterval     = 500 * time.Millisecond
	DefaultRandomizationFactor = 0.5
	DefaultMultiplier          = 1.5
	DefaultMaxInterval         = 60 * time.Second
	DefaultMaxElapsedTime      = 15 * time.Minute
)

// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
func NewExponentialBackOff() *ExponentialBackOff {
	b := &ExponentialBackOff{
		InitialInterval:     DefaultInitialInterval,
		RandomizationFactor: DefaultRandomizationFactor,
		Multiplier:          DefaultMultiplier,
		MaxInterval:         DefaultMaxInterval,
		MaxElapsedTime:      DefaultMaxElapsedTime,
		Clock:               SystemClock,
	}
	b.Reset()
	return b
}

type systemClock struct{}

func (t systemClock) Now() time.Time {
	return time.Now()
}

// SystemClock implements Clock interface that uses time.Now().
var SystemClock = systemClock{}

// Reset the interval back to the initial retry interval and restarts the timer.
func (b *ExponentialBackOff) Reset() {
	b.currentInterval = b.InitialInterval
	b.startTime = b.Clock.Now()
}

// NextBackOff calculates the next backoff interval using the formula:
// 	Randomized interval = RetryInterval +/- (RandomizationFactor * RetryInterval)
func (b *ExponentialBackOff) NextBackOff() time.Duration {
	// Make sure we have not gone over the maximum elapsed time.
	if b.MaxElapsedTime != 0 && b.GetElapsedTime() > b.MaxElapsedTime {
		return Stop
	}
	defer b.incrementCurrentInterval()
	return getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval)
}

// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance
// is created and is reset when Reset() is called.
//
// The elapsed time is computed using time.Now().UnixNano(). It is
// safe to call even while the backoff policy is used by a running
// ticker.
func (b *ExponentialBackOff) GetElapsedTime() time.Duration {
	return b.Clock.Now().Sub(b.startTime)
}

// Increments the current interval by multiplying it with the multiplier.
func (b *ExponentialBackOff) incrementCurrentInterval() {
	// Check for overflow, if overflow is detected set the current interval to the max interval.
	if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier {
		b.currentInterval = b.MaxInterval
	} else {
		b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier)
	}
}

// Returns a random value from the following interval:
// 	[randomizationFactor * currentInterval, randomizationFactor * currentInterval].
func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration {
	var delta = randomizationFactor * float64(currentInterval)
	var minInterval = float64(currentInterval) - delta
	var maxInterval = float64(currentInterval) + delta

	// Get a random value from the range [minInterval, maxInterval].
	// The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
	// we want a 33% chance for selecting either 1, 2 or 3.
	return time.Duration(minInterval + (random * (maxInterval - minInterval + 1)))
}

A src/vendor/github.com/cenkalti/backoff/retry.go => src/vendor/github.com/cenkalti/backoff/retry.go +82 -0
@@ 0,0 1,82 @@
package backoff

import "time"

// An Operation is executing by Retry() or RetryNotify().
// The operation will be retried using a backoff policy if it returns an error.
type Operation func() error

// Notify is a notify-on-error function. It receives an operation error and
// backoff delay if the operation failed (with an error).
//
// NOTE that if the backoff policy stated to stop retrying,
// the notify function isn't called.
type Notify func(error, time.Duration)

// Retry the operation o until it does not return error or BackOff stops.
// o is guaranteed to be run at least once.
//
// If o returns a *PermanentError, the operation is not retried, and the
// wrapped error is returned.
//
// Retry sleeps the goroutine for the duration returned by BackOff after a
// failed operation returns.
func Retry(o Operation, b BackOff) error { return RetryNotify(o, b, nil) }

// RetryNotify calls notify function with the error and wait duration
// for each failed attempt before sleep.
func RetryNotify(operation Operation, b BackOff, notify Notify) error {
	var err error
	var next time.Duration
	var t *time.Timer

	cb := ensureContext(b)

	b.Reset()
	for {
		if err = operation(); err == nil {
			return nil
		}

		if permanent, ok := err.(*PermanentError); ok {
			return permanent.Err
		}

		if next = cb.NextBackOff(); next == Stop {
			return err
		}

		if notify != nil {
			notify(err, next)
		}

		if t == nil {
			t = time.NewTimer(next)
			defer t.Stop()
		} else {
			t.Reset(next)
		}

		select {
		case <-cb.Context().Done():
			return err
		case <-t.C:
		}
	}
}

// PermanentError signals that the operation should not be retried.
type PermanentError struct {
	Err error
}

func (e *PermanentError) Error() string {
	return e.Err.Error()
}

// Permanent wraps the given err in a *PermanentError.
func Permanent(err error) *PermanentError {
	return &PermanentError{
		Err: err,
	}
}

A src/vendor/github.com/cenkalti/backoff/ticker.go => src/vendor/github.com/cenkalti/backoff/ticker.go +82 -0
@@ 0,0 1,82 @@
package backoff

import (
	"sync"
	"time"
)

// Ticker holds a channel that delivers `ticks' of a clock at times reported by a BackOff.
//
// Ticks will continue to arrive when the previous operation is still running,
// so operations that take a while to fail could run in quick succession.
type Ticker struct {
	C        <-chan time.Time
	c        chan time.Time
	b        BackOffContext
	stop     chan struct{}
	stopOnce sync.Once
}

// NewTicker returns a new Ticker containing a channel that will send
// the time at times specified by the BackOff argument. Ticker is
// guaranteed to tick at least once.  The channel is closed when Stop
// method is called or BackOff stops. It is not safe to manipulate the
// provided backoff policy (notably calling NextBackOff or Reset)
// while the ticker is running.
func NewTicker(b BackOff) *Ticker {
	c := make(chan time.Time)
	t := &Ticker{
		C:    c,
		c:    c,
		b:    ensureContext(b),
		stop: make(chan struct{}),
	}
	t.b.Reset()
	go t.run()
	return t
}

// Stop turns off a ticker. After Stop, no more ticks will be sent.
func (t *Ticker) Stop() {
	t.stopOnce.Do(func() { close(t.stop) })
}

func (t *Ticker) run() {
	c := t.c
	defer close(c)

	// Ticker is guaranteed to tick at least once.
	afterC := t.send(time.Now())

	for {
		if afterC == nil {
			return
		}

		select {
		case tick := <-afterC:
			afterC = t.send(tick)
		case <-t.stop:
			t.c = nil // Prevent future ticks from being sent to the channel.
			return
		case <-t.b.Context().Done():
			return
		}
	}
}

func (t *Ticker) send(tick time.Time) <-chan time.Time {
	select {
	case t.c <- tick:
	case <-t.stop:
		return nil
	}

	next := t.b.NextBackOff()
	if next == Stop {
		t.Stop()
		return nil
	}

	return time.After(next)
}

A src/vendor/github.com/cenkalti/backoff/tries.go => src/vendor/github.com/cenkalti/backoff/tries.go +35 -0
@@ 0,0 1,35 @@
package backoff

import "time"

/*
WithMaxRetries creates a wrapper around another BackOff, which will
return Stop if NextBackOff() has been called too many times since
the last time Reset() was called

Note: Implementation is not thread-safe.
*/
func WithMaxRetries(b BackOff, max uint64) BackOff {
	return &backOffTries{delegate: b, maxTries: max}
}

type backOffTries struct {
	delegate BackOff
	maxTries uint64
	numTries uint64
}

func (b *backOffTries) NextBackOff() time.Duration {
	if b.maxTries > 0 {
		if b.maxTries <= b.numTries {
			return Stop
		}
		b.numTries++
	}
	return b.delegate.NextBackOff()
}

func (b *backOffTries) Reset() {
	b.numTries = 0
	b.delegate.Reset()
}

M src/vendor/modules.txt => src/vendor/modules.txt +2 -0
@@ 1,5 1,7 @@
# github.com/bvinc/go-sqlite-lite v0.6.1
github.com/bvinc/go-sqlite-lite/sqlite3
# github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cenkalti/backoff
# github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/chzyer/readline
# github.com/gofrs/flock v0.7.1