Merge branch 'cancel-next'

This implements more robust interrupt handling by propagating cancels
throughout more core Packer components.
This commit is contained in:
Mitchell Hashimoto 2013-08-30 23:56:06 -07:00
commit 3238b65bf9
25 changed files with 450 additions and 129 deletions

View File

@ -4,6 +4,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log" "log"
"time"
) )
// StepProvision runs the provisioners. // StepProvision runs the provisioners.
@ -22,13 +23,31 @@ func (*StepProvision) Run(state map[string]interface{}) multistep.StepAction {
hook := state["hook"].(packer.Hook) hook := state["hook"].(packer.Hook)
ui := state["ui"].(packer.Ui) ui := state["ui"].(packer.Ui)
// Run the provisioner in a goroutine so we can continually check
// for cancellations...
log.Println("Running the provision hook") log.Println("Running the provision hook")
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil { errCh := make(chan error, 1)
state["error"] = err go func() {
return multistep.ActionHalt errCh <- hook.Run(packer.HookProvision, ui, comm, nil)
} }()
return multistep.ActionContinue for {
select {
case err := <-errCh:
if err != nil {
state["error"] = err
return multistep.ActionHalt
}
return multistep.ActionContinue
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
log.Println("Cancelling provisioning due to interrupt...")
hook.Cancel()
return multistep.ActionHalt
}
}
}
} }
func (*StepProvision) Cleanup(map[string]interface{}) {} func (*StepProvision) Cleanup(map[string]interface{}) {}

View File

@ -207,10 +207,12 @@ func (b *coreBuild) Run(originalUi Ui, cache Cache) ([]Artifact, error) {
hooks[HookProvision] = make([]Hook, 0, 1) hooks[HookProvision] = make([]Hook, 0, 1)
} }
hooks[HookProvision] = append(hooks[HookProvision], &ProvisionHook{provisioners}) hooks[HookProvision] = append(hooks[HookProvision], &ProvisionHook{
Provisioners: provisioners,
})
} }
hook := &DispatchHook{hooks} hook := &DispatchHook{Mapping: hooks}
artifacts := make([]Artifact, 0, 1) artifacts := make([]Artifact, 0, 1)
// The builder just has a normal Ui, but targetted // The builder just has a normal Ui, but targetted

View File

@ -13,10 +13,10 @@ func testBuild() *coreBuild {
builderConfig: 42, builderConfig: 42,
builderType: "foo", builderType: "foo",
hooks: map[string][]Hook{ hooks: map[string][]Hook{
"foo": []Hook{&TestHook{}}, "foo": []Hook{&MockHook{}},
}, },
provisioners: []coreBuildProvisioner{ provisioners: []coreBuildProvisioner{
coreBuildProvisioner{&TestProvisioner{}, []interface{}{42}}, coreBuildProvisioner{&MockProvisioner{}, []interface{}{42}},
}, },
postProcessors: [][]coreBuildPostProcessor{ postProcessors: [][]coreBuildPostProcessor{
[]coreBuildPostProcessor{ []coreBuildPostProcessor{
@ -59,9 +59,9 @@ func TestBuild_Prepare(t *testing.T) {
assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42") assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42")
coreProv := build.provisioners[0] coreProv := build.provisioners[0]
prov := coreProv.provisioner.(*TestProvisioner) prov := coreProv.provisioner.(*MockProvisioner)
assert.True(prov.prepCalled, "prepare should be called") assert.True(prov.PrepCalled, "prepare should be called")
assert.Equal(prov.prepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config") assert.Equal(prov.PrepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config")
corePP := build.postProcessors[0][0] corePP := build.postProcessors[0][0]
pp := corePP.processor.(*TestPostProcessor) pp := corePP.processor.(*TestPostProcessor)
@ -104,9 +104,9 @@ func TestBuild_Prepare_Debug(t *testing.T) {
assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42") assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42")
coreProv := build.provisioners[0] coreProv := build.provisioners[0]
prov := coreProv.provisioner.(*TestProvisioner) prov := coreProv.provisioner.(*MockProvisioner)
assert.True(prov.prepCalled, "prepare should be called") assert.True(prov.PrepCalled, "prepare should be called")
assert.Equal(prov.prepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config") assert.Equal(prov.PrepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config")
} }
func TestBuildPrepare_variables_default(t *testing.T) { func TestBuildPrepare_variables_default(t *testing.T) {
@ -187,14 +187,14 @@ func TestBuild_Run(t *testing.T) {
dispatchHook := builder.runHook dispatchHook := builder.runHook
dispatchHook.Run("foo", nil, nil, 42) dispatchHook.Run("foo", nil, nil, 42)
hook := build.hooks["foo"][0].(*TestHook) hook := build.hooks["foo"][0].(*MockHook)
assert.True(hook.runCalled, "run should be called") assert.True(hook.RunCalled, "run should be called")
assert.Equal(hook.runData, 42, "should have correct data") assert.Equal(hook.RunData, 42, "should have correct data")
// Verify provisioners run // Verify provisioners run
dispatchHook.Run(HookProvision, nil, nil, 42) dispatchHook.Run(HookProvision, nil, nil, 42)
prov := build.provisioners[0].provisioner.(*TestProvisioner) prov := build.provisioners[0].provisioner.(*MockProvisioner)
assert.True(prov.provCalled, "provision should be called") assert.True(prov.ProvCalled, "provision should be called")
// Verify post-processor was run // Verify post-processor was run
pp := build.postProcessors[0][0].processor.(*TestPostProcessor) pp := build.postProcessors[0][0].processor.(*TestPostProcessor)

View File

@ -20,7 +20,7 @@ func init() {
func testComponentFinder() *ComponentFinder { func testComponentFinder() *ComponentFinder {
builderFactory := func(n string) (Builder, error) { return testBuilder(), nil } builderFactory := func(n string) (Builder, error) { return testBuilder(), nil }
ppFactory := func(n string) (PostProcessor, error) { return new(TestPostProcessor), nil } ppFactory := func(n string) (PostProcessor, error) { return new(TestPostProcessor), nil }
provFactory := func(n string) (Provisioner, error) { return new(TestProvisioner), nil } provFactory := func(n string) (Provisioner, error) { return new(MockProvisioner), nil }
return &ComponentFinder{ return &ComponentFinder{
Builder: builderFactory, Builder: builderFactory,
PostProcessor: ppFactory, PostProcessor: ppFactory,
@ -227,7 +227,7 @@ func TestEnvironment_DefaultCli_Version(t *testing.T) {
func TestEnvironment_Hook(t *testing.T) { func TestEnvironment_Hook(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) assert := asserts.NewTestingAsserts(t, true)
hook := &TestHook{} hook := &MockHook{}
hooks := make(map[string]Hook) hooks := make(map[string]Hook)
hooks["foo"] = hook hooks["foo"] = hook
@ -309,7 +309,7 @@ func TestEnvironment_PostProcessor_Error(t *testing.T) {
func TestEnvironmentProvisioner(t *testing.T) { func TestEnvironmentProvisioner(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) assert := asserts.NewTestingAsserts(t, true)
p := &TestProvisioner{} p := &MockProvisioner{}
ps := make(map[string]Provisioner) ps := make(map[string]Provisioner)
ps["foo"] = p ps["foo"] = p

View File

@ -1,5 +1,9 @@
package packer package packer
import (
"sync"
)
// This is the hook that should be fired for provisioners to run. // This is the hook that should be fired for provisioners to run.
const HookProvision = "packer_provision" const HookProvision = "packer_provision"
@ -11,19 +15,40 @@ const HookProvision = "packer_provision"
// you must reference the documentation for the specific hook you're interested // you must reference the documentation for the specific hook you're interested
// in. In addition to that, the Hook is given access to a UI so that it can // in. In addition to that, the Hook is given access to a UI so that it can
// output things to the user. // output things to the user.
//
// Cancel is called when the hook needs to be cancelled. This will usually
// be called when Run is still in progress so the mechanism that handles this
// must be race-free. Cancel should attempt to cancel the hook in the
// quickest, safest way possible.
type Hook interface { type Hook interface {
Run(string, Ui, Communicator, interface{}) error Run(string, Ui, Communicator, interface{}) error
Cancel()
} }
// A Hook implementation that dispatches based on an internal mapping. // A Hook implementation that dispatches based on an internal mapping.
type DispatchHook struct { type DispatchHook struct {
Mapping map[string][]Hook Mapping map[string][]Hook
l sync.Mutex
cancelled bool
runningHook Hook
} }
// Runs the hook with the given name by dispatching it to the proper // Runs the hook with the given name by dispatching it to the proper
// hooks if a mapping exists. If a mapping doesn't exist, then nothing // hooks if a mapping exists. If a mapping doesn't exist, then nothing
// happens. // happens.
func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface{}) error { func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
h.l.Lock()
h.cancelled = false
h.l.Unlock()
// Make sure when we exit that we reset the running hook.
defer func() {
h.l.Lock()
defer h.l.Unlock()
h.runningHook = nil
}()
hooks, ok := h.Mapping[name] hooks, ok := h.Mapping[name]
if !ok { if !ok {
// No hooks for that name. No problem. // No hooks for that name. No problem.
@ -31,6 +56,15 @@ func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface
} }
for _, hook := range hooks { for _, hook := range hooks {
h.l.Lock()
if h.cancelled {
h.l.Unlock()
return nil
}
h.runningHook = hook
h.l.Unlock()
if err := hook.Run(name, ui, comm, data); err != nil { if err := hook.Run(name, ui, comm, data); err != nil {
return err return err
} }
@ -38,3 +72,16 @@ func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface
return nil return nil
} }
// Cancels all the hooks that are currently in-flight, if any. This will
// block until the hooks are all cancelled.
func (h *DispatchHook) Cancel() {
h.l.Lock()
defer h.l.Unlock()
if h.runningHook != nil {
h.runningHook.Cancel()
}
h.cancelled = true
}

31
packer/hook_mock.go Normal file
View File

@ -0,0 +1,31 @@
package packer
// MockHook is an implementation of Hook that can be used for tests.
type MockHook struct {
RunFunc func() error
RunCalled bool
RunComm Communicator
RunData interface{}
RunName string
RunUi Ui
CancelCalled bool
}
func (t *MockHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
t.RunCalled = true
t.RunComm = comm
t.RunData = data
t.RunName = name
t.RunUi = ui
if t.RunFunc == nil {
return nil
}
return t.RunFunc()
}
func (t *MockHook) Cancel() {
t.CancelCalled = true
}

View File

@ -2,52 +2,89 @@ package packer
import ( import (
"cgl.tideland.biz/asserts" "cgl.tideland.biz/asserts"
"sync"
"testing" "testing"
"time"
) )
type TestHook struct { // A helper Hook implementation for testing cancels.
runCalled bool type CancelHook struct {
runComm Communicator sync.Mutex
runData interface{} cancelCh chan struct{}
runName string doneCh chan struct{}
runUi Ui
Cancelled bool
} }
func (t *TestHook) Run(name string, ui Ui, comm Communicator, data interface{}) error { func (h *CancelHook) Run(string, Ui, Communicator, interface{}) error {
t.runCalled = true h.Lock()
t.runComm = comm h.cancelCh = make(chan struct{})
t.runData = data h.doneCh = make(chan struct{})
t.runName = name h.Unlock()
t.runUi = ui
defer close(h.doneCh)
select {
case <-h.cancelCh:
h.Cancelled = true
case <-time.After(1 * time.Second):
}
return nil return nil
} }
func (h *CancelHook) Cancel() {
h.Lock()
close(h.cancelCh)
h.Unlock()
<-h.doneCh
}
func TestDispatchHook_Implements(t *testing.T) { func TestDispatchHook_Implements(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) assert := asserts.NewTestingAsserts(t, true)
var r Hook var r Hook
c := &DispatchHook{nil} c := &DispatchHook{}
assert.Implementor(c, &r, "should be a Hook") assert.Implementor(c, &r, "should be a Hook")
} }
func TestDispatchHook_Run_NoHooks(t *testing.T) { func TestDispatchHook_Run_NoHooks(t *testing.T) {
// Just make sure nothing blows up // Just make sure nothing blows up
dh := &DispatchHook{make(map[string][]Hook)} dh := &DispatchHook{}
dh.Run("foo", nil, nil, nil) dh.Run("foo", nil, nil, nil)
} }
func TestDispatchHook_Run(t *testing.T) { func TestDispatchHook_Run(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) assert := asserts.NewTestingAsserts(t, true)
hook := &TestHook{} hook := &MockHook{}
mapping := make(map[string][]Hook) mapping := make(map[string][]Hook)
mapping["foo"] = []Hook{hook} mapping["foo"] = []Hook{hook}
dh := &DispatchHook{mapping} dh := &DispatchHook{Mapping: mapping}
dh.Run("foo", nil, nil, 42) dh.Run("foo", nil, nil, 42)
assert.True(hook.runCalled, "run should be called") assert.True(hook.RunCalled, "run should be called")
assert.Equal(hook.runName, "foo", "should be proper event") assert.Equal(hook.RunName, "foo", "should be proper event")
assert.Equal(hook.runData, 42, "should be correct data") assert.Equal(hook.RunData, 42, "should be correct data")
}
func TestDispatchHook_cancel(t *testing.T) {
hook := new(CancelHook)
dh := &DispatchHook{
Mapping: map[string][]Hook{
"foo": []Hook{hook},
},
}
go dh.Run("foo", nil, nil, 42)
time.Sleep(100 * time.Millisecond)
dh.Cancel()
if !hook.Cancelled {
t.Fatal("hook should've cancelled")
}
} }

View File

@ -19,8 +19,17 @@ func (c *cmdHook) Run(name string, ui packer.Ui, comm packer.Communicator, data
return c.hook.Run(name, ui, comm, data) return c.hook.Run(name, ui, comm, data)
} }
func (c *cmdHook) Cancel() {
defer func() {
r := recover()
c.checkExit(r, nil)
}()
c.hook.Cancel()
}
func (c *cmdHook) checkExit(p interface{}, cb func()) { func (c *cmdHook) checkExit(p interface{}, cb func()) {
if c.client.Exited() { if c.client.Exited() && cb != nil {
cb() cb()
} else if p != nil && !Killed { } else if p != nil && !Killed {
log.Panic(p) log.Panic(p)

View File

@ -1,17 +1,10 @@
package plugin package plugin
import ( import (
"github.com/mitchellh/packer/packer"
"os/exec" "os/exec"
"testing" "testing"
) )
type helperHook byte
func (helperHook) Run(string, packer.Ui, packer.Communicator, interface{}) error {
return nil
}
func TestHook_NoExist(t *testing.T) { func TestHook_NoExist(t *testing.T) {
c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")}) c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
defer c.Kill() defer c.Kill()

View File

@ -2,6 +2,7 @@ package plugin
import ( import (
"fmt" "fmt"
"github.com/mitchellh/packer/packer"
"log" "log"
"os" "os"
"os/exec" "os/exec"
@ -54,7 +55,7 @@ func TestHelperProcess(*testing.T) {
case "command": case "command":
ServeCommand(new(helperCommand)) ServeCommand(new(helperCommand))
case "hook": case "hook":
ServeHook(new(helperHook)) ServeHook(new(packer.MockHook))
case "invalid-rpc-address": case "invalid-rpc-address":
fmt.Println("lolinvalid") fmt.Println("lolinvalid")
case "mock": case "mock":
@ -63,7 +64,7 @@ func TestHelperProcess(*testing.T) {
case "post-processor": case "post-processor":
ServePostProcessor(new(helperPostProcessor)) ServePostProcessor(new(helperPostProcessor))
case "provisioner": case "provisioner":
ServeProvisioner(new(helperProvisioner)) ServeProvisioner(new(packer.MockProvisioner))
case "start-timeout": case "start-timeout":
time.Sleep(1 * time.Minute) time.Sleep(1 * time.Minute)
os.Exit(1) os.Exit(1)

View File

@ -28,6 +28,15 @@ func (c *cmdProvisioner) Provision(ui packer.Ui, comm packer.Communicator) error
return c.p.Provision(ui, comm) return c.p.Provision(ui, comm)
} }
func (c *cmdProvisioner) Cancel() {
defer func() {
r := recover()
c.checkExit(r, nil)
}()
c.p.Cancel()
}
func (c *cmdProvisioner) checkExit(p interface{}, cb func()) { func (c *cmdProvisioner) checkExit(p interface{}, cb func()) {
if c.client.Exited() && cb != nil { if c.client.Exited() && cb != nil {
cb() cb()

View File

@ -1,21 +1,10 @@
package plugin package plugin
import ( import (
"github.com/mitchellh/packer/packer"
"os/exec" "os/exec"
"testing" "testing"
) )
type helperProvisioner byte
func (helperProvisioner) Prepare(...interface{}) error {
return nil
}
func (helperProvisioner) Provision(packer.Ui, packer.Communicator) error {
return nil
}
func TestProvisioner_NoExist(t *testing.T) { func TestProvisioner_NoExist(t *testing.T) {
c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")}) c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
defer c.Kill() defer c.Kill()

View File

@ -1,5 +1,9 @@
package packer package packer
import (
"sync"
)
// A provisioner is responsible for installing and configuring software // A provisioner is responsible for installing and configuring software
// on a machine prior to building the actual image. // on a machine prior to building the actual image.
type Provisioner interface { type Provisioner interface {
@ -13,6 +17,11 @@ type Provisioner interface {
// is guaranteed to be connected to some machine so that provisioning // is guaranteed to be connected to some machine so that provisioning
// can be done. // can be done.
Provision(Ui, Communicator) error Provision(Ui, Communicator) error
// Cancel is called to cancel the provisioning. This is usually called
// while Provision is still being called. The Provisioner should act
// to stop its execution as quickly as possible in a race-free way.
Cancel()
} }
// A Hook implementation that runs the given provisioners. // A Hook implementation that runs the given provisioners.
@ -20,11 +29,25 @@ type ProvisionHook struct {
// The provisioners to run as part of the hook. These should already // The provisioners to run as part of the hook. These should already
// be prepared (by calling Prepare) at some earlier stage. // be prepared (by calling Prepare) at some earlier stage.
Provisioners []Provisioner Provisioners []Provisioner
lock sync.Mutex
runningProvisioner Provisioner
} }
// Runs the provisioners in order. // Runs the provisioners in order.
func (h *ProvisionHook) Run(name string, ui Ui, comm Communicator, data interface{}) error { func (h *ProvisionHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
defer func() {
h.lock.Lock()
defer h.lock.Unlock()
h.runningProvisioner = nil
}()
for _, p := range h.Provisioners { for _, p := range h.Provisioners {
h.lock.Lock()
h.runningProvisioner = p
h.lock.Unlock()
if err := p.Provision(ui, comm); err != nil { if err := p.Provision(ui, comm); err != nil {
return err return err
} }
@ -32,3 +55,13 @@ func (h *ProvisionHook) Run(name string, ui Ui, comm Communicator, data interfac
return nil return nil
} }
// Cancels the privisioners that are still running.
func (h *ProvisionHook) Cancel() {
h.lock.Lock()
defer h.lock.Unlock()
if h.runningProvisioner != nil {
h.runningProvisioner.Cancel()
}
}

View File

@ -0,0 +1,34 @@
package packer
// MockProvisioner is an implementation of Provisioner that can be
// used for tests.
type MockProvisioner struct {
ProvFunc func() error
PrepCalled bool
PrepConfigs []interface{}
ProvCalled bool
ProvUi Ui
CancelCalled bool
}
func (t *MockProvisioner) Prepare(configs ...interface{}) error {
t.PrepCalled = true
t.PrepConfigs = configs
return nil
}
func (t *MockProvisioner) Provision(ui Ui, comm Communicator) error {
t.ProvCalled = true
t.ProvUi = ui
if t.ProvFunc == nil {
return nil
}
return t.ProvFunc()
}
func (t *MockProvisioner) Cancel() {
t.CancelCalled = true
}

View File

@ -1,23 +1,10 @@
package packer package packer
import "testing" import (
"sync"
type TestProvisioner struct { "testing"
prepCalled bool "time"
prepConfigs []interface{} )
provCalled bool
}
func (t *TestProvisioner) Prepare(configs ...interface{}) error {
t.prepCalled = true
t.prepConfigs = configs
return nil
}
func (t *TestProvisioner) Provision(Ui, Communicator) error {
t.provCalled = true
return nil
}
func TestProvisionHook_Impl(t *testing.T) { func TestProvisionHook_Impl(t *testing.T) {
var raw interface{} var raw interface{}
@ -28,23 +15,68 @@ func TestProvisionHook_Impl(t *testing.T) {
} }
func TestProvisionHook(t *testing.T) { func TestProvisionHook(t *testing.T) {
pA := &TestProvisioner{} pA := &MockProvisioner{}
pB := &TestProvisioner{} pB := &MockProvisioner{}
ui := testUi() ui := testUi()
var comm Communicator = nil var comm Communicator = nil
var data interface{} = nil var data interface{} = nil
hook := &ProvisionHook{[]Provisioner{pA, pB}} hook := &ProvisionHook{
Provisioners: []Provisioner{pA, pB},
}
hook.Run("foo", ui, comm, data) hook.Run("foo", ui, comm, data)
if !pA.provCalled { if !pA.ProvCalled {
t.Error("provision should be called on pA") t.Error("provision should be called on pA")
} }
if !pB.provCalled { if !pB.ProvCalled {
t.Error("provision should be called on pB") t.Error("provision should be called on pB")
} }
} }
func TestProvisionHook_cancel(t *testing.T) {
var lock sync.Mutex
order := make([]string, 0, 2)
p := &MockProvisioner{
ProvFunc: func() error {
time.Sleep(50 * time.Millisecond)
lock.Lock()
defer lock.Unlock()
order = append(order, "prov")
return nil
},
}
hook := &ProvisionHook{
Provisioners: []Provisioner{p},
}
finished := make(chan struct{})
go func() {
hook.Run("foo", nil, nil, nil)
close(finished)
}()
// Cancel it while it is running
time.Sleep(10 * time.Millisecond)
hook.Cancel()
lock.Lock()
order = append(order, "cancel")
lock.Unlock()
// Wait
<-finished
// Verify order
if order[0] != "cancel" || order[1] != "prov" {
t.Fatalf("bad: %#v", order)
}
}
// TODO(mitchellh): Test that they're run in the proper order // TODO(mitchellh): Test that they're run in the proper order

View File

@ -72,7 +72,7 @@ func TestBuilderRPC(t *testing.T) {
// Test Run // Test Run
cache := new(testCache) cache := new(testCache)
hook := &testHook{} hook := &packer.MockHook{}
ui := &testUi{} ui := &testUi{}
artifact, err := bClient.Run(ui, hook, cache) artifact, err := bClient.Run(ui, hook, cache)
assert.Nil(err, "should have no error") assert.Nil(err, "should have no error")
@ -83,7 +83,7 @@ func TestBuilderRPC(t *testing.T) {
assert.True(cache.lockCalled, "lock should be called") assert.True(cache.lockCalled, "lock should be called")
b.runHook.Run("foo", nil, nil, nil) b.runHook.Run("foo", nil, nil, nil)
assert.True(hook.runCalled, "run should be called") assert.True(hook.RunCalled, "run should be called")
b.runUi.Say("format") b.runUi.Say("format")
assert.True(ui.sayCalled, "say should be called") assert.True(ui.sayCalled, "say should be called")

View File

@ -2,6 +2,7 @@ package rpc
import ( import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log"
"net/rpc" "net/rpc"
) )
@ -37,6 +38,13 @@ func (h *hook) Run(name string, ui packer.Ui, comm packer.Communicator, data int
return h.client.Call("Hook.Run", args, new(interface{})) return h.client.Call("Hook.Run", args, new(interface{}))
} }
func (h *hook) Cancel() {
err := h.client.Call("Hook.Cancel", new(interface{}), new(interface{}))
if err != nil {
log.Printf("Hook.Cancel error: %s", err)
}
}
func (h *HookServer) Run(args *HookRunArgs, reply *interface{}) error { func (h *HookServer) Run(args *HookRunArgs, reply *interface{}) error {
client, err := rpc.Dial("tcp", args.RPCAddress) client, err := rpc.Dial("tcp", args.RPCAddress)
if err != nil { if err != nil {
@ -50,3 +58,8 @@ func (h *HookServer) Run(args *HookRunArgs, reply *interface{}) error {
*reply = nil *reply = nil
return nil return nil
} }
func (h *HookServer) Cancel(args *interface{}, reply *interface{}) error {
h.hook.Cancel()
return nil
}

View File

@ -4,24 +4,17 @@ import (
"cgl.tideland.biz/asserts" "cgl.tideland.biz/asserts"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"net/rpc" "net/rpc"
"reflect"
"sync"
"testing" "testing"
"time"
) )
type testHook struct {
runCalled bool
runUi packer.Ui
}
func (h *testHook) Run(name string, ui packer.Ui, comm packer.Communicator, data interface{}) error {
h.runCalled = true
return nil
}
func TestHookRPC(t *testing.T) { func TestHookRPC(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) assert := asserts.NewTestingAsserts(t, true)
// Create the UI to test // Create the UI to test
h := new(testHook) h := new(packer.MockHook)
// Serve // Serve
server := rpc.NewServer() server := rpc.NewServer()
@ -37,7 +30,11 @@ func TestHookRPC(t *testing.T) {
// Test Run // Test Run
ui := &testUi{} ui := &testUi{}
hClient.Run("foo", ui, nil, 42) hClient.Run("foo", ui, nil, 42)
assert.True(h.runCalled, "run should be called") assert.True(h.RunCalled, "run should be called")
// Test Cancel
hClient.Cancel()
assert.True(h.CancelCalled, "cancel should be called")
} }
func TestHook_Implements(t *testing.T) { func TestHook_Implements(t *testing.T) {
@ -48,3 +45,56 @@ func TestHook_Implements(t *testing.T) {
assert.Implementor(h, &r, "should be a Hook") assert.Implementor(h, &r, "should be a Hook")
} }
func TestHook_cancelWhileRun(t *testing.T) {
var finishLock sync.Mutex
finishOrder := make([]string, 0, 2)
h := &packer.MockHook{
RunFunc: func() error {
time.Sleep(100 * time.Millisecond)
finishLock.Lock()
finishOrder = append(finishOrder, "run")
finishLock.Unlock()
return nil
},
}
// Serve
server := rpc.NewServer()
RegisterHook(server, h)
address := serveSingleConn(server)
// Create the client over RPC and run some methods to verify it works
client, err := rpc.Dial("tcp", address)
if err != nil {
t.Fatalf("err: %s", err)
}
hClient := Hook(client)
// Start the run
finished := make(chan struct{})
go func() {
hClient.Run("foo", nil, nil, nil)
close(finished)
}()
// Cancel it pretty quickly.
time.Sleep(10 * time.Millisecond)
hClient.Cancel()
finishLock.Lock()
finishOrder = append(finishOrder, "cancel")
finishLock.Unlock()
// Verify things are good
<-finished
// Check the results
expected := []string{"cancel", "run"}
if !reflect.DeepEqual(finishOrder, expected) {
t.Fatalf("bad: %#v", finishOrder)
}
}

View File

@ -2,6 +2,7 @@ package rpc
import ( import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log"
"net/rpc" "net/rpc"
) )
@ -47,6 +48,13 @@ func (p *provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
return p.client.Call("Provisioner.Provision", args, new(interface{})) return p.client.Call("Provisioner.Provision", args, new(interface{}))
} }
func (p *provisioner) Cancel() {
err := p.client.Call("Provisioner.Cancel", new(interface{}), new(interface{}))
if err != nil {
log.Printf("Provisioner.Cancel err: %s", err)
}
}
func (p *ProvisionerServer) Prepare(args *ProvisionerPrepareArgs, reply *error) error { func (p *ProvisionerServer) Prepare(args *ProvisionerPrepareArgs, reply *error) error {
*reply = p.p.Prepare(args.Configs...) *reply = p.p.Prepare(args.Configs...)
if *reply != nil { if *reply != nil {
@ -71,3 +79,8 @@ func (p *ProvisionerServer) Provision(args *ProvisionerProvisionArgs, reply *int
return nil return nil
} }
func (p *ProvisionerServer) Cancel(args *interface{}, reply *interface{}) error {
p.p.Cancel()
return nil
}

View File

@ -7,32 +7,11 @@ import (
"testing" "testing"
) )
type testProvisioner struct {
prepareCalled bool
prepareConfigs []interface{}
provCalled bool
provComm packer.Communicator
provUi packer.Ui
}
func (p *testProvisioner) Prepare(configs ...interface{}) error {
p.prepareCalled = true
p.prepareConfigs = configs
return nil
}
func (p *testProvisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
p.provCalled = true
p.provComm = comm
p.provUi = ui
return nil
}
func TestProvisionerRPC(t *testing.T) { func TestProvisionerRPC(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) assert := asserts.NewTestingAsserts(t, true)
// Create the interface to test // Create the interface to test
p := new(testProvisioner) p := new(packer.MockProvisioner)
// Start the server // Start the server
server := rpc.NewServer() server := rpc.NewServer()
@ -47,17 +26,23 @@ func TestProvisionerRPC(t *testing.T) {
config := 42 config := 42
pClient := Provisioner(client) pClient := Provisioner(client)
pClient.Prepare(config) pClient.Prepare(config)
assert.True(p.prepareCalled, "prepare should be called") assert.True(p.PrepCalled, "prepare should be called")
assert.Equal(p.prepareConfigs, []interface{}{42}, "prepare should be called with right arg") assert.Equal(p.PrepConfigs, []interface{}{42}, "prepare should be called with right arg")
// Test Provision // Test Provision
ui := &testUi{} ui := &testUi{}
comm := new(packer.MockCommunicator) comm := &packer.MockCommunicator{}
pClient.Provision(ui, comm) pClient.Provision(ui, comm)
assert.True(p.provCalled, "provision should be called") assert.True(p.ProvCalled, "provision should be called")
p.provUi.Say("foo") p.ProvUi.Say("foo")
assert.True(ui.sayCalled, "say should be called") assert.True(ui.sayCalled, "say should be called")
// Test Cancel
pClient.Cancel()
if !p.CancelCalled {
t.Fatal("cancel should be called")
}
} }
func TestProvisioner_Implements(t *testing.T) { func TestProvisioner_Implements(t *testing.T) {

View File

@ -589,7 +589,7 @@ func TestTemplate_Build(t *testing.T) {
"test-builder": builder, "test-builder": builder,
} }
provisioner := &TestProvisioner{} provisioner := &MockProvisioner{}
provisionerMap := map[string]Provisioner{ provisionerMap := map[string]Provisioner{
"test-prov": provisioner, "test-prov": provisioner,
} }
@ -677,7 +677,7 @@ func TestTemplate_Build_ProvisionerOverride(t *testing.T) {
"test-builder": builder, "test-builder": builder,
} }
provisioner := &TestProvisioner{} provisioner := &MockProvisioner{}
provisionerMap := map[string]Provisioner{ provisionerMap := map[string]Provisioner{
"test-prov": provisioner, "test-prov": provisioner,
} }

View File

@ -183,6 +183,12 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
return nil return nil
} }
func (p *Provisioner) Cancel() {
// Just hard quit. It isn't a big deal if what we're doing keeps
// running on the other side.
os.Exit(0)
}
func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
if err := p.createDir(ui, comm, dst); err != nil { if err := p.createDir(ui, comm, dst); err != nil {
return err return err

View File

@ -84,3 +84,9 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
} }
return err return err
} }
func (p *Provisioner) Cancel() {
// Just hard quit. It isn't a big deal if what we're doing keeps
// running on the other side.
os.Exit(0)
}

View File

@ -200,6 +200,12 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
return nil return nil
} }
func (p *Provisioner) Cancel() {
// Just hard quit. It isn't a big deal if what we're doing keeps
// running on the other side.
os.Exit(0)
}
func UploadLocalDirectory(localDir string, remoteDir string, comm packer.Communicator, ui packer.Ui) (err error) { func UploadLocalDirectory(localDir string, remoteDir string, comm packer.Communicator, ui packer.Ui) (err error) {
visitPath := func(localPath string, f os.FileInfo, err error) (err2 error) { visitPath := func(localPath string, f os.FileInfo, err error) (err2 error) {
localRelPath := strings.Replace(localPath, localDir, "", 1) localRelPath := strings.Replace(localPath, localDir, "", 1)

View File

@ -281,6 +281,12 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
return nil return nil
} }
func (p *Provisioner) Cancel() {
// Just hard quit. It isn't a big deal if what we're doing keeps
// running on the other side.
os.Exit(0)
}
// retryable will retry the given function over and over until a // retryable will retry the given function over and over until a
// non-error is returned. // non-error is returned.
func (p *Provisioner) retryable(f func() error) error { func (p *Provisioner) retryable(f func() error) error {