2022-12-08 13:05:19 -03:00

211 lines
4.0 KiB
Go

// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.
package lift
// Lift can execute commands in a namespace context and return the stdin/out/err FDs
// to the caller. This allows us to lift commands into ring1, akin to `docker exec`.
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"os"
"os/exec"
"time"
"github.com/gitpod-io/gitpod/common-go/log"
"golang.org/x/sys/unix"
"golang.org/x/xerrors"
)
const (
DefaultSocketPath = "/tmp/workspacekit-lift.socket"
)
type LiftRequest struct {
Command []string `json:"command"`
}
func ServeLift(ctx context.Context, socket string) error {
skt, err := net.Listen("unix", socket)
if err != nil {
return err
}
defer func() {
err := skt.Close()
if err != nil {
log.WithError(err).Error("unexpected error closing listener")
}
}()
for {
select {
case <-ctx.Done():
break
default:
// pass through
}
conn, err := skt.Accept()
if err != nil {
log.WithError(err).Error("cannot accept lift connection")
continue
}
go func() {
err := serveLiftClient(conn)
if err != nil {
log.WithError(err).Error("cannot serve lift connection")
}
}()
}
}
func serveLiftClient(conn net.Conn) error {
unixConn := conn.(*net.UnixConn)
f, err := unixConn.File()
if err != nil {
return err
}
defer func() {
err := f.Close()
if err != nil {
log.WithError(err).Error("unexpected error closing connection")
}
err = conn.Close()
if err != nil {
log.WithError(err).Error("unexpected error closing connection")
}
}()
buf := make([]byte, unix.CmsgSpace(3*4)) // we expect 3 FDs
_, _, _, _, err = unix.Recvmsg(int(f.Fd()), nil, buf, 0)
if err != nil {
return err
}
msgs, err := unix.ParseSocketControlMessage(buf)
if err != nil {
return err
}
if len(msgs) != 1 {
return xerrors.Errorf("expected a single socket control message")
}
fds, err := unix.ParseUnixRights(&msgs[0])
if err != nil {
return err
}
if len(fds) != 3 {
return xerrors.Errorf("expected three file descriptors")
}
rd := bufio.NewReader(f)
line, err := rd.ReadBytes('\n')
if err != nil {
return err
}
var msg LiftRequest
err = json.Unmarshal(line, &msg)
if err != nil {
return err
}
if len(msg.Command) == 0 {
return xerrors.Errorf("expected non-empty command")
}
log.WithField("command", msg.Command).Info("running lifted command")
cmd := exec.Command(msg.Command[0], msg.Command[1:]...)
cmd.SysProcAttr = &unix.SysProcAttr{
Setpgid: true,
}
cmd.Stdout = os.NewFile(uintptr(fds[0]), "stdout")
cmd.Stderr = os.NewFile(uintptr(fds[1]), "stderr")
cmd.Stdin = os.NewFile(uintptr(fds[2]), "stdin")
err = cmd.Start()
if err != nil {
return err
}
defer func() {
err := cmd.Process.Kill()
if err != nil && err.Error() != "os: process already finished" {
log.WithError(err).Error("unexpected error terminating process")
}
}()
err = cmd.Wait()
if err != nil {
log.WithError(err).Error("unexpected error running process")
}
return nil
}
func RunCommand(socket string, command []string) error {
rconn, err := net.Dial("unix", socket)
if err != nil {
return err
}
conn := rconn.(*net.UnixConn)
f, err := conn.File()
if err != nil {
return err
}
defer func() {
err := f.Close()
if err != nil {
log.WithError(err).Error("unexpected error closing lift connection")
}
}()
err = unix.Sendmsg(int(f.Fd()), nil, unix.UnixRights(int(os.Stdout.Fd()), int(os.Stderr.Fd()), int(os.Stdin.Fd())), nil, 0)
if err != nil {
return err
}
msg, err := json.Marshal(LiftRequest{Command: command})
if err != nil {
return err
}
_, err = conn.Write(msg)
if err != nil {
return err
}
_, err = conn.Write([]byte{'\n'})
if err != nil {
return err
}
buf := make([]byte, 128)
for n, err := conn.Read(buf); err == nil; n, err = conn.Read(buf) {
if bytes.Equal([]byte("done"), buf[:n]) {
break
}
time.Sleep(10 * time.Millisecond)
}
fmt.Println("done")
return nil
}