github.com/projecteru2/core@v0.0.0-20240321043226-06bcc1c23f58/cluster/calcium/stream.go (about)

     1  package calcium
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"encoding/json"
     7  	"io"
     8  	"sync"
     9  
    10  	"github.com/cockroachdb/errors"
    11  	"github.com/projecteru2/core/engine"
    12  	enginetypes "github.com/projecteru2/core/engine/types"
    13  	"github.com/projecteru2/core/log"
    14  	"github.com/projecteru2/core/types"
    15  	"github.com/projecteru2/core/utils"
    16  )
    17  
    18  var winchCommand = []byte{0x80}  // 128, non-ASCII
    19  var escapeCommand = []byte{0x1d} // 29, ^]
    20  
    21  type window struct {
    22  	Height uint `json:"Row"`
    23  	Width  uint `json:"Col"`
    24  }
    25  
    26  func (c *Calcium) execuateInside(ctx context.Context, client engine.API, ID, cmd, user string, env []string, privileged bool) ([]byte, error) {
    27  	cmds := utils.MakeCommandLineArgs(cmd)
    28  	execConfig := &enginetypes.ExecConfig{
    29  		User:         user,
    30  		Cmd:          cmds,
    31  		Privileged:   privileged,
    32  		Env:          env,
    33  		AttachStderr: true,
    34  		AttachStdout: true,
    35  	}
    36  	b := []byte{}
    37  	execID, stdout, stderr, _, err := client.Execute(ctx, ID, execConfig)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  
    42  	for m := range c.processStdStream(ctx, stdout, stderr, bufio.ScanLines, byte('\n')) {
    43  		b = append(b, m.Data...)
    44  	}
    45  
    46  	exitCode, err := client.ExecExitCode(ctx, ID, execID)
    47  	if err != nil {
    48  		return b, err
    49  	}
    50  	if exitCode != 0 {
    51  		return b, errors.New(string(b))
    52  	}
    53  	return b, nil
    54  }
    55  
    56  func (c *Calcium) processVirtualizationInStream(
    57  	ctx context.Context,
    58  	inStream io.WriteCloser,
    59  	inCh <-chan []byte,
    60  	resizeFunc func(height, width uint) error,
    61  ) <-chan struct{} { //nolint
    62  	logger := log.WithFunc("calcium.processVirtualizationInStream")
    63  	specialPrefixCallback := map[string]func([]byte){
    64  		string(winchCommand): func(body []byte) {
    65  			w := &window{}
    66  			if err := json.Unmarshal(body, w); err != nil {
    67  				logger.Errorf(ctx, err, "invalid winch command: %q", body)
    68  				return
    69  			}
    70  			if err := resizeFunc(w.Height, w.Width); err != nil {
    71  				logger.Error(ctx, err, "resize window error")
    72  				return
    73  			}
    74  		},
    75  
    76  		string(escapeCommand): func(_ []byte) {
    77  			inStream.Close()
    78  		},
    79  	}
    80  	return c.rawProcessVirtualizationInStream(ctx, inStream, inCh, specialPrefixCallback)
    81  }
    82  
    83  func (c *Calcium) rawProcessVirtualizationInStream(
    84  	ctx context.Context,
    85  	inStream io.WriteCloser,
    86  	inCh <-chan []byte,
    87  	specialPrefixCallback map[string]func([]byte),
    88  ) <-chan struct{} {
    89  	done := make(chan struct{})
    90  	_ = c.pool.Invoke(func() {
    91  		defer close(done)
    92  		defer inStream.Close()
    93  
    94  		for cmd := range inCh {
    95  			if len(cmd) == 0 {
    96  				continue
    97  			}
    98  			if f, ok := specialPrefixCallback[string(cmd[:1])]; ok {
    99  				f(cmd[1:])
   100  				continue
   101  			}
   102  			if _, err := inStream.Write(cmd); err != nil {
   103  				log.WithFunc("calcium.rawProcessVirtualizationInStream").Error(ctx, err, "failed to write virtual input stream")
   104  				continue
   105  			}
   106  		}
   107  	})
   108  
   109  	return done
   110  }
   111  
   112  func (c *Calcium) processVirtualizationOutStream(
   113  	ctx context.Context,
   114  	outStream io.ReadCloser,
   115  	splitFunc bufio.SplitFunc,
   116  	split byte,
   117  
   118  ) <-chan []byte {
   119  	outCh := make(chan []byte)
   120  	_ = c.pool.Invoke(func() {
   121  		defer close(outCh)
   122  		if outStream == nil {
   123  			return
   124  		}
   125  		defer outStream.Close()
   126  		scanner := bufio.NewScanner(outStream)
   127  		scanner.Split(splitFunc)
   128  		for scanner.Scan() {
   129  			bs := scanner.Bytes()
   130  			if split != 0 {
   131  				bs = append(bs, split)
   132  			}
   133  			outCh <- bs
   134  		}
   135  		if err := scanner.Err(); err != nil {
   136  			log.WithFunc("calcium.processVirtualizationOutStream").Warnf(ctx, "failed to read output from output stream: %+v", err)
   137  		}
   138  	})
   139  	return outCh
   140  }
   141  
   142  func (c *Calcium) processBuildImageStream(ctx context.Context, reader io.ReadCloser) chan *types.BuildImageMessage {
   143  	ch := make(chan *types.BuildImageMessage)
   144  	_ = c.pool.Invoke(func() {
   145  		defer close(ch)
   146  		defer utils.EnsureReaderClosed(ctx, reader)
   147  		decoder := json.NewDecoder(reader)
   148  		for {
   149  			message := &types.BuildImageMessage{}
   150  			err := decoder.Decode(message)
   151  			if err != nil {
   152  				if err != io.EOF {
   153  					malformed, _ := io.ReadAll(decoder.Buffered()) // TODO err check
   154  					log.WithFunc("calcium.processBuildImageStream").Errorf(ctx, err, "decode image message failed, buffered: %s", string(malformed))
   155  					message.Error = err.Error()
   156  					ch <- message
   157  				}
   158  				break
   159  			}
   160  			ch <- message
   161  		}
   162  	})
   163  	return ch
   164  }
   165  
   166  func (c *Calcium) processStdStream(ctx context.Context, stdout, stderr io.ReadCloser, splitFunc bufio.SplitFunc, split byte) chan types.StdStreamMessage {
   167  	ch := make(chan types.StdStreamMessage)
   168  
   169  	wg := sync.WaitGroup{}
   170  
   171  	wg.Add(1)
   172  	_ = c.pool.Invoke(func() {
   173  		defer wg.Done()
   174  		for data := range c.processVirtualizationOutStream(ctx, stdout, splitFunc, split) {
   175  			ch <- types.StdStreamMessage{Data: data, StdStreamType: types.Stdout}
   176  		}
   177  	})
   178  
   179  	wg.Add(1)
   180  	_ = c.pool.Invoke(func() {
   181  		defer wg.Done()
   182  		for data := range c.processVirtualizationOutStream(ctx, stderr, splitFunc, split) {
   183  			ch <- types.StdStreamMessage{Data: data, StdStreamType: types.Stderr}
   184  		}
   185  	})
   186  
   187  	_ = c.pool.Invoke(func() {
   188  		defer close(ch)
   189  		wg.Wait()
   190  	})
   191  
   192  	return ch
   193  }