diff --git a/packer/build.go b/packer/build.go index 1187e49d7..bb441b522 100644 --- a/packer/build.go +++ b/packer/build.go @@ -200,10 +200,18 @@ func (b *coreBuild) Run(originalUi Ui, cache Cache) ([]Artifact, error) { if len(p.config) > 0 { pConfig = p.config[0] } - hookedProvisioners[i] = &HookedProvisioner{ - p.provisioner, - pConfig, - p.pType, + if b.debug { + hookedProvisioners[i] = &HookedProvisioner{ + &DebuggedProvisioner{Provisioner: p.provisioner}, + pConfig, + p.pType, + } + } else { + hookedProvisioners[i] = &HookedProvisioner{ + p.provisioner, + pConfig, + p.pType, + } } } diff --git a/packer/provisioner.go b/packer/provisioner.go index 93411808d..a565c30e2 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -2,6 +2,7 @@ package packer import ( "fmt" + "log" "sync" "time" ) @@ -168,3 +169,90 @@ func (p *PausedProvisioner) Cancel() { func (p *PausedProvisioner) provision(result chan<- error, ui Ui, comm Communicator) { result <- p.Provisioner.Provision(ui, comm) } + +// DebuggedProvisioner is a Provisioner implementation that waits until a key +// press before the provisioner is actually run. +type DebuggedProvisioner struct { + Provisioner Provisioner + + cancelCh chan struct{} + doneCh chan struct{} + lock sync.Mutex +} + +func (p *DebuggedProvisioner) Prepare(raws ...interface{}) error { + return p.Provisioner.Prepare(raws...) +} + +func (p *DebuggedProvisioner) Provision(ui Ui, comm Communicator) error { + p.lock.Lock() + cancelCh := make(chan struct{}) + p.cancelCh = cancelCh + + // Setup the done channel, which is trigger when we're done + doneCh := make(chan struct{}) + defer close(doneCh) + p.doneCh = doneCh + p.lock.Unlock() + + defer func() { + p.lock.Lock() + defer p.lock.Unlock() + if p.cancelCh == cancelCh { + p.cancelCh = nil + } + if p.doneCh == doneCh { + p.doneCh = nil + } + }() + + // Use a select to determine if we get cancelled during the wait + message := "Pausing before the next provisioner . Press enter to continue." + + result := make(chan string, 1) + go func() { + line, err := ui.Ask(message) + if err != nil { + log.Printf("Error asking for input: %s", err) + } + + result <- line + }() + + select { + case <-result: + case <-cancelCh: + return nil + } + + provDoneCh := make(chan error, 1) + go p.provision(provDoneCh, ui, comm) + + select { + case err := <-provDoneCh: + return err + case <-cancelCh: + p.Provisioner.Cancel() + return <-provDoneCh + } +} + +func (p *DebuggedProvisioner) Cancel() { + var doneCh chan struct{} + + p.lock.Lock() + if p.cancelCh != nil { + close(p.cancelCh) + p.cancelCh = nil + } + if p.doneCh != nil { + doneCh = p.doneCh + } + p.lock.Unlock() + + <-doneCh +} + +func (p *DebuggedProvisioner) provision(result chan<- error, ui Ui, comm Communicator) { + result <- p.Provisioner.Provision(ui, comm) +} diff --git a/packer/provisioner_test.go b/packer/provisioner_test.go index 22d34806f..4d370ef39 100644 --- a/packer/provisioner_test.go +++ b/packer/provisioner_test.go @@ -197,3 +197,67 @@ func TestPausedProvisionerCancel(t *testing.T) { t.Fatal("cancel should be called") } } + +func TestDebuggedProvisioner_impl(t *testing.T) { + var _ Provisioner = new(DebuggedProvisioner) +} + +func TestDebuggedProvisionerPrepare(t *testing.T) { + mock := new(MockProvisioner) + prov := &DebuggedProvisioner{ + Provisioner: mock, + } + + prov.Prepare(42) + if !mock.PrepCalled { + t.Fatal("prepare should be called") + } + if mock.PrepConfigs[0] != 42 { + t.Fatal("should have proper configs") + } +} + +func TestDebuggedProvisionerProvision(t *testing.T) { + mock := new(MockProvisioner) + prov := &DebuggedProvisioner{ + Provisioner: mock, + } + + ui := testUi() + comm := new(MockCommunicator) + writeReader(ui, "\n") + prov.Provision(ui, comm) + if !mock.ProvCalled { + t.Fatal("prov should be called") + } + if mock.ProvUi != ui { + t.Fatal("should have proper ui") + } + if mock.ProvCommunicator != comm { + t.Fatal("should have proper comm") + } +} + +func TestDebuggedProvisionerCancel(t *testing.T) { + mock := new(MockProvisioner) + prov := &DebuggedProvisioner{ + Provisioner: mock, + } + + provCh := make(chan struct{}) + mock.ProvFunc = func() error { + close(provCh) + time.Sleep(10 * time.Millisecond) + return nil + } + + // Start provisioning and wait for it to start + go prov.Provision(testUi(), new(MockCommunicator)) + <-provCh + + // Cancel it + prov.Cancel() + if !mock.CancelCalled { + t.Fatal("cancel should be called") + } +}