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 }