1
// Copyright 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
16
jc "github.com/juju/testing/checkers"
17
gc "gopkg.in/check.v1"
19
"github.com/juju/juju/testing"
22
type DebugHooksServerSuite struct {
29
var _ = gc.Suite(&DebugHooksServerSuite{})
31
// echocommand outputs its name and arguments to stdout for verification,
32
// and exits with the value of $EXIT_CODE
33
var echocommand = `#!/bin/bash --norc
34
echo $(basename $0) $@
38
var fakecommands = []string{"tmux"}
40
func (s *DebugHooksServerSuite) SetUpTest(c *gc.C) {
41
if runtime.GOOS == "windows" {
42
c.Skip("bug 1403084: Currently debug does not work on windows")
46
s.PatchEnvPathPrepend(s.fakebin)
47
s.PatchEnvironment("TMPDIR", s.tmpdir)
48
s.PatchEnvironment("TEST_RESULT", "")
49
for _, name := range fakecommands {
50
err := ioutil.WriteFile(filepath.Join(s.fakebin, name), []byte(echocommand), 0777)
51
c.Assert(err, jc.ErrorIsNil)
53
s.ctx = NewHooksContext("foo/8")
54
s.ctx.FlockDir = s.tmpdir
55
s.PatchEnvironment("JUJU_UNIT_NAME", s.ctx.Unit)
58
func (s *DebugHooksServerSuite) TestFindSession(c *gc.C) {
59
// Test "tmux has-session" failure. The error
60
// message is the output of tmux has-session.
61
os.Setenv("EXIT_CODE", "1")
62
session, err := s.ctx.FindSession()
63
c.Assert(session, gc.IsNil)
64
c.Assert(err, gc.ErrorMatches, regexp.QuoteMeta("tmux has-session -t "+s.ctx.Unit+"\n"))
65
os.Setenv("EXIT_CODE", "")
67
// tmux session exists, but missing debug-hooks file: error.
68
session, err = s.ctx.FindSession()
69
c.Assert(session, gc.IsNil)
70
c.Assert(err, gc.NotNil)
71
c.Assert(err, jc.Satisfies, os.IsNotExist)
73
// Hooks file is present, empty.
74
err = ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777)
75
c.Assert(err, jc.ErrorIsNil)
76
session, err = s.ctx.FindSession()
77
c.Assert(session, gc.NotNil)
78
c.Assert(err, jc.ErrorIsNil)
79
// If session.hooks is empty, it'll match anything.
80
c.Assert(session.MatchHook(""), jc.IsTrue)
81
c.Assert(session.MatchHook("something"), jc.IsTrue)
83
// Hooks file is present, non-empty
84
err = ioutil.WriteFile(s.ctx.ClientFileLock(), []byte(`hooks: [foo, bar, baz]`), 0777)
85
c.Assert(err, jc.ErrorIsNil)
86
session, err = s.ctx.FindSession()
87
c.Assert(session, gc.NotNil)
88
c.Assert(err, jc.ErrorIsNil)
89
// session should only match "foo", "bar" or "baz".
90
c.Assert(session.MatchHook(""), jc.IsFalse)
91
c.Assert(session.MatchHook("something"), jc.IsFalse)
92
c.Assert(session.MatchHook("foo"), jc.IsTrue)
93
c.Assert(session.MatchHook("bar"), jc.IsTrue)
94
c.Assert(session.MatchHook("baz"), jc.IsTrue)
95
c.Assert(session.MatchHook("foo bar baz"), jc.IsFalse)
98
func (s *DebugHooksServerSuite) TestRunHookExceptional(c *gc.C) {
99
err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777)
100
c.Assert(err, jc.ErrorIsNil)
101
session, err := s.ctx.FindSession()
102
c.Assert(session, gc.NotNil)
103
c.Assert(err, jc.ErrorIsNil)
105
flockAcquired := make(chan struct{}, 1)
106
waitForFlock := func() {
108
case <-flockAcquired:
109
case <-time.After(testing.ShortWait):
110
c.Fatalf("timed out waiting for hook to acquire flock")
114
// Run the hook in debug mode with no exit flock held.
115
// The exit flock will be acquired immediately, and the
116
// debug-hooks server process killed.
117
s.PatchValue(&waitClientExit, func(*ServerSession) {
118
flockAcquired <- struct{}{}
120
err = session.RunHook("myhook", s.tmpdir, os.Environ())
121
c.Assert(err, gc.ErrorMatches, "signal: [kK]illed")
124
// Run the hook in debug mode, simulating the holding
125
// of the exit flock. This simulates the client process
126
// starting but not cleanly exiting (normally the .pid
127
// file is updated, and the server waits on the client
129
ch := make(chan bool) // acquire the flock
130
var clientExited bool
131
s.PatchValue(&waitClientExit, func(*ServerSession) {
133
flockAcquired <- struct{}{}
135
go func() { ch <- true }() // asynchronously release the flock
136
err = session.RunHook("myhook", s.tmpdir, os.Environ())
138
c.Assert(clientExited, jc.IsTrue)
139
c.Assert(err, gc.ErrorMatches, "signal: [kK]illed")
142
func (s *DebugHooksServerSuite) TestRunHook(c *gc.C) {
143
err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777)
144
c.Assert(err, jc.ErrorIsNil)
145
session, err := s.ctx.FindSession()
146
c.Assert(session, gc.NotNil)
147
c.Assert(err, jc.ErrorIsNil)
149
const hookName = "myhook"
151
// Run the hook in debug mode with the exit flock held,
152
// and also create the .pid file. We'll populate it with
153
// an invalid PID; this will cause the server process to
154
// exit cleanly (as if the PID were real and no longer running).
155
cmd := exec.Command("flock", s.ctx.ClientExitFileLock(), "-c", "sleep 5s")
156
c.Assert(cmd.Start(), gc.IsNil)
157
ch := make(chan error)
159
ch <- session.RunHook(hookName, s.tmpdir, os.Environ())
162
// Wait until either we find the debug dir, or the flock is released.
163
ticker := time.Tick(10 * time.Millisecond)
164
var debugdir os.FileInfo
165
for debugdir == nil {
168
// flock was released before we found the debug dir.
169
c.Error("could not find hook.sh")
172
tmpdir, err := os.Open(s.tmpdir)
174
c.Fatalf("Failed to open $TMPDIR: %s", err)
176
fi, err := tmpdir.Readdir(-1)
178
c.Fatalf("Failed to read $TMPDIR: %s", err)
181
for _, fi := range fi {
183
hooksh := filepath.Join(s.tmpdir, fi.Name(), "hook.sh")
184
if _, err = os.Stat(hooksh); err == nil {
193
time.Sleep(10 * time.Millisecond)
197
envsh := filepath.Join(s.tmpdir, debugdir.Name(), "env.sh")
198
s.verifyEnvshFile(c, envsh, hookName)
200
hookpid := filepath.Join(s.tmpdir, debugdir.Name(), "hook.pid")
201
err = ioutil.WriteFile(hookpid, []byte("not a pid"), 0777)
202
c.Assert(err, jc.ErrorIsNil)
204
// RunHook should complete without waiting to be
205
// killed, and despite the exit lock being held.
207
c.Assert(err, jc.ErrorIsNil)
208
cmd.Process.Kill() // kill flock
211
func (s *DebugHooksServerSuite) verifyEnvshFile(c *gc.C, envshPath string, hookName string) {
212
data, err := ioutil.ReadFile(envshPath)
213
c.Assert(err, jc.ErrorIsNil)
214
contents := string(data)
215
c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_UNIT_NAME=%q", s.ctx.Unit))
216
c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_HOOK_NAME=%q", hookName))
217
c.Assert(contents, jc.Contains, fmt.Sprintf(`PS1="%s:%s %% "`, s.ctx.Unit, hookName))