github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/worker/uniter/runner/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 "gopkg.in/check.v1" 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, jc.ErrorIsNil) 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, jc.ErrorIsNil) 72 session, err = s.ctx.FindSession() 73 c.Assert(session, gc.NotNil) 74 c.Assert(err, jc.ErrorIsNil) 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, jc.ErrorIsNil) 82 session, err = s.ctx.FindSession() 83 c.Assert(session, gc.NotNil) 84 c.Assert(err, jc.ErrorIsNil) 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, jc.ErrorIsNil) 97 session, err := s.ctx.FindSession() 98 c.Assert(session, gc.NotNil) 99 c.Assert(err, jc.ErrorIsNil) 100 101 flockAcquired := make(chan struct{}, 1) 102 waitForFlock := func() { 103 select { 104 case <-flockAcquired: 105 case <-time.After(testing.ShortWait): 106 c.Fatalf("timed out waiting for hook to acquire flock") 107 } 108 } 109 110 // Run the hook in debug mode with no exit flock held. 111 // The exit flock will be acquired immediately, and the 112 // debug-hooks server process killed. 113 s.PatchValue(&waitClientExit, func(*ServerSession) { 114 flockAcquired <- struct{}{} 115 }) 116 err = session.RunHook("myhook", s.tmpdir, os.Environ()) 117 c.Assert(err, gc.ErrorMatches, "signal: [kK]illed") 118 waitForFlock() 119 120 // Run the hook in debug mode, simulating the holding 121 // of the exit flock. This simulates the client process 122 // starting but not cleanly exiting (normally the .pid 123 // file is updated, and the server waits on the client 124 // process' death). 125 ch := make(chan bool) // acquire the flock 126 var clientExited bool 127 s.PatchValue(&waitClientExit, func(*ServerSession) { 128 clientExited = <-ch 129 flockAcquired <- struct{}{} 130 }) 131 go func() { ch <- true }() // asynchronously release the flock 132 err = session.RunHook("myhook", s.tmpdir, os.Environ()) 133 waitForFlock() 134 c.Assert(clientExited, jc.IsTrue) 135 c.Assert(err, gc.ErrorMatches, "signal: [kK]illed") 136 } 137 138 func (s *DebugHooksServerSuite) TestRunHook(c *gc.C) { 139 err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777) 140 c.Assert(err, jc.ErrorIsNil) 141 session, err := s.ctx.FindSession() 142 c.Assert(session, gc.NotNil) 143 c.Assert(err, jc.ErrorIsNil) 144 145 const hookName = "myhook" 146 147 // Run the hook in debug mode with the exit flock held, 148 // and also create the .pid file. We'll populate it with 149 // an invalid PID; this will cause the server process to 150 // exit cleanly (as if the PID were real and no longer running). 151 cmd := exec.Command("flock", s.ctx.ClientExitFileLock(), "-c", "sleep 5s") 152 c.Assert(cmd.Start(), gc.IsNil) 153 ch := make(chan error) 154 go func() { 155 ch <- session.RunHook(hookName, s.tmpdir, os.Environ()) 156 }() 157 158 // Wait until either we find the debug dir, or the flock is released. 159 ticker := time.Tick(10 * time.Millisecond) 160 var debugdir os.FileInfo 161 for debugdir == nil { 162 select { 163 case err = <-ch: 164 // flock was released before we found the debug dir. 165 c.Error("could not find hook.sh") 166 167 case <-ticker: 168 tmpdir, err := os.Open(s.tmpdir) 169 if err != nil { 170 c.Fatalf("Failed to open $TMPDIR: %s", err) 171 } 172 fi, err := tmpdir.Readdir(-1) 173 if err != nil { 174 c.Fatalf("Failed to read $TMPDIR: %s", err) 175 } 176 tmpdir.Close() 177 for _, fi := range fi { 178 if fi.IsDir() { 179 hooksh := filepath.Join(s.tmpdir, fi.Name(), "hook.sh") 180 if _, err = os.Stat(hooksh); err == nil { 181 debugdir = fi 182 break 183 } 184 } 185 } 186 if debugdir != nil { 187 break 188 } 189 time.Sleep(10 * time.Millisecond) 190 } 191 } 192 193 envsh := filepath.Join(s.tmpdir, debugdir.Name(), "env.sh") 194 s.verifyEnvshFile(c, envsh, hookName) 195 196 hookpid := filepath.Join(s.tmpdir, debugdir.Name(), "hook.pid") 197 err = ioutil.WriteFile(hookpid, []byte("not a pid"), 0777) 198 c.Assert(err, jc.ErrorIsNil) 199 200 // RunHook should complete without waiting to be 201 // killed, and despite the exit lock being held. 202 err = <-ch 203 c.Assert(err, jc.ErrorIsNil) 204 cmd.Process.Kill() // kill flock 205 } 206 207 func (s *DebugHooksServerSuite) verifyEnvshFile(c *gc.C, envshPath string, hookName string) { 208 data, err := ioutil.ReadFile(envshPath) 209 c.Assert(err, jc.ErrorIsNil) 210 contents := string(data) 211 c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_UNIT_NAME=%q", s.ctx.Unit)) 212 c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_HOOK_NAME=%q", hookName)) 213 c.Assert(contents, jc.Contains, fmt.Sprintf(`PS1="%s:%s %% "`, s.ctx.Unit, hookName)) 214 }