]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/go-plugin/client.go
Merge pull request #27 from terraform-providers/go-modules-2019-02-22
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / go-plugin / client.go
1 package plugin
2
3 import (
4 "bufio"
5 "context"
6 "crypto/subtle"
7 "crypto/tls"
8 "errors"
9 "fmt"
10 "hash"
11 "io"
12 "io/ioutil"
13 "log"
14 "net"
15 "os"
16 "os/exec"
17 "path/filepath"
18 "strconv"
19 "strings"
20 "sync"
21 "sync/atomic"
22 "time"
23 "unicode"
24
25 hclog "github.com/hashicorp/go-hclog"
26 )
27
28 // If this is 1, then we've called CleanupClients. This can be used
29 // by plugin RPC implementations to change error behavior since you
30 // can expected network connection errors at this point. This should be
31 // read by using sync/atomic.
32 var Killed uint32 = 0
33
34 // This is a slice of the "managed" clients which are cleaned up when
35 // calling Cleanup
36 var managedClients = make([]*Client, 0, 5)
37 var managedClientsLock sync.Mutex
38
39 // Error types
40 var (
41 // ErrProcessNotFound is returned when a client is instantiated to
42 // reattach to an existing process and it isn't found.
43 ErrProcessNotFound = errors.New("Reattachment process not found")
44
45 // ErrChecksumsDoNotMatch is returned when binary's checksum doesn't match
46 // the one provided in the SecureConfig.
47 ErrChecksumsDoNotMatch = errors.New("checksums did not match")
48
49 // ErrSecureNoChecksum is returned when an empty checksum is provided to the
50 // SecureConfig.
51 ErrSecureConfigNoChecksum = errors.New("no checksum provided")
52
53 // ErrSecureNoHash is returned when a nil Hash object is provided to the
54 // SecureConfig.
55 ErrSecureConfigNoHash = errors.New("no hash implementation provided")
56
57 // ErrSecureConfigAndReattach is returned when both Reattach and
58 // SecureConfig are set.
59 ErrSecureConfigAndReattach = errors.New("only one of Reattach or SecureConfig can be set")
60 )
61
62 // Client handles the lifecycle of a plugin application. It launches
63 // plugins, connects to them, dispenses interface implementations, and handles
64 // killing the process.
65 //
66 // Plugin hosts should use one Client for each plugin executable. To
67 // dispense a plugin type, use the `Client.Client` function, and then
68 // cal `Dispense`. This awkward API is mostly historical but is used to split
69 // the client that deals with subprocess management and the client that
70 // does RPC management.
71 //
72 // See NewClient and ClientConfig for using a Client.
73 type Client struct {
74 config *ClientConfig
75 exited bool
76 doneLogging chan struct{}
77 l sync.Mutex
78 address net.Addr
79 process *os.Process
80 client ClientProtocol
81 protocol Protocol
82 logger hclog.Logger
83 doneCtx context.Context
84 }
85
86 // ClientConfig is the configuration used to initialize a new
87 // plugin client. After being used to initialize a plugin client,
88 // that configuration must not be modified again.
89 type ClientConfig struct {
90 // HandshakeConfig is the configuration that must match servers.
91 HandshakeConfig
92
93 // Plugins are the plugins that can be consumed.
94 Plugins map[string]Plugin
95
96 // One of the following must be set, but not both.
97 //
98 // Cmd is the unstarted subprocess for starting the plugin. If this is
99 // set, then the Client starts the plugin process on its own and connects
100 // to it.
101 //
102 // Reattach is configuration for reattaching to an existing plugin process
103 // that is already running. This isn't common.
104 Cmd *exec.Cmd
105 Reattach *ReattachConfig
106
107 // SecureConfig is configuration for verifying the integrity of the
108 // executable. It can not be used with Reattach.
109 SecureConfig *SecureConfig
110
111 // TLSConfig is used to enable TLS on the RPC client.
112 TLSConfig *tls.Config
113
114 // Managed represents if the client should be managed by the
115 // plugin package or not. If true, then by calling CleanupClients,
116 // it will automatically be cleaned up. Otherwise, the client
117 // user is fully responsible for making sure to Kill all plugin
118 // clients. By default the client is _not_ managed.
119 Managed bool
120
121 // The minimum and maximum port to use for communicating with
122 // the subprocess. If not set, this defaults to 10,000 and 25,000
123 // respectively.
124 MinPort, MaxPort uint
125
126 // StartTimeout is the timeout to wait for the plugin to say it
127 // has started successfully.
128 StartTimeout time.Duration
129
130 // If non-nil, then the stderr of the client will be written to here
131 // (as well as the log). This is the original os.Stderr of the subprocess.
132 // This isn't the output of synced stderr.
133 Stderr io.Writer
134
135 // SyncStdout, SyncStderr can be set to override the
136 // respective os.Std* values in the plugin. Care should be taken to
137 // avoid races here. If these are nil, then this will automatically be
138 // hooked up to os.Stdin, Stdout, and Stderr, respectively.
139 //
140 // If the default values (nil) are used, then this package will not
141 // sync any of these streams.
142 SyncStdout io.Writer
143 SyncStderr io.Writer
144
145 // AllowedProtocols is a list of allowed protocols. If this isn't set,
146 // then only netrpc is allowed. This is so that older go-plugin systems
147 // can show friendly errors if they see a plugin with an unknown
148 // protocol.
149 //
150 // By setting this, you can cause an error immediately on plugin start
151 // if an unsupported protocol is used with a good error message.
152 //
153 // If this isn't set at all (nil value), then only net/rpc is accepted.
154 // This is done for legacy reasons. You must explicitly opt-in to
155 // new protocols.
156 AllowedProtocols []Protocol
157
158 // Logger is the logger that the client will used. If none is provided,
159 // it will default to hclog's default logger.
160 Logger hclog.Logger
161 }
162
163 // ReattachConfig is used to configure a client to reattach to an
164 // already-running plugin process. You can retrieve this information by
165 // calling ReattachConfig on Client.
166 type ReattachConfig struct {
167 Protocol Protocol
168 Addr net.Addr
169 Pid int
170 }
171
172 // SecureConfig is used to configure a client to verify the integrity of an
173 // executable before running. It does this by verifying the checksum is
174 // expected. Hash is used to specify the hashing method to use when checksumming
175 // the file. The configuration is verified by the client by calling the
176 // SecureConfig.Check() function.
177 //
178 // The host process should ensure the checksum was provided by a trusted and
179 // authoritative source. The binary should be installed in such a way that it
180 // can not be modified by an unauthorized user between the time of this check
181 // and the time of execution.
182 type SecureConfig struct {
183 Checksum []byte
184 Hash hash.Hash
185 }
186
187 // Check takes the filepath to an executable and returns true if the checksum of
188 // the file matches the checksum provided in the SecureConfig.
189 func (s *SecureConfig) Check(filePath string) (bool, error) {
190 if len(s.Checksum) == 0 {
191 return false, ErrSecureConfigNoChecksum
192 }
193
194 if s.Hash == nil {
195 return false, ErrSecureConfigNoHash
196 }
197
198 file, err := os.Open(filePath)
199 if err != nil {
200 return false, err
201 }
202 defer file.Close()
203
204 _, err = io.Copy(s.Hash, file)
205 if err != nil {
206 return false, err
207 }
208
209 sum := s.Hash.Sum(nil)
210
211 return subtle.ConstantTimeCompare(sum, s.Checksum) == 1, nil
212 }
213
214 // This makes sure all the managed subprocesses are killed and properly
215 // logged. This should be called before the parent process running the
216 // plugins exits.
217 //
218 // This must only be called _once_.
219 func CleanupClients() {
220 // Set the killed to true so that we don't get unexpected panics
221 atomic.StoreUint32(&Killed, 1)
222
223 // Kill all the managed clients in parallel and use a WaitGroup
224 // to wait for them all to finish up.
225 var wg sync.WaitGroup
226 managedClientsLock.Lock()
227 for _, client := range managedClients {
228 wg.Add(1)
229
230 go func(client *Client) {
231 client.Kill()
232 wg.Done()
233 }(client)
234 }
235 managedClientsLock.Unlock()
236
237 log.Println("[DEBUG] plugin: waiting for all plugin processes to complete...")
238 wg.Wait()
239 }
240
241 // Creates a new plugin client which manages the lifecycle of an external
242 // plugin and gets the address for the RPC connection.
243 //
244 // The client must be cleaned up at some point by calling Kill(). If
245 // the client is a managed client (created with NewManagedClient) you
246 // can just call CleanupClients at the end of your program and they will
247 // be properly cleaned.
248 func NewClient(config *ClientConfig) (c *Client) {
249 if config.MinPort == 0 && config.MaxPort == 0 {
250 config.MinPort = 10000
251 config.MaxPort = 25000
252 }
253
254 if config.StartTimeout == 0 {
255 config.StartTimeout = 1 * time.Minute
256 }
257
258 if config.Stderr == nil {
259 config.Stderr = ioutil.Discard
260 }
261
262 if config.SyncStdout == nil {
263 config.SyncStdout = ioutil.Discard
264 }
265 if config.SyncStderr == nil {
266 config.SyncStderr = ioutil.Discard
267 }
268
269 if config.AllowedProtocols == nil {
270 config.AllowedProtocols = []Protocol{ProtocolNetRPC}
271 }
272
273 if config.Logger == nil {
274 config.Logger = hclog.New(&hclog.LoggerOptions{
275 Output: hclog.DefaultOutput,
276 Level: hclog.Trace,
277 Name: "plugin",
278 })
279 }
280
281 c = &Client{
282 config: config,
283 logger: config.Logger,
284 }
285 if config.Managed {
286 managedClientsLock.Lock()
287 managedClients = append(managedClients, c)
288 managedClientsLock.Unlock()
289 }
290
291 return
292 }
293
294 // Client returns the protocol client for this connection.
295 //
296 // Subsequent calls to this will return the same client.
297 func (c *Client) Client() (ClientProtocol, error) {
298 _, err := c.Start()
299 if err != nil {
300 return nil, err
301 }
302
303 c.l.Lock()
304 defer c.l.Unlock()
305
306 if c.client != nil {
307 return c.client, nil
308 }
309
310 switch c.protocol {
311 case ProtocolNetRPC:
312 c.client, err = newRPCClient(c)
313
314 case ProtocolGRPC:
315 c.client, err = newGRPCClient(c.doneCtx, c)
316
317 default:
318 return nil, fmt.Errorf("unknown server protocol: %s", c.protocol)
319 }
320
321 if err != nil {
322 c.client = nil
323 return nil, err
324 }
325
326 return c.client, nil
327 }
328
329 // Tells whether or not the underlying process has exited.
330 func (c *Client) Exited() bool {
331 c.l.Lock()
332 defer c.l.Unlock()
333 return c.exited
334 }
335
336 // End the executing subprocess (if it is running) and perform any cleanup
337 // tasks necessary such as capturing any remaining logs and so on.
338 //
339 // This method blocks until the process successfully exits.
340 //
341 // This method can safely be called multiple times.
342 func (c *Client) Kill() {
343 // Grab a lock to read some private fields.
344 c.l.Lock()
345 process := c.process
346 addr := c.address
347 doneCh := c.doneLogging
348 c.l.Unlock()
349
350 // If there is no process, we never started anything. Nothing to kill.
351 if process == nil {
352 return
353 }
354
355 // We need to check for address here. It is possible that the plugin
356 // started (process != nil) but has no address (addr == nil) if the
357 // plugin failed at startup. If we do have an address, we need to close
358 // the plugin net connections.
359 graceful := false
360 if addr != nil {
361 // Close the client to cleanly exit the process.
362 client, err := c.Client()
363 if err == nil {
364 err = client.Close()
365
366 // If there is no error, then we attempt to wait for a graceful
367 // exit. If there was an error, we assume that graceful cleanup
368 // won't happen and just force kill.
369 graceful = err == nil
370 if err != nil {
371 // If there was an error just log it. We're going to force
372 // kill in a moment anyways.
373 c.logger.Warn("error closing client during Kill", "err", err)
374 }
375 }
376 }
377
378 // If we're attempting a graceful exit, then we wait for a short period
379 // of time to allow that to happen. To wait for this we just wait on the
380 // doneCh which would be closed if the process exits.
381 if graceful {
382 select {
383 case <-doneCh:
384 return
385 case <-time.After(250 * time.Millisecond):
386 }
387 }
388
389 // If graceful exiting failed, just kill it
390 process.Kill()
391
392 // Wait for the client to finish logging so we have a complete log
393 <-doneCh
394 }
395
396 // Starts the underlying subprocess, communicating with it to negotiate
397 // a port for RPC connections, and returning the address to connect via RPC.
398 //
399 // This method is safe to call multiple times. Subsequent calls have no effect.
400 // Once a client has been started once, it cannot be started again, even if
401 // it was killed.
402 func (c *Client) Start() (addr net.Addr, err error) {
403 c.l.Lock()
404 defer c.l.Unlock()
405
406 if c.address != nil {
407 return c.address, nil
408 }
409
410 // If one of cmd or reattach isn't set, then it is an error. We wrap
411 // this in a {} for scoping reasons, and hopeful that the escape
412 // analysis will pop the stock here.
413 {
414 cmdSet := c.config.Cmd != nil
415 attachSet := c.config.Reattach != nil
416 secureSet := c.config.SecureConfig != nil
417 if cmdSet == attachSet {
418 return nil, fmt.Errorf("Only one of Cmd or Reattach must be set")
419 }
420
421 if secureSet && attachSet {
422 return nil, ErrSecureConfigAndReattach
423 }
424 }
425
426 // Create the logging channel for when we kill
427 c.doneLogging = make(chan struct{})
428 // Create a context for when we kill
429 var ctxCancel context.CancelFunc
430 c.doneCtx, ctxCancel = context.WithCancel(context.Background())
431
432 if c.config.Reattach != nil {
433 // Verify the process still exists. If not, then it is an error
434 p, err := os.FindProcess(c.config.Reattach.Pid)
435 if err != nil {
436 return nil, err
437 }
438
439 // Attempt to connect to the addr since on Unix systems FindProcess
440 // doesn't actually return an error if it can't find the process.
441 conn, err := net.Dial(
442 c.config.Reattach.Addr.Network(),
443 c.config.Reattach.Addr.String())
444 if err != nil {
445 p.Kill()
446 return nil, ErrProcessNotFound
447 }
448 conn.Close()
449
450 // Goroutine to mark exit status
451 go func(pid int) {
452 // Wait for the process to die
453 pidWait(pid)
454
455 // Log so we can see it
456 c.logger.Debug("reattached plugin process exited")
457
458 // Mark it
459 c.l.Lock()
460 defer c.l.Unlock()
461 c.exited = true
462
463 // Close the logging channel since that doesn't work on reattach
464 close(c.doneLogging)
465
466 // Cancel the context
467 ctxCancel()
468 }(p.Pid)
469
470 // Set the address and process
471 c.address = c.config.Reattach.Addr
472 c.process = p
473 c.protocol = c.config.Reattach.Protocol
474 if c.protocol == "" {
475 // Default the protocol to net/rpc for backwards compatibility
476 c.protocol = ProtocolNetRPC
477 }
478
479 return c.address, nil
480 }
481
482 env := []string{
483 fmt.Sprintf("%s=%s", c.config.MagicCookieKey, c.config.MagicCookieValue),
484 fmt.Sprintf("PLUGIN_MIN_PORT=%d", c.config.MinPort),
485 fmt.Sprintf("PLUGIN_MAX_PORT=%d", c.config.MaxPort),
486 }
487
488 stdout_r, stdout_w := io.Pipe()
489 stderr_r, stderr_w := io.Pipe()
490
491 cmd := c.config.Cmd
492 cmd.Env = append(cmd.Env, os.Environ()...)
493 cmd.Env = append(cmd.Env, env...)
494 cmd.Stdin = os.Stdin
495 cmd.Stderr = stderr_w
496 cmd.Stdout = stdout_w
497
498 if c.config.SecureConfig != nil {
499 if ok, err := c.config.SecureConfig.Check(cmd.Path); err != nil {
500 return nil, fmt.Errorf("error verifying checksum: %s", err)
501 } else if !ok {
502 return nil, ErrChecksumsDoNotMatch
503 }
504 }
505
506 c.logger.Debug("starting plugin", "path", cmd.Path, "args", cmd.Args)
507 err = cmd.Start()
508 if err != nil {
509 return
510 }
511
512 // Set the process
513 c.process = cmd.Process
514
515 // Make sure the command is properly cleaned up if there is an error
516 defer func() {
517 r := recover()
518
519 if err != nil || r != nil {
520 cmd.Process.Kill()
521 }
522
523 if r != nil {
524 panic(r)
525 }
526 }()
527
528 // Start goroutine to wait for process to exit
529 exitCh := make(chan struct{})
530 go func() {
531 // Make sure we close the write end of our stderr/stdout so
532 // that the readers send EOF properly.
533 defer stderr_w.Close()
534 defer stdout_w.Close()
535
536 // Wait for the command to end.
537 cmd.Wait()
538
539 // Log and make sure to flush the logs write away
540 c.logger.Debug("plugin process exited", "path", cmd.Path)
541 os.Stderr.Sync()
542
543 // Mark that we exited
544 close(exitCh)
545
546 // Cancel the context, marking that we exited
547 ctxCancel()
548
549 // Set that we exited, which takes a lock
550 c.l.Lock()
551 defer c.l.Unlock()
552 c.exited = true
553 }()
554
555 // Start goroutine that logs the stderr
556 go c.logStderr(stderr_r)
557
558 // Start a goroutine that is going to be reading the lines
559 // out of stdout
560 linesCh := make(chan []byte)
561 go func() {
562 defer close(linesCh)
563
564 buf := bufio.NewReader(stdout_r)
565 for {
566 line, err := buf.ReadBytes('\n')
567 if line != nil {
568 linesCh <- line
569 }
570
571 if err == io.EOF {
572 return
573 }
574 }
575 }()
576
577 // Make sure after we exit we read the lines from stdout forever
578 // so they don't block since it is an io.Pipe
579 defer func() {
580 go func() {
581 for _ = range linesCh {
582 }
583 }()
584 }()
585
586 // Some channels for the next step
587 timeout := time.After(c.config.StartTimeout)
588
589 // Start looking for the address
590 c.logger.Debug("waiting for RPC address", "path", cmd.Path)
591 select {
592 case <-timeout:
593 err = errors.New("timeout while waiting for plugin to start")
594 case <-exitCh:
595 err = errors.New("plugin exited before we could connect")
596 case lineBytes := <-linesCh:
597 // Trim the line and split by "|" in order to get the parts of
598 // the output.
599 line := strings.TrimSpace(string(lineBytes))
600 parts := strings.SplitN(line, "|", 6)
601 if len(parts) < 4 {
602 err = fmt.Errorf(
603 "Unrecognized remote plugin message: %s\n\n"+
604 "This usually means that the plugin is either invalid or simply\n"+
605 "needs to be recompiled to support the latest protocol.", line)
606 return
607 }
608
609 // Check the core protocol. Wrapped in a {} for scoping.
610 {
611 var coreProtocol int64
612 coreProtocol, err = strconv.ParseInt(parts[0], 10, 0)
613 if err != nil {
614 err = fmt.Errorf("Error parsing core protocol version: %s", err)
615 return
616 }
617
618 if int(coreProtocol) != CoreProtocolVersion {
619 err = fmt.Errorf("Incompatible core API version with plugin. "+
620 "Plugin version: %s, Core version: %d\n\n"+
621 "To fix this, the plugin usually only needs to be recompiled.\n"+
622 "Please report this to the plugin author.", parts[0], CoreProtocolVersion)
623 return
624 }
625 }
626
627 // Parse the protocol version
628 var protocol int64
629 protocol, err = strconv.ParseInt(parts[1], 10, 0)
630 if err != nil {
631 err = fmt.Errorf("Error parsing protocol version: %s", err)
632 return
633 }
634
635 // Test the API version
636 if uint(protocol) != c.config.ProtocolVersion {
637 err = fmt.Errorf("Incompatible API version with plugin. "+
638 "Plugin version: %s, Core version: %d", parts[1], c.config.ProtocolVersion)
639 return
640 }
641
642 switch parts[2] {
643 case "tcp":
644 addr, err = net.ResolveTCPAddr("tcp", parts[3])
645 case "unix":
646 addr, err = net.ResolveUnixAddr("unix", parts[3])
647 default:
648 err = fmt.Errorf("Unknown address type: %s", parts[3])
649 }
650
651 // If we have a server type, then record that. We default to net/rpc
652 // for backwards compatibility.
653 c.protocol = ProtocolNetRPC
654 if len(parts) >= 5 {
655 c.protocol = Protocol(parts[4])
656 }
657
658 found := false
659 for _, p := range c.config.AllowedProtocols {
660 if p == c.protocol {
661 found = true
662 break
663 }
664 }
665 if !found {
666 err = fmt.Errorf("Unsupported plugin protocol %q. Supported: %v",
667 c.protocol, c.config.AllowedProtocols)
668 return
669 }
670
671 }
672
673 c.address = addr
674 return
675 }
676
677 // ReattachConfig returns the information that must be provided to NewClient
678 // to reattach to the plugin process that this client started. This is
679 // useful for plugins that detach from their parent process.
680 //
681 // If this returns nil then the process hasn't been started yet. Please
682 // call Start or Client before calling this.
683 func (c *Client) ReattachConfig() *ReattachConfig {
684 c.l.Lock()
685 defer c.l.Unlock()
686
687 if c.address == nil {
688 return nil
689 }
690
691 if c.config.Cmd != nil && c.config.Cmd.Process == nil {
692 return nil
693 }
694
695 // If we connected via reattach, just return the information as-is
696 if c.config.Reattach != nil {
697 return c.config.Reattach
698 }
699
700 return &ReattachConfig{
701 Protocol: c.protocol,
702 Addr: c.address,
703 Pid: c.config.Cmd.Process.Pid,
704 }
705 }
706
707 // Protocol returns the protocol of server on the remote end. This will
708 // start the plugin process if it isn't already started. Errors from
709 // starting the plugin are surpressed and ProtocolInvalid is returned. It
710 // is recommended you call Start explicitly before calling Protocol to ensure
711 // no errors occur.
712 func (c *Client) Protocol() Protocol {
713 _, err := c.Start()
714 if err != nil {
715 return ProtocolInvalid
716 }
717
718 return c.protocol
719 }
720
721 func netAddrDialer(addr net.Addr) func(string, time.Duration) (net.Conn, error) {
722 return func(_ string, _ time.Duration) (net.Conn, error) {
723 // Connect to the client
724 conn, err := net.Dial(addr.Network(), addr.String())
725 if err != nil {
726 return nil, err
727 }
728 if tcpConn, ok := conn.(*net.TCPConn); ok {
729 // Make sure to set keep alive so that the connection doesn't die
730 tcpConn.SetKeepAlive(true)
731 }
732
733 return conn, nil
734 }
735 }
736
737 // dialer is compatible with grpc.WithDialer and creates the connection
738 // to the plugin.
739 func (c *Client) dialer(_ string, timeout time.Duration) (net.Conn, error) {
740 conn, err := netAddrDialer(c.address)("", timeout)
741 if err != nil {
742 return nil, err
743 }
744
745 // If we have a TLS config we wrap our connection. We only do this
746 // for net/rpc since gRPC uses its own mechanism for TLS.
747 if c.protocol == ProtocolNetRPC && c.config.TLSConfig != nil {
748 conn = tls.Client(conn, c.config.TLSConfig)
749 }
750
751 return conn, nil
752 }
753
754 func (c *Client) logStderr(r io.Reader) {
755 bufR := bufio.NewReader(r)
756 for {
757 line, err := bufR.ReadString('\n')
758 if line != "" {
759 c.config.Stderr.Write([]byte(line))
760 line = strings.TrimRightFunc(line, unicode.IsSpace)
761
762 l := c.logger.Named(filepath.Base(c.config.Cmd.Path))
763
764 entry, err := parseJSON(line)
765 // If output is not JSON format, print directly to Debug
766 if err != nil {
767 l.Debug(line)
768 } else {
769 out := flattenKVPairs(entry.KVPairs)
770
771 l = l.With("timestamp", entry.Timestamp.Format(hclog.TimeFormat))
772 switch hclog.LevelFromString(entry.Level) {
773 case hclog.Trace:
774 l.Trace(entry.Message, out...)
775 case hclog.Debug:
776 l.Debug(entry.Message, out...)
777 case hclog.Info:
778 l.Info(entry.Message, out...)
779 case hclog.Warn:
780 l.Warn(entry.Message, out...)
781 case hclog.Error:
782 l.Error(entry.Message, out...)
783 }
784 }
785 }
786
787 if err == io.EOF {
788 break
789 }
790 }
791
792 // Flag that we've completed logging for others
793 close(c.doneLogging)
794 }