github.com/containerd/nerdctl@v1.7.7/pkg/cioutil/container_io.go (about)

     1  /*
     2     Copyright The containerd Authors.
     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 cioutil
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net/url"
    25  	"os"
    26  	"os/exec"
    27  	"runtime"
    28  	"sync"
    29  	"syscall"
    30  	"time"
    31  
    32  	"github.com/containerd/containerd/cio"
    33  	"github.com/containerd/containerd/defaults"
    34  	"github.com/containerd/containerd/pkg/process"
    35  )
    36  
    37  const binaryIOProcTermTimeout = 12 * time.Second // Give logger process 10 seconds for cleanup
    38  
    39  // ncio is a basic container IO implementation.
    40  type ncio struct {
    41  	cmd     *exec.Cmd
    42  	config  cio.Config
    43  	wg      *sync.WaitGroup
    44  	closers []io.Closer
    45  	cancel  context.CancelFunc
    46  }
    47  
    48  var bufPool = sync.Pool{
    49  	New: func() interface{} {
    50  		buffer := make([]byte, 32<<10)
    51  		return &buffer
    52  	},
    53  }
    54  
    55  func (c *ncio) Config() cio.Config {
    56  	return c.config
    57  }
    58  
    59  func (c *ncio) Wait() {
    60  	if c.wg != nil {
    61  		c.wg.Wait()
    62  	}
    63  }
    64  
    65  func (c *ncio) Close() error {
    66  
    67  	var lastErr error
    68  
    69  	if c.cmd != nil && c.cmd.Process != nil {
    70  
    71  		// Send SIGTERM first, so logger process has a chance to flush and exit properly
    72  		if err := c.cmd.Process.Signal(syscall.SIGTERM); err != nil {
    73  			lastErr = fmt.Errorf("failed to send SIGTERM: %w", err)
    74  
    75  			if err := c.cmd.Process.Kill(); err != nil {
    76  				lastErr = errors.Join(lastErr, fmt.Errorf("failed to kill process after faulty SIGTERM: %w", err))
    77  			}
    78  
    79  		}
    80  
    81  		done := make(chan error, 1)
    82  		go func() {
    83  			done <- c.cmd.Wait()
    84  		}()
    85  
    86  		select {
    87  		case err := <-done:
    88  			return err
    89  		case <-time.After(binaryIOProcTermTimeout):
    90  
    91  			err := c.cmd.Process.Kill()
    92  			if err != nil {
    93  				lastErr = fmt.Errorf("failed to kill shim logger process: %w", err)
    94  			}
    95  
    96  		}
    97  	}
    98  
    99  	for _, closer := range c.closers {
   100  		if closer == nil {
   101  			continue
   102  		}
   103  		if err := closer.Close(); err != nil {
   104  			lastErr = err
   105  		}
   106  	}
   107  	return lastErr
   108  }
   109  
   110  func (c *ncio) Cancel() {
   111  	if c.cancel != nil {
   112  		c.cancel()
   113  	}
   114  }
   115  
   116  func NewContainerIO(namespace string, logURI string, tty bool, stdin io.Reader, stdout, stderr io.Writer) cio.Creator {
   117  	return func(id string) (_ cio.IO, err error) {
   118  		var (
   119  			cmd     *exec.Cmd
   120  			closers []func() error
   121  			streams = &cio.Streams{
   122  				Terminal: tty,
   123  			}
   124  		)
   125  
   126  		defer func() {
   127  			if err == nil {
   128  				return
   129  			}
   130  			result := []error{err}
   131  			for _, fn := range closers {
   132  				result = append(result, fn())
   133  			}
   134  			err = errors.Join(result...)
   135  		}()
   136  
   137  		if stdin != nil {
   138  			streams.Stdin = stdin
   139  		}
   140  
   141  		var stdoutWriters []io.Writer
   142  		if stdout != nil {
   143  			stdoutWriters = append(stdoutWriters, stdout)
   144  		}
   145  
   146  		var stderrWriters []io.Writer
   147  		if stderr != nil {
   148  			stderrWriters = append(stderrWriters, stderr)
   149  		}
   150  
   151  		if runtime.GOOS != "windows" {
   152  			// starting logging binary logic is from https://github.com/containerd/containerd/blob/194a1fdd2cde35bc019ef138f30485e27fe0913e/cmd/containerd-shim-runc-v2/process/io.go#L247
   153  			stdoutr, stdoutw, err := os.Pipe()
   154  			if err != nil {
   155  				return nil, err
   156  			}
   157  			closers = append(closers, stdoutr.Close, stdoutw.Close)
   158  
   159  			stderrr, stderrw, err := os.Pipe()
   160  			if err != nil {
   161  				return nil, err
   162  			}
   163  			closers = append(closers, stderrr.Close, stderrw.Close)
   164  
   165  			r, w, err := os.Pipe()
   166  			if err != nil {
   167  				return nil, err
   168  			}
   169  			closers = append(closers, r.Close, w.Close)
   170  
   171  			u, err := url.Parse(logURI)
   172  			if err != nil {
   173  				return nil, err
   174  			}
   175  			cmd = process.NewBinaryCmd(u, id, namespace)
   176  			cmd.ExtraFiles = append(cmd.ExtraFiles, stdoutr, stderrr, w)
   177  
   178  			if err := cmd.Start(); err != nil {
   179  				return nil, fmt.Errorf("failed to start binary process with cmdArgs %v: %w", cmd.Args, err)
   180  			}
   181  
   182  			closers = append(closers, func() error { return cmd.Process.Kill() })
   183  
   184  			// close our side of the pipe after start
   185  			if err := w.Close(); err != nil {
   186  				return nil, fmt.Errorf("failed to close write pipe after start: %w", err)
   187  			}
   188  
   189  			// wait for the logging binary to be ready
   190  			b := make([]byte, 1)
   191  			if _, err := r.Read(b); err != nil && err != io.EOF {
   192  				return nil, fmt.Errorf("failed to read from logging binary: %w", err)
   193  			}
   194  
   195  			stdoutWriters = append(stdoutWriters, stdoutw)
   196  			stderrWriters = append(stderrWriters, stderrw)
   197  		}
   198  
   199  		streams.Stdout = io.MultiWriter(stdoutWriters...)
   200  		streams.Stderr = io.MultiWriter(stderrWriters...)
   201  
   202  		if streams.FIFODir == "" {
   203  			streams.FIFODir = defaults.DefaultFIFODir
   204  		}
   205  		fifos, err := cio.NewFIFOSetInDir(streams.FIFODir, id, streams.Terminal)
   206  		if err != nil {
   207  			return nil, err
   208  		}
   209  
   210  		if streams.Stdin == nil {
   211  			fifos.Stdin = ""
   212  		}
   213  		if streams.Stdout == nil {
   214  			fifos.Stdout = ""
   215  		}
   216  		if streams.Stderr == nil {
   217  			fifos.Stderr = ""
   218  		}
   219  		return copyIO(cmd, fifos, streams)
   220  	}
   221  }