diff --git a/exec.go b/exec.go index 73753d43..0369f171 100644 --- a/exec.go +++ b/exec.go @@ -3,14 +3,17 @@ package main import ( + "encoding/json" "fmt" "os" "path" "strconv" "strings" + "syscall" "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" + "github.com/opencontainers/specs" ) var execCommand = cli.Command{ @@ -37,15 +40,25 @@ var execCommand = cli.Command{ Name: "user, u", Usage: "UID (format: [:])", }, + cli.StringFlag{ + Name: "process,p", + Usage: "path to the process.json", + }, + cli.BoolFlag{ + Name: "detach,d", + Usage: "detach from the container's process", + }, + cli.StringFlag{ + Name: "pid-file", + Value: "", + Usage: "specify the file to write the process id to", + }, }, Action: func(context *cli.Context) { - if len(context.Args()) == 0 { - logrus.Fatal("pass the command") - } if os.Geteuid() != 0 { logrus.Fatal("runc should be run as root") } - status, err := execProcess(context, context.Args()) + status, err := execProcess(context) if err != nil { logrus.Fatalf("exec failed: %v", err) } @@ -53,72 +66,96 @@ var execCommand = cli.Command{ }, } -func execProcess(context *cli.Context, args []string) (int, error) { +func execProcess(context *cli.Context) (int, error) { container, err := getContainer(context) if err != nil { return -1, err } - - bundle := container.Config().Rootfs - if err := os.Chdir(path.Dir(bundle)); err != nil { - return -1, err - } - spec, _, err := loadSpec(specConfig, runtimeConfig) + var ( + detach = context.Bool("detach") + rootfs = container.Config().Rootfs + ) + rootuid, err := container.Config().HostUID() if err != nil { return -1, err } + p, err := getProcess(context, path.Dir(rootfs)) + if err != nil { + return -1, err + } + process := newProcess(*p) + tty, err := setupIO(process, rootuid, context.String("console"), p.Terminal, detach) + if err != nil { + return -1, err + } + if err := container.Start(process); err != nil { + return -1, err + } + if pidFile := context.String("pid-file"); pidFile != "" { + if err := createPidFile(pidFile, process); err != nil { + process.Signal(syscall.SIGKILL) + process.Wait() + return -1, err + } + } + if detach { + return 0, nil + } + handler := newSignalHandler(tty) + defer handler.Close() + return handler.forward(process) +} +func getProcess(context *cli.Context, bundle string) (*specs.Process, error) { + if path := context.String("process"); path != "" { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + var p specs.Process + if err := json.NewDecoder(f).Decode(&p); err != nil { + return nil, err + } + return &p, nil + } + // process via cli flags + if err := os.Chdir(bundle); err != nil { + return nil, err + } + spec, _, err := loadSpec(specConfig, runtimeConfig) + if err != nil { + return nil, err + } p := spec.Process - + p.Args = context.Args() // override the cwd, if passed if context.String("cwd") != "" { p.Cwd = context.String("cwd") } - // append the passed env variables for _, e := range context.StringSlice("env") { p.Env = append(p.Env, e) } - // set the tty if context.IsSet("tty") { p.Terminal = context.Bool("tty") } - // override the user, if passed if context.String("user") != "" { u := strings.SplitN(context.String("user"), ":", 2) if len(u) > 1 { gid, err := strconv.Atoi(u[1]) if err != nil { - return -1, fmt.Errorf("parsing %s as int for gid failed: %v", u[1], err) + return nil, fmt.Errorf("parsing %s as int for gid failed: %v", u[1], err) } p.User.GID = uint32(gid) } uid, err := strconv.Atoi(u[0]) if err != nil { - return -1, fmt.Errorf("parsing %s as int for uid failed: %v", u[0], err) + return nil, fmt.Errorf("parsing %s as int for uid failed: %v", u[0], err) } p.User.UID = uint32(uid) } - - process := newProcess(p) - process.Args = args - rootuid, err := container.Config().HostUID() - if err != nil { - return -1, err - } - - tty, err := newTty(p.Terminal, process, rootuid, context.String("console")) - if err != nil { - return -1, err - } - - handler := newSignalHandler(tty) - defer handler.Close() - if err := container.Start(process); err != nil { - return -1, err - } - - return handler.forward(process) + return &p, nil } diff --git a/restore.go b/restore.go index 55cff56a..b05160d6 100644 --- a/restore.go +++ b/restore.go @@ -5,6 +5,7 @@ package main import ( "fmt" "os" + "syscall" "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" @@ -53,6 +54,15 @@ var restoreCommand = cli.Command{ Value: "", Usage: "path to the root of the bundle directory", }, + cli.BoolFlag{ + Name: "detach,d", + Usage: "detach from the container's process", + }, + cli.StringFlag{ + Name: "pid-file", + Value: "", + Usage: "specify the file to write the process id to", + }, }, Action: func(context *cli.Context) { imagePath := context.String("image-path") @@ -108,21 +118,30 @@ func restoreContainer(context *cli.Context, spec *specs.LinuxSpec, config *confi // ensure that the container is always removed if we were the process // that created it. - defer destroy(container) - process := &libcontainer.Process{ - Stdin: os.Stdin, - Stdout: os.Stdout, - Stderr: os.Stderr, + detach := context.Bool("detach") + if !detach { + defer destroy(container) } - tty, err := newTty(spec.Process.Terminal, process, rootuid, "") + process := &libcontainer.Process{} + tty, err := setupIO(process, rootuid, "", false, detach) if err != nil { return -1, err } - handler := newSignalHandler(tty) - defer handler.Close() if err := container.Restore(process, options); err != nil { return -1, err } + if pidFile := context.String("pid-file"); pidFile != "" { + if err := createPidFile(pidFile, process); err != nil { + process.Signal(syscall.SIGKILL) + process.Wait() + return -1, err + } + } + if detach { + return 0, nil + } + handler := newSignalHandler(tty) + defer handler.Close() return handler.forward(process) } diff --git a/start.go b/start.go index 7ba4d589..46039c6c 100644 --- a/start.go +++ b/start.go @@ -132,38 +132,19 @@ func startContainer(context *cli.Context, spec *specs.LinuxSpec, rspec *specs.Li process.ExtraFiles = append(process.ExtraFiles, os.NewFile(uintptr(i), "")) } } - var tty *tty - if spec.Process.Terminal { - if tty, err = createTty(process, rootuid, context.String("console")); err != nil { - return -1, err - } - } else if detach { - if err := dupStdio(process, rootuid); err != nil { - return -1, err - } - } else { - if tty, err = createStdioPipes(process, rootuid); err != nil { - return -1, err - } + tty, err := setupIO(process, rootuid, context.String("console"), spec.Process.Terminal, detach) + if err != nil { + return -1, err } if err := container.Start(process); err != nil { return -1, err } if pidFile := context.String("pid-file"); pidFile != "" { - pid, err := process.Pid() - if err != nil { + if err := createPidFile(pidFile, process); err != nil { + process.Signal(syscall.SIGKILL) + process.Wait() return -1, err } - f, err := os.Create(pidFile) - if err != nil { - logrus.WithField("pid", pid).Error("create pid file") - } else { - _, err = fmt.Fprintf(f, "%d", pid) - f.Close() - if err != nil { - logrus.WithField("error", err).Error("write pid file") - } - } } if detach { return 0, nil @@ -172,40 +153,3 @@ func startContainer(context *cli.Context, spec *specs.LinuxSpec, rspec *specs.Li defer handler.Close() return handler.forward(process) } - -func dupStdio(process *libcontainer.Process, rootuid int) error { - process.Stdin = os.Stdin - process.Stdout = os.Stdout - process.Stderr = os.Stderr - for _, fd := range []uintptr{ - os.Stdin.Fd(), - os.Stdout.Fd(), - os.Stderr.Fd(), - } { - if err := syscall.Fchown(int(fd), rootuid, rootuid); err != nil { - return err - } - } - return nil -} - -// If systemd is supporting sd_notify protocol, this function will add support -// for sd_notify protocol from within the container. -func setupSdNotify(spec *specs.LinuxSpec, rspec *specs.LinuxRuntimeSpec, notifySocket string) { - mountName := "sdNotify" - spec.Mounts = append(spec.Mounts, specs.MountPoint{Name: mountName, Path: notifySocket}) - spec.Process.Env = append(spec.Process.Env, fmt.Sprintf("NOTIFY_SOCKET=%s", notifySocket)) - rspec.Mounts[mountName] = specs.Mount{Type: "bind", Source: notifySocket, Options: []string{"bind"}} -} - -// If systemd is supporting on-demand socket activation, this function will add support -// for on-demand socket activation for the containerized service. -func setupSocketActivation(spec *specs.LinuxSpec, listenFds string) { - spec.Process.Env = append(spec.Process.Env, fmt.Sprintf("LISTEN_FDS=%s", listenFds), "LISTEN_PID=1") -} - -func destroy(container libcontainer.Container) { - if err := container.Destroy(); err != nil { - logrus.Error(err) - } -} diff --git a/tty.go b/tty.go index 77d275af..b552621b 100644 --- a/tty.go +++ b/tty.go @@ -35,7 +35,10 @@ func createStdioPipes(p *libcontainer.Process, rootuid int) (*tty, error) { i.Stderr, }, } - go io.Copy(i.Stdin, os.Stdin) + go func() { + io.Copy(i.Stdin, os.Stdin) + i.Stdin.Close() + }() go io.Copy(os.Stdout, i.Stdout) go io.Copy(os.Stderr, i.Stderr) return t, nil diff --git a/utils.go b/utils.go index c6a8fde7..4e510e7b 100644 --- a/utils.go +++ b/utils.go @@ -6,7 +6,9 @@ import ( "fmt" "os" "path/filepath" + "syscall" + "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" "github.com/opencontainers/runc/libcontainer" "github.com/opencontainers/runc/libcontainer/configs" @@ -154,3 +156,72 @@ func newProcess(p specs.Process) *libcontainer.Process { Cwd: p.Cwd, } } + +func dupStdio(process *libcontainer.Process, rootuid int) error { + process.Stdin = os.Stdin + process.Stdout = os.Stdout + process.Stderr = os.Stderr + for _, fd := range []uintptr{ + os.Stdin.Fd(), + os.Stdout.Fd(), + os.Stderr.Fd(), + } { + if err := syscall.Fchown(int(fd), rootuid, rootuid); err != nil { + return err + } + } + return nil +} + +// If systemd is supporting sd_notify protocol, this function will add support +// for sd_notify protocol from within the container. +func setupSdNotify(spec *specs.LinuxSpec, rspec *specs.LinuxRuntimeSpec, notifySocket string) { + mountName := "sdNotify" + spec.Mounts = append(spec.Mounts, specs.MountPoint{Name: mountName, Path: notifySocket}) + spec.Process.Env = append(spec.Process.Env, fmt.Sprintf("NOTIFY_SOCKET=%s", notifySocket)) + rspec.Mounts[mountName] = specs.Mount{Type: "bind", Source: notifySocket, Options: []string{"bind"}} +} + +// If systemd is supporting on-demand socket activation, this function will add support +// for on-demand socket activation for the containerized service. +func setupSocketActivation(spec *specs.LinuxSpec, listenFds string) { + spec.Process.Env = append(spec.Process.Env, fmt.Sprintf("LISTEN_FDS=%s", listenFds), "LISTEN_PID=1") +} + +func destroy(container libcontainer.Container) { + if err := container.Destroy(); err != nil { + logrus.Error(err) + } +} + +func setupIO(process *libcontainer.Process, rootuid int, console string, createTTY, detach bool) (*tty, error) { + // detach and createTty will not work unless a console path is passed + // so error out here before changing any terminal settings + if createTTY && detach && console == "" { + return nil, fmt.Errorf("cannot allocate tty if runc will detach") + } + if createTTY { + return createTty(process, rootuid, console) + } + if detach { + if err := dupStdio(process, rootuid); err != nil { + return nil, err + } + return nil, nil + } + return createStdioPipes(process, rootuid) +} + +func createPidFile(path string, process *libcontainer.Process) error { + pid, err := process.Pid() + if err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + _, err = fmt.Fprintf(f, "%d", pid) + return err +}