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 }