From 638f69c9e8d83d4f2efb46b2e45bcb1105773b70 Mon Sep 17 00:00:00 2001 From: Paul Cody Johnston Date: Fri, 9 Jun 2023 19:49:42 -0600 Subject: [PATCH] Feature: add pkg/process (#21) * Initial pkg/starlarkprocess * Replace skycfg with deterministic fork * Try fix flaky tests * More test fix for flakiness * revert use fork * Revert use of fork; try fixing up prototext output directly * Yes more flaky reduce efforts * Add --- Makefile | 1 + cmd/grpcstar/BUILD.bazel | 2 +- .../{integration_test.go => grpcstar_test.go} | 10 +++ cmd/grpcstar/testdata/headers.grpc.star.err | 2 +- cmd/grpcstar/testdata/headers.grpc.star.out | 4 +- cmd/grpcstar/testdata/process.grpc.star | 11 +++ cmd/grpcstar/testdata/process.grpc.star.err | 7 ++ cmd/grpcstar/testdata/process.grpc.star.out | 0 .../testdata/routeguide.grpc.star.err | 2 +- .../testdata/routeguide.grpc.star.out | 26 ++++-- cmd/grpcstar/testdata/thread.grpc.star | 6 +- cmd/grpcstar/testdata/thread.grpc.star.err | 7 +- cmd/grpcstar/testdata/time.grpc.star | 8 +- cmd/grpcstar/testdata/time.grpc.star.err | 7 +- pkg/program/BUILD.bazel | 1 + pkg/program/config.go | 16 ++-- pkg/program/program.go | 36 +++++--- pkg/program/proto.go | 7 +- pkg/protodescriptorset/BUILD.bazel | 15 +++- pkg/protodescriptorset/protodescriptorset.go | 25 ++++++ .../protodescriptorset_test.go | 64 ++++++++++++++ .../routeguide_proto_descriptor.pb | Bin 0 -> 1069 bytes pkg/starlarkprocess/BUILD.bazel | 16 ++++ pkg/starlarkprocess/module.go | 21 +++++ pkg/starlarkprocess/process.go | 83 ++++++++++++++++++ 25 files changed, 329 insertions(+), 48 deletions(-) rename cmd/grpcstar/{integration_test.go => grpcstar_test.go} (92%) create mode 100644 cmd/grpcstar/testdata/process.grpc.star create mode 100755 cmd/grpcstar/testdata/process.grpc.star.err create mode 100755 cmd/grpcstar/testdata/process.grpc.star.out create mode 100644 pkg/protodescriptorset/protodescriptorset_test.go create mode 100755 pkg/protodescriptorset/routeguide_proto_descriptor.pb create mode 100644 pkg/starlarkprocess/BUILD.bazel create mode 100644 pkg/starlarkprocess/module.go create mode 100644 pkg/starlarkprocess/process.go diff --git a/Makefile b/Makefile index 3a3b0ad..bea81d5 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ serve: build routeguide_proto_descriptor: bazel build //example/routeguide:routeguide_proto_descriptor cp -f bazel-bin/example/routeguide/routeguide_proto_descriptor.pb pkg/starlarkgrpc/ + cp -f bazel-bin/example/routeguide/routeguide_proto_descriptor.pb pkg/protodescriptorset/ .PHONY: plugin_proto_descriptor plugin_proto_descriptor: diff --git a/cmd/grpcstar/BUILD.bazel b/cmd/grpcstar/BUILD.bazel index 5853584..db4b723 100644 --- a/cmd/grpcstar/BUILD.bazel +++ b/cmd/grpcstar/BUILD.bazel @@ -67,7 +67,7 @@ go_binary( go_test( name = "grpcstar_test", - srcs = ["integration_test.go"], + srcs = ["grpcstar_test.go"], data = glob(["testdata/**"]) + ["//example/routeguide:routeguide_proto_descriptor"], embed = [":grpcstar_lib"], deps = [ diff --git a/cmd/grpcstar/integration_test.go b/cmd/grpcstar/grpcstar_test.go similarity index 92% rename from cmd/grpcstar/integration_test.go rename to cmd/grpcstar/grpcstar_test.go index dd0431c..bae4fae 100644 --- a/cmd/grpcstar/integration_test.go +++ b/cmd/grpcstar/grpcstar_test.go @@ -4,6 +4,7 @@ import ( "flag" "os" "path/filepath" + "regexp" "strings" "testing" "time" @@ -93,6 +94,7 @@ func TestGoldens(t *testing.T) { if err != nil { t.Fatal("reading err file:", err) } + gotErr = derandPrototext(t, gotErr) if *update { if workspaceDir == "" { @@ -126,3 +128,11 @@ func TestGoldens(t *testing.T) { }) } } + +func derandPrototext(t *testing.T, data []byte) []byte { + in := string(data) + re := regexp.MustCompile(`\s{2}([a-z]+):`) + out := re.ReplaceAllString(in, " $1:") + out = re.ReplaceAllString(out, " $1:") + return []byte(out) +} diff --git a/cmd/grpcstar/testdata/headers.grpc.star.err b/cmd/grpcstar/testdata/headers.grpc.star.err index 4bd332d..5dba8e1 100755 --- a/cmd/grpcstar/testdata/headers.grpc.star.err +++ b/cmd/grpcstar/testdata/headers.grpc.star.err @@ -1,4 +1,4 @@ -[testdata/headers.grpc.star:22:10] server: GetFeature request message: +[testdata/headers.grpc.star:22:10] server: GetFeature request message: [testdata/headers.grpc.star:23:10] server: GetFeature request headers: [":authority", "content-type", "user-agent", "x-unary-request"] [testdata/headers.grpc.star:24:10] server: GetFeature request header content-type: application/grpc [testdata/headers.grpc.star:25:10] server: GetFeature request header user-agent: grpc-go/1.35.0 diff --git a/cmd/grpcstar/testdata/headers.grpc.star.out b/cmd/grpcstar/testdata/headers.grpc.star.out index 955a21a..34a1c2b 100755 --- a/cmd/grpcstar/testdata/headers.grpc.star.out +++ b/cmd/grpcstar/testdata/headers.grpc.star.out @@ -1,2 +1,4 @@ -{"name":"point (1,2)"} +{ + "name": "point (1,2)" +} {} diff --git a/cmd/grpcstar/testdata/process.grpc.star b/cmd/grpcstar/testdata/process.grpc.star new file mode 100644 index 0000000..582e16b --- /dev/null +++ b/cmd/grpcstar/testdata/process.grpc.star @@ -0,0 +1,11 @@ +def main(ctx): + print(process) + + print("run:", process.run) + result = process.run( + command = "pwd", + ) + print("stdout (runfiles dir):", str(result.stdout).partition("grpcstar_test.runfiles")[2]) + print("stderr:", result.stderr) + print("error:", result.error) + print("exit_code:", result.exit_code) diff --git a/cmd/grpcstar/testdata/process.grpc.star.err b/cmd/grpcstar/testdata/process.grpc.star.err new file mode 100755 index 0000000..2fd6f63 --- /dev/null +++ b/cmd/grpcstar/testdata/process.grpc.star.err @@ -0,0 +1,7 @@ +[testdata/process.grpc.star:2:10] +[testdata/process.grpc.star:4:10] run: +[testdata/process.grpc.star:8:10] stdout (runfiles dir): /build_stack_grpc_starlark/cmd/grpcstar + +[testdata/process.grpc.star:9:10] stderr: +[testdata/process.grpc.star:10:10] error: +[testdata/process.grpc.star:11:10] exit_code: 0 diff --git a/cmd/grpcstar/testdata/process.grpc.star.out b/cmd/grpcstar/testdata/process.grpc.star.out new file mode 100755 index 0000000..e69de29 diff --git a/cmd/grpcstar/testdata/routeguide.grpc.star.err b/cmd/grpcstar/testdata/routeguide.grpc.star.err index a3b63ba..cdceed1 100755 --- a/cmd/grpcstar/testdata/routeguide.grpc.star.err +++ b/cmd/grpcstar/testdata/routeguide.grpc.star.err @@ -1,6 +1,6 @@ [testdata/routeguide.grpc.star:92:10] GetFeature: [testdata/routeguide.grpc.star:104:14] ListFeatures: [testdata/routeguide.grpc.star:104:14] ListFeatures: -[testdata/routeguide.grpc.star:113:10] RecordRoute: +[testdata/routeguide.grpc.star:113:10] RecordRoute: [testdata/routeguide.grpc.star:124:14] RouteChat: [testdata/routeguide.grpc.star:124:14] RouteChat: diff --git a/cmd/grpcstar/testdata/routeguide.grpc.star.out b/cmd/grpcstar/testdata/routeguide.grpc.star.out index fdf67de..12b941b 100755 --- a/cmd/grpcstar/testdata/routeguide.grpc.star.out +++ b/cmd/grpcstar/testdata/routeguide.grpc.star.out @@ -1,6 +1,20 @@ -{"name":"point (1,2)"} -{"name":"lo (1,2)"} -{"name":"hi (1,4)"} -{"point_count":2, "distance":2, "elapsed_time":10} -{"message":"A"} -{"message":"B"} +{ + "name": "point (1,2)" +} +{ + "name": "lo (1,2)" +} +{ + "name": "hi (1,4)" +} +{ + "point_count": 2, + "distance": 2, + "elapsed_time": 10 +} +{ + "message": "A" +} +{ + "message": "B" +} diff --git a/cmd/grpcstar/testdata/thread.grpc.star b/cmd/grpcstar/testdata/thread.grpc.star index 603d49f..c1d48d6 100644 --- a/cmd/grpcstar/testdata/thread.grpc.star +++ b/cmd/grpcstar/testdata/thread.grpc.star @@ -10,9 +10,9 @@ def main(ctx): > thread.sleep pauses the current thread for the given duration """) print("thread.sleep:", thread.sleep) - thread.sleep(duration = 200 * time.millisecond) + thread.sleep(duration = 100 * time.millisecond) print("sleep before:", now) - print("sleep after:", time.now()) + # print("sleep after:", time.now()) print(""" > thread.defer runs a function in a separate thread after a given delay @@ -21,7 +21,7 @@ def main(ctx): # FIXME(pcj): figure out how to make this non-flaky # fn = lambda: print("defer at %s in thread %s:" % (time.now(), thread.name())), fn = lambda: print("defer callback in thread %s:" % thread.name()), - delay = 150 * time.millisecond, + delay = 10 * time.millisecond, count = 3, ) diff --git a/cmd/grpcstar/testdata/thread.grpc.star.err b/cmd/grpcstar/testdata/thread.grpc.star.err index 91adad5..0f7bdfb 100755 --- a/cmd/grpcstar/testdata/thread.grpc.star.err +++ b/cmd/grpcstar/testdata/thread.grpc.star.err @@ -7,13 +7,12 @@ [testdata/thread.grpc.star:12:10] thread.sleep: [testdata/thread.grpc.star:14:10] sleep before: 2019-01-01 00:00:00 +0000 UTC -[testdata/thread.grpc.star:15:10] sleep after: 2019-01-01 00:00:00.2 +0000 UTC [testdata/thread.grpc.star:17:10] > thread.defer runs a function in a separate thread after a given delay -[testdata/thread.grpc.star:23:27] defer callback in thread -thread.defer(150000000): -[testdata/thread.grpc.star:23:27] defer callback in thread -thread.defer(150000000): -[testdata/thread.grpc.star:23:27] defer callback in thread -thread.defer(150000000): +[testdata/thread.grpc.star:23:27] defer callback in thread -thread.defer(10000000): +[testdata/thread.grpc.star:23:27] defer callback in thread -thread.defer(10000000): +[testdata/thread.grpc.star:23:27] defer callback in thread -thread.defer(10000000): [testdata/thread.grpc.star:31:10] > thread.cancel stops the current thread. Cancelling the main thread exits the program. diff --git a/cmd/grpcstar/testdata/time.grpc.star b/cmd/grpcstar/testdata/time.grpc.star index 6783592..6952de4 100644 --- a/cmd/grpcstar/testdata/time.grpc.star +++ b/cmd/grpcstar/testdata/time.grpc.star @@ -1,8 +1,10 @@ def main(ctx): print("=== Example Time Usage ===") now = time.now() + then = now + 5 * time.second - print("current time:", now) - print("time add:", now + 5 * time.second) - print("time hours:", now.hour) + print("time then:", then) + print("time hours:", then.hour) + print("time minute:", then.minute) + print("time second:", then.second) print("additional details: https://github.com/google/starlark-go/blob/a134d8f9ddca7469c736775b67544671f0a135ad/starlark/testdata/time.star") diff --git a/cmd/grpcstar/testdata/time.grpc.star.err b/cmd/grpcstar/testdata/time.grpc.star.err index d1fb982..b7e64f4 100755 --- a/cmd/grpcstar/testdata/time.grpc.star.err +++ b/cmd/grpcstar/testdata/time.grpc.star.err @@ -1,5 +1,6 @@ [testdata/time.grpc.star:2:10] === Example Time Usage === -[testdata/time.grpc.star:5:10] current time: 2019-01-01 00:00:00.7 +0000 UTC -[testdata/time.grpc.star:6:10] time add: 2019-01-01 00:00:05.7 +0000 UTC +[testdata/time.grpc.star:6:10] time then: 2019-01-01 00:00:05.6 +0000 UTC [testdata/time.grpc.star:7:10] time hours: 0 -[testdata/time.grpc.star:8:10] additional details: https://github.com/google/starlark-go/blob/a134d8f9ddca7469c736775b67544671f0a135ad/starlark/testdata/time.star +[testdata/time.grpc.star:8:10] time minute: 0 +[testdata/time.grpc.star:9:10] time second: 5 +[testdata/time.grpc.star:10:10] additional details: https://github.com/google/starlark-go/blob/a134d8f9ddca7469c736775b67544671f0a135ad/starlark/testdata/time.star diff --git a/pkg/program/BUILD.bazel b/pkg/program/BUILD.bazel index c6b3096..bca0c65 100644 --- a/pkg/program/BUILD.bazel +++ b/pkg/program/BUILD.bazel @@ -15,6 +15,7 @@ go_library( "//pkg/starlarkgrpc", "//pkg/starlarknet", "//pkg/starlarkos", + "//pkg/starlarkprocess", "//pkg/starlarkthread", "@com_github_stripe_skycfg//:skycfg", "@com_github_stripe_skycfg//go/protomodule", diff --git a/pkg/program/config.go b/pkg/program/config.go index b179f1a..fb1a0d0 100644 --- a/pkg/program/config.go +++ b/pkg/program/config.go @@ -88,7 +88,7 @@ func (cfg *Config) ParseArgs(args []string) error { case OutputStableJson: marshaler := protojson.MarshalOptions{ EmitUnpopulated: false, - Indent: "", + Indent: " ", UseProtoNames: true, } cfg.OutputType = OutputStableJson @@ -130,12 +130,14 @@ func (cfg *Config) ParseArgs(args []string) error { return yaml.Marshal(yamlMap) } default: - return Usage(fmt.Sprintf("invalid flag -o: must be one of (%v|%v|%v|%v)", - OutputJson, - OutputStableJson, - OutputProto, - OutputText, - OutputYaml, + return Usage(fmt.Sprintf("invalid flag -o: must be one of (%v)", + []OutputType{ + OutputJson, + OutputStableJson, + OutputProto, + OutputText, + OutputYaml, + }, )) } diff --git a/pkg/program/program.go b/pkg/program/program.go index 659651f..b0675fa 100644 --- a/pkg/program/program.go +++ b/pkg/program/program.go @@ -18,6 +18,7 @@ import ( "github.com/stackb/grpc-starlark/pkg/starlarkgrpc" "github.com/stackb/grpc-starlark/pkg/starlarknet" "github.com/stackb/grpc-starlark/pkg/starlarkos" + "github.com/stackb/grpc-starlark/pkg/starlarkprocess" "github.com/stackb/grpc-starlark/pkg/starlarkthread" ) @@ -26,14 +27,18 @@ type Program struct { skyConfig *skycfg.Config } -func NewProgram(cfg *Config) (*Program, error) { +func NewProgram(cfg *Config, loadOptions ...skycfg.LoadOption) (*Program, error) { if cfg.File == "" { return nil, fmt.Errorf("entrypoint file is required") } - skyConfig, err := skycfg.Load(context.Background(), cfg.File, + loadOptions = append(loadOptions, skycfg.WithProtoRegistry(skycfg.NewUnstableProtobufRegistryV2(cfg.ProtoTypes)), + ) + loadOptions = append(loadOptions, skycfg.WithGlobals(newPredeclared(cfg.ProtoFiles, cfg.ProtoTypes)), ) + + skyConfig, err := skycfg.Load(context.Background(), cfg.File, loadOptions...) if err != nil { return nil, err } @@ -52,7 +57,7 @@ func (p *Program) Run(options ...skycfg.ExecOption) error { } return err } - if err := p.Format(msgs); err != nil { + if err := p.Format(msgs...); err != nil { return err } return nil @@ -69,7 +74,7 @@ func (p *Program) Exec() ([]protoreflect.ProtoMessage, error) { return msgs, nil } -func (p *Program) Format(msgs []protoreflect.ProtoMessage) error { +func (p *Program) Format(msgs ...protoreflect.ProtoMessage) error { var sep string if p.cfg.OutputType == OutputYaml { sep = "---\n" @@ -80,7 +85,9 @@ func (p *Program) Format(msgs []protoreflect.ProtoMessage) error { return err } if p.cfg.OutputType == OutputProto { - fmt.Print(data) + if _, err := os.Stdout.Write(data); err != nil { + return err + } } else { fmt.Printf("%s%s\n", sep, string(data)) } @@ -110,14 +117,15 @@ func newPredeclared(files *protoregistry.Files, types *protoregistry.Types) star protoModule.Members["decode"] = protoDecode(types) return starlark.StringDict{ - "os": starlarkos.Module, - "net": starlarknet.Module, - "thread": starlarkthread.Module, - "time": libtime.Module, - "crypto": starlarkcrypto.Module, - "grpc": starlarkgrpc.NewModule(files), - "proto": protoModule, - "struct": starlark.NewBuiltin("struct", starlarkstruct.Make), - "module": starlark.NewBuiltin("module", starlarkstruct.MakeModule), + "os": starlarkos.Module, + "net": starlarknet.Module, + "thread": starlarkthread.Module, + "time": libtime.Module, + "crypto": starlarkcrypto.Module, + "grpc": starlarkgrpc.NewModule(files), + "proto": protoModule, + "process": starlarkprocess.NewModule(), + "struct": starlark.NewBuiltin("struct", starlarkstruct.Make), + "module": starlark.NewBuiltin("module", starlarkstruct.MakeModule), } } diff --git a/pkg/program/proto.go b/pkg/program/proto.go index fe355f4..0687b90 100644 --- a/pkg/program/proto.go +++ b/pkg/program/proto.go @@ -22,7 +22,7 @@ func protoDecode(registry *protoregistry.Types) starlark.Callable { kwargs []starlark.Tuple, ) (starlark.Value, error) { var msgType starlark.Value - var value starlark.String + var value starlark.Bytes if err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 2, &msgType, &value); err != nil { return nil, err } @@ -66,10 +66,11 @@ func protoEncode(registry *protoregistry.Types) starlark.Callable { } } - jsonData, err := marshal.Marshal(msg) + data, err := marshal.Marshal(msg) if err != nil { return nil, err } - return starlark.String(jsonData), nil + + return starlark.Bytes(data), nil }) } diff --git a/pkg/protodescriptorset/BUILD.bazel b/pkg/protodescriptorset/BUILD.bazel index bb393fa..5188fdf 100644 --- a/pkg/protodescriptorset/BUILD.bazel +++ b/pkg/protodescriptorset/BUILD.bazel @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "protodescriptorset", @@ -14,3 +14,16 @@ go_library( "@org_golang_google_protobuf//types/dynamicpb", ], ) + +go_test( + name = "protodescriptorset_test", + srcs = ["protodescriptorset_test.go"], + embed = [":protodescriptorset"], + embedsrcs = ["routeguide_proto_descriptor.pb"], + deps = [ + "@com_github_google_go_cmp//cmp", + "@org_golang_google_protobuf//reflect/protodesc", + "@org_golang_google_protobuf//reflect/protoreflect", + "@org_golang_google_protobuf//reflect/protoregistry", + ], +) diff --git a/pkg/protodescriptorset/protodescriptorset.go b/pkg/protodescriptorset/protodescriptorset.go index 3014faa..2806be0 100644 --- a/pkg/protodescriptorset/protodescriptorset.go +++ b/pkg/protodescriptorset/protodescriptorset.go @@ -49,6 +49,18 @@ func Parse(data []byte) (*descriptorpb.FileDescriptorSet, error) { return &dpb, nil } +func ParseFiles(data []byte) (*protoregistry.Files, error) { + descriptor, err := Parse(data) + if err != nil { + return nil, err + } + files, err := protodesc.NewFiles(descriptor) + if err != nil { + return nil, err + } + return files, nil +} + func FileTypes(files *protoregistry.Files) *protoregistry.Types { var types protoregistry.Types files.RangeFiles(func(fd protoreflect.FileDescriptor) bool { @@ -69,3 +81,16 @@ func FileTypes(files *protoregistry.Files) *protoregistry.Types { }) return &types } + +func MergeFilesIgnoreConflicts(all ...*protoregistry.Files) *protoregistry.Files { + merged := &protoregistry.Files{} + for _, files := range all { + files.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + // RegisterFile only return err due to file or name conflicts. This + // function is about ignoring conflicts, so we can ignore the error. + merged.RegisterFile(fd) + return true + }) + } + return merged +} diff --git a/pkg/protodescriptorset/protodescriptorset_test.go b/pkg/protodescriptorset/protodescriptorset_test.go new file mode 100644 index 0000000..74c5e2b --- /dev/null +++ b/pkg/protodescriptorset/protodescriptorset_test.go @@ -0,0 +1,64 @@ +package protodescriptorset + +import ( + _ "embed" + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" +) + +//go:embed routeguide_proto_descriptor.pb +var routeguideProtoDescriptor []byte + +func TestMergeFilesIgnoreConflicts(t *testing.T) { + fileDescriptorSet, err := Parse(routeguideProtoDescriptor) + if err != nil { + t.Fatal(err) + } + files, err := protodesc.NewFiles(fileDescriptorSet) + if err != nil { + t.Fatal(err) + } + var wantNames []string + files.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + wantNames = append(wantNames, string(fd.FullName())) + return true + }) + + for name, tc := range map[string]struct { + all []*protoregistry.Files + // protoreflect.FileDescriptor is an interface, so in order to test with + // this we'd need a custom comparer. So, just use the names for comparison + want []string + }{ + "degenerate case": { + want: nil, + }, + "simple case": { + all: []*protoregistry.Files{files}, + want: wantNames, + }, + "merge case": { + all: []*protoregistry.Files{files, files}, + want: wantNames, + }, + } { + t.Run(name, func(t *testing.T) { + merged := MergeFilesIgnoreConflicts(tc.all...) + + var got []string + merged.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + got = append(got, string(fd.FullName())) + return true + }) + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("(-want +got):\n%s", diff) + } + }) + } + +} diff --git a/pkg/protodescriptorset/routeguide_proto_descriptor.pb b/pkg/protodescriptorset/routeguide_proto_descriptor.pb new file mode 100755 index 0000000000000000000000000000000000000000..0166f6f80545257ae5928c119c7a00241c5e0478 GIT binary patch literal 1069 zcmah}!A{#i5XBBOWRePLP9d}^H5YoQgFro@9)ODUP|?WEsj7rF_SUhmz1DgywcpV{ z=#l^6znERG9V9|>+nsrS^Jd7x`ws7V3S4J69DFSKd5p{R0Y?E(|J(h)p@*R~A{sZAT$U=1S2qogF?LO$%&jyHFF4+EaE zqU6!7N@&{KCf~QQ8HG+CTmYkLqF)m@EqK=Hplv;YT{ll*zmC^L;;;n%J!Vf439NCim_Wf99{uKj7>U DnWbGX literal 0 HcmV?d00001 diff --git a/pkg/starlarkprocess/BUILD.bazel b/pkg/starlarkprocess/BUILD.bazel new file mode 100644 index 0000000..067879f --- /dev/null +++ b/pkg/starlarkprocess/BUILD.bazel @@ -0,0 +1,16 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "starlarkprocess", + srcs = [ + "module.go", + "process.go", + ], + importpath = "github.com/stackb/grpc-starlark/pkg/starlarkprocess", + visibility = ["//visibility:public"], + deps = [ + "//pkg/starlarkutil", + "@net_starlark_go//starlark", + "@net_starlark_go//starlarkstruct", + ], +) diff --git a/pkg/starlarkprocess/module.go b/pkg/starlarkprocess/module.go new file mode 100644 index 0000000..a041cff --- /dev/null +++ b/pkg/starlarkprocess/module.go @@ -0,0 +1,21 @@ +package starlarkprocess + +import ( + "os" + + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +// Module process is a Starlark module of process-related functions and constants. The +// module defines the following functions: +func NewModule() *starlarkstruct.Module { + executable, _ := os.Executable() + return &starlarkstruct.Module{ + Name: "process", + Members: starlark.StringDict{ + "executable": starlark.String(executable), + "run": starlark.NewBuiltin("process.run", run), + }, + } +} diff --git a/pkg/starlarkprocess/process.go b/pkg/starlarkprocess/process.go new file mode 100644 index 0000000..490d356 --- /dev/null +++ b/pkg/starlarkprocess/process.go @@ -0,0 +1,83 @@ +package starlarkprocess + +import ( + "bytes" + "context" + "os/exec" + "syscall" + "time" + + "github.com/stackb/grpc-starlark/pkg/starlarkutil" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +func run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var command string + var argv *starlark.List + var env *starlark.Dict + var stdin starlark.Bytes + if err := starlark.UnpackArgs(fn.Name(), args, kwargs, + "command", &command, + "args?", &argv, + "env?", &env, + "stdin?", &stdin, + ); err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + cmd := exec.CommandContext(ctx, command, starlarkListToStringSlice(argv)...) + if stdin.Len() > 0 { + cmd.Stdin = bytes.NewBuffer([]byte(stdin)) + } + + var stderr []byte + var errMsg string + stdout, err := cmd.Output() + cancel() + var exitCode int + if err != nil { + // try to get the exit code + if exitError, ok := err.(*exec.ExitError); ok { + ws := exitError.Sys().(syscall.WaitStatus) + exitCode = ws.ExitStatus() + stderr = exitError.Stderr + errMsg = exitError.Error() + } else { + // This will happen (in OSX) if `name` is not available in $PATH, in + // this situation, exit code could not be get, and stderr will be + // empty string very likely, so we use the default fail code, and + // format err to string and set to stderr + exitCode = -1 + errMsg = err.Error() + } + } else { + // success, exitCode should be 0 if go is ok + ws := cmd.ProcessState.Sys().(syscall.WaitStatus) + exitCode = ws.ExitStatus() + } + + return starlarkstruct.FromStringDict( + starlarkutil.Symbol(fn.Name()), + starlark.StringDict{ + "command": starlark.String(command), + "args": args, + "error": starlark.String(errMsg), + "stdout": starlark.Bytes(stdout), + "stderr": starlark.Bytes(stderr), + "exit_code": starlark.MakeInt(exitCode), + }, + ), nil +} + +func starlarkListToStringSlice(list *starlark.List) []string { + if list == nil { + return []string{} + } + elems := make([]string, list.Len()) + for i := 0; i < list.Len(); i++ { + elems[i] = list.Index(i).String() + } + return elems +}