github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/worker/uniter/runner/debug/server.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  	"errors"
     9  	"io"
    10  	"io/ioutil"
    11  	"os"
    12  	"os/exec"
    13  
    14  	"github.com/juju/utils/set"
    15  	goyaml "gopkg.in/yaml.v2"
    16  )
    17  
    18  // ServerSession represents a "juju debug-hooks" session.
    19  type ServerSession struct {
    20  	*HooksContext
    21  	hooks set.Strings
    22  
    23  	output io.Writer
    24  }
    25  
    26  // MatchHook returns true if the specified hook name matches
    27  // the hook specified by the debug-hooks client.
    28  func (s *ServerSession) MatchHook(hookName string) bool {
    29  	return s.hooks.IsEmpty() || s.hooks.Contains(hookName)
    30  }
    31  
    32  // waitClientExit executes flock, waiting for the SSH client to exit.
    33  // This is a var so it can be replaced for testing.
    34  var waitClientExit = func(s *ServerSession) {
    35  	path := s.ClientExitFileLock()
    36  	exec.Command("flock", path, "-c", "true").Run()
    37  }
    38  
    39  // RunHook "runs" the hook with the specified name via debug-hooks.
    40  func (s *ServerSession) RunHook(hookName, charmDir string, env []string) error {
    41  	env = append(env, "JUJU_HOOK_NAME="+hookName)
    42  	cmd := exec.Command("/bin/bash", "-s")
    43  	cmd.Env = env
    44  	cmd.Dir = charmDir
    45  	cmd.Stdin = bytes.NewBufferString(debugHooksServerScript)
    46  	if s.output != nil {
    47  		cmd.Stdout = s.output
    48  		cmd.Stderr = s.output
    49  	}
    50  	if err := cmd.Start(); err != nil {
    51  		return err
    52  	}
    53  	go func(proc *os.Process) {
    54  		// Wait for the SSH client to exit (i.e. release the flock),
    55  		// then kill the server hook process in case the client
    56  		// exited uncleanly.
    57  		waitClientExit(s)
    58  		proc.Kill()
    59  	}(cmd.Process)
    60  	return cmd.Wait()
    61  }
    62  
    63  // FindSession attempts to find a debug hooks session for the unit specified
    64  // in the context, and returns a new ServerSession structure for it.
    65  func (c *HooksContext) FindSession() (*ServerSession, error) {
    66  	cmd := exec.Command("tmux", "has-session", "-t", c.tmuxSessionName())
    67  	out, err := cmd.CombinedOutput()
    68  	if err != nil {
    69  		if len(out) != 0 {
    70  			return nil, errors.New(string(out))
    71  		} else {
    72  			return nil, err
    73  		}
    74  	}
    75  	// Parse the debug-hooks file for an optional hook name.
    76  	data, err := ioutil.ReadFile(c.ClientFileLock())
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	var args hookArgs
    81  	err = goyaml.Unmarshal(data, &args)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  	hooks := set.NewStrings(args.Hooks...)
    86  	session := &ServerSession{HooksContext: c, hooks: hooks}
    87  	return session, nil
    88  }
    89  
    90  const debugHooksServerScript = `set -e
    91  export JUJU_DEBUG=$(mktemp -d)
    92  exec > $JUJU_DEBUG/debug.log >&1
    93  
    94  # Set a useful prompt.
    95  export PS1="$JUJU_UNIT_NAME:$JUJU_HOOK_NAME % "
    96  
    97  # Save environment variables and export them for sourcing.
    98  FILTER='^\(LS_COLORS\|LESSOPEN\|LESSCLOSE\|PWD\)='
    99  export | grep -v $FILTER > $JUJU_DEBUG/env.sh
   100  
   101  # Create welcome message display for the hook environment.
   102  cat > $JUJU_DEBUG/welcome.msg <<END
   103  This is a Juju debug-hooks tmux session. Remember:
   104  1. You need to execute hooks manually if you want them to run for trapped events.
   105  2. When you are finished with an event, you can run 'exit' to close the current window and allow Juju to continue running.
   106  3. CTRL+a is tmux prefix.
   107  
   108  More help and info is available in the online documentation:
   109  https://jujucharms.com/docs/authors-hook-debug.html
   110  
   111  END
   112  
   113  cat > $JUJU_DEBUG/init.sh <<END
   114  #!/bin/bash
   115  cat $JUJU_DEBUG/welcome.msg
   116  trap 'echo \$? > $JUJU_DEBUG/hook_exit_status' EXIT
   117  END
   118  chmod +x $JUJU_DEBUG/init.sh
   119  
   120  # Create an internal script which will load the hook environment.
   121  cat > $JUJU_DEBUG/hook.sh <<END
   122  #!/bin/bash
   123  . $JUJU_DEBUG/env.sh
   124  echo \$\$ > $JUJU_DEBUG/hook.pid
   125  exec /bin/bash --noprofile --init-file $JUJU_DEBUG/init.sh
   126  END
   127  chmod +x $JUJU_DEBUG/hook.sh
   128  
   129  tmux new-window -t $JUJU_UNIT_NAME -n $JUJU_HOOK_NAME "$JUJU_DEBUG/hook.sh"
   130  
   131  # If we exit for whatever reason, kill the hook shell.
   132  exit_handler() {
   133      if [ -f $JUJU_DEBUG/hook.pid ]; then
   134          kill -9 $(cat $JUJU_DEBUG/hook.pid) || true
   135      fi
   136  }
   137  trap exit_handler EXIT
   138  
   139  # Wait for the hook shell to start, and then wait for it to exit.
   140  while [ ! -f $JUJU_DEBUG/hook.pid ]; do
   141      sleep 1
   142  done
   143  HOOK_PID=$(cat $JUJU_DEBUG/hook.pid)
   144  while kill -0 "$HOOK_PID" 2> /dev/null; do
   145      sleep 1
   146  done
   147  typeset -i exitstatus=$(cat $JUJU_DEBUG/hook_exit_status)
   148  exit $exitstatus
   149  `