github.com/vmware/govmomi@v0.43.0/simulator/container.go (about) 1 /* 2 Copyright (c) 2018 VMware, Inc. All Rights Reserved. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package simulator 18 19 import ( 20 "archive/tar" 21 "bufio" 22 "bytes" 23 "context" 24 "encoding/json" 25 "errors" 26 "fmt" 27 "io" 28 "log" 29 "net" 30 "os" 31 "os/exec" 32 "path" 33 "regexp" 34 "strings" 35 "sync" 36 "time" 37 ) 38 39 var ( 40 shell = "/bin/sh" 41 eventWatch eventWatcher 42 ) 43 44 const ( 45 deleteWithContainer = "lifecycle=container" 46 createdByVcsim = "createdBy=vcsim" 47 ) 48 49 func init() { 50 if sh, err := exec.LookPath("bash"); err != nil { 51 shell = sh 52 } 53 } 54 55 type eventWatcher struct { 56 sync.Mutex 57 58 stdin io.WriteCloser 59 stdout io.ReadCloser 60 process *os.Process 61 62 // watches is a map of container IDs to container objects 63 watches map[string]*container 64 } 65 66 // container provides methods to manage a container within a simulator VM lifecycle. 67 type container struct { 68 sync.Mutex 69 70 id string 71 name string 72 73 cancelWatch context.CancelFunc 74 changes chan struct{} 75 } 76 77 type networkSettings struct { 78 Gateway string 79 IPAddress string 80 IPPrefixLen int 81 MacAddress string 82 } 83 84 type containerDetails struct { 85 State struct { 86 Running bool 87 Paused bool 88 } 89 NetworkSettings struct { 90 networkSettings 91 Networks map[string]networkSettings 92 } 93 } 94 95 type unknownContainer error 96 type uninitializedContainer error 97 98 var sanitizeNameRx = regexp.MustCompile(`[\(\)\s]`) 99 100 func sanitizeName(name string) string { 101 return sanitizeNameRx.ReplaceAllString(name, "-") 102 } 103 104 func constructContainerName(name, uid string) string { 105 return fmt.Sprintf("vcsim-%s-%s", sanitizeName(name), uid) 106 } 107 108 func constructVolumeName(containerName, uid, volumeName string) string { 109 return constructContainerName(containerName, uid) + "--" + sanitizeName(volumeName) 110 } 111 112 func extractNameAndUid(containerName string) (name string, uid string, err error) { 113 parts := strings.Split(strings.TrimPrefix(containerName, "vcsim-"), "-") 114 if len(parts) != 2 { 115 err = fmt.Errorf("container name does not match expected vcsim-name-uid format: %s", containerName) 116 return 117 } 118 119 return parts[0], parts[1], nil 120 } 121 122 func prefixToMask(prefix int) string { 123 mask := net.CIDRMask(prefix, 32) 124 return fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3]) 125 } 126 127 type tarEntry struct { 128 header *tar.Header 129 content []byte 130 } 131 132 // From https://docs.docker.com/engine/reference/commandline/cp/ : 133 // > It is not possible to copy certain system files such as resources under /proc, /sys, /dev, tmpfs, and mounts created by the user in the container. 134 // > However, you can still copy such files by manually running tar in docker exec. 135 func copyToGuest(id string, dest string, length int64, reader io.Reader) error { 136 cmd := exec.Command("docker", "exec", "-i", id, "tar", "Cxf", path.Dir(dest), "-") 137 cmd.Stderr = os.Stderr 138 stdin, err := cmd.StdinPipe() 139 if err != nil { 140 return err 141 } 142 143 err = cmd.Start() 144 if err != nil { 145 return err 146 } 147 148 tw := tar.NewWriter(stdin) 149 _ = tw.WriteHeader(&tar.Header{ 150 Name: path.Base(dest), 151 Size: length, 152 Mode: 0444, 153 ModTime: time.Now(), 154 }) 155 156 _, err = io.Copy(tw, reader) 157 158 twErr := tw.Close() 159 stdinErr := stdin.Close() 160 161 waitErr := cmd.Wait() 162 163 if err != nil || twErr != nil || stdinErr != nil || waitErr != nil { 164 return fmt.Errorf("copy: {%s}, tw: {%s}, stdin: {%s}, wait: {%s}", err, twErr, stdinErr, waitErr) 165 } 166 167 return nil 168 } 169 170 func copyFromGuest(id string, src string, sink func(int64, io.Reader) error) error { 171 cmd := exec.Command("docker", "exec", id, "tar", "Ccf", path.Dir(src), "-", path.Base(src)) 172 cmd.Stderr = os.Stderr 173 stdout, err := cmd.StdoutPipe() 174 if err != nil { 175 return err 176 } 177 if err = cmd.Start(); err != nil { 178 return err 179 } 180 181 tr := tar.NewReader(stdout) 182 header, err := tr.Next() 183 if err != nil { 184 return err 185 } 186 187 err = sink(header.Size, tr) 188 waitErr := cmd.Wait() 189 190 if err != nil || waitErr != nil { 191 return fmt.Errorf("err: {%s}, wait: {%s}", err, waitErr) 192 } 193 194 return nil 195 } 196 197 // createVolume creates a volume populated with the provided files 198 // If the header.Size is omitted or set to zero, then len(content+1) is used. 199 // Docker appears to treat this volume create command as idempotent so long as it's identical 200 // to an existing volume, so we can use this both for creating volumes inline in container create (for labelling) and 201 // for population after. 202 // returns: 203 // 204 // uid - string 205 // err - error or nil 206 func createVolume(volumeName string, labels []string, files []tarEntry) (string, error) { 207 image := os.Getenv("VCSIM_BUSYBOX") 208 if image == "" { 209 image = "busybox" 210 } 211 212 name := sanitizeName(volumeName) 213 uid := "" 214 215 // label the volume if specified - this requires the volume be created before use 216 if len(labels) > 0 { 217 run := []string{"volume", "create"} 218 for i := range labels { 219 run = append(run, "--label", labels[i]) 220 } 221 run = append(run, name) 222 cmd := exec.Command("docker", run...) 223 out, err := cmd.Output() 224 if err != nil { 225 return "", err 226 } 227 uid = strings.TrimSpace(string(out)) 228 229 if name == "" { 230 name = uid 231 } 232 } 233 234 run := []string{"run", "--rm", "-i"} 235 run = append(run, "-v", name+":/"+name) 236 run = append(run, image, "tar", "-C", "/"+name, "-xf", "-") 237 cmd := exec.Command("docker", run...) 238 stdin, err := cmd.StdinPipe() 239 if err != nil { 240 return uid, err 241 } 242 243 err = cmd.Start() 244 if err != nil { 245 return uid, err 246 } 247 248 tw := tar.NewWriter(stdin) 249 250 for _, file := range files { 251 header := file.header 252 253 if header.Size == 0 && len(file.content) > 0 { 254 header.Size = int64(len(file.content)) 255 } 256 257 if header.ModTime.IsZero() { 258 header.ModTime = time.Now() 259 } 260 261 if header.Mode == 0 { 262 header.Mode = 0444 263 } 264 265 tarErr := tw.WriteHeader(header) 266 if tarErr == nil { 267 _, tarErr = tw.Write(file.content) 268 } 269 } 270 271 err = nil 272 twErr := tw.Close() 273 stdinErr := stdin.Close() 274 if twErr != nil || stdinErr != nil { 275 err = fmt.Errorf("tw: {%s}, stdin: {%s}", twErr, stdinErr) 276 } 277 278 if waitErr := cmd.Wait(); waitErr != nil { 279 stderr := "" 280 if xerr, ok := waitErr.(*exec.ExitError); ok { 281 stderr = string(xerr.Stderr) 282 } 283 log.Printf("%s %s: %s %s", name, cmd.Args, waitErr, stderr) 284 285 err = fmt.Errorf("%s, wait: {%s}", err, waitErr) 286 return uid, err 287 } 288 289 return uid, err 290 } 291 292 func getBridge(bridgeName string) (string, error) { 293 // {"CreatedAt":"2023-07-11 19:22:25.45027052 +0000 UTC","Driver":"bridge","ID":"fe52c7502c5d","IPv6":"false","Internal":"false","Labels":"goodbye=,hello=","Name":"testnet","Scope":"local"} 294 // podman has distinctly different fields at v4.4.1 so commented out fields that don't match. We only actually care about ID 295 type bridgeNet struct { 296 // CreatedAt string 297 Driver string 298 ID string 299 // IPv6 string 300 // Internal string 301 // Labels string 302 Name string 303 // Scope string 304 } 305 306 // if the underlay bridge already exists, return that 307 // we don't check for a specific label or similar so that it's possible to use a bridge created by other frameworks for composite testing 308 var bridge bridgeNet 309 cmd := exec.Command("docker", "network", "ls", "--format={{json .}}", "-f", fmt.Sprintf("name=%s$", bridgeName)) 310 out, err := cmd.Output() 311 if err != nil { 312 log.Printf("vcsim %s: %s, %s", cmd.Args, err, out) 313 return "", err 314 } 315 316 // unfortunately docker returns an empty string not an empty json doc and podman returns '[]' 317 // podman also returns an array of matches even when there's only one, so we normalize. 318 str := strings.TrimSpace(string(out)) 319 str = strings.TrimPrefix(str, "[") 320 str = strings.TrimSuffix(str, "]") 321 if len(str) == 0 { 322 return "", nil 323 } 324 325 err = json.Unmarshal([]byte(str), &bridge) 326 if err != nil { 327 log.Printf("vcsim %s: %s, %s", cmd.Args, err, str) 328 return "", err 329 } 330 331 return bridge.ID, nil 332 } 333 334 // createBridge creates a bridge network if one does not already exist 335 // returns: 336 // 337 // uid - string 338 // err - error or nil 339 func createBridge(bridgeName string, labels ...string) (string, error) { 340 341 id, err := getBridge(bridgeName) 342 if err != nil { 343 return "", err 344 } 345 346 if id != "" { 347 return id, nil 348 } 349 350 run := []string{"network", "create", "--label", createdByVcsim} 351 for i := range labels { 352 run = append(run, "--label", labels[i]) 353 } 354 run = append(run, bridgeName) 355 356 cmd := exec.Command("docker", run...) 357 out, err := cmd.Output() 358 if err != nil { 359 log.Printf("vcsim %s: %s: %s", cmd.Args, out, err) 360 return "", err 361 } 362 363 // docker returns the ID regardless of whether you supply a name when creating the network, however 364 // podman returns the pretty name, so we have to normalize 365 id, err = getBridge(bridgeName) 366 if err != nil { 367 return "", err 368 } 369 370 return id, nil 371 } 372 373 // create 374 // - name - pretty name, eg. vm name 375 // - id - uuid or similar - this is merged into container name rather than dictating containerID 376 // - networks - set of bridges to connect the container to 377 // - volumes - colon separated tuple of volume name to mount path. Passed directly to docker via -v so mount options can be postfixed. 378 // - env - array of environment vairables in name=value form 379 // - optsAndImage - pass-though options and must include at least the container image to use, including tag if necessary 380 // - args - the command+args to pass to the container 381 func create(ctx *Context, name string, id string, networks []string, volumes []string, ports []string, env []string, image string, args []string) (*container, error) { 382 if len(image) == 0 { 383 return nil, errors.New("cannot create container backing without an image") 384 } 385 386 var c container 387 c.name = constructContainerName(name, id) 388 c.changes = make(chan struct{}) 389 390 for i := range volumes { 391 // we'll pre-create anonymous volumes, simply for labelling consistency 392 volName := strings.Split(volumes[i], ":") 393 createVolume(volName[0], []string{deleteWithContainer, "container=" + c.name}, nil) 394 } 395 396 // assemble env 397 var dockerNet []string 398 var dockerVol []string 399 var dockerPort []string 400 var dockerEnv []string 401 402 for i := range env { 403 dockerEnv = append(dockerEnv, "--env", env[i]) 404 } 405 406 for i := range volumes { 407 dockerVol = append(dockerVol, "-v", volumes[i]) 408 } 409 410 for i := range ports { 411 dockerPort = append(dockerPort, "-p", ports[i]) 412 } 413 414 for i := range networks { 415 dockerNet = append(dockerNet, "--network", networks[i]) 416 } 417 418 run := []string{"docker", "create", "--name", c.name} 419 run = append(run, dockerNet...) 420 run = append(run, dockerVol...) 421 run = append(run, dockerPort...) 422 run = append(run, dockerEnv...) 423 run = append(run, image) 424 run = append(run, args...) 425 426 // this combines all the run options into a single string that's passed to /bin/bash -c as the single argument to force bash parsing. 427 // TODO: make this configurable behaviour so users also have the option of not escaping everything for bash 428 cmd := exec.Command(shell, "-c", strings.Join(run, " ")) 429 out, err := cmd.Output() 430 if err != nil { 431 stderr := "" 432 if xerr, ok := err.(*exec.ExitError); ok { 433 stderr = string(xerr.Stderr) 434 } 435 log.Printf("%s %s: %s %s", name, cmd.Args, err, stderr) 436 437 return nil, err 438 } 439 440 c.id = strings.TrimSpace(string(out)) 441 442 return &c, nil 443 } 444 445 // createVolume takes the specified files and writes them into a volume named for the container. 446 func (c *container) createVolume(name string, labels []string, files []tarEntry) (string, error) { 447 return createVolume(c.name+"--"+name, append(labels, "container="+c.name), files) 448 } 449 450 // inspect retrieves and parses container properties into directly usable struct 451 // returns: 452 // 453 // out - the stdout of the command 454 // detail - basic struct populated with container details 455 // err: 456 // * if c.id is empty, or docker returns "No such object", will return an uninitializedContainer error 457 // * err from either execution or parsing of json output 458 func (c *container) inspect() (out []byte, detail containerDetails, err error) { 459 c.Lock() 460 id := c.id 461 c.Unlock() 462 463 if id == "" { 464 err = uninitializedContainer(errors.New("inspect of uninitialized container")) 465 return 466 } 467 468 var details []containerDetails 469 470 cmd := exec.Command("docker", "inspect", c.id) 471 out, err = cmd.Output() 472 if eErr, ok := err.(*exec.ExitError); ok { 473 if strings.Contains(string(eErr.Stderr), "No such object") { 474 err = uninitializedContainer(errors.New("inspect of uninitialized container")) 475 } 476 } 477 478 if err != nil { 479 return 480 } 481 482 if err = json.NewDecoder(bytes.NewReader(out)).Decode(&details); err != nil { 483 return 484 } 485 486 if len(details) != 1 { 487 err = fmt.Errorf("multiple containers (%d) match ID: %s", len(details), c.id) 488 return 489 } 490 491 detail = details[0] 492 return 493 } 494 495 // start 496 // - if the container already exists, start it or unpause it. 497 func (c *container) start(ctx *Context) error { 498 c.Lock() 499 id := c.id 500 c.Unlock() 501 502 if id == "" { 503 return uninitializedContainer(errors.New("start of uninitialized container")) 504 } 505 506 start := "start" 507 _, detail, err := c.inspect() 508 if err != nil { 509 return err 510 } 511 512 if detail.State.Paused { 513 start = "unpause" 514 } 515 516 cmd := exec.Command("docker", start, c.id) 517 err = cmd.Run() 518 if err != nil { 519 log.Printf("%s %s: %s", c.name, cmd.Args, err) 520 } 521 522 return err 523 } 524 525 // pause the container (if any) for the given vm. 526 func (c *container) pause(ctx *Context) error { 527 c.Lock() 528 id := c.id 529 c.Unlock() 530 531 if id == "" { 532 return uninitializedContainer(errors.New("pause of uninitialized container")) 533 } 534 535 cmd := exec.Command("docker", "pause", c.id) 536 err := cmd.Run() 537 if err != nil { 538 log.Printf("%s %s: %s", c.name, cmd.Args, err) 539 } 540 541 return err 542 } 543 544 // restart the container (if any) for the given vm. 545 func (c *container) restart(ctx *Context) error { 546 c.Lock() 547 id := c.id 548 c.Unlock() 549 550 if id == "" { 551 return uninitializedContainer(errors.New("restart of uninitialized container")) 552 } 553 554 cmd := exec.Command("docker", "restart", c.id) 555 err := cmd.Run() 556 if err != nil { 557 log.Printf("%s %s: %s", c.name, cmd.Args, err) 558 } 559 560 return err 561 } 562 563 // stop the container (if any) for the given vm. 564 func (c *container) stop(ctx *Context) error { 565 c.Lock() 566 id := c.id 567 c.Unlock() 568 569 if id == "" { 570 return uninitializedContainer(errors.New("stop of uninitialized container")) 571 } 572 573 cmd := exec.Command("docker", "stop", c.id) 574 err := cmd.Run() 575 if err != nil { 576 log.Printf("%s %s: %s", c.name, cmd.Args, err) 577 } 578 579 return err 580 } 581 582 // exec invokes the specified command, with executable being the first of the args, in the specified container 583 // returns 584 // 585 // string - combined stdout and stderr from command 586 // err 587 // * uninitializedContainer error - if c.id is empty 588 // * err from cmd execution 589 func (c *container) exec(ctx *Context, args []string) (string, error) { 590 c.Lock() 591 id := c.id 592 c.Unlock() 593 594 if id == "" { 595 return "", uninitializedContainer(errors.New("exec into uninitialized container")) 596 } 597 598 args = append([]string{"exec", c.id}, args...) 599 cmd := exec.Command("docker", args...) 600 res, err := cmd.CombinedOutput() 601 if err != nil { 602 log.Printf("%s: %s (%s)", c.name, cmd.Args, string(res)) 603 return "", err 604 } 605 606 return strings.TrimSpace(string(res)), nil 607 } 608 609 // remove the container (if any) for the given vm. Considers removal of an uninitialized container success. 610 // Also removes volumes and networks that indicate they are lifecycle coupled with this container. 611 // returns: 612 // 613 // err - joined err from deletion of container and any volumes or networks that have coupled lifecycle 614 func (c *container) remove(ctx *Context) error { 615 c.Lock() 616 defer c.Unlock() 617 618 if c.id == "" { 619 // consider absence success 620 return nil 621 } 622 623 cmd := exec.Command("docker", "rm", "-v", "-f", c.id) 624 err := cmd.Run() 625 if err != nil { 626 log.Printf("%s %s: %s", c.name, cmd.Args, err) 627 return err 628 } 629 630 cmd = exec.Command("docker", "volume", "ls", "-q", "--filter", "label=container="+c.name, "--filter", "label="+deleteWithContainer) 631 volumesToReap, lsverr := cmd.Output() 632 if lsverr != nil { 633 log.Printf("%s %s: %s", c.name, cmd.Args, lsverr) 634 } 635 log.Printf("%s volumes: %s", c.name, volumesToReap) 636 637 var rmverr error 638 if len(volumesToReap) > 0 { 639 run := []string{"volume", "rm", "-f"} 640 run = append(run, strings.Split(string(volumesToReap), "\n")...) 641 cmd = exec.Command("docker", run...) 642 out, rmverr := cmd.Output() 643 if rmverr != nil { 644 log.Printf("%s %s: %s, %s", c.name, cmd.Args, rmverr, out) 645 } 646 } 647 648 cmd = exec.Command("docker", "network", "ls", "-q", "--filter", "label=container="+c.name, "--filter", "label="+deleteWithContainer) 649 networksToReap, lsnerr := cmd.Output() 650 if lsnerr != nil { 651 log.Printf("%s %s: %s", c.name, cmd.Args, lsnerr) 652 } 653 654 var rmnerr error 655 if len(networksToReap) > 0 { 656 run := []string{"network", "rm", "-f"} 657 run = append(run, strings.Split(string(volumesToReap), "\n")...) 658 cmd = exec.Command("docker", run...) 659 rmnerr = cmd.Run() 660 if rmnerr != nil { 661 log.Printf("%s %s: %s", c.name, cmd.Args, rmnerr) 662 } 663 } 664 665 if err != nil || lsverr != nil || rmverr != nil || lsnerr != nil || rmnerr != nil { 666 return fmt.Errorf("err: {%s}, lsverr: {%s}, rmverr: {%s}, lsnerr:{%s}, rmerr: {%s}", err, lsverr, rmverr, lsnerr, rmnerr) 667 } 668 669 if c.cancelWatch != nil { 670 c.cancelWatch() 671 eventWatch.ignore(c) 672 } 673 c.id = "" 674 return nil 675 } 676 677 // updated is a simple trigger allowing a caller to indicate that something has likely changed about the container 678 // and interested parties should re-inspect as needed. 679 func (c *container) updated() { 680 consolidationWindow := 250 * time.Millisecond 681 if d, err := time.ParseDuration(os.Getenv("VCSIM_EVENT_CONSOLIDATION_WINDOW")); err == nil { 682 consolidationWindow = d 683 } 684 685 select { 686 case c.changes <- struct{}{}: 687 time.Sleep(consolidationWindow) 688 // as this is only a hint to avoid waiting for the full inspect interval, we don't care about accumulating 689 // multiple triggers. We do pause to allow large numbers of sequential updates to consolidate 690 default: 691 } 692 } 693 694 // watchContainer monitors the underlying container and updates 695 // properties based on the container status. This occurs until either 696 // the container or the VM is removed. 697 // returns: 698 // 699 // err - uninitializedContainer error - if c.id is empty 700 func (c *container) watchContainer(ctx context.Context, updateFn func(*containerDetails, *container) error) error { 701 c.Lock() 702 defer c.Unlock() 703 704 if c.id == "" { 705 return uninitializedContainer(errors.New("Attempt to watch uninitialized container")) 706 } 707 708 eventWatch.watch(c) 709 710 cancelCtx, cancelFunc := context.WithCancel(ctx) 711 c.cancelWatch = cancelFunc 712 713 // Update the VM from the container at regular intervals until the done 714 // channel is closed. 715 go func() { 716 inspectInterval := 10 * time.Second 717 if d, err := time.ParseDuration(os.Getenv("VCSIM_INSPECT_INTERVAL")); err == nil { 718 inspectInterval = d 719 } 720 ticker := time.NewTicker(inspectInterval) 721 722 update := func() { 723 _, details, err := c.inspect() 724 var rmErr error 725 var removing bool 726 if _, ok := err.(uninitializedContainer); ok { 727 removing = true 728 rmErr = c.remove(SpoofContext()) 729 } 730 731 updateErr := updateFn(&details, c) 732 // if we don't succeed we want to re-try 733 if removing && rmErr == nil && updateErr == nil { 734 ticker.Stop() 735 return 736 } 737 if updateErr != nil { 738 log.Printf("vcsim container watch: %s %s", c.id, updateErr) 739 } 740 } 741 742 for { 743 select { 744 case <-c.changes: 745 update() 746 case <-ticker.C: 747 update() 748 case <-cancelCtx.Done(): 749 return 750 } 751 } 752 }() 753 754 return nil 755 } 756 757 func (w *eventWatcher) watch(c *container) { 758 w.Lock() 759 defer w.Unlock() 760 761 if w.watches == nil { 762 w.watches = make(map[string]*container) 763 } 764 765 w.watches[c.id] = c 766 767 if w.stdin == nil { 768 cmd := exec.Command("docker", "events", "--format", "'{{.ID}}'", "--filter", "Type=container") 769 w.stdout, _ = cmd.StdoutPipe() 770 w.stdin, _ = cmd.StdinPipe() 771 err := cmd.Start() 772 if err != nil { 773 log.Printf("docker event watcher: %s %s", cmd.Args, err) 774 w.stdin = nil 775 w.stdout = nil 776 w.process = nil 777 778 return 779 } 780 781 w.process = cmd.Process 782 783 go w.monitor() 784 } 785 } 786 787 func (w *eventWatcher) ignore(c *container) { 788 w.Lock() 789 790 delete(w.watches, c.id) 791 792 if len(w.watches) == 0 && w.stdin != nil { 793 w.stop() 794 } 795 796 w.Unlock() 797 } 798 799 func (w *eventWatcher) monitor() { 800 w.Lock() 801 watches := len(w.watches) 802 w.Unlock() 803 804 if watches == 0 { 805 return 806 } 807 808 scanner := bufio.NewScanner(w.stdout) 809 for scanner.Scan() { 810 id := strings.TrimSpace(scanner.Text()) 811 812 w.Lock() 813 container := w.watches[id] 814 w.Unlock() 815 816 if container != nil { 817 // this is called in a routine to allow an event consolidation window 818 go container.updated() 819 } 820 } 821 } 822 823 func (w *eventWatcher) stop() { 824 if w.stdin != nil { 825 w.stdin.Close() 826 w.stdin = nil 827 } 828 if w.stdout != nil { 829 w.stdout.Close() 830 w.stdout = nil 831 } 832 w.process.Kill() 833 }