github.com/SagerNet/gvisor@v0.0.0-20210707092255-7731c139d75c/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/docker/docker/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  // Exec creates a process inside the container.
    46  func (c *Container) Exec(ctx context.Context, opts ExecOpts, args ...string) (string, error) {
    47  	p, err := c.doExec(ctx, opts, args)
    48  	if err != nil {
    49  		return "", err
    50  	}
    51  
    52  	if exitStatus, err := p.WaitExitStatus(ctx); err != nil {
    53  		return "", err
    54  	} else if exitStatus != 0 {
    55  		out, _ := p.Logs()
    56  		return out, fmt.Errorf("process terminated with status: %d", exitStatus)
    57  	}
    58  
    59  	return p.Logs()
    60  }
    61  
    62  // ExecProcess creates a process inside the container and returns a process struct
    63  // for the caller to use.
    64  func (c *Container) ExecProcess(ctx context.Context, opts ExecOpts, args ...string) (Process, error) {
    65  	return c.doExec(ctx, opts, args)
    66  }
    67  
    68  func (c *Container) doExec(ctx context.Context, r ExecOpts, args []string) (Process, error) {
    69  	config := c.execConfig(r, args)
    70  	resp, err := c.client.ContainerExecCreate(ctx, c.id, config)
    71  	if err != nil {
    72  		return Process{}, fmt.Errorf("exec create failed with err: %v", err)
    73  	}
    74  
    75  	hijack, err := c.client.ContainerExecAttach(ctx, resp.ID, types.ExecStartCheck{})
    76  	if err != nil {
    77  		return Process{}, fmt.Errorf("exec attach failed with err: %v", err)
    78  	}
    79  
    80  	return Process{
    81  		container: c,
    82  		execid:    resp.ID,
    83  		conn:      hijack,
    84  	}, nil
    85  }
    86  
    87  func (c *Container) execConfig(r ExecOpts, cmd []string) types.ExecConfig {
    88  	env := append(r.Env, fmt.Sprintf("RUNSC_TEST_NAME=%s", c.Name))
    89  	return types.ExecConfig{
    90  		AttachStdin:  r.UseTTY,
    91  		AttachStderr: true,
    92  		AttachStdout: true,
    93  		Cmd:          cmd,
    94  		Privileged:   r.Privileged,
    95  		WorkingDir:   r.WorkDir,
    96  		Env:          env,
    97  		Tty:          r.UseTTY,
    98  		User:         r.User,
    99  	}
   100  
   101  }
   102  
   103  // Process represents a containerized process.
   104  type Process struct {
   105  	container *Container
   106  	execid    string
   107  	conn      types.HijackedResponse
   108  }
   109  
   110  // Write writes buf to the process's stdin.
   111  func (p *Process) Write(timeout time.Duration, buf []byte) (int, error) {
   112  	p.conn.Conn.SetDeadline(time.Now().Add(timeout))
   113  	return p.conn.Conn.Write(buf)
   114  }
   115  
   116  // Read returns process's stdout and stderr.
   117  func (p *Process) Read() (string, string, error) {
   118  	var stdout, stderr bytes.Buffer
   119  	if err := p.read(&stdout, &stderr); err != nil {
   120  		return "", "", err
   121  	}
   122  	return stdout.String(), stderr.String(), nil
   123  }
   124  
   125  // Logs returns combined stdout/stderr from the process.
   126  func (p *Process) Logs() (string, error) {
   127  	var out bytes.Buffer
   128  	if err := p.read(&out, &out); err != nil {
   129  		return "", err
   130  	}
   131  	return out.String(), nil
   132  }
   133  
   134  func (p *Process) read(stdout, stderr *bytes.Buffer) error {
   135  	_, err := stdcopy.StdCopy(stdout, stderr, p.conn.Reader)
   136  	return err
   137  }
   138  
   139  // ExitCode returns the process's exit code.
   140  func (p *Process) ExitCode(ctx context.Context) (int, error) {
   141  	_, exitCode, err := p.runningExitCode(ctx)
   142  	return exitCode, err
   143  }
   144  
   145  // IsRunning checks if the process is running.
   146  func (p *Process) IsRunning(ctx context.Context) (bool, error) {
   147  	running, _, err := p.runningExitCode(ctx)
   148  	return running, err
   149  }
   150  
   151  // WaitExitStatus until process completes and returns exit status.
   152  func (p *Process) WaitExitStatus(ctx context.Context) (int, error) {
   153  	waitChan := make(chan (int))
   154  	errChan := make(chan (error))
   155  
   156  	go func() {
   157  		for {
   158  			running, exitcode, err := p.runningExitCode(ctx)
   159  			if err != nil {
   160  				errChan <- fmt.Errorf("error waiting process %s: container %v", p.execid, p.container.Name)
   161  			}
   162  			if !running {
   163  				waitChan <- exitcode
   164  			}
   165  			time.Sleep(time.Millisecond * 500)
   166  		}
   167  	}()
   168  
   169  	select {
   170  	case ws := <-waitChan:
   171  		return ws, nil
   172  	case err := <-errChan:
   173  		return -1, err
   174  	}
   175  }
   176  
   177  // runningExitCode collects if the process is running and the exit code.
   178  // The exit code is only valid if the process has exited.
   179  func (p *Process) runningExitCode(ctx context.Context) (bool, int, error) {
   180  	// If execid is not empty, this is a execed process.
   181  	if p.execid != "" {
   182  		status, err := p.container.client.ContainerExecInspect(ctx, p.execid)
   183  		return status.Running, status.ExitCode, err
   184  	}
   185  	// else this is the root process.
   186  	status, err := p.container.Status(ctx)
   187  	return status.Running, status.ExitCode, err
   188  }