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