github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "path/filepath" 12 "regexp" 13 "runtime" 14 "strings" 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{"sleep", "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 47 // Create a clean $TMPDIR for the debug hooks scripts. 48 s.tmpdir = filepath.Join(c.MkDir(), "debug-hooks") 49 err := os.RemoveAll(s.tmpdir) 50 c.Assert(err, jc.ErrorIsNil) 51 err = os.MkdirAll(s.tmpdir, 0755) 52 c.Assert(err, jc.ErrorIsNil) 53 54 s.PatchEnvPathPrepend(s.fakebin) 55 s.PatchEnvironment("TMPDIR", s.tmpdir) 56 s.PatchEnvironment("TEST_RESULT", "") 57 for _, name := range fakecommands { 58 err := ioutil.WriteFile(filepath.Join(s.fakebin, name), []byte(echocommand), 0777) 59 c.Assert(err, jc.ErrorIsNil) 60 } 61 s.ctx = NewHooksContext("foo/8") 62 s.ctx.FlockDir = c.MkDir() 63 s.PatchEnvironment("JUJU_UNIT_NAME", s.ctx.Unit) 64 } 65 66 func (s *DebugHooksServerSuite) TestFindSession(c *gc.C) { 67 // Test "tmux has-session" failure. The error 68 // message is the output of tmux has-session. 69 os.Setenv("EXIT_CODE", "1") 70 session, err := s.ctx.FindSession() 71 c.Assert(session, gc.IsNil) 72 c.Assert(err, gc.ErrorMatches, regexp.QuoteMeta("tmux has-session -t "+s.ctx.Unit+"\n")) 73 os.Setenv("EXIT_CODE", "") 74 75 // tmux session exists, but missing debug-hooks file: error. 76 session, err = s.ctx.FindSession() 77 c.Assert(session, gc.IsNil) 78 c.Assert(err, gc.NotNil) 79 c.Assert(err, jc.Satisfies, os.IsNotExist) 80 81 // Hooks file is present, empty. 82 err = ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777) 83 c.Assert(err, jc.ErrorIsNil) 84 session, err = s.ctx.FindSession() 85 c.Assert(session, gc.NotNil) 86 c.Assert(err, jc.ErrorIsNil) 87 // If session.hooks is empty, it'll match anything. 88 c.Assert(session.MatchHook(""), jc.IsTrue) 89 c.Assert(session.MatchHook("something"), jc.IsTrue) 90 91 // Hooks file is present, non-empty 92 err = ioutil.WriteFile(s.ctx.ClientFileLock(), []byte(`hooks: [foo, bar, baz]`), 0777) 93 c.Assert(err, jc.ErrorIsNil) 94 session, err = s.ctx.FindSession() 95 c.Assert(session, gc.NotNil) 96 c.Assert(err, jc.ErrorIsNil) 97 // session should only match "foo", "bar" or "baz". 98 c.Assert(session.MatchHook(""), jc.IsFalse) 99 c.Assert(session.MatchHook("something"), jc.IsFalse) 100 c.Assert(session.MatchHook("foo"), jc.IsTrue) 101 c.Assert(session.MatchHook("bar"), jc.IsTrue) 102 c.Assert(session.MatchHook("baz"), jc.IsTrue) 103 c.Assert(session.MatchHook("foo bar baz"), jc.IsFalse) 104 } 105 106 func (s *DebugHooksServerSuite) TestRunHookExceptional(c *gc.C) { 107 err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777) 108 c.Assert(err, jc.ErrorIsNil) 109 session, err := s.ctx.FindSession() 110 c.Assert(session, gc.NotNil) 111 c.Assert(err, jc.ErrorIsNil) 112 113 flockAcquired := make(chan struct{}, 1) 114 waitForFlock := func() { 115 select { 116 case <-flockAcquired: 117 case <-time.After(testing.ShortWait): 118 c.Fatalf("timed out waiting for hook to acquire flock") 119 } 120 } 121 122 // Run the hook in debug mode with no exit flock held. 123 // The exit flock will be acquired immediately, and the 124 // debug-hooks server process killed. 125 s.PatchValue(&waitClientExit, func(*ServerSession) { 126 flockAcquired <- struct{}{} 127 }) 128 err = session.RunHook("myhook", s.tmpdir, os.Environ()) 129 c.Assert(err, gc.ErrorMatches, "signal: [kK]illed") 130 waitForFlock() 131 132 // Run the hook in debug mode, simulating the holding 133 // of the exit flock. This simulates the client process 134 // starting but not cleanly exiting (normally the .pid 135 // file is updated, and the server waits on the client 136 // process' death). 137 ch := make(chan bool) // acquire the flock 138 var clientExited bool 139 s.PatchValue(&waitClientExit, func(*ServerSession) { 140 clientExited = <-ch 141 flockAcquired <- struct{}{} 142 }) 143 go func() { ch <- true }() // asynchronously release the flock 144 err = session.RunHook("myhook", s.tmpdir, os.Environ()) 145 waitForFlock() 146 c.Assert(clientExited, jc.IsTrue) 147 c.Assert(err, gc.ErrorMatches, "signal: [kK]illed") 148 } 149 150 func (s *DebugHooksServerSuite) TestRunHook(c *gc.C) { 151 err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777) 152 c.Assert(err, jc.ErrorIsNil) 153 var output bytes.Buffer 154 session, err := s.ctx.FindSessionWithWriter(&output) 155 c.Assert(session, gc.NotNil) 156 c.Assert(err, jc.ErrorIsNil) 157 158 flockRequestCh := make(chan chan struct{}) 159 s.PatchValue(&waitClientExit, func(*ServerSession) { 160 <-<-flockRequestCh 161 }) 162 defer close(flockRequestCh) 163 164 const hookName = "myhook" 165 runHookCh := make(chan error) 166 go func() { 167 runHookCh <- session.RunHook(hookName, s.tmpdir, os.Environ()) 168 }() 169 170 flockCh := make(chan struct{}) 171 select { 172 case flockRequestCh <- flockCh: 173 case <-time.After(testing.LongWait): 174 c.Fatal("timed out waiting for flock to be requested") 175 } 176 defer close(flockCh) 177 178 // Look for the debug hooks temporary dir, inside $TMPDIR. 179 tmpdir, err := os.Open(s.tmpdir) 180 if err != nil { 181 c.Fatalf("Failed to open $TMPDIR: %s", err) 182 } 183 defer tmpdir.Close() 184 entries, err := tmpdir.Readdir(-1) 185 if err != nil { 186 c.Fatalf("Failed to read $TMPDIR: %s", err) 187 } 188 c.Assert(entries, gc.HasLen, 1) 189 c.Assert(entries[0].IsDir(), jc.IsTrue) 190 c.Assert(strings.HasPrefix(entries[0].Name(), "juju-debug-hooks-"), jc.IsTrue) 191 192 debugDir := filepath.Join(s.tmpdir, entries[0].Name()) 193 hookScript := filepath.Join(debugDir, "hook.sh") 194 _, err = os.Stat(hookScript) 195 c.Assert(err, jc.ErrorIsNil) 196 197 // Check that the debug hooks script exports the environment, 198 // and the values are as expected. When RunHook completes, 199 // it removes the temporary directory in which the scripts 200 // reside; so we must wait for it to be written before we 201 // wait for RunHook to return. 202 timeout := time.After(testing.LongWait) 203 envsh := filepath.Join(debugDir, "env.sh") 204 for { 205 // Wait for env.sh to show up, and have some content. If it exists and 206 // is size 0, we managed to see it at exactly the time it is being 207 // written. 208 if st, err := os.Stat(envsh); err == nil { 209 if st.Size() != 0 { 210 break 211 } 212 } 213 select { 214 case <-time.After(time.Millisecond): 215 case <-timeout: 216 c.Fatal("timed out waiting for env.sh to be written") 217 } 218 } 219 s.verifyEnvshFile(c, envsh, hookName) 220 221 // Write the hook.pid file, causing the debug hooks script to exit. 222 hookpid := filepath.Join(debugDir, "hook.pid") 223 err = ioutil.WriteFile(hookpid, []byte("not a pid"), 0777) 224 c.Assert(err, jc.ErrorIsNil) 225 226 // RunHook should complete without waiting to be 227 // killed, and despite the exit lock being held. 228 select { 229 case err := <-runHookCh: 230 c.Assert(err, jc.ErrorIsNil) 231 case <-time.After(testing.LongWait): 232 c.Fatal("RunHook did not complete") 233 } 234 } 235 236 func (s *DebugHooksServerSuite) verifyEnvshFile(c *gc.C, envshPath string, hookName string) { 237 data, err := ioutil.ReadFile(envshPath) 238 c.Assert(err, jc.ErrorIsNil) 239 contents := string(data) 240 c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_UNIT_NAME=%q", s.ctx.Unit)) 241 c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_HOOK_NAME=%q", hookName)) 242 c.Assert(contents, jc.Contains, fmt.Sprintf(`PS1="%s:%s %% "`, s.ctx.Unit, hookName)) 243 }