gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/test/dockerutil/exec.go (about) 1 // Copyright 2020 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package dockerutil 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "time" 22 23 "github.com/docker/docker/api/types" 24 "github.com/moby/moby/pkg/stdcopy" 25 ) 26 27 // ExecOpts holds arguments for Exec calls. 28 type ExecOpts struct { 29 // Env are additional environment variables. 30 Env []string 31 32 // Privileged enables privileged mode. 33 Privileged bool 34 35 // User is the user to use. 36 User string 37 38 // Enables Tty and stdin for the created process. 39 UseTTY bool 40 41 // WorkDir is the working directory of the process. 42 WorkDir string 43 } 44 45 // ExecError is returned when a process terminated with a non-zero exit status. 46 // It implements `error`. 47 type ExecError struct { 48 ExitStatus int 49 } 50 51 // Error implements `error.Error`. 52 func (ee *ExecError) Error() string { 53 return fmt.Sprintf("process terminated with status: %d", ee.ExitStatus) 54 } 55 56 // Exec creates a process inside the container. 57 // If the process exits with a non-zero error code, the error will be of 58 // type `ExecError`. 59 func (c *Container) Exec(ctx context.Context, opts ExecOpts, args ...string) (string, error) { 60 p, err := c.doExec(ctx, opts, args) 61 if err != nil { 62 return "", err 63 } 64 done := make(chan struct{}) 65 var ( 66 out string 67 outErr error 68 ) 69 // Read logs from another go-routine to be sure that it doesn't block on 70 // writing into standard file descriptors. 71 go func() { 72 out, outErr = p.Logs() 73 close(done) 74 }() 75 76 if exitStatus, err := p.WaitExitStatus(ctx); err != nil { 77 return "", err 78 } else if exitStatus != 0 { 79 <-done 80 return out, &ExecError{exitStatus} 81 } 82 83 <-done 84 return out, outErr 85 } 86 87 // ExecProcess creates a process inside the container and returns a process struct 88 // for the caller to use. 89 func (c *Container) ExecProcess(ctx context.Context, opts ExecOpts, args ...string) (Process, error) { 90 return c.doExec(ctx, opts, args) 91 } 92 93 func (c *Container) doExec(ctx context.Context, r ExecOpts, args []string) (Process, error) { 94 config := c.execConfig(r, args) 95 resp, err := c.client.ContainerExecCreate(ctx, c.id, config) 96 if err != nil { 97 return Process{}, fmt.Errorf("exec create failed with err: %v", err) 98 } 99 100 hijack, err := c.client.ContainerExecAttach(ctx, resp.ID, types.ExecStartCheck{}) 101 if err != nil { 102 return Process{}, fmt.Errorf("exec attach failed with err: %v", err) 103 } 104 105 return Process{ 106 container: c, 107 execid: resp.ID, 108 conn: hijack, 109 }, nil 110 } 111 112 func (c *Container) execConfig(r ExecOpts, cmd []string) types.ExecConfig { 113 env := append(r.Env, fmt.Sprintf("RUNSC_TEST_NAME=%s", c.Name)) 114 return types.ExecConfig{ 115 AttachStdin: r.UseTTY, 116 AttachStderr: true, 117 AttachStdout: true, 118 Cmd: cmd, 119 Privileged: r.Privileged, 120 WorkingDir: r.WorkDir, 121 Env: env, 122 Tty: r.UseTTY, 123 User: r.User, 124 } 125 } 126 127 // Process represents a containerized process. 128 type Process struct { 129 container *Container 130 execid string 131 conn types.HijackedResponse 132 } 133 134 // Write writes buf to the process's stdin. 135 func (p *Process) Write(timeout time.Duration, buf []byte) (int, error) { 136 p.conn.Conn.SetDeadline(time.Now().Add(timeout)) 137 return p.conn.Conn.Write(buf) 138 } 139 140 // Read returns process's stdout and stderr. 141 func (p *Process) Read() (string, string, error) { 142 var stdout, stderr bytes.Buffer 143 if err := p.read(&stdout, &stderr); err != nil { 144 return "", "", err 145 } 146 return stdout.String(), stderr.String(), nil 147 } 148 149 // Logs returns combined stdout/stderr from the process. 150 func (p *Process) Logs() (string, error) { 151 var out bytes.Buffer 152 if err := p.read(&out, &out); err != nil { 153 return "", err 154 } 155 return out.String(), nil 156 } 157 158 func (p *Process) read(stdout, stderr *bytes.Buffer) error { 159 _, err := stdcopy.StdCopy(stdout, stderr, p.conn.Reader) 160 return err 161 } 162 163 // ExitCode returns the process's exit code. 164 func (p *Process) ExitCode(ctx context.Context) (int, error) { 165 _, exitCode, err := p.runningExitCode(ctx) 166 return exitCode, err 167 } 168 169 // IsRunning checks if the process is running. 170 func (p *Process) IsRunning(ctx context.Context) (bool, error) { 171 running, _, err := p.runningExitCode(ctx) 172 return running, err 173 } 174 175 // WaitExitStatus until process completes and returns exit status. 176 func (p *Process) WaitExitStatus(ctx context.Context) (int, error) { 177 waitChan := make(chan (int)) 178 errChan := make(chan (error)) 179 180 go func() { 181 for { 182 running, exitcode, err := p.runningExitCode(ctx) 183 if err != nil { 184 errChan <- fmt.Errorf("error waiting process %s: container %v", p.execid, p.container.Name) 185 } 186 if !running { 187 waitChan <- exitcode 188 } 189 time.Sleep(time.Millisecond * 500) 190 } 191 }() 192 193 select { 194 case ws := <-waitChan: 195 return ws, nil 196 case err := <-errChan: 197 return -1, err 198 } 199 } 200 201 // runningExitCode collects if the process is running and the exit code. 202 // The exit code is only valid if the process has exited. 203 func (p *Process) runningExitCode(ctx context.Context) (bool, int, error) { 204 // If execid is not empty, this is a execed process. 205 if p.execid != "" { 206 status, err := p.container.client.ContainerExecInspect(ctx, p.execid) 207 return status.Running, status.ExitCode, err 208 } 209 // else this is the root process. 210 status, err := p.container.Status(ctx) 211 return status.Running, status.ExitCode, err 212 }