Skip to content

Commit

Permalink
Merge pull request #69 from coryb/started-chan
Browse files Browse the repository at this point in the history
allow for optional started channel to receive runc pid
  • Loading branch information
estesp authored Oct 20, 2020
2 parents ee817f5 + 8abfc31 commit 16b287b
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 9 deletions.
35 changes: 26 additions & 9 deletions runc.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const (

// List returns all containers created inside the provided runc root directory
func (r *Runc) List(context context.Context) ([]*Container, error) {
data, err := cmdOutput(r.command(context, "list", "--format=json"), false)
data, err := cmdOutput(r.command(context, "list", "--format=json"), false, nil)
defer putBuf(data)
if err != nil {
return nil, err
Expand All @@ -70,7 +70,7 @@ func (r *Runc) List(context context.Context) ([]*Container, error) {

// State returns the state for the container provided by id
func (r *Runc) State(context context.Context, id string) (*Container, error) {
data, err := cmdOutput(r.command(context, "state", id), true)
data, err := cmdOutput(r.command(context, "state", id), true, nil)
defer putBuf(data)
if err != nil {
return nil, fmt.Errorf("%s: %s", err, data.String())
Expand All @@ -95,6 +95,7 @@ type CreateOpts struct {
NoPivot bool
NoNewKeyring bool
ExtraFiles []*os.File
Started chan<- int
}

func (o *CreateOpts) args() (out []string, err error) {
Expand Down Expand Up @@ -140,7 +141,7 @@ func (r *Runc) Create(context context.Context, id, bundle string, opts *CreateOp
cmd.ExtraFiles = opts.ExtraFiles

if cmd.Stdout == nil && cmd.Stderr == nil {
data, err := cmdOutput(cmd, true)
data, err := cmdOutput(cmd, true, nil)
defer putBuf(data)
if err != nil {
return fmt.Errorf("%s: %s", err, data.String())
Expand Down Expand Up @@ -175,6 +176,7 @@ type ExecOpts struct {
PidFile string
ConsoleSocket ConsoleSocket
Detach bool
Started chan<- int
}

func (o *ExecOpts) args() (out []string, err error) {
Expand All @@ -197,6 +199,9 @@ func (o *ExecOpts) args() (out []string, err error) {
// Exec executes an additional process inside the container based on a full
// OCI Process specification
func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts *ExecOpts) error {
if opts.Started != nil {
defer close(opts.Started)
}
f, err := ioutil.TempFile(os.Getenv("XDG_RUNTIME_DIR"), "runc-process")
if err != nil {
return err
Expand All @@ -220,7 +225,7 @@ func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts
opts.Set(cmd)
}
if cmd.Stdout == nil && cmd.Stderr == nil {
data, err := cmdOutput(cmd, true)
data, err := cmdOutput(cmd, true, opts.Started)
defer putBuf(data)
if err != nil {
return fmt.Errorf("%w: %s", err, data.String())
Expand All @@ -231,6 +236,9 @@ func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts
if err != nil {
return err
}
if opts.Started != nil {
opts.Started <- cmd.Process.Pid
}
if opts != nil && opts.IO != nil {
if c, ok := opts.IO.(StartCloser); ok {
if err := c.CloseAfterStart(); err != nil {
Expand All @@ -248,6 +256,9 @@ func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts
// Run runs the create, start, delete lifecycle of the container
// and returns its exit status after it has exited
func (r *Runc) Run(context context.Context, id, bundle string, opts *CreateOpts) (int, error) {
if opts.Started != nil {
defer close(opts.Started)
}
args := []string{"run", "--bundle", bundle}
if opts != nil {
oargs, err := opts.args()
Expand All @@ -264,6 +275,9 @@ func (r *Runc) Run(context context.Context, id, bundle string, opts *CreateOpts)
if err != nil {
return -1, err
}
if opts.Started != nil {
opts.Started <- cmd.Process.Pid
}
status, err := Monitor.Wait(cmd, ec)
if err == nil && status != 0 {
err = fmt.Errorf("%s did not terminate successfully: %w", cmd.Args[0], &ExitError{status})
Expand Down Expand Up @@ -387,7 +401,7 @@ func (r *Runc) Resume(context context.Context, id string) error {

// Ps lists all the processes inside the container returning their pids
func (r *Runc) Ps(context context.Context, id string) ([]int, error) {
data, err := cmdOutput(r.command(context, "ps", "--format", "json", id), true)
data, err := cmdOutput(r.command(context, "ps", "--format", "json", id), true, nil)
defer putBuf(data)
if err != nil {
return nil, fmt.Errorf("%s: %s", err, data.String())
Expand All @@ -401,7 +415,7 @@ func (r *Runc) Ps(context context.Context, id string) ([]int, error) {

// Top lists all the processes inside the container returning the full ps data
func (r *Runc) Top(context context.Context, id string, psOptions string) (*TopResults, error) {
data, err := cmdOutput(r.command(context, "ps", "--format", "table", id, psOptions), true)
data, err := cmdOutput(r.command(context, "ps", "--format", "table", id, psOptions), true, nil)
defer putBuf(data)
if err != nil {
return nil, fmt.Errorf("%s: %s", err, data.String())
Expand Down Expand Up @@ -613,7 +627,7 @@ type Version struct {

// Version returns the runc and runtime-spec versions
func (r *Runc) Version(context context.Context) (Version, error) {
data, err := cmdOutput(r.command(context, "--version"), false)
data, err := cmdOutput(r.command(context, "--version"), false, nil)
defer putBuf(data)
if err != nil {
return Version{}, err
Expand Down Expand Up @@ -685,7 +699,7 @@ func (r *Runc) runOrError(cmd *exec.Cmd) error {
}
return err
}
data, err := cmdOutput(cmd, true)
data, err := cmdOutput(cmd, true, nil)
defer putBuf(data)
if err != nil {
return fmt.Errorf("%s: %s", err, data.String())
Expand All @@ -695,7 +709,7 @@ func (r *Runc) runOrError(cmd *exec.Cmd) error {

// callers of cmdOutput are expected to call putBuf on the returned Buffer
// to ensure it is released back to the shared pool after use.
func cmdOutput(cmd *exec.Cmd, combined bool) (*bytes.Buffer, error) {
func cmdOutput(cmd *exec.Cmd, combined bool, started chan<- int) (*bytes.Buffer, error) {
b := getBuf()

cmd.Stdout = b
Expand All @@ -706,6 +720,9 @@ func cmdOutput(cmd *exec.Cmd, combined bool) (*bytes.Buffer, error) {
if err != nil {
return nil, err
}
if started != nil {
started <- cmd.Process.Pid
}

status, err := Monitor.Wait(cmd, ec)
if err == nil && status != 0 {
Expand Down
122 changes: 122 additions & 0 deletions runc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ package runc
import (
"context"
"errors"
"io/ioutil"
"os"
"sync"
"syscall"
"testing"
"time"

specs "github.com/opencontainers/runtime-spec/specs-go"
)
Expand Down Expand Up @@ -182,6 +186,78 @@ func TestRuncExecExit(t *testing.T) {
}
}

func TestRuncStarted(t *testing.T) {
ctx, timeout := context.WithTimeout(context.Background(), 10*time.Second)
defer timeout()

dummyCommand, err := dummySleepRunc()
if err != nil {
t.Fatalf("Failed to create dummy sleep runc: %s", err)
}
defer os.Remove(dummyCommand)
sleepRunc := &Runc{
Command: dummyCommand,
}

var wg sync.WaitGroup
defer wg.Wait()

started := make(chan int)
wg.Add(1)
go func() {
defer wg.Done()
interrupt(ctx, t, started)
}()
status, err := sleepRunc.Run(ctx, "fake-id", "fake-bundle", &CreateOpts{
Started: started,
})
if err == nil {
t.Fatal("Expected error from Run, but got nil")
}
if status != -1 {
t.Fatalf("Expected exit status 0 from Run, got %d", status)
}

started = make(chan int)
wg.Add(1)
go func() {
defer wg.Done()
interrupt(ctx, t, started)
}()
err = sleepRunc.Exec(ctx, "fake-id", specs.Process{}, &ExecOpts{
Started: started,
})
if err == nil {
t.Fatal("Expected error from Exec, but got nil")
}
status = extractStatus(err)
if status != -1 {
t.Fatalf("Expected exit status -1 from Exec, got %d", status)
}

started = make(chan int)
wg.Add(1)
go func() {
defer wg.Done()
interrupt(ctx, t, started)
}()
io, err := NewSTDIO()
if err != nil {
t.Fatalf("Unexpected error from NewSTDIO: %s", err)
}
err = sleepRunc.Exec(ctx, "fake-id", specs.Process{}, &ExecOpts{
IO: io,
Started: started,
})
if err == nil {
t.Fatal("Expected error from Exec, but got nil")
}
status = extractStatus(err)
if status != -1 {
t.Fatalf("Expected exit status 1 from Exec, got %d", status)
}
}

func extractStatus(err error) int {
if err == nil {
return 0
Expand All @@ -192,3 +268,49 @@ func extractStatus(err error) int {
}
return -1
}

// interrupt waits for the pid over the started channel then sends a
// SIGINT to the process.
func interrupt(ctx context.Context, t *testing.T, started <-chan int) {
select {
case <-ctx.Done():
t.Fatal("Timed out waiting for started message")
case pid, ok := <-started:
if !ok {
t.Fatal("Started channel closed without sending pid")
}
process, _ := os.FindProcess(pid)
defer process.Release()
err := process.Signal(syscall.SIGINT)
if err != nil {
t.Fatalf("Failed to send SIGINT to %d: %s", pid, err)
}
}
}

// dummySleepRunc creates s simple script that just runs `sleep 10` to replace
// runc for testing process that are longer running.
func dummySleepRunc() (_ string, err error) {
fh, err := ioutil.TempFile("", "*.sh")
if err != nil {
return "", err
}
defer func() {
if err != nil {
os.Remove(fh.Name())
}
}()
_, err = fh.Write([]byte("#!/bin/sh\nexec /bin/sleep 10"))
if err != nil {
return "", err
}
err = fh.Close()
if err != nil {
return "", err
}
err = os.Chmod(fh.Name(), 0755)
if err != nil {
return "", err
}
return fh.Name(), nil
}

0 comments on commit 16b287b

Please sign in to comment.