func (s *btrfsMigrationSourceDriver) send(conn *websocket.Conn, btrfsPath string, btrfsParent string) error { args := []string{"send", btrfsPath} if btrfsParent != "" { args = append(args, "-p", btrfsParent) } cmd := exec.Command("btrfs", args...) stdout, err := cmd.StdoutPipe() if err != nil { return err } stderr, err := cmd.StderrPipe() if err != nil { return err } if err := cmd.Start(); err != nil { return err } <-shared.WebsocketSendStream(conn, stdout, 4*1024*1024) output, err := ioutil.ReadAll(stderr) if err != nil { shared.LogError("problem reading btrfs send stderr", log.Ctx{"err": err}) } err = cmd.Wait() if err != nil { shared.LogError("problem with btrfs send", log.Ctx{"output": string(output)}) } return err }
func (s btrfsMigrationSource) Send(conn *websocket.Conn) error { args := []string{"send", s.btrfsPath} if s.btrfsParent != "" { args = append(args, "-p", s.btrfsParent) } cmd := exec.Command("btrfs", args...) deleteAfterSending := func(path string) { s.btrfs.subvolsDelete(path) os.Remove(filepath.Dir(path)) } stdout, err := cmd.StdoutPipe() if err != nil { if s.deleteAfterSending { deleteAfterSending(s.btrfsPath) } return err } stderr, err := cmd.StderrPipe() if err != nil { if s.deleteAfterSending { deleteAfterSending(s.btrfsPath) } return err } if err := cmd.Start(); err != nil { if s.deleteAfterSending { deleteAfterSending(s.btrfsPath) } return err } <-shared.WebsocketSendStream(conn, stdout) output, err := ioutil.ReadAll(stderr) if err != nil { shared.Log.Error("problem reading btrfs send stderr", "err", err) } err = cmd.Wait() if err != nil { shared.Log.Error("problem with btrfs send", "output", string(output)) } if s.deleteAfterSending { deleteAfterSending(s.btrfsPath) } return err }
func (s *zfsMigrationSourceDriver) send(conn *websocket.Conn, zfsName string, zfsParent string, readWrapper func(io.ReadCloser) io.ReadCloser) error { fields := strings.SplitN(s.container.Name(), shared.SnapshotDelimiter, 2) args := []string{"send", fmt.Sprintf("%s/containers/%s@%s", s.zfs.zfsPool, fields[0], zfsName)} if zfsParent != "" { args = append(args, "-i", fmt.Sprintf("%s/containers/%s@%s", s.zfs.zfsPool, s.container.Name(), zfsParent)) } cmd := exec.Command("zfs", args...) stdout, err := cmd.StdoutPipe() if err != nil { return err } readPipe := io.ReadCloser(stdout) if readWrapper != nil { readPipe = readWrapper(stdout) } stderr, err := cmd.StderrPipe() if err != nil { return err } if err := cmd.Start(); err != nil { return err } <-shared.WebsocketSendStream(conn, readPipe, 4*1024*1024) output, err := ioutil.ReadAll(stderr) if err != nil { shared.LogError("problem reading zfs send stderr", log.Ctx{"err": err}) } err = cmd.Wait() if err != nil { shared.LogError("problem with zfs send", log.Ctx{"output": string(output)}) } return err }
// Exec runs a command inside the LXD container. For "interactive" use such as // `lxc exec ...`, one should pass a controlHandler that talks over the control // socket and handles things like SIGWINCH. If running non-interactive, passing // a nil controlHandler will cause Exec to return when all of the command // output is sent to the output buffers. func (c *Client) Exec(name string, cmd []string, env map[string]string, stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser, controlHandler func(*Client, *websocket.Conn)) (int, error) { body := shared.Jmap{ "command": cmd, "wait-for-websocket": true, "interactive": controlHandler != nil, "environment": env, } resp, err := c.post(fmt.Sprintf("containers/%s/exec", name), body, Async) if err != nil { return -1, err } var fds shared.Jmap op, err := resp.MetadataAsOperation() if err == nil && op.Metadata != nil { fds, err = op.Metadata.GetMap("fds") if err != nil { return -1, err } } else { // FIXME: This is a backward compatibility codepath md := execMd{} if err := json.Unmarshal(resp.Metadata, &md); err != nil { return -1, err } fds, err = shared.ParseMetadata(md.FDs) if err != nil { return -1, err } } if controlHandler != nil { var control *websocket.Conn if wsControl, ok := fds["control"]; ok { control, err = c.websocket(resp.Operation, wsControl.(string)) if err != nil { return -1, err } defer control.Close() go controlHandler(c, control) } conn, err := c.websocket(resp.Operation, fds["0"].(string)) if err != nil { return -1, err } shared.WebsocketSendStream(conn, stdin) <-shared.WebsocketRecvStream(stdout, conn) conn.Close() } else { conns := make([]*websocket.Conn, 3) dones := make([]chan bool, 3) conns[0], err = c.websocket(resp.Operation, fds[strconv.Itoa(0)].(string)) if err != nil { return -1, err } defer conns[0].Close() dones[0] = shared.WebsocketSendStream(conns[0], stdin) outputs := []io.WriteCloser{stdout, stderr} for i := 1; i < 3; i++ { conns[i], err = c.websocket(resp.Operation, fds[strconv.Itoa(i)].(string)) if err != nil { return -1, err } defer conns[i].Close() dones[i] = shared.WebsocketRecvStream(outputs[i-1], conns[i]) } /* * We'll get a read signal from each of stdout, stderr when they've * both died. We need to wait for these in addition to the operation, * because the server may indicate that the operation is done before we * can actually read the last bits of data off these sockets and print * it to the screen. * * We don't wait for stdin here, because if we're interactive, the user * may not have closed it (e.g. if the command exits but the user * didn't ^D). */ for i := 1; i < 3; i++ { <-dones[i] } // Once we're done, we explicitly close stdin, to signal the websockets // we're done. stdin.Close() } // Now, get the operation's status too. op, err = c.WaitFor(resp.Operation) if err != nil { return -1, err } if op.StatusCode == shared.Failure { return -1, fmt.Errorf(op.Err) } if op.StatusCode != shared.Success { return -1, fmt.Errorf(i18n.G("got bad op status %s"), op.Status) } if op.Metadata == nil { return -1, fmt.Errorf(i18n.G("no metadata received")) } return op.Metadata.GetInt("return") }
func (s *execWs) Do(id string) shared.OperationResult { <-s.allConnected var err error var ttys []*os.File var ptys []*os.File if s.interactive { ttys = make([]*os.File, 1) ptys = make([]*os.File, 1) ptys[0], ttys[0], err = shared.OpenPty(s.rootUid, s.rootGid) s.options.StdinFd = ttys[0].Fd() s.options.StdoutFd = ttys[0].Fd() s.options.StderrFd = ttys[0].Fd() } else { ttys = make([]*os.File, 3) ptys = make([]*os.File, 3) for i := 0; i < len(ttys); i++ { ptys[i], ttys[i], err = shared.Pipe() if err != nil { return shared.OperationError(err) } } s.options.StdinFd = ptys[0].Fd() s.options.StdoutFd = ttys[1].Fd() s.options.StderrFd = ttys[2].Fd() } controlExit := make(chan bool) var wgEOF sync.WaitGroup if s.interactive { wgEOF.Add(1) go func() { select { case <-s.controlConnected: break case <-controlExit: return } for { mt, r, err := s.conns[-1].NextReader() if mt == websocket.CloseMessage { break } if err != nil { shared.Debugf("Got error getting next reader %s", err) break } buf, err := ioutil.ReadAll(r) if err != nil { shared.Debugf("Failed to read message %s", err) break } command := shared.ContainerExecControl{} if err := json.Unmarshal(buf, &command); err != nil { shared.Debugf("Failed to unmarshal control socket command: %s", err) continue } if command.Command == "window-resize" { winchWidth, err := strconv.Atoi(command.Args["width"]) if err != nil { shared.Debugf("Unable to extract window width: %s", err) continue } winchHeight, err := strconv.Atoi(command.Args["height"]) if err != nil { shared.Debugf("Unable to extract window height: %s", err) continue } err = shared.SetSize(int(ptys[0].Fd()), winchWidth, winchHeight) if err != nil { shared.Debugf("Failed to set window size to: %dx%d", winchWidth, winchHeight) continue } } if err != nil { shared.Debugf("Got error writing to writer %s", err) break } } }() go func() { <-shared.WebsocketMirror(s.conns[0], ptys[0], ptys[0]) wgEOF.Done() }() } else { wgEOF.Add(len(ttys) - 1) for i := 0; i < len(ttys); i++ { go func(i int) { if i == 0 { <-shared.WebsocketRecvStream(ttys[i], s.conns[i]) ttys[i].Close() } else { <-shared.WebsocketSendStream(s.conns[i], ptys[i]) ptys[i].Close() wgEOF.Done() } }(i) } } result := runCommand( s.container, s.command, s.options, ) for _, tty := range ttys { tty.Close() } if s.conns[-1] == nil { if s.interactive { controlExit <- true } } else { s.conns[-1].Close() } wgEOF.Wait() for _, pty := range ptys { pty.Close() } return result }
func (s zfsMigrationSource) Send(conn *websocket.Conn) error { args := []string{"send", fmt.Sprintf("%s/%s", s.zfs.zfsPool, s.zfsName)} if s.zfsParent != "" { args = append(args, "-i", fmt.Sprintf("%s/%s", s.zfs.zfsPool, s.zfsParent)) } cmd := exec.Command("zfs", args...) stdout, err := cmd.StdoutPipe() if err != nil { /* If this is not a lxd snapshot, that means it is the root container. * The way we zfs send a root container is by taking a temporary zfs * snapshot and sending that, then deleting that snapshot. Here's where * we delete it. * * Note that we can't use a defer here, because zfsDestroy * takes some time, and defer doesn't block the current * goroutine. Due to our retry mechanism for network failures * (and because zfsDestroy takes a while), we might retry * moving (and thus creating a temporary snapshot) before the * last one is deleted, resulting in either a snapshot name * collision if it was fast enough, or an extra snapshot with * an odd name on the destination side. Instead, we don't use * defer so we always block until the snapshot is dead. */ if s.deleteAfterSending { s.zfs.zfsDestroy(s.zfsName) } return err } stderr, err := cmd.StderrPipe() if err != nil { if s.deleteAfterSending { s.zfs.zfsDestroy(s.zfsName) } return err } if err := cmd.Start(); err != nil { if s.deleteAfterSending { s.zfs.zfsDestroy(s.zfsName) } return err } <-shared.WebsocketSendStream(conn, stdout) output, err := ioutil.ReadAll(stderr) if err != nil { shared.Log.Error("problem reading zfs send stderr", "err", err) } err = cmd.Wait() if err != nil { shared.Log.Error("problem with zfs send", "output", string(output)) } if s.deleteAfterSending { s.zfs.zfsDestroy(s.zfsName) } return err }
func (c *Client) Exec(name string, cmd []string, env map[string]string, stdin *os.File, stdout *os.File, stderr *os.File) (int, error) { interactive := terminal.IsTerminal(int(stdin.Fd())) body := shared.Jmap{"command": cmd, "wait-for-websocket": true, "interactive": interactive, "environment": env} resp, err := c.post(fmt.Sprintf("containers/%s/exec", name), body, Async) if err != nil { return -1, err } md := execMd{} if err := json.Unmarshal(resp.Metadata, &md); err != nil { return -1, err } if interactive { if wsControl, ok := md.FDs["control"]; ok { go func() { control, err := c.websocket(resp.Operation, wsControl) if err != nil { return } for { width, height, err := terminal.GetSize(syscall.Stdout) if err != nil { continue } shared.Debugf("Window size is now: %dx%d", width, height) w, err := control.NextWriter(websocket.TextMessage) if err != nil { shared.Debugf("Got error getting next writer %s", err) break } msg := shared.ContainerExecControl{} msg.Command = "window-resize" msg.Args = make(map[string]string) msg.Args["width"] = strconv.Itoa(width) msg.Args["height"] = strconv.Itoa(height) buf, err := json.Marshal(msg) if err != nil { shared.Debugf("Failed to convert to json %s", err) break } _, err = w.Write(buf) w.Close() if err != nil { shared.Debugf("Got err writing %s", err) break } ch := make(chan os.Signal) signal.Notify(ch, syscall.SIGWINCH) sig := <-ch shared.Debugf("Received '%s signal', updating window geometry.", sig) } closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") control.WriteMessage(websocket.CloseMessage, closeMsg) }() } conn, err := c.websocket(resp.Operation, md.FDs["0"]) if err != nil { return -1, err } shared.WebsocketSendStream(conn, stdin) <-shared.WebsocketRecvStream(stdout, conn) } else { sources := []*os.File{stdin, stdout, stderr} conns := make([]*websocket.Conn, 3) dones := make([]chan bool, 3) for i := 0; i < 3; i++ { conns[i], err = c.websocket(resp.Operation, md.FDs[strconv.Itoa(i)]) if err != nil { return -1, err } if i == 0 { dones[i] = shared.WebsocketSendStream(conns[i], sources[i]) } else { dones[i] = shared.WebsocketRecvStream(sources[i], conns[i]) } } /* * We'll get a read signal from each of stdout, stderr when they've * both died. We need to wait for these in addition to the operation, * because the server may indicate that the operation is done before we * can actually read the last bits of data off these sockets and print * it to the screen. * * We don't wait for stdin here, because if we're interactive, the user * may not have closed it (e.g. if the command exits but the user * didn't ^D). */ for i := 1; i < 3; i++ { <-dones[i] } // Once we're done, we explicitly close stdin, to signal the websockets // we're done. sources[0].Close() } // Now, get the operation's status too. op, err := c.WaitFor(resp.Operation) if err != nil { return -1, err } if op.StatusCode == shared.Failure { return -1, op.GetError() } if op.StatusCode != shared.Success { return -1, fmt.Errorf(gettext.Gettext("got bad op status %s"), op.Status) } opMd, err := op.MetadataAsMap() if err != nil { return -1, err } return opMd.GetInt("return") }
func (s *execWs) Do(op *operation) error { <-s.allConnected var err error var ttys []*os.File var ptys []*os.File var stdin *os.File var stdout *os.File var stderr *os.File if s.interactive { ttys = make([]*os.File, 1) ptys = make([]*os.File, 1) ptys[0], ttys[0], err = shared.OpenPty(s.rootUid, s.rootGid) stdin = ttys[0] stdout = ttys[0] stderr = ttys[0] if s.width > 0 && s.height > 0 { shared.SetSize(int(ptys[0].Fd()), s.width, s.height) } } else { ttys = make([]*os.File, 3) ptys = make([]*os.File, 3) for i := 0; i < len(ttys); i++ { ptys[i], ttys[i], err = shared.Pipe() if err != nil { return err } } stdin = ptys[0] stdout = ttys[1] stderr = ttys[2] } controlExit := make(chan bool) var wgEOF sync.WaitGroup if s.interactive { wgEOF.Add(1) go func() { select { case <-s.controlConnected: break case <-controlExit: return } for { mt, r, err := s.conns[-1].NextReader() if mt == websocket.CloseMessage { break } if err != nil { shared.Debugf("Got error getting next reader %s", err) break } buf, err := ioutil.ReadAll(r) if err != nil { shared.Debugf("Failed to read message %s", err) break } command := shared.ContainerExecControl{} if err := json.Unmarshal(buf, &command); err != nil { shared.Debugf("Failed to unmarshal control socket command: %s", err) continue } if command.Command == "window-resize" { winchWidth, err := strconv.Atoi(command.Args["width"]) if err != nil { shared.Debugf("Unable to extract window width: %s", err) continue } winchHeight, err := strconv.Atoi(command.Args["height"]) if err != nil { shared.Debugf("Unable to extract window height: %s", err) continue } err = shared.SetSize(int(ptys[0].Fd()), winchWidth, winchHeight) if err != nil { shared.Debugf("Failed to set window size to: %dx%d", winchWidth, winchHeight) continue } } } }() go func() { readDone, writeDone := shared.WebsocketMirror(s.conns[0], ptys[0], ptys[0]) <-readDone <-writeDone s.conns[0].Close() wgEOF.Done() }() } else { wgEOF.Add(len(ttys) - 1) for i := 0; i < len(ttys); i++ { go func(i int) { if i == 0 { <-shared.WebsocketRecvStream(ttys[i], s.conns[i]) ttys[i].Close() } else { <-shared.WebsocketSendStream(s.conns[i], ptys[i]) ptys[i].Close() wgEOF.Done() } }(i) } } cmdResult, cmdErr := s.container.Exec(s.command, s.env, stdin, stdout, stderr) for _, tty := range ttys { tty.Close() } if s.conns[-1] == nil { if s.interactive { controlExit <- true } } else { s.conns[-1].Close() } wgEOF.Wait() for _, pty := range ptys { pty.Close() } metadata := shared.Jmap{"return": cmdResult} err = op.UpdateMetadata(metadata) if err != nil { return err } return cmdErr }
func (s *execWs) Do(op *operation) error { <-s.allConnected var err error var ttys []*os.File var ptys []*os.File var stdin *os.File var stdout *os.File var stderr *os.File if s.interactive { ttys = make([]*os.File, 1) ptys = make([]*os.File, 1) ptys[0], ttys[0], err = shared.OpenPty(s.rootUid, s.rootGid) stdin = ttys[0] stdout = ttys[0] stderr = ttys[0] if s.width > 0 && s.height > 0 { shared.SetSize(int(ptys[0].Fd()), s.width, s.height) } } else { ttys = make([]*os.File, 3) ptys = make([]*os.File, 3) for i := 0; i < len(ttys); i++ { ptys[i], ttys[i], err = shared.Pipe() if err != nil { return err } } stdin = ptys[0] stdout = ttys[1] stderr = ttys[2] } controlExit := make(chan bool) receivePid := make(chan int) var wgEOF sync.WaitGroup if s.interactive { wgEOF.Add(1) go func() { receivedPid := <-receivePid select { case <-s.controlConnected: break case <-controlExit: return } for { mt, r, err := s.conns[-1].NextReader() if mt == websocket.CloseMessage { break } if err != nil { shared.LogDebugf("Got error getting next reader %s", err) break } buf, err := ioutil.ReadAll(r) if err != nil { shared.LogDebugf("Failed to read message %s", err) break } command := shared.ContainerExecControl{} if err := json.Unmarshal(buf, &command); err != nil { shared.LogDebugf("Failed to unmarshal control socket command: %s", err) continue } if command.Command == "window-resize" { winchWidth, err := strconv.Atoi(command.Args["width"]) if err != nil { shared.LogDebugf("Unable to extract window width: %s", err) continue } winchHeight, err := strconv.Atoi(command.Args["height"]) if err != nil { shared.LogDebugf("Unable to extract window height: %s", err) continue } err = shared.SetSize(int(ptys[0].Fd()), winchWidth, winchHeight) if err != nil { shared.LogDebugf("Failed to set window size to: %dx%d", winchWidth, winchHeight) continue } } else if command.Command == "signal" { if err := syscall.Kill(receivedPid, command.Signal); err != nil { shared.LogDebugf("Failed forwarding signal '%s' to PID %d.", command.Signal, receivedPid) continue } shared.LogDebugf("Forwarded signal '%s' to PID %d.", command.Signal, receivedPid) } } }() go func() { readDone, writeDone := shared.WebsocketMirror(s.conns[0], ptys[0], ptys[0]) <-readDone <-writeDone s.conns[0].Close() wgEOF.Done() }() } else { wgEOF.Add(len(ttys) - 1) for i := 0; i < len(ttys); i++ { go func(i int) { if i == 0 { <-shared.WebsocketRecvStream(ttys[i], s.conns[i]) ttys[i].Close() } else { <-shared.WebsocketSendStream(s.conns[i], ptys[i], -1) ptys[i].Close() wgEOF.Done() } }(i) } } finisher := func(cmdResult int, cmdErr error) error { for _, tty := range ttys { tty.Close() } if s.conns[-1] == nil { if s.interactive { controlExit <- true } } else { s.conns[-1].Close() } wgEOF.Wait() for _, pty := range ptys { pty.Close() } metadata := shared.Jmap{"return": cmdResult} err = op.UpdateMetadata(metadata) if err != nil { return err } return cmdErr } r, w, err := shared.Pipe() defer r.Close() if err != nil { shared.LogErrorf("s", err) return err } cmd, err := s.container.ExecNoWait(s.command, s.env, stdin, stdout, stderr, w) if err != nil { w.Close() return err } err = cmd.Start() if err != nil { w.Close() return err } w.Close() attachedPid := -1 if err := json.NewDecoder(r).Decode(&attachedPid); err != nil { shared.LogErrorf("Failed to retrieve PID of executing child process: %s", err) return finisher(-1, err) } if s.interactive { receivePid <- attachedPid } err = cmd.Wait() if err != nil { exitErr, ok := err.(*exec.ExitError) if ok { status, ok := exitErr.Sys().(syscall.WaitStatus) if ok { return finisher(status.ExitStatus(), nil) } } } return finisher(0, nil) }
func (s *execWs) Do(op *operation) error { <-s.allConnected var err error var ttys []*os.File var ptys []*os.File var stdin *os.File var stdout *os.File var stderr *os.File if s.interactive { ttys = make([]*os.File, 1) ptys = make([]*os.File, 1) ptys[0], ttys[0], err = shared.OpenPty(s.rootUid, s.rootGid) stdin = ttys[0] stdout = ttys[0] stderr = ttys[0] if s.width > 0 && s.height > 0 { shared.SetSize(int(ptys[0].Fd()), s.width, s.height) } } else { ttys = make([]*os.File, 3) ptys = make([]*os.File, 3) for i := 0; i < len(ttys); i++ { ptys[i], ttys[i], err = shared.Pipe() if err != nil { return err } } stdin = ptys[0] stdout = ttys[1] stderr = ttys[2] } controlExit := make(chan bool) receivePid := make(chan int) var wgEOF sync.WaitGroup if s.interactive { wgEOF.Add(1) go func() { receivedPid := <-receivePid select { case <-s.controlConnected: break case <-controlExit: return } for { mt, r, err := s.conns[-1].NextReader() if mt == websocket.CloseMessage { break } if err != nil { shared.LogDebugf("Got error getting next reader %s", err) break } buf, err := ioutil.ReadAll(r) if err != nil { shared.LogDebugf("Failed to read message %s", err) break } command := shared.ContainerExecControl{} if err := json.Unmarshal(buf, &command); err != nil { shared.LogDebugf("Failed to unmarshal control socket command: %s", err) continue } if command.Command == "window-resize" { winchWidth, err := strconv.Atoi(command.Args["width"]) if err != nil { shared.LogDebugf("Unable to extract window width: %s", err) continue } winchHeight, err := strconv.Atoi(command.Args["height"]) if err != nil { shared.LogDebugf("Unable to extract window height: %s", err) continue } err = shared.SetSize(int(ptys[0].Fd()), winchWidth, winchHeight) if err != nil { shared.LogDebugf("Failed to set window size to: %dx%d", winchWidth, winchHeight) continue } } else if command.Command == "signal" { if err := syscall.Kill(receivedPid, command.Signal); err != nil { shared.LogDebugf("Failed forwarding signal '%s' to PID %d.", command.Signal, receivedPid) continue } shared.LogDebugf("Forwarded signal '%s' to PID %d.", command.Signal, receivedPid) } } }() go func() { readDone, writeDone := shared.WebsocketMirror(s.conns[0], ptys[0], ptys[0]) <-readDone <-writeDone s.conns[0].Close() wgEOF.Done() }() } else { wgEOF.Add(len(ttys) - 1) for i := 0; i < len(ttys); i++ { go func(i int) { if i == 0 { <-shared.WebsocketRecvStream(ttys[i], s.conns[i]) ttys[i].Close() } else { <-shared.WebsocketSendStream(s.conns[i], ptys[i], -1) ptys[i].Close() wgEOF.Done() } }(i) } } finisher := func(cmdResult int, cmdErr error) error { for _, tty := range ttys { tty.Close() } if s.conns[-1] == nil { if s.interactive { controlExit <- true } } else { s.conns[-1].Close() } wgEOF.Wait() for _, pty := range ptys { pty.Close() } metadata := shared.Jmap{"return": cmdResult} err = op.UpdateMetadata(metadata) if err != nil { return err } return cmdErr } pid, attachedPid, err := s.container.Exec(s.command, s.env, stdin, stdout, stderr, false) if err != nil { return err } if s.interactive { receivePid <- attachedPid } proc, err := os.FindProcess(pid) if err != nil { return finisher(-1, fmt.Errorf("Failed finding process: %q", err)) } procState, err := proc.Wait() if err != nil { return finisher(-1, fmt.Errorf("Failed waiting on process %d: %q", pid, err)) } if procState.Success() { return finisher(0, nil) } status, ok := procState.Sys().(syscall.WaitStatus) if ok { if status.Exited() { return finisher(status.ExitStatus(), nil) } // Backwards compatible behavior. Report success when we exited // due to a signal. Otherwise this may break Jenkins, e.g. when // lxc exec foo reboot receives SIGTERM and status.Exitstats() // would report -1. if status.Signaled() { return finisher(0, nil) } } return finisher(-1, nil) }