168 lines
4.4 KiB
Go
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
|
|
}
|