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  }