~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
191 lines · 4.4 KB Raw History
package embedded

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync/atomic"
	"time"

	policyv1alpha1 "github.com/johnandersen777/socialweb-computer-kcp/api/policy/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/sandbox"
)

const (
	Kind = policyv1alpha1.PolicySandboxProviderEmbedded

	DefaultTimeout = 2 * time.Minute

	OutputLimit = 64 << 10

	Grace = 5 * time.Second
)

var (
	_ sandbox.Runner = (*Runner)(nil)
	_ sandbox.Prober = (*Runner)(nil)
)

var ErrNoHost = errors.New("the app provided no policy sandbox (pkg/embedded Config.PolicySandbox), so every policy decision is a Deny")

type Host interface {
	Run(requestJSON []byte) ([]byte, error)

	Kill(runID string)

	PlantProbe(marker string) ([]byte, error)

	ClearProbe(marker string) error
}

type Request struct {
	ID string `json:"id"`

	Source string `json:"source"`

	Env map[string]string `json:"env"`

	TimeoutMs int64 `json:"timeoutMs"`
}

type Answer struct {
	ExitCode int `json:"exitCode"`

	Stdout string `json:"stdout"`

	Stderr string `json:"stderr"`

	TimedOut bool `json:"timedOut"`
}

type Runner struct {
	host Host

	timeout time.Duration

	slot chan struct{}

	next atomic.Uint64

	fenced atomic.Bool
}

func Open(_ context.Context, host Host, spec *policyv1alpha1.PolicySandboxConfig) (sandbox.Runner, error) {
	if name := spec.ProviderName(); name != Kind {
		return nil, fmt.Errorf("sandbox/embedded: this host runs only the %s policy sandbox, not %s", Kind, name)
	}
	timeout := DefaultTimeout
	if spec.Embedded != nil && spec.Embedded.TimeoutSeconds != nil && *spec.Embedded.TimeoutSeconds > 0 {
		timeout = time.Duration(*spec.Embedded.TimeoutSeconds) * time.Second
	}
	return New(host, timeout), nil
}

func New(host Host, timeout time.Duration) *Runner {
	if timeout <= 0 {
		timeout = DefaultTimeout
	}
	return &Runner{host: host, timeout: timeout, slot: make(chan struct{}, 1)}
}

func (r *Runner) Kind() string { return Kind }

func (r *Runner) Available() bool { return r.host != nil }

func (r *Runner) Fenced() bool { return r.host != nil && r.fenced.Load() }

func (r *Runner) Run(ctx context.Context, p sandbox.Program) (sandbox.Result, error) {
	if r.host == nil {
		return sandbox.Result{ExitCode: -1}, ErrNoHost
	}
	if p.Dir != "" {
		return sandbox.Result{}, errors.New("sandbox/embedded: the embedded sandbox has no filesystem (Program.Dir)")
	}
	if !p.Net {
		return sandbox.Result{}, errors.New("sandbox/embedded: the embedded sandbox cannot deny the network (Program.Net=false)")
	}
	if strings.TrimSpace(p.Source) == "" {
		return sandbox.Result{}, errors.New("sandbox: the program is empty")
	}
	answer, hung, err := r.run(ctx, p)
	if err != nil || hung {
		return sandbox.Result{ExitCode: -1, TimedOut: hung}, err
	}
	return sandbox.Result{ExitCode: answer.ExitCode, TimedOut: answer.TimedOut, Stdout: capped(answer.Stdout), Stderr: capped(answer.Stderr)}, nil
}

func (r *Runner) run(ctx context.Context, p sandbox.Program) (Answer, bool, error) {
	timeout := p.Timeout
	if timeout <= 0 {
		timeout = r.timeout
	}
	select {
	case r.slot <- struct{}{}:
	case <-ctx.Done():
		return Answer{}, false, ctx.Err()
	}
	id := fmt.Sprintf("r%d", r.next.Add(1))
	body, err := json.Marshal(Request{ID: id, Source: p.Source, Env: p.Env, TimeoutMs: timeout.Milliseconds()})
	if err != nil {
		<-r.slot
		return Answer{}, false, err
	}
	runCtx, cancel := context.WithTimeout(ctx, timeout+Grace)
	defer cancel()
	done := call(func() ([]byte, error) { return r.host.Run(body) })
	select {
	case out := <-done:
		<-r.slot
		if out.err != nil {
			return Answer{}, false, fmt.Errorf("sandbox/embedded: run: %w", out.err)
		}
		var answer Answer
		if err := json.Unmarshal(out.raw, &answer); err != nil {
			return Answer{}, false, fmt.Errorf("sandbox/embedded: run answer: %w", err)
		}
		return answer, false, nil
	case <-runCtx.Done():
		hung := ctx.Err() == nil
		r.host.Kill(id)
		select {
		case <-done:
			<-r.slot
		case <-time.After(Grace):
			hung = true
			go func() {
				<-done
				<-r.slot
			}()
		}
		if hung {
			r.fenced.Store(false)
		}
		if ctx.Err() != nil {
			return Answer{}, false, ctx.Err()
		}
		return Answer{}, true, nil
	}
}

type hostAnswer struct {
	raw []byte
	err error
}

func call(fn func() ([]byte, error)) <-chan hostAnswer {
	done := make(chan hostAnswer, 1)
	go func() {
		raw, err := fn()
		done <- hostAnswer{raw: raw, err: err}
	}()
	return done
}

func capped(s string) string {
	if len(s) > OutputLimit {
		return s[:OutputLimit]
	}
	return s
}