buildr/modules/plugin/exec_context.go
Peter 34c431790e
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
refactor: use connect-go instead of regular Google gRPC
- support binary name for plugins
- register plugins for container jobs
2023-09-12 18:43:34 +02:00

168 lines
4.4 KiB
Go

package plugin
import (
"bytes"
"context"
"errors"
"log/slog"
"os/exec"
"reflect"
"time"
"github.com/tetratelabs/wazero/api"
bapi "code.icb4dc0.de/buildr/api"
remotev1 "code.icb4dc0.de/buildr/api/generated/remote/v1"
wasiv1 "code.icb4dc0.de/buildr/api/generated/wasi/v1"
"code.icb4dc0.de/buildr/buildr/internal/sh"
"code.icb4dc0.de/buildr/buildr/modules"
)
const defaultIntegerSize = 32
func NewHostFuncExport[TRequest bapi.ProtoMessage, TResponse bapi.ProtoMessage](f func(ctx context.Context, req TRequest) (TResponse, error)) HostFuncExportWrapper[TRequest, TResponse] {
return f
}
type HostFuncExportWrapper[TRequest bapi.ProtoMessage, TResponse bapi.ProtoMessage] func(ctx context.Context, req TRequest) (TResponse, error)
func (w HostFuncExportWrapper[TRequest, TResponse]) Call(ctx context.Context, m api.Module, offset, byteCount uint32) uint64 {
buf, ok := m.Memory().Read(offset, byteCount)
if !ok {
return 0
}
var req TRequest
if reflect.TypeOf(req).Kind() == reflect.Ptr {
req = reflect.New(reflect.TypeOf(req).Elem()).Interface().(TRequest)
}
if err := req.UnmarshalVT(buf); err != nil {
panic(err)
}
resp, err := w(ctx, req)
if err != nil {
panic(err)
}
data, err := resp.MarshalVT()
if err != nil {
panic(err)
}
ptr, err := w.allocate(ctx, m, uint64(len(data)))
if err != nil {
panic(err)
}
if !m.Memory().Write(uint32(ptr), data) {
panic("failed to write message to memory")
}
return (ptr << uint64(defaultIntegerSize)) | uint64(len(data))
}
func (w HostFuncExportWrapper[TRequest, TResponse]) allocate(ctx context.Context, m api.Module, size uint64) (ptr uint64, err error) {
results, err := m.ExportedFunction("allocate").Call(ctx, size)
if err != nil {
return 0, err
}
return results[0], nil
}
type wasiPluginExecContext struct {
ctx modules.ExecutionContext
}
func (c *wasiPluginExecContext) lookPath(_ context.Context, lookupPathReq *wasiv1.LookupPathRequest) (*wasiv1.LookupPathResponse, error) {
if lookupPathReq.Command == "" {
return nil, errors.New("cannot lookup empty command")
}
foundPath, err := exec.LookPath(lookupPathReq.Command)
lookupPathResp := &wasiv1.LookupPathResponse{
Path: foundPath,
}
if err != nil {
lookupPathResp.Error = err.Error()
}
return lookupPathResp, nil
}
func (c *wasiPluginExecContext) exec(_ context.Context, execReq *wasiv1.ProcessStartRequest) (*wasiv1.ProcessStartResponse, error) {
execResp := new(wasiv1.ProcessStartResponse)
//nolint:contextcheck // that ain't a context.Context
cmd, err := sh.PrepareCommand(c.ctx, execReq.Command, execReq.Args...)
if err != nil {
c.ctx.Logger().Error("failed to prepare command", slog.String("err", err.Error()))
return nil, err
}
cmd.AddEnv(execReq.Environment)
if len(execReq.Stdin) > 0 {
cmd.SetStdIn(bytes.NewReader(execReq.Stdin))
}
if len(execReq.WorkingDirectory) > 0 {
cmd.SetWorkingDir(execReq.WorkingDirectory)
}
if err := cmd.Run(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
execResp.ExitCode = int32(exitErr.ExitCode())
execResp.Stderr = exitErr.Stderr
} else {
execResp.ExitCode = 1
}
execResp.Error = err.Error()
}
return execResp, nil
}
func (c *wasiPluginExecContext) log(ctx context.Context, taskLog *remotev1.TaskLog) (*remotev1.Result, error) {
rec := slog.NewRecord(time.UnixMicro(taskLog.Time), slog.Level(taskLog.Level), taskLog.Message, 0)
for i := range taskLog.Attributes {
attr := taskLog.Attributes[i]
rec.AddAttrs(slog.String(attr.Key, attr.Value))
}
if err := c.ctx.Logger().Handler().Handle(ctx, rec); err != nil {
return &remotev1.Result{Error: err.Error()}, nil
}
return new(remotev1.Result), nil
}
func (c *wasiPluginExecContext) getState(ctx context.Context, getStateReq *remotev1.GetStateRequest) (*remotev1.GetStateResponse, error) {
resp := new(remotev1.GetStateResponse)
val, _, err := c.ctx.GetState(ctx, string(getStateReq.Key))
if err != nil {
c.ctx.Logger().Error("failed to get state", slog.String("err", err.Error()))
return nil, err
}
resp.Data = val
return resp, nil
}
func (c *wasiPluginExecContext) setState(ctx context.Context, setState *remotev1.SetState) (*remotev1.Result, error) {
var resp remotev1.Result
if err := c.ctx.SetState(ctx, string(setState.Key), setState.Data); err != nil {
c.ctx.Logger().Error("failed to set state", slog.String("err", err.Error()))
resp.Error = err.Error()
}
return &resp, nil
}