diff --git a/action/edit.go b/action/edit.go index 6359203c53..bc672b7720 100644 --- a/action/edit.go +++ b/action/edit.go @@ -4,10 +4,10 @@ import ( "bytes" "fmt" "io/ioutil" - "log" "os" "os/exec" + "github.com/fatih/color" "github.com/justwatchcom/gopass/fsutil" "github.com/justwatchcom/gopass/pwgen" "github.com/justwatchcom/gopass/store" @@ -65,21 +65,21 @@ func (s *Action) editor(content []byte) ([]byte, error) { editor = "editor" } - tmpfile, err := ioutil.TempFile(fsutil.Tempdir(), "gopass-edit") + tmpfile, err := fsutil.TempFile("gopass-edit") if err != nil { - return []byte{}, fmt.Errorf("failed to create tmpfile to start with %s: %v", editor, tmpfile.Name()) + return []byte{}, fmt.Errorf("failed to create tmpfile %s: %s", editor, err) } defer func() { - if err := os.Remove(tmpfile.Name()); err != nil { - log.Fatal(err) + if err := tmpfile.Remove(); err != nil { + color.Red("Failed to remove tempfile at %s: %s", tmpfile.Name(), err) } }() if _, err := tmpfile.Write([]byte(content)); err != nil { - return []byte{}, fmt.Errorf("failed to create tmpfile to start with %s: %v", editor, tmpfile.Name()) + return []byte{}, fmt.Errorf("failed to write tmpfile to start with %s %v: %s", editor, tmpfile.Name(), err) } if err := tmpfile.Close(); err != nil { - return []byte{}, fmt.Errorf("failed to create tmpfile to start with %s: %v", editor, tmpfile.Name()) + return []byte{}, fmt.Errorf("failed to close tmpfile to start with %s %v: %s", editor, tmpfile.Name(), err) } cmdArgs, err := shellquote.Split(editor) diff --git a/fsutil/fsutil_test.go b/fsutil/fsutil_test.go index 49dbbb1f35..62559ca501 100644 --- a/fsutil/fsutil_test.go +++ b/fsutil/fsutil_test.go @@ -44,6 +44,7 @@ func TestIsDir(t *testing.T) { t.Errorf("Should be not dir: %s", fn) } } + func TestIsFile(t *testing.T) { tempdir, err := ioutil.TempDir("", "gopass-") if err != nil { @@ -63,12 +64,3 @@ func TestIsFile(t *testing.T) { t.Errorf("Should be not dir: %s", fn) } } -func TestTempdir(t *testing.T) { - tempdir, err := ioutil.TempDir(Tempdir(), "gopass-") - if err != nil { - t.Fatalf("Failed to create tempdir: %s", err) - } - defer func() { - _ = os.RemoveAll(tempdir) - }() -} diff --git a/fsutil/fsutil_windows.go b/fsutil/fsutil_windows.go deleted file mode 100644 index 6a4b419396..0000000000 --- a/fsutil/fsutil_windows.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build windows - -package fsutil - -// Tempdir returns a temporary directory suiteable for sensitive data. On -// Windows, just return empty string for ioutil.TempFile. -func Tempdir() string { - return "" -} diff --git a/fsutil/tempdir.go b/fsutil/tempdir.go new file mode 100644 index 0000000000..077e08cc41 --- /dev/null +++ b/fsutil/tempdir.go @@ -0,0 +1,79 @@ +package fsutil + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" +) + +type tempfile struct { + dir string + dev string + fh *os.File + dbg bool +} + +// TempFiler is a tempfile interface +type TempFiler interface { + io.WriteCloser + Name() string + Remove() error +} + +// TempFile returns a new tempfile wrapper +func TempFile(prefix string) (TempFiler, error) { + td, err := ioutil.TempDir(tempdirBase(), prefix) + if err != nil { + return nil, err + } + tf := &tempfile{ + dir: td, + } + if gdb := os.Getenv("GOPASS_DEBUG"); gdb == "true" { + tf.dbg = true + } + if err := tf.mount(); err != nil { + _ = os.RemoveAll(tf.dir) + return nil, err + } + + fn := filepath.Join(tf.dir, "tempfile") + fh, err := os.OpenFile(fn, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) + if err != nil { + return nil, fmt.Errorf("Failed to open file %s: %s", fn, err) + } + tf.fh = fh + + return tf, nil +} + +func (t *tempfile) Name() string { + if t.fh == nil { + return "" + } + return t.fh.Name() +} + +func (t *tempfile) Write(p []byte) (int, error) { + if t.fh == nil { + return 0, fmt.Errorf("not initialized") + } + return t.fh.Write(p) +} + +func (t *tempfile) Close() error { + if t.fh == nil { + return nil + } + return t.fh.Close() +} + +func (t *tempfile) Remove() error { + _ = t.Close() + if err := t.unmount(); err != nil { + return fmt.Errorf("Failed to unmount %s from %s: %s", t.dev, t.dir, err) + } + return os.RemoveAll(t.dir) +} diff --git a/fsutil/tempdir_darwin.go b/fsutil/tempdir_darwin.go new file mode 100644 index 0000000000..8627c28fe6 --- /dev/null +++ b/fsutil/tempdir_darwin.go @@ -0,0 +1,93 @@ +// +build darwin + +package fsutil + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/cenkalti/backoff" +) + +func tempdirBase() string { + return "" +} + +func (t *tempfile) mount() error { + // create 16MB ramdisk + cmd := exec.Command("hdid", "-drivekey", "system-image=yes", "-nomount", "ram://32768") + cmd.Stderr = os.Stderr + if t.dbg { + fmt.Printf("[DEBUG] CMD: %s %+v\n", cmd.Path, cmd.Args) + } + out, err := cmd.Output() + if err != nil { + return fmt.Errorf("Failed to create disk with hdid: %s", err) + } + if t.dbg { + fmt.Printf("[DEBUG] Output: %s\n", out) + } + p := strings.Split(string(out), " ") + if len(p) < 1 { + return fmt.Errorf("Unhandeled hdid output: %s", string(out)) + } + t.dev = p[0] + + // create filesystem on ramdisk + cmd = exec.Command("newfs_hfs", "-M", "700", t.dev) + cmd.Stderr = os.Stderr + if t.dbg { + cmd.Stdout = os.Stdout + fmt.Printf("[DEBUG] CMD: %s %+v\n", cmd.Path, cmd.Args) + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to make filesystem on %s: %s", t.dev, err) + } + + // mount ramdisk + cmd = exec.Command("mount", "-t", "hfs", "-o", "noatime", "-o", "nobrowse", t.dev, t.dir) + cmd.Stderr = os.Stderr + if t.dbg { + cmd.Stdout = os.Stdout + fmt.Printf("[DEBUG] CMD: %s %+v\n", cmd.Path, cmd.Args) + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to mount filesystem %s to %s: %s", t.dev, t.dir, err) + } + time.Sleep(100 * time.Millisecond) + return nil +} + +func (t *tempfile) unmount() error { + bo := backoff.NewExponentialBackOff() + bo.MaxElapsedTime = 10 * time.Second + return backoff.Retry(t.tryUnmount, bo) +} + +func (t *tempfile) tryUnmount() error { + if t.dir == "" || t.dev == "" { + return fmt.Errorf("need dir and dev") + } + // unmount ramdisk + cmd := exec.Command("diskutil", "unmountDisk", t.dev) + cmd.Stderr = os.Stderr + if t.dbg { + cmd.Stdout = os.Stdout + fmt.Printf("[DEBUG] CMD: %s %+v\n", cmd.Path, cmd.Args) + } + if err := cmd.Run(); err != nil { + return err + } + + // eject disk + cmd = exec.Command("diskutil", "quiet", "eject", t.dev) + cmd.Stderr = os.Stderr + if t.dbg { + cmd.Stdout = os.Stdout + fmt.Printf("[DEBUG] CMD: %s %+v\n", cmd.Path, cmd.Args) + } + return cmd.Run() +} diff --git a/fsutil/fsutil_others.go b/fsutil/tempdir_linux.go similarity index 62% rename from fsutil/fsutil_others.go rename to fsutil/tempdir_linux.go index 9b1dc7ca7b..febaae5580 100644 --- a/fsutil/fsutil_others.go +++ b/fsutil/tempdir_linux.go @@ -1,4 +1,4 @@ -// +build !windows +// +build linux package fsutil @@ -8,10 +8,10 @@ import ( "golang.org/x/sys/unix" ) -// Tempdir returns a temporary directory suiteable for sensitive data. It tries +// tempdir returns a temporary directory suiteable for sensitive data. It tries // /dev/shm but if this isn't working it will return an empty string. Using // this with ioutil.Tempdir will ensure that we're getting the "best" tempdir. -func Tempdir() string { +func tempdirBase() string { shmDir := "/dev/shm" if fi, err := os.Stat(shmDir); err == nil { if fi.IsDir() { @@ -22,3 +22,12 @@ func Tempdir() string { } return "" } + +func (t *tempfile) mount() error { + _ = t.dev // to trick megacheck + return nil +} + +func (t *tempfile) unmount() error { + return nil +} diff --git a/fsutil/tempdir_others.go b/fsutil/tempdir_others.go new file mode 100644 index 0000000000..ea25d88a22 --- /dev/null +++ b/fsutil/tempdir_others.go @@ -0,0 +1,18 @@ +// +build !linux,!darwin + +package fsutil + +// tempdir returns a temporary directory suiteable for sensitive data. On +// Windows, just return empty string for ioutil.TempFile. +func tempdirBase() string { + return "" +} + +func (t *tempfile) mount() error { + _ = t.dev // to trick megacheck + return nil +} + +func (t *tempfile) unmount() error { + return nil +} diff --git a/fsutil/tempdir_test.go b/fsutil/tempdir_test.go new file mode 100644 index 0000000000..3bf6ff0974 --- /dev/null +++ b/fsutil/tempdir_test.go @@ -0,0 +1,17 @@ +package fsutil + +import ( + "io/ioutil" + "os" + "testing" +) + +func TestTempdirBase(t *testing.T) { + tempdir, err := ioutil.TempDir(tempdirBase(), "gopass-") + if err != nil { + t.Fatalf("Failed to create tempdir: %s", err) + } + defer func() { + _ = os.RemoveAll(tempdir) + }() +} diff --git a/vendor/github.com/cenkalti/backoff/LICENSE b/vendor/github.com/cenkalti/backoff/LICENSE new file mode 100644 index 0000000000..89b8179965 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/LICENSE @@ -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. diff --git a/vendor/github.com/cenkalti/backoff/README.md b/vendor/github.com/cenkalti/backoff/README.md new file mode 100644 index 0000000000..0338a31a73 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/README.md @@ -0,0 +1,118 @@ +# 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. + +## How To + +We define two functions, `Retry()` and `RetryNotify()`. +They receive an `Operation` to execute, a `BackOff` algorithm, +and an optional `Notify` error handler. + +The operation will be executed, and will be retried on failure with delay +as given by the backoff algorithm. The backoff algorithm can also decide when to stop +retrying. +In addition, the notify error handler will be called after each failed attempt, +except for the last time, whose error should be handled by the caller. + +```go +// 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) + +func Retry(Operation, BackOff) error +func RetryNotify(Operation, BackOff, Notify) +``` + +## Examples + +### Retry + +Simple retry helper that uses the default exponential backoff algorithm: + +```go +operation := func() error { + // An operation that might fail. + return nil // or return errors.New("some error") +} + +err := Retry(operation, NewExponentialBackOff()) +if err != nil { + // Handle error. + return err +} + +// Operation is successful. +return nil +``` + +### Ticker + +Ticker is for using backoff algorithms with channels. + +```go +operation := func() error { + // An operation that might fail + return nil // or return errors.New("some error") +} + +b := NewExponentialBackOff() +ticker := NewTicker(b) + +var err error + +// 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. +for range ticker.C { + if err = operation(); err != nil { + log.Println(err, "will retry...") + continue + } + + ticker.Stop() + break +} + +if err != nil { + // Operation has failed. + return err +} + +// Operation is successful. +return nil +``` + +## Getting Started + +```bash +# install +$ go get github.com/cenk/backoff + +# test +$ cd $GOPATH/src/github.com/cenk/backoff +$ go get -t ./... +$ go test -v -cover +``` + +[godoc]: https://godoc.org/github.com/cenk/backoff +[godoc image]: https://godoc.org/github.com/cenk/backoff?status.png +[travis]: https://travis-ci.org/cenk/backoff +[travis image]: https://travis-ci.org/cenk/backoff.png?branch=master +[coveralls]: https://coveralls.io/github/cenk/backoff?branch=master +[coveralls image]: https://coveralls.io/repos/github/cenk/backoff/badge.svg?branch=master + +[google-http-java-client]: https://github.com/google/google-http-java-client +[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff + +[advanced example]: https://godoc.org/github.com/cenk/backoff#example_ diff --git a/vendor/github.com/cenkalti/backoff/backoff.go b/vendor/github.com/cenkalti/backoff/backoff.go new file mode 100644 index 0000000000..61bd6df66c --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/backoff.go @@ -0,0 +1,59 @@ +// Package backoff implements backoff algorithms for retrying operations. +// +// Also has a Retry() helper for retrying operations that may fail. +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() +} + +// 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} +} diff --git a/vendor/github.com/cenkalti/backoff/exponential.go b/vendor/github.com/cenkalti/backoff/exponential.go new file mode 100644 index 0000000000..ae65516dc0 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/exponential.go @@ -0,0 +1,156 @@ +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, + } + if b.RandomizationFactor < 0 { + b.RandomizationFactor = 0 + } else if b.RandomizationFactor > 1 { + b.RandomizationFactor = 1 + } + 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(). +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))) +} diff --git a/vendor/github.com/cenkalti/backoff/retry.go b/vendor/github.com/cenkalti/backoff/retry.go new file mode 100644 index 0000000000..6bc88ce750 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/retry.go @@ -0,0 +1,46 @@ +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. +// It is the caller's responsibility to reset b after Retry returns. +// +// 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 + + b.Reset() + for { + if err = operation(); err == nil { + return nil + } + + if next = b.NextBackOff(); next == Stop { + return err + } + + if notify != nil { + notify(err, next) + } + + time.Sleep(next) + } +} diff --git a/vendor/github.com/cenkalti/backoff/ticker.go b/vendor/github.com/cenkalti/backoff/ticker.go new file mode 100644 index 0000000000..7a5ff4ed1f --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/ticker.go @@ -0,0 +1,79 @@ +package backoff + +import ( + "runtime" + "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 BackOff + 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. +func NewTicker(b BackOff) *Ticker { + c := make(chan time.Time) + t := &Ticker{ + C: c, + c: c, + b: b, + stop: make(chan struct{}), + } + go t.run() + runtime.SetFinalizer(t, (*Ticker).Stop) + 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) + t.b.Reset() + + // 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 + } + } +} + +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) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 5b223a3ff0..91f0d678af 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -8,6 +8,12 @@ "revision": "bb272b845f1112e10117e3e45ce39f690c0001ad", "revisionTime": "2016-02-19T03:44:21Z" }, + { + "checksumSHA1": "9NoxW1ZvnX4inGkH4SA9RW6rIgM=", + "path": "github.com/cenkalti/backoff", + "revision": "cdf48bbc1eb78d1349cbda326a4a037f7ba565c6", + "revisionTime": "2016-06-10T10:09:12Z" + }, { "checksumSHA1": "Lf3uUXTkKK5DJ37BxQvxO1Fq+K8=", "origin": "github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew",