github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/worker/uniter/debug/server_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package debug 5 6 import ( 7 "fmt" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "regexp" 13 "time" 14 15 jc "github.com/juju/testing/checkers" 16 gc "launchpad.net/gocheck" 17 18 "github.com/juju/juju/testing" 19 ) 20 21 type DebugHooksServerSuite struct { 22 testing.BaseSuite 23 ctx *HooksContext 24 fakebin string 25 tmpdir string 26 } 27 28 var _ = gc.Suite(&DebugHooksServerSuite{}) 29 30 // echocommand outputs its name and arguments to stdout for verification, 31 // and exits with the value of $EXIT_CODE 32 var echocommand = `#!/bin/bash --norc 33 echo $(basename $0) $@ 34 exit $EXIT_CODE 35 ` 36 37 var fakecommands = []string{"tmux"} 38 39 func (s *DebugHooksServerSuite) SetUpTest(c *gc.C) { 40 s.fakebin = c.MkDir() 41 s.tmpdir = c.MkDir() 42 s.PatchEnvPathPrepend(s.fakebin) 43 s.PatchEnvironment("TMPDIR", s.tmpdir) 44 s.PatchEnvironment("TEST_RESULT", "") 45 for _, name := range fakecommands { 46 err := ioutil.WriteFile(filepath.Join(s.fakebin, name), []byte(echocommand), 0777) 47 c.Assert(err, gc.IsNil) 48 } 49 s.ctx = NewHooksContext("foo/8") 50 s.ctx.FlockDir = s.tmpdir 51 s.PatchEnvironment("JUJU_UNIT_NAME", s.ctx.Unit) 52 } 53 54 func (s *DebugHooksServerSuite) TestFindSession(c *gc.C) { 55 // Test "tmux has-session" failure. The error 56 // message is the output of tmux has-session. 57 os.Setenv("EXIT_CODE", "1") 58 session, err := s.ctx.FindSession() 59 c.Assert(session, gc.IsNil) 60 c.Assert(err, gc.ErrorMatches, regexp.QuoteMeta("tmux has-session -t "+s.ctx.Unit+"\n")) 61 os.Setenv("EXIT_CODE", "") 62 63 // tmux session exists, but missing debug-hooks file: error. 64 session, err = s.ctx.FindSession() 65 c.Assert(session, gc.IsNil) 66 c.Assert(err, gc.NotNil) 67 c.Assert(err, jc.Satisfies, os.IsNotExist) 68 69 // Hooks file is present, empty. 70 err = ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777) 71 c.Assert(err, gc.IsNil) 72 session, err = s.ctx.FindSession() 73 c.Assert(session, gc.NotNil) 74 c.Assert(err, gc.IsNil) 75 // If session.hooks is empty, it'll match anything. 76 c.Assert(session.MatchHook(""), jc.IsTrue) 77 c.Assert(session.MatchHook("something"), jc.IsTrue) 78 79 // Hooks file is present, non-empty 80 err = ioutil.WriteFile(s.ctx.ClientFileLock(), []byte(`hooks: [foo, bar, baz]`), 0777) 81 c.Assert(err, gc.IsNil) 82 session, err = s.ctx.FindSession() 83 c.Assert(session, gc.NotNil) 84 c.Assert(err, gc.IsNil) 85 // session should only match "foo", "bar" or "baz". 86 c.Assert(session.MatchHook(""), jc.IsFalse) 87 c.Assert(session.MatchHook("something"), jc.IsFalse) 88 c.Assert(session.MatchHook("foo"), jc.IsTrue) 89 c.Assert(session.MatchHook("bar"), jc.IsTrue) 90 c.Assert(session.MatchHook("baz"), jc.IsTrue) 91 c.Assert(session.MatchHook("foo bar baz"), jc.IsFalse) 92 } 93 94 func (s *DebugHooksServerSuite) TestRunHookExceptional(c *gc.C) { 95 err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777) 96 c.Assert(err, gc.IsNil) 97 session, err := s.ctx.FindSession() 98 c.Assert(session, gc.NotNil) 99 c.Assert(err, gc.IsNil) 100 101 // Run the hook in debug mode with no exit flock held. 102 // The exit flock will be acquired immediately, and the 103 // debug-hooks server process killed. 104 err = session.RunHook("myhook", s.tmpdir, os.Environ()) 105 c.Assert(err, gc.ErrorMatches, "signal: [kK]illed") 106 107 // Run the hook in debug mode, simulating the holding 108 // of the exit flock. This simulates the client process 109 // starting but not cleanly exiting (normally the .pid 110 // file is updated, and the server waits on the client 111 // process' death). 112 ch := make(chan bool) 113 var clientExited bool 114 s.PatchValue(&waitClientExit, func(*ServerSession) { 115 clientExited = <-ch 116 }) 117 go func() { ch <- true }() 118 err = session.RunHook("myhook", s.tmpdir, os.Environ()) 119 c.Assert(clientExited, jc.IsTrue) 120 c.Assert(err, gc.ErrorMatches, "signal: [kK]illed") 121 } 122 123 func (s *DebugHooksServerSuite) TestRunHook(c *gc.C) { 124 err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777) 125 c.Assert(err, gc.IsNil) 126 session, err := s.ctx.FindSession() 127 c.Assert(session, gc.NotNil) 128 c.Assert(err, gc.IsNil) 129 130 const hookName = "myhook" 131 132 // Run the hook in debug mode with the exit flock held, 133 // and also create the .pid file. We'll populate it with 134 // an invalid PID; this will cause the server process to 135 // exit cleanly (as if the PID were real and no longer running). 136 cmd := exec.Command("flock", s.ctx.ClientExitFileLock(), "-c", "sleep 5s") 137 c.Assert(cmd.Start(), gc.IsNil) 138 ch := make(chan error) 139 go func() { 140 ch <- session.RunHook(hookName, s.tmpdir, os.Environ()) 141 }() 142 143 // Wait until either we find the debug dir, or the flock is released. 144 ticker := time.Tick(10 * time.Millisecond) 145 var debugdir os.FileInfo 146 for debugdir == nil { 147 select { 148 case err = <-ch: 149 // flock was released before we found the debug dir. 150 c.Error("could not find hook.sh") 151 152 case <-ticker: 153 tmpdir, err := os.Open(s.tmpdir) 154 if err != nil { 155 c.Fatalf("Failed to open $TMPDIR: %s", err) 156 } 157 fi, err := tmpdir.Readdir(-1) 158 if err != nil { 159 c.Fatalf("Failed to read $TMPDIR: %s", err) 160 } 161 tmpdir.Close() 162 for _, fi := range fi { 163 if fi.IsDir() { 164 hooksh := filepath.Join(s.tmpdir, fi.Name(), "hook.sh") 165 if _, err = os.Stat(hooksh); err == nil { 166 debugdir = fi 167 break 168 } 169 } 170 } 171 if debugdir != nil { 172 break 173 } 174 time.Sleep(10 * time.Millisecond) 175 } 176 } 177 178 envsh := filepath.Join(s.tmpdir, debugdir.Name(), "env.sh") 179 s.verifyEnvshFile(c, envsh, hookName) 180 181 hookpid := filepath.Join(s.tmpdir, debugdir.Name(), "hook.pid") 182 err = ioutil.WriteFile(hookpid, []byte("not a pid"), 0777) 183 c.Assert(err, gc.IsNil) 184 185 // RunHook should complete without waiting to be 186 // killed, and despite the exit lock being held. 187 err = <-ch 188 c.Assert(err, gc.IsNil) 189 cmd.Process.Kill() // kill flock 190 } 191 192 func (s *DebugHooksServerSuite) verifyEnvshFile(c *gc.C, envshPath string, hookName string) { 193 data, err := ioutil.ReadFile(envshPath) 194 c.Assert(err, gc.IsNil) 195 contents := string(data) 196 c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_UNIT_NAME=%q", s.ctx.Unit)) 197 c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_HOOK_NAME=%q", hookName)) 198 c.Assert(contents, jc.Contains, fmt.Sprintf(`PS1="%s:%s %% "`, s.ctx.Unit, hookName)) 199 }