Ctrl K
ComunidadGuías…

bldr/util/gocompiler/goscript-worker.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package gocompiler

import (
	"bytes"
	"context"
	"io"
	"os"
	"os/exec"
	"strings"

	"github.com/aperturerobotics/fastjson"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// goScriptWorkerEnv marks a process that ExecGoScriptCompile started to run
// one GoScript compile.
const goScriptWorkerEnv = "BLDR_GOSCRIPT_COMPILE_WORKER"

// init turns a process started with goScriptWorkerEnv into the compile
// worker before main runs. The worker reads its request as JSON from stdin,
// logs to stderr, writes a failure to stdout and exits.
func init() {
	if os.Getenv(goScriptWorkerEnv) != "" {
		os.Exit(serveGoScriptWorker())
	}
}

// runGoScriptWorker runs one GoScript compile in a child process of the
// current executable and returns its failure.
func runGoScriptWorker(ctx context.Context, le *logrus.Entry, opts GoScriptCompileOptions) error {
	// Start the current executable as a worker with the options' environment.
	exe, err := os.Executable()
	if err != nil {
		return errors.Wrap(err, "find the executable for the goscript worker")
	}
	cmd := exec.CommandContext(ctx, exe)
	cmd.Env = append(os.Environ(), opts.Env...)
	cmd.Env = append(cmd.Env, goScriptWorkerEnv+"=1")

	// Send the request, forward the worker's logs, and keep its failure.
	cmd.Stdin = bytes.NewReader(marshalGoScriptWorkerRequest(opts, le.Logger.GetLevel()))
	cmd.Stderr = le.Logger.Out
	var failure bytes.Buffer
	cmd.Stdout = &failure

	// Report the worker's own failure before its exit status.
	if err := cmd.Run(); err != nil {
		if msg := strings.TrimSpace(failure.String()); msg != "" {
			return errors.New(msg)
		}
		return errors.Wrap(err, "run the goscript worker")
	}
	return nil
}

// serveGoScriptWorker compiles the request read from stdin and returns the
// process exit code.
func serveGoScriptWorker() int {
	// Keep the marker from the compiler's own child processes.
	_ = os.Unsetenv(goScriptWorkerEnv)

	// Compile the request, reporting a failure on stdout for the parent.
	if err := compileGoScriptRequest(os.Stdin); err != nil {
		_, _ = os.Stdout.WriteString(err.Error())
		return 1
	}
	return 0
}

// compileGoScriptRequest reads a worker request from r and compiles it,
// logging to stderr.
func compileGoScriptRequest(r io.Reader) error {
	// Read and decode the request.
	data, err := io.ReadAll(r)
	if err != nil {
		return errors.Wrap(err, "read the goscript worker request")
	}
	opts, level, err := parseGoScriptWorkerRequest(data)
	if err != nil {
		return err
	}

	// Compile at the parent's log level.
	log := logrus.New()
	log.SetLevel(level)
	return compileGoScript(context.Background(), logrus.NewEntry(log), opts)
}

// marshalGoScriptWorkerRequest encodes the compile options and log level for
// the worker.
func marshalGoScriptWorkerRequest(opts GoScriptCompileOptions, level logrus.Level) []byte {
	// Encode the log level and the string options.
	var a fastjson.Arena
	o := a.NewObject()
	o.Set("level", a.NewString(level.String()))
	o.Set("workDir", a.NewString(opts.WorkDir))
	o.Set("outputPath", a.NewString(opts.OutputPath))
	o.Set("cacheRoot", a.NewString(opts.CacheRoot))

	// Encode the boolean options.
	o.Set("protobufTypeScriptBinding", a.NewFalse())
	if opts.ProtobufTypeScriptBinding {
		o.Set("protobufTypeScriptBinding", a.NewTrue())
	}
	o.Set("eliminateDeadCode", a.NewFalse())
	if opts.EliminateDeadCode {
		o.Set("eliminateDeadCode", a.NewTrue())
	}

	// Encode the list options.
	o.Set("packages", newStringArray(&a, opts.Packages))
	o.Set("buildFlags", newStringArray(&a, opts.BuildFlags))
	o.Set("overrideDirs", newStringArray(&a, opts.OverrideDirs))
	o.Set("bindingRoots", newStringArray(&a, opts.BindingRoots))
	o.Set("deferredFunctions", newStringArray(&a, opts.DeferredFunctions))
	return o.MarshalTo(nil)
}

// parseGoScriptWorkerRequest decodes a request from
// marshalGoScriptWorkerRequest. The worker's environment already holds the
// options' Env.
func parseGoScriptWorkerRequest(data []byte) (GoScriptCompileOptions, logrus.Level, error) {
	// Parse the request and its log level.
	var p fastjson.Parser
	v, err := p.ParseBytes(data)
	if err != nil {
		return GoScriptCompileOptions{}, 0, errors.Wrap(err, "parse the goscript worker request")
	}
	level, err := logrus.ParseLevel(string(v.GetStringBytes("level")))
	if err != nil {
		return GoScriptCompileOptions{}, 0, err
	}

	// Decode the options.
	return GoScriptCompileOptions{
		WorkDir:                   string(v.GetStringBytes("workDir")),
		OutputPath:                string(v.GetStringBytes("outputPath")),
		CacheRoot:                 string(v.GetStringBytes("cacheRoot")),
		Packages:                  getStringArray(v, "packages"),
		BuildFlags:                getStringArray(v, "buildFlags"),
		OverrideDirs:              getStringArray(v, "overrideDirs"),
		BindingRoots:              getStringArray(v, "bindingRoots"),
		DeferredFunctions:         getStringArray(v, "deferredFunctions"),
		ProtobufTypeScriptBinding: v.GetBool("protobufTypeScriptBinding"),
		EliminateDeadCode:         v.GetBool("eliminateDeadCode"),
	}, level, nil
}

// newStringArray returns a JSON array of strs allocated in a.
func newStringArray(a *fastjson.Arena, strs []string) *fastjson.Value {
	arr := a.NewArray()
	for i, s := range strs {
		arr.SetArrayItem(i, a.NewString(s))
	}
	return arr
}

// getStringArray returns the strings of the array at key in v.
func getStringArray(v *fastjson.Value, key string) []string {
	// Leave an absent or empty array nil.
	items := v.GetArray(key)
	if len(items) == 0 {
		return nil
	}

	// Copy each string out of the parser's buffer.
	strs := make([]string, len(items))
	for i, item := range items {
		strs[i] = string(item.GetStringBytes())
	}
	return strs
}