github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/uniter/runner/runner.go (about)

     1  // Copyright 2012-2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package runner
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/base64"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strings"
    16  	"sync"
    17  	"syscall"
    18  	"time"
    19  	"unicode/utf8"
    20  
    21  	"github.com/juju/clock"
    22  	"github.com/juju/cmd/v3"
    23  	"github.com/juju/errors"
    24  	"github.com/juju/loggo"
    25  	"github.com/juju/utils/v3"
    26  	utilexec "github.com/juju/utils/v3/exec"
    27  	"github.com/kballard/go-shellquote"
    28  
    29  	"github.com/juju/juju/core/actions"
    30  	"github.com/juju/juju/core/model"
    31  	"github.com/juju/juju/worker/common/charmrunner"
    32  	"github.com/juju/juju/worker/uniter/runner/context"
    33  	"github.com/juju/juju/worker/uniter/runner/debug"
    34  	"github.com/juju/juju/worker/uniter/runner/jujuc"
    35  )
    36  
    37  // Logger is here to stop the desire of creating a package level Logger.
    38  // Don't do this, instead use the method defined in the Runner.
    39  type logger interface{}
    40  
    41  var _ logger = struct{}{}
    42  
    43  type runMode int
    44  
    45  const (
    46  	runOnUnknown runMode = iota
    47  	runOnLocal
    48  	runOnRemote
    49  )
    50  
    51  // RunLocation dictates where to execute commands.
    52  type RunLocation string
    53  
    54  const (
    55  	// Operator runs where the operator/uniter is running.
    56  	Operator = RunLocation("operator")
    57  	// Workload runs where the workload is running.
    58  	Workload = RunLocation("workload")
    59  )
    60  
    61  // HookHandlerType is used to indicate the type of script used for handling a
    62  // particular hook type.
    63  type HookHandlerType string
    64  
    65  // String implements fmt.Stringer for HookHandlerType.
    66  func (t HookHandlerType) String() string {
    67  	switch t {
    68  	case ExplicitHookHandler:
    69  		return "explicit, bespoke hook script"
    70  	case DispatchingHookHandler:
    71  		return "hook dispatching script: " + hookDispatcherScript
    72  	default:
    73  		return "unknown/invalid hook handler"
    74  	}
    75  }
    76  
    77  const (
    78  	InvalidHookHandler = HookHandlerType("invalid")
    79  
    80  	// ExplicitHookHandler indicates that a bespoke, per-hook script was
    81  	// used for handling a particular hook.
    82  	ExplicitHookHandler = HookHandlerType("explicit")
    83  
    84  	// DispatchingHookHandler indicates the use of a specialized script that
    85  	// acts as a dispatcher for all types of hooks. This functionality has
    86  	// been introduced with the operator framework changes.
    87  	DispatchingHookHandler = HookHandlerType("dispatch")
    88  
    89  	hookDispatcherScript = "dispatch"
    90  )
    91  
    92  // Runner is responsible for invoking commands in a context.
    93  type Runner interface {
    94  	// Context returns the context against which the runner executes.
    95  	Context() context.Context
    96  
    97  	// RunHook executes the hook with the supplied name and returns back
    98  	// the type of script handling hook that was used or whether any errors
    99  	// occurred.
   100  	RunHook(name string) (HookHandlerType, error)
   101  
   102  	// RunAction executes the action with the supplied name.
   103  	RunAction(name string) (HookHandlerType, error)
   104  
   105  	// RunCommands executes the supplied script.
   106  	RunCommands(commands string, runLocation RunLocation) (*utilexec.ExecResponse, error)
   107  }
   108  
   109  // NewRunnerFunc returns a func used to create a Runner backed by the supplied context and paths.
   110  type NewRunnerFunc func(context context.Context, paths context.Paths, remoteExecutor ExecFunc) Runner
   111  
   112  // NewRunner returns a Runner backed by the supplied context and paths.
   113  func NewRunner(context context.Context, paths context.Paths, remoteExecutor ExecFunc) Runner {
   114  	return &runner{context, paths, remoteExecutor}
   115  }
   116  
   117  // ExecParams holds all the necessary parameters for ExecFunc.
   118  type ExecParams struct {
   119  	Commands      []string
   120  	Env           []string
   121  	WorkingDir    string
   122  	Clock         clock.Clock
   123  	ProcessSetter func(context.HookProcess)
   124  	Cancel        <-chan struct{}
   125  
   126  	Stdout       io.ReadWriter
   127  	StdoutLogger charmrunner.Stopper
   128  
   129  	Stderr       io.ReadWriter
   130  	StderrLogger charmrunner.Stopper
   131  }
   132  
   133  // execOnMachine executes commands on current machine.
   134  func execOnMachine(params ExecParams) (*utilexec.ExecResponse, error) {
   135  	command := utilexec.RunParams{
   136  		Commands:    strings.Join(params.Commands, " "),
   137  		WorkingDir:  params.WorkingDir,
   138  		Environment: params.Env,
   139  		Clock:       params.Clock,
   140  	}
   141  	err := command.Run()
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	// TODO: refactor kill process and implement kill for caas exec.
   146  	params.ProcessSetter(hookProcess{command.Process()})
   147  	// Block and wait for process to finish
   148  	return command.WaitWithCancel(params.Cancel)
   149  }
   150  
   151  // ExecFunc is the exec func type.
   152  type ExecFunc func(ExecParams) (*utilexec.ExecResponse, error)
   153  
   154  // runner implements Runner.
   155  type runner struct {
   156  	context context.Context
   157  	paths   context.Paths
   158  	// remoteExecutor executes commands on a remote workload pod for CAAS.
   159  	remoteExecutor ExecFunc
   160  }
   161  
   162  func (runner *runner) logger() loggo.Logger {
   163  	return runner.context.GetLogger("juju.worker.uniter.runner")
   164  }
   165  
   166  func (runner *runner) Context() context.Context {
   167  	return runner.context
   168  }
   169  
   170  func (runner *runner) getExecutor(rMode runMode) (ExecFunc, error) {
   171  	switch rMode {
   172  	case runOnLocal:
   173  		return execOnMachine, nil
   174  	case runOnRemote:
   175  		if runner.remoteExecutor != nil {
   176  			return runner.remoteExecutor, nil
   177  		}
   178  	}
   179  	return nil, errors.NotSupportedf("run command mode %q", rMode)
   180  }
   181  
   182  func (runner *runner) runLocationToMode(runLocation RunLocation) (runMode, error) {
   183  	switch runLocation {
   184  	case Operator:
   185  		return runOnLocal, nil
   186  	case Workload:
   187  		if runner.context.ModelType() == model.CAAS && runner.remoteExecutor != nil {
   188  			return runOnRemote, nil
   189  		}
   190  		return runOnLocal, nil
   191  	default:
   192  		return runOnUnknown, errors.NotValidf("RunLocation %q", runLocation)
   193  	}
   194  }
   195  
   196  // RunCommands exists to satisfy the Runner interface.
   197  func (runner *runner) RunCommands(commands string, runLocation RunLocation) (*utilexec.ExecResponse, error) {
   198  	rMode, err := runner.runLocationToMode(runLocation)
   199  	if err != nil {
   200  		return nil, errors.Trace(err)
   201  	}
   202  	result, err := runner.runCommandsWithTimeout(commands, 0, clock.WallClock, rMode, nil)
   203  	return result, runner.context.Flush("run commands", err)
   204  }
   205  
   206  // runCommandsWithTimeout is a helper to abstract common code between run commands and
   207  // juju-exec as an action
   208  func (runner *runner) runCommandsWithTimeout(commands string, timeout time.Duration, clock clock.Clock, rMode runMode, abort <-chan struct{}) (*utilexec.ExecResponse, error) {
   209  	var err error
   210  	token := ""
   211  	if rMode == runOnRemote {
   212  		token, err = utils.RandomPassword()
   213  		if err != nil {
   214  			return nil, errors.Trace(err)
   215  		}
   216  	}
   217  	srv, err := runner.startJujucServer(token, rMode)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	defer srv.Close()
   222  
   223  	environmenter := context.NewHostEnvironmenter()
   224  	if rMode == runOnRemote {
   225  		env, err := runner.getRemoteEnviron(abort)
   226  		if err != nil {
   227  			return nil, errors.Annotatef(err, "getting remote environ")
   228  		}
   229  		environmenter = context.NewRemoteEnvironmenter(
   230  			func() []string {
   231  				rval := make([]string, 0, len(env))
   232  				for k, v := range env {
   233  					rval = append(rval, fmt.Sprintf("%s=%s", k, v))
   234  				}
   235  				return rval
   236  			},
   237  			func(k string) string {
   238  				return env[k]
   239  			},
   240  			func(k string) (string, bool) {
   241  				v, t := env[k]
   242  				return v, t
   243  			},
   244  		)
   245  	}
   246  	env, err := runner.context.HookVars(runner.paths, rMode == runOnRemote, environmenter)
   247  	if err != nil {
   248  		return nil, errors.Trace(err)
   249  	}
   250  	if rMode == runOnRemote {
   251  		env = append(env, "JUJU_AGENT_TOKEN="+token)
   252  	}
   253  
   254  	var cancel chan struct{}
   255  	if timeout != 0 {
   256  		cancel = make(chan struct{})
   257  		go func() {
   258  			<-clock.After(timeout)
   259  			close(cancel)
   260  		}()
   261  	}
   262  
   263  	executor, err := runner.getExecutor(rMode)
   264  	if err != nil {
   265  		return nil, errors.Trace(err)
   266  	}
   267  	var stdout, stderr bytes.Buffer
   268  	return executor(ExecParams{
   269  		Commands:      []string{commands},
   270  		Env:           env,
   271  		WorkingDir:    runner.paths.GetCharmDir(),
   272  		Clock:         clock,
   273  		ProcessSetter: runner.context.SetProcess,
   274  		Cancel:        cancel,
   275  		Stdout:        &stdout,
   276  		Stderr:        &stderr,
   277  	})
   278  }
   279  
   280  // runJujuExecAction is the function that executes when a juju-exec action is ran.
   281  func (runner *runner) runJujuExecAction() (err error) {
   282  	logger := runner.logger()
   283  	logger.Debugf("juju-exec action is running")
   284  	data, err := runner.context.ActionData()
   285  	if err != nil {
   286  		return errors.Trace(err)
   287  	}
   288  	params := data.Params
   289  	command, ok := params["command"].(string)
   290  	if !ok {
   291  		return errors.New("no command parameter to juju-exec action")
   292  	}
   293  
   294  	// The timeout is passed in in nanoseconds(which are represented in go as int64)
   295  	// But due to serialization it comes out as float64
   296  	timeout, ok := params["timeout"].(float64)
   297  	if !ok {
   298  		logger.Debugf("unable to read juju-exec action timeout, will continue running action without one")
   299  	}
   300  
   301  	runLocation := Operator
   302  	if workloadContext, _ := params["workload-context"].(bool); workloadContext {
   303  		runLocation = Workload
   304  	}
   305  	rMode, err := runner.runLocationToMode(runLocation)
   306  	if err != nil {
   307  		return errors.Trace(err)
   308  	}
   309  
   310  	results, err := runner.runCommandsWithTimeout(command, time.Duration(timeout), clock.WallClock, rMode, data.Cancel)
   311  	if results != nil {
   312  		if err := runner.updateActionResults(results); err != nil {
   313  			return runner.context.Flush("juju-exec", err)
   314  		}
   315  	}
   316  	return runner.context.Flush("juju-exec", err)
   317  }
   318  
   319  func encodeBytes(input []byte) (value string, encoding string) {
   320  	if utf8.Valid(input) {
   321  		value = string(input)
   322  		encoding = "utf8"
   323  	} else {
   324  		value = base64.StdEncoding.EncodeToString(input)
   325  		encoding = "base64"
   326  	}
   327  	return value, encoding
   328  }
   329  
   330  func (runner *runner) updateActionResults(results *utilexec.ExecResponse) error {
   331  	if err := runner.context.UpdateActionResults([]string{"return-code"}, results.Code); err != nil {
   332  		return errors.Trace(err)
   333  	}
   334  
   335  	stdout, encoding := encodeBytes(results.Stdout)
   336  	if stdout != "" {
   337  		if err := runner.context.UpdateActionResults([]string{"stdout"}, stdout); err != nil {
   338  			return errors.Trace(err)
   339  		}
   340  	}
   341  	if encoding != "utf8" {
   342  		if err := runner.context.UpdateActionResults([]string{"stdout-encoding"}, encoding); err != nil {
   343  			return errors.Trace(err)
   344  		}
   345  	}
   346  
   347  	stderr, encoding := encodeBytes(results.Stderr)
   348  	if stderr != "" {
   349  		if err := runner.context.UpdateActionResults([]string{"stderr"}, stderr); err != nil {
   350  			return errors.Trace(err)
   351  		}
   352  	}
   353  	if encoding != "utf8" {
   354  		if err := runner.context.UpdateActionResults([]string{"stderr-encoding"}, encoding); err != nil {
   355  			return errors.Trace(err)
   356  		}
   357  	}
   358  	return nil
   359  }
   360  
   361  // RunAction exists to satisfy the Runner interface.
   362  func (runner *runner) RunAction(actionName string) (HookHandlerType, error) {
   363  	data, err := runner.context.ActionData()
   364  	if err != nil {
   365  		return InvalidHookHandler, errors.Trace(err)
   366  	}
   367  	if actions.IsJujuExecAction(actionName) {
   368  		return InvalidHookHandler, runner.runJujuExecAction()
   369  	}
   370  	runLocation := Operator
   371  	if workloadContext, ok := data.Params["workload-context"].(bool); !ok || workloadContext {
   372  		runLocation = Workload
   373  	}
   374  	rMode, err := runner.runLocationToMode(runLocation)
   375  	if err != nil {
   376  		return InvalidHookHandler, errors.Trace(err)
   377  	}
   378  	runner.logger().Debugf("running action %q on %v", actionName, rMode)
   379  	return runner.runCharmHookWithLocation(actionName, "actions", rMode)
   380  }
   381  
   382  // RunHook exists to satisfy the Runner interface.
   383  func (runner *runner) RunHook(hookName string) (HookHandlerType, error) {
   384  	return runner.runCharmHookWithLocation(hookName, "hooks", runOnLocal)
   385  }
   386  
   387  func (runner *runner) runCharmHookWithLocation(hookName, charmLocation string, rMode runMode) (hookHandlerType HookHandlerType, err error) {
   388  	token := ""
   389  	if rMode == runOnRemote {
   390  		token, err = utils.RandomPassword()
   391  		if err != nil {
   392  			return InvalidHookHandler, errors.Trace(err)
   393  		}
   394  	}
   395  	srv, err := runner.startJujucServer(token, rMode)
   396  	if err != nil {
   397  		return InvalidHookHandler, errors.Trace(err)
   398  	}
   399  	defer srv.Close()
   400  
   401  	environmenter := context.NewHostEnvironmenter()
   402  	if rMode == runOnRemote {
   403  		var cancel <-chan struct{}
   404  		actionData, err := runner.context.ActionData()
   405  		if err == nil && actionData != nil {
   406  			cancel = actionData.Cancel
   407  		}
   408  		env, err := runner.getRemoteEnviron(cancel)
   409  		if err != nil {
   410  			return InvalidHookHandler, errors.Annotatef(err, "getting remote environ")
   411  		}
   412  		environmenter = context.NewRemoteEnvironmenter(
   413  			func() []string {
   414  				rval := make([]string, 0, len(env))
   415  				for k, v := range env {
   416  					rval = append(rval, fmt.Sprintf("%s=%s", k, v))
   417  				}
   418  				return rval
   419  			},
   420  			func(k string) string {
   421  				return env[k]
   422  			},
   423  			func(k string) (string, bool) {
   424  				v, t := env[k]
   425  				return v, t
   426  			},
   427  		)
   428  	}
   429  
   430  	env, err := runner.context.HookVars(runner.paths, rMode == runOnRemote, environmenter)
   431  	if err != nil {
   432  		return InvalidHookHandler, errors.Trace(err)
   433  	}
   434  	if rMode == runOnRemote {
   435  		env = append(env, "JUJU_AGENT_TOKEN="+token)
   436  	}
   437  	env = append(env, "JUJU_DISPATCH_PATH="+charmLocation+"/"+hookName)
   438  
   439  	defer func() {
   440  		err = runner.context.Flush(hookName, err)
   441  	}()
   442  
   443  	logger := runner.logger()
   444  	debugctx := debug.NewHooksContext(runner.context.UnitName())
   445  	if session, _ := debugctx.FindSession(); session != nil && session.MatchHook(hookName) {
   446  		// Note: hookScript might be relative but the debug session only requires its name
   447  		hookHandlerType, hookScript, err := runner.discoverHookHandler(
   448  			hookName, runner.paths.GetCharmDir(), charmLocation)
   449  		if session.DebugAt() != "" {
   450  			if hookHandlerType == InvalidHookHandler {
   451  				logger.Infof("debug-code active, but hook %s not implemented (skipping)", hookName)
   452  				return InvalidHookHandler, err
   453  			}
   454  			logger.Infof("executing %s via debug-code; %s", hookName, hookHandlerType)
   455  		} else {
   456  			logger.Infof("executing %s via debug-hooks; %s", hookName, hookHandlerType)
   457  		}
   458  		return hookHandlerType, session.RunHook(hookName, runner.paths.GetCharmDir(), env, hookScript)
   459  	}
   460  
   461  	charmDir := runner.paths.GetCharmDir()
   462  	hookHandlerType, hookScript, err := runner.discoverHookHandler(hookName, charmDir, charmLocation)
   463  	if err != nil {
   464  		return InvalidHookHandler, err
   465  	}
   466  	if rMode == runOnRemote {
   467  		return hookHandlerType, runner.runCharmProcessOnRemote(hookScript, hookName, charmDir, env)
   468  	}
   469  	return hookHandlerType, runner.runCharmProcessOnLocal(hookScript, hookName, charmDir, env)
   470  }
   471  
   472  // loggerAdaptor implements MessageReceiver and
   473  // sends messages to a logger.
   474  type loggerAdaptor struct {
   475  	loggo.Logger
   476  	level loggo.Level
   477  }
   478  
   479  // Messagef implements the charmrunner MessageReceiver interface
   480  func (l *loggerAdaptor) Messagef(isPrefix bool, message string, args ...interface{}) {
   481  	l.Logf(l.level, message, args...)
   482  }
   483  
   484  // bufferAdaptor implements MessageReceiver and
   485  // is used with the out writer from os.Pipe().
   486  // It allows the hook logger to grab console output
   487  // as well as passing the output to an action result.
   488  type bufferAdaptor struct {
   489  	io.ReadWriter
   490  
   491  	mu      sync.Mutex
   492  	outCopy bytes.Buffer
   493  }
   494  
   495  // Read implements the io.Reader interface
   496  func (b *bufferAdaptor) Read(p []byte) (n int, err error) {
   497  	b.mu.Lock()
   498  	defer b.mu.Unlock()
   499  	return b.outCopy.Read(p)
   500  }
   501  
   502  // Messagef implements the charmrunner MessageReceiver interface
   503  func (b *bufferAdaptor) Messagef(isPrefix bool, message string, args ...interface{}) {
   504  	formattedMessage := message
   505  	if len(args) > 0 {
   506  		formattedMessage = fmt.Sprintf(message, args...)
   507  	}
   508  	if !isPrefix {
   509  		formattedMessage += "\n"
   510  	}
   511  
   512  	b.mu.Lock()
   513  	defer b.mu.Unlock()
   514  	b.outCopy.WriteString(formattedMessage)
   515  }
   516  
   517  // Bytes exposes the underlying buffered bytes.
   518  func (b *bufferAdaptor) Bytes() []byte {
   519  	b.mu.Lock()
   520  	defer b.mu.Unlock()
   521  	return b.outCopy.Bytes()
   522  }
   523  
   524  func (runner *runner) runCharmProcessOnRemote(hook, hookName, charmDir string, env []string) error {
   525  	var cancel <-chan struct{}
   526  	outReader, outWriter, err := os.Pipe()
   527  	if err != nil {
   528  		return errors.Errorf("cannot make stdout logging pipe: %v", err)
   529  	}
   530  	defer func() { _ = outWriter.Close() }()
   531  
   532  	actionOut := &bufferAdaptor{ReadWriter: outWriter}
   533  	hookOutLogger := charmrunner.NewHookLogger(outReader,
   534  		&loggerAdaptor{Logger: runner.getLogger(hookName), level: loggo.DEBUG},
   535  		actionOut,
   536  	)
   537  	defer hookOutLogger.Stop()
   538  	go hookOutLogger.Run()
   539  
   540  	// When running an action, We capture stdout and stderr
   541  	// separately to pass back.
   542  	var actionErr = actionOut
   543  	var hookErrLogger *charmrunner.HookLogger
   544  	actionData, err := runner.context.ActionData()
   545  	runningAction := err == nil && actionData != nil
   546  	if runningAction {
   547  		cancel = actionData.Cancel
   548  
   549  		errReader, errWriter, err := os.Pipe()
   550  		if err != nil {
   551  			return errors.Errorf("cannot make stderr logging pipe: %v", err)
   552  		}
   553  		defer func() { _ = errWriter.Close() }()
   554  
   555  		actionErr = &bufferAdaptor{ReadWriter: errWriter}
   556  		hookErrLogger = charmrunner.NewHookLogger(errReader,
   557  			&loggerAdaptor{Logger: runner.getLogger(hookName), level: loggo.WARNING},
   558  			actionErr,
   559  		)
   560  		defer hookErrLogger.Stop()
   561  		go hookErrLogger.Run()
   562  	}
   563  
   564  	executor, err := runner.getExecutor(runOnRemote)
   565  	if err != nil {
   566  		return errors.Trace(err)
   567  	}
   568  	resp, err := executor(
   569  		ExecParams{
   570  			Commands:     []string{hook},
   571  			Env:          env,
   572  			WorkingDir:   charmDir,
   573  			Cancel:       cancel,
   574  			Stdout:       actionOut,
   575  			StdoutLogger: hookOutLogger,
   576  			Stderr:       actionErr,
   577  			StderrLogger: hookErrLogger,
   578  		},
   579  	)
   580  
   581  	// If we are running an action, record stdout and stderr.
   582  	if runningAction && resp != nil {
   583  		if err := runner.updateActionResults(resp); err != nil {
   584  			return errors.Trace(err)
   585  		}
   586  	}
   587  
   588  	return errors.Trace(err)
   589  }
   590  
   591  const (
   592  	// ErrTerminated indicate the hook or action exited due to a SIGTERM or SIGKILL signal.
   593  	ErrTerminated = errors.ConstError("terminated")
   594  )
   595  
   596  // Check still tested
   597  func (runner *runner) runCharmProcessOnLocal(hook, hookName, charmDir string, env []string) error {
   598  	ps := exec.Command(hook)
   599  	ps.Env = env
   600  	ps.Dir = charmDir
   601  	outReader, outWriter, err := os.Pipe()
   602  	if err != nil {
   603  		return errors.Errorf("cannot make logging pipe: %v", err)
   604  	}
   605  	defer func() { _ = outWriter.Close() }()
   606  
   607  	ps.Stdout = outWriter
   608  	hookOutLogger := charmrunner.NewHookLogger(outReader,
   609  		&loggerAdaptor{Logger: runner.getLogger(hookName), level: loggo.DEBUG},
   610  	)
   611  	go hookOutLogger.Run()
   612  	defer hookOutLogger.Stop()
   613  
   614  	errReader, errWriter, err := os.Pipe()
   615  	if err != nil {
   616  		return errors.Errorf("cannot make stderr logging pipe: %v", err)
   617  	}
   618  	defer func() { _ = errWriter.Close() }()
   619  
   620  	ps.Stderr = errWriter
   621  	hookErrLogger := charmrunner.NewHookLogger(errReader,
   622  		&loggerAdaptor{Logger: runner.getLogger(hookName), level: loggo.WARNING},
   623  	)
   624  	defer hookErrLogger.Stop()
   625  	go hookErrLogger.Run()
   626  
   627  	var cancel <-chan struct{}
   628  	var actionOut *bufferAdaptor
   629  	var actionErr *bufferAdaptor
   630  	actionData, err := runner.context.ActionData()
   631  	runningAction := err == nil && actionData != nil
   632  	if runningAction {
   633  		actionOut = &bufferAdaptor{ReadWriter: outWriter}
   634  		hookOutLogger.AddReceiver(actionOut)
   635  		actionErr = &bufferAdaptor{ReadWriter: errWriter}
   636  		hookErrLogger.AddReceiver(actionErr)
   637  		cancel = actionData.Cancel
   638  	}
   639  
   640  	err = ps.Start()
   641  	var exitErr error
   642  	if err == nil {
   643  		done := make(chan struct{})
   644  		if cancel != nil {
   645  			go func() {
   646  				select {
   647  				case <-cancel:
   648  					_ = ps.Process.Kill()
   649  				case <-done:
   650  				}
   651  			}()
   652  		}
   653  		// Record the *os.Process of the hook
   654  		runner.context.SetProcess(hookProcess{ps.Process})
   655  		// Block until execution finishes
   656  		exitErr = ps.Wait()
   657  		close(done)
   658  	} else {
   659  		exitErr = err
   660  	}
   661  
   662  	// Ensure hook loggers are stopped before reading stdout/stderr
   663  	// so all the output is captured.
   664  	hookOutLogger.Stop()
   665  	hookErrLogger.Stop()
   666  
   667  	// If we are running an action, record stdout and stderr.
   668  	if runningAction {
   669  		resp := &utilexec.ExecResponse{
   670  			Code:   ps.ProcessState.ExitCode(),
   671  			Stdout: actionOut.Bytes(),
   672  			Stderr: actionErr.Bytes(),
   673  		}
   674  		if err := runner.updateActionResults(resp); err != nil {
   675  			return errors.Trace(err)
   676  		}
   677  	}
   678  	if exitError, ok := exitErr.(*exec.ExitError); ok && exitError != nil {
   679  		waitStatus := exitError.ProcessState.Sys().(syscall.WaitStatus)
   680  		if waitStatus.Signal() == syscall.SIGTERM || waitStatus.Signal() == syscall.SIGKILL {
   681  			return errors.Trace(ErrTerminated)
   682  		}
   683  	}
   684  
   685  	return errors.Trace(exitErr)
   686  }
   687  
   688  // discoverHookHandler checks to see if the dispatch script exists, if not,
   689  // check for the given hookName.  Based on what is discovered, return the
   690  // HookHandlerType and the actual script to be run.
   691  func (runner *runner) discoverHookHandler(hookName, charmDir, charmLocation string) (HookHandlerType, string, error) {
   692  	err := checkCharmExists(charmDir)
   693  	if err != nil {
   694  		return InvalidHookHandler, "", errors.Trace(err)
   695  	}
   696  	hook, err := discoverHookScript(charmDir, hookDispatcherScript)
   697  	if err == nil {
   698  		return DispatchingHookHandler, hook, nil
   699  	}
   700  	if !charmrunner.IsMissingHookError(err) {
   701  		return InvalidHookHandler, "", err
   702  	}
   703  	if hook, err = discoverHookScript(charmDir, filepath.Join(charmLocation, hookName)); err == nil {
   704  		return ExplicitHookHandler, hook, nil
   705  	}
   706  	return InvalidHookHandler, hook, err
   707  }
   708  
   709  func (runner *runner) startJujucServer(token string, rMode runMode) (*jujuc.Server, error) {
   710  	// Prepare server.
   711  	getCmd := func(ctxId, cmdName string) (cmd.Command, error) {
   712  		if ctxId != runner.context.Id() {
   713  			return nil, errors.Errorf("expected context id %q, got %q", runner.context.Id(), ctxId)
   714  		}
   715  		return jujuc.NewCommand(runner.context, cmdName)
   716  	}
   717  
   718  	socket := runner.paths.GetJujucServerSocket(rMode == runOnRemote)
   719  	runner.logger().Debugf("starting jujuc server %s %v", token, socket)
   720  	srv, err := jujuc.NewServer(getCmd, socket, token)
   721  	if err != nil {
   722  		return nil, errors.Annotate(err, "starting jujuc server")
   723  	}
   724  	go func() { _ = srv.Run() }()
   725  	return srv, nil
   726  }
   727  
   728  // getLogger returns the logger for a particular unit's hook.
   729  func (runner *runner) getLogger(hookName string) loggo.Logger {
   730  	return runner.context.GetLogger(fmt.Sprintf("unit.%s.%s", runner.context.UnitName(), hookName))
   731  }
   732  
   733  var exportLineRegexp = regexp.MustCompile("(?m)^export ([^=]+)=(.*)$")
   734  
   735  func (runner *runner) getRemoteEnviron(abort <-chan struct{}) (map[string]string, error) {
   736  	remoteExecutor, err := runner.getExecutor(runOnRemote)
   737  	if err != nil {
   738  		return nil, errors.Trace(err)
   739  	}
   740  
   741  	var stdout, stderr bytes.Buffer
   742  	res, err := remoteExecutor(ExecParams{
   743  		Commands: []string{"unset _; export"},
   744  		Cancel:   abort,
   745  		Stdout:   &stdout,
   746  		Stderr:   &stderr,
   747  	})
   748  	if err != nil {
   749  		if res != nil {
   750  			err = errors.Annotatef(err, "stdout: %q stderr: %q", string(res.Stdout), string(res.Stderr))
   751  		}
   752  		return nil, errors.Trace(err)
   753  	}
   754  	matches := exportLineRegexp.FindAllStringSubmatch(string(res.Stdout), -1)
   755  	env := map[string]string{}
   756  	for _, values := range matches {
   757  		if len(values) != 3 {
   758  			return nil, errors.Errorf("regex returned incorrect submatch count")
   759  		}
   760  		key := values[1]
   761  		value := values[2]
   762  		unquoted, err := shellquote.Split(value)
   763  		if err != nil {
   764  			return nil, errors.Annotatef(err, "failed to unquote %s", value)
   765  		}
   766  		if len(unquoted) != 1 {
   767  			return nil, errors.Errorf("shellquote returned too many strings")
   768  		}
   769  		unquotedValue := unquoted[0]
   770  		env[key] = unquotedValue
   771  	}
   772  	runner.logger().Debugf("fetched remote env %+q", env)
   773  	return env, nil
   774  }
   775  
   776  type hookProcess struct {
   777  	*os.Process
   778  }
   779  
   780  func (p hookProcess) Pid() int {
   781  	return p.Process.Pid
   782  }