github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 "runtime" 14 "time" 15 16 jc "github.com/juju/testing/checkers" 17 gc "gopkg.in/check.v1" 18 19 "github.com/juju/juju/testing" 20 ) 21 22 type DebugHooksServerSuite struct { 23 testing.BaseSuite 24 ctx *HooksContext 25 fakebin string 26 tmpdir string 27 } 28 29 var _ = gc.Suite(&DebugHooksServerSuite{}) 30 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) $@ 35 exit $EXIT_CODE 36 ` 37 38 var fakecommands = []string{"tmux"} 39 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") 43 } 44 s.fakebin = c.MkDir() 45 s.tmpdir = c.MkDir() 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) 52 } 53 s.ctx = NewHooksContext("foo/8") 54 s.ctx.FlockDir = s.tmpdir 55 s.PatchEnvironment("JUJU_UNIT_NAME", s.ctx.Unit) 56 } 57 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", "") 66 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) 72 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) 82 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) 96 } 97 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) 104 105 flockAcquired := make(chan struct{}, 1) 106 waitForFlock := func() { 107 select { 108 case <-flockAcquired: 109 case <-time.After(testing.ShortWait): 110 c.Fatalf("timed out waiting for hook to acquire flock") 111 } 112 } 113 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{}{} 119 }) 120 err = session.RunHook("myhook", s.tmpdir, os.Environ()) 121 c.Assert(err, gc.ErrorMatches, "signal: [kK]illed") 122 waitForFlock() 123 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 128 // process' death). 129 ch := make(chan bool) // acquire the flock 130 var clientExited bool 131 s.PatchValue(&waitClientExit, func(*ServerSession) { 132 clientExited = <-ch 133 flockAcquired <- struct{}{} 134 }) 135 go func() { ch <- true }() // asynchronously release the flock 136 err = session.RunHook("myhook", s.tmpdir, os.Environ()) 137 waitForFlock() 138 c.Assert(clientExited, jc.IsTrue) 139 c.Assert(err, gc.ErrorMatches, "signal: [kK]illed") 140 } 141 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) 148 149 const hookName = "myhook" 150 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) 158 go func() { 159 ch <- session.RunHook(hookName, s.tmpdir, os.Environ()) 160 }() 161 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 { 166 select { 167 case err = <-ch: 168 // flock was released before we found the debug dir. 169 c.Error("could not find hook.sh") 170 171 case <-ticker: 172 tmpdir, err := os.Open(s.tmpdir) 173 if err != nil { 174 c.Fatalf("Failed to open $TMPDIR: %s", err) 175 } 176 fi, err := tmpdir.Readdir(-1) 177 if err != nil { 178 c.Fatalf("Failed to read $TMPDIR: %s", err) 179 } 180 tmpdir.Close() 181 for _, fi := range fi { 182 if fi.IsDir() { 183 hooksh := filepath.Join(s.tmpdir, fi.Name(), "hook.sh") 184 if _, err = os.Stat(hooksh); err == nil { 185 debugdir = fi 186 break 187 } 188 } 189 } 190 if debugdir != nil { 191 break 192 } 193 time.Sleep(10 * time.Millisecond) 194 } 195 } 196 197 envsh := filepath.Join(s.tmpdir, debugdir.Name(), "env.sh") 198 s.verifyEnvshFile(c, envsh, hookName) 199 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) 203 204 // RunHook should complete without waiting to be 205 // killed, and despite the exit lock being held. 206 err = <-ch 207 c.Assert(err, jc.ErrorIsNil) 208 cmd.Process.Kill() // kill flock 209 } 210 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)) 218 }