Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't partly omit nested messages using --dig-manually #554

Closed
relipocere opened this issue Jun 30, 2022 · 16 comments
Closed

Can't partly omit nested messages using --dig-manually #554

relipocere opened this issue Jun 30, 2022 · 16 comments
Labels

Comments

@relipocere
Copy link
Contributor

relipocere commented Jun 30, 2022

Describe the bug

There's no way to partly omit nested messages in repl mode. --dig-manually allows to omit only the currently selected field and then skips the filling of the rest of the fields. There needs to be a way to skip a field and move to the next one.

To reproduce

syntax = "proto3";
package mypackage;

service MyService{
    rpc UpdateGroup (UpdateGroupRequest) returns (UpdateGroupResponse);
}

message UpdateGroupRequest{
  int64 group_id = 1;
  OrganizationsValue organizations = 2;
  PointsValue points = 3;

  message OrganizationsValue{
    repeated int64 organization_ids = 1;
  }

  message PointsValue{
    repeated int64 point_ids = 1;
  }
}

message UpdateGroupResponse{
}

Then try skipping organizations field completely(make it nil), but enter points.

Expected behavior

There is way to omit field organizations, but fill points so the request will look like this:

{"group_id": 20, "points": {"point_ids": [1, 2, 3, 4, 5]}}

Environment

  • OS: macOS 11.6.4
  • Terminal: zsh
  • Evans version: 0.10.6
  • protoc version: -
  • protoc plugin version (if you are using): -

Additional context

The one way to solve this issue is to add a new option to --dig-manually mode that will allow to skip a field without skipping the rest of them. The name can be skip once for example.

@relipocere relipocere added the bug label Jun 30, 2022
@ktr0731
Copy link
Owner

ktr0731 commented Jul 5, 2022

Thanks for the report.
This looks like an unexpected behavior. It should not skip points even if organizations field is skipped.

I think this can be fixed easily. We can define a new error such like errSkip and return it here, instead of ErrAbort. And make the callee to handle errSkip error.

If you are interested, would you like to fix this bug?

@relipocere
Copy link
Contributor Author

relipocere commented Jul 5, 2022

Yeah, sounds good, if it will make the fix come out faster I'm all for it. I've glanced at the code, so can I potentially handle it by simply using continue if I encounter my newly defined error?

@relipocere
Copy link
Contributor Author

I added new error to prompt/prompt.go

var (
	ErrAbort = errors.New("abort")
	ErrSkip  = errors.New("skip")
)

changed ErrAbort to ErrSkip in resolveField function

if r.skipMessage(f) {
	return nil, prompt.ErrSkip
}

and added new check in resolve

err := r.resolveField(f)
if errors.Is(err, prompt.ErrSkip) {
	continue
}
if errors.Is(err, prompt.ErrAbort) {
	return r.msg, nil
}

and now skip in --dig-manually mode works as expected

However this change broke something. When the program tries to parse not nil response from the gRPC API it crashes with (when the response is nil everything is OK):

[email protected]:7002> call GetUser
✔ user_id
user_id (TYPE_INT64) => 120
Good Bye :)
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x104ce88f0]

goroutine 1 [running]:
google.golang.org/protobuf/types/dynamicpb.(*Message).Descriptor(0x105099440?)
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/types/dynamicpb/dynamic.go:109
google.golang.org/protobuf/internal/impl.Export.MessageDescriptorOf({}, {0x105099440, 0x0})
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/internal/impl/api_export.go:156 +0x50
github.com/golang/protobuf/proto.MessageName({0x12c68d818, 0x0})
	/Users/sol/go/pkg/mod/github.com/golang/[email protected]/proto/registry.go:271 +0x5c
github.com/jhump/protoreflect/dynamic.isWellKnownType({0x1050d4eb0, 0x105099440})
	/Users/sol/go/pkg/mod/github.com/jhump/[email protected]/dynamic/message_factory.go:167 +0x78
github.com/jhump/protoreflect/dynamic.(*KnownTypeRegistry).GetKnownType(0x140001ea758?, {0x140000392c0?, 0x140001ea768?})
	/Users/sol/go/pkg/mod/github.com/jhump/[email protected]/dynamic/message_factory.go:181 +0x1d8
github.com/jhump/protoreflect/dynamic.(*KnownTypeRegistry).CreateIfKnown(0xa8?, {0x140000392c0?, 0x140001ea7b8?})
	/Users/sol/go/pkg/mod/github.com/jhump/[email protected]/dynamic/message_factory.go:150 +0x24
github.com/jhump/protoreflect/dynamic.(*MessageFactory).NewMessage(0x0, 0x14000397590)
	/Users/sol/go/pkg/mod/github.com/jhump/[email protected]/dynamic/message_factory.go:64 +0x48
github.com/jhump/protoreflect/codec.DecodeLengthDelimitedField(0x140001eaa58?, {0x1400011e5a2, 0x50, 0x50}, {0x1050c8ad8?, 0x0?})
	/Users/sol/go/pkg/mod/github.com/jhump/[email protected]/codec/decode_fields.go:195 +0xc8
github.com/jhump/protoreflect/codec.(*Buffer).decodeKnownField(0x140001eac98?, 0x140005b3f00, 0x0?, {0x1050c8ad8, 0x0})
	/Users/sol/go/pkg/mod/github.com/jhump/[email protected]/codec/decode_fields.go:270 +0x120
github.com/jhump/protoreflect/codec.(*Buffer).DecodeFieldValue(0x140001eaa58, 0x140001ea9a0, {0x1050c8ad8, 0x0})
	/Users/sol/go/pkg/mod/github.com/jhump/[email protected]/codec/decode_fields.go:109 +0x94
github.com/jhump/protoreflect/dynamic.(*Message).unmarshal(0x140003fe2d0, 0x140001eaa58, 0x0)
	/Users/sol/go/pkg/mod/github.com/jhump/[email protected]/dynamic/binary.go:154 +0x7c
github.com/jhump/protoreflect/dynamic.(*Message).UnmarshalMerge(...)
	/Users/sol/go/pkg/mod/github.com/jhump/[email protected]/dynamic/binary.go:149
github.com/jhump/protoreflect/dynamic.(*Message).Unmarshal(0x140003fe2d0, {0x1400011e5a0, 0x52, 0x52})
	/Users/sol/go/pkg/mod/github.com/jhump/[email protected]/dynamic/binary.go:138 +0x120
google.golang.org/protobuf/internal/impl.legacyUnmarshal({{}, {0x1050d4760, 0x1400048e7b0}, {0x1400011e5a0, 0x52, 0x52}, 0x0, {0x1050cc9c8, 0x1400019f950}, 0x2710})
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/internal/impl/legacy_message.go:419 +0x130
google.golang.org/protobuf/proto.UnmarshalOptions.unmarshal({{}, 0x1, 0x1, 0x0, {0x1050cc9c8, 0x1400019f950}, 0x2710}, {0x1400011e5a0, 0x52, 0x52}, ...)
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/proto/decode.go:104 +0x16c
google.golang.org/protobuf/proto.UnmarshalOptions.UnmarshalState({{}, 0x1, 0x1, 0x0, {0x0, 0x0}, 0x2710}, {{}, {0x1050d4760, 0x1400048e7b0}, ...})
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/proto/decode.go:76 +0x98
github.com/golang/protobuf/proto.UnmarshalMerge({0x1400011e5a0, 0x52, 0x52}, {0x1050cd758?, 0x140003fe2d0?})
	/Users/sol/go/pkg/mod/github.com/golang/[email protected]/proto/wire.go:67 +0x100
github.com/golang/protobuf/proto.Unmarshal({0x1400011e5a0, 0x52, 0x52}, {0x1050cd758, 0x140003fe2d0?})
	/Users/sol/go/pkg/mod/github.com/golang/[email protected]/proto/wire.go:58 +0x58
google.golang.org/grpc/encoding/proto.codec.Unmarshal({}, {0x1400011e5a0, 0x52, 0x52}, {0x1050c2840, 0x140003fe2d0})
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/encoding/proto/proto.go:53 +0x60
google.golang.org/grpc.recv(0x140003ab440?, {0x12c88e5c0, 0x10560b0f8}, 0x140004be0f8?, {0x0?, 0x0?}, {0x1050c2840, 0x140003fe2d0}, 0x5?, 0x0, ...)
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/rpc_util.go:760 +0x94
google.golang.org/grpc.(*csAttempt).recvMsg(0x140000cc6e0, {0x1050c2840?, 0x140003fe2d0}, 0x0?)
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/stream.go:983 +0x244
google.golang.org/grpc.(*clientStream).RecvMsg.func1(0x105009400?)
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/stream.go:834 +0x2c
google.golang.org/grpc.(*clientStream).withRetry(0x140003ab200, 0x140001eb148, 0x140001eb118)
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/stream.go:692 +0x150
google.golang.org/grpc.(*clientStream).RecvMsg(0x140003ab200, {0x1050c2840?, 0x140003fe2d0?})
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/stream.go:833 +0xbc
google.golang.org/grpc.invoke({0x1050cedd0?, 0x140003fe2a0?}, {0x140001d4d40?, 0x12c6620f8?}, {0x1050c2840, 0x14000118d50}, {0x1050c2840, 0x140003fe2d0}, 0x0?, {0x140001d0d60?, ...})
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/call.go:73 +0xb8
google.golang.org/grpc.(*ClientConn).Invoke(0x14000432000?, {0x1050cedd0?, 0x140003fe2a0?}, {0x140001d4d40?, 0x1400049e000?}, {0x1050c2840?, 0x14000118d50?}, {0x1050c2840?, 0x140003fe2d0?}, {0x140001d0d60?, ...})
	/Users/sol/go/pkg/mod/google.golang.org/[email protected]/call.go:37 +0x1a8
github.com/ktr0731/evans/grpc.(*client).Invoke(0x1400007e140, {0x1050cedd0, 0x140003fe2a0}, {0x14000037100, 0x31}, {0x1050c2840, 0x14000118d50}, {0x1050c2840, 0x140003fe2d0})
	/Users/sol/go_projects/evans/grpc/grpc.go:188 +0x1d8
github.com/ktr0731/evans/usecase.(*dependencyManager).CallRPC(0x1400047c200, {0x1050ced60, 0x140001a8008}, {0x14000120030?, 0x0?}, {0x140004926b0, 0x7}, 0x0, {0x1050c8c98?, 0x140001a4310})
	/Users/sol/go_projects/evans/usecase/call_rpc.go:411 +0xbe0
github.com/ktr0731/evans/usecase.(*dependencyManager).CallRPCInteractively(0x1400047c200, {0x1050ced60, 0x140001a8008}, {0x1050ca698, 0x140001a4008}, {0x140004926b0, 0x7}, 0x0, 0x0, 0x49?, ...)
	/Users/sol/go_projects/evans/usecase/call_rpc.go:486 +0x104
github.com/ktr0731/evans/usecase.CallRPCInteractively(...)
	/Users/sol/go_projects/evans/usecase/call_rpc.go:482
github.com/ktr0731/evans/repl.(*callCommand).Run(0x14000383020?, {0x1050ca698?, 0x140001a4008?}, {0x14000314a10?, 0x1?, 0xc?})
	/Users/sol/go_projects/evans/repl/commands.go:204 +0x2b4
github.com/ktr0731/evans/repl.(*REPL).runCommand(0x1400042f1c0, {0x14000492688, 0x4}, {0x140004949d0, 0x1, 0x1})
	/Users/sol/go_projects/evans/repl/repl.go:156 +0x264
github.com/ktr0731/evans/repl.(*REPL).Run(0x1400042f1c0, {0x1050d1818?, 0x140001477a0?})
	/Users/sol/go_projects/evans/repl/repl.go:111 +0x1a8
github.com/ktr0731/evans/mode.RunAsREPLMode(0x140003b1b00, {0x1050d09d0, 0x1400041f9d0}, 0x140002eb6d0)
	/Users/sol/go_projects/evans/mode/repl.go:68 +0x678
github.com/ktr0731/evans/app.runREPLCommand(0x140003ff470, {0x1050d09d0?, 0x1400041f9d0})
	/Users/sol/go_projects/evans/app/commands.go:280 +0x28c
github.com/ktr0731/evans/app.newREPLCommand.func1(0x1400028fca8?, 0x140003ff470?)
	/Users/sol/go_projects/evans/app/commands.go:248 +0xe8
github.com/ktr0731/evans/app.runFunc.func1(0x140003e3180, {0x140003f00c0, 0x0, 0x6})
	/Users/sol/go_projects/evans/app/commands.go:87 +0x134
github.com/spf13/cobra.(*Command).execute(0x140003e3180, {0x140003f0060, 0x6, 0x6})
	/Users/sol/go/pkg/mod/github.com/spf13/[email protected]/command.go:856 +0x4c4
github.com/spf13/cobra.(*Command).ExecuteC(0x140003e2500)
	/Users/sol/go/pkg/mod/github.com/spf13/[email protected]/command.go:974 +0x354
github.com/spf13/cobra.(*Command).Execute(...)
	/Users/sol/go/pkg/mod/github.com/spf13/[email protected]/command.go:902
github.com/ktr0731/evans/app.(*App).Run(0x140001ebf50, {0x140001ae010?, 0x7?, 0x104765c08?})
	/Users/sol/go_projects/evans/app/app.go:51 +0x84
main.main()
	/Users/sol/go_projects/evans/main.go:11 +0xa

Any ideas on what may have caused it and how it can be fixed? I'm, honestly, quite perplexed as to why a small change in behaviour of message creator caused segmentation violation in during response handling.

@ktr0731
Copy link
Owner

ktr0731 commented Jul 7, 2022

Umm...
I tried to reproduce it but it didn't. At least in my local environment, it is working correctly without panic.
Could you show me a minimum code to reproduce it?

@relipocere
Copy link
Contributor Author

Ok, here are the changes to the functions and vars:

fill/proto/interactive_filler.go

func (r *resolver) resolve() (*dynamic.Message, error) {
	selectedOneof := make(map[string]interface{})

	for _, f := range r.m.GetFields() {
		if isOneOfField := f.GetOneOf() != nil; isOneOfField {
			fqn := f.GetOneOf().GetFullyQualifiedName()
			if _, selected := selectedOneof[fqn]; selected {
				// Skip if one of choices is already selected.
				continue
			}

			selectedOneof[fqn] = nil
			if err := r.resolveOneof(f.GetOneOf()); err != nil {
				return nil, err
			}
			continue
		}

		err := r.resolveField(f)
		if errors.Is(err, prompt.ErrSkip) {
			continue
		}
		if errors.Is(err, prompt.ErrAbort) {
			return r.msg, nil
		}
		if err != nil {
			return nil, err
		}
	}

	return r.msg, nil
}


func (r *resolver) resolveField(f *desc.FieldDescriptor) error {
	resolve := func(f *desc.FieldDescriptor) (interface{}, error) {
		var converter func(string) (interface{}, error)

		switch t := f.GetType(); t {
		case descriptorpb.FieldDescriptorProto_TYPE_MESSAGE:
			if r.skipMessage(f) {
				return nil, prompt.ErrSkip
			}

			msgr := newResolver(
				r.prompt,
				r.prefixFormat,
				r.color.NextVal(),
				dynamic.NewMessage(f.GetMessageType()),
				append(r.ancestors, f.GetName()),
				r.repeated || f.IsRepeated(),
				r.opts,
			)
			return msgr.resolve()
		case descriptorpb.FieldDescriptorProto_TYPE_ENUM:
			return r.resolveEnum(r.makePrefix(f), f.GetEnumType())
		case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE:
			converter = func(v string) (interface{}, error) { return strconv.ParseFloat(v, 64) }

		case descriptorpb.FieldDescriptorProto_TYPE_FLOAT:
			converter = func(v string) (interface{}, error) {
				f, err := strconv.ParseFloat(v, 32)
				return float32(f), err
			}

		case descriptorpb.FieldDescriptorProto_TYPE_INT64,
			descriptorpb.FieldDescriptorProto_TYPE_SFIXED64,
			descriptorpb.FieldDescriptorProto_TYPE_SINT64:
			converter = func(v string) (interface{}, error) { return strconv.ParseInt(v, 10, 64) }

		case descriptorpb.FieldDescriptorProto_TYPE_UINT64,
			descriptorpb.FieldDescriptorProto_TYPE_FIXED64:
			converter = func(v string) (interface{}, error) { return strconv.ParseUint(v, 10, 64) }

		case descriptorpb.FieldDescriptorProto_TYPE_INT32,
			descriptorpb.FieldDescriptorProto_TYPE_SFIXED32,
			descriptorpb.FieldDescriptorProto_TYPE_SINT32:
			converter = func(v string) (interface{}, error) {
				i, err := strconv.ParseInt(v, 10, 32)
				return int32(i), err
			}

		case descriptorpb.FieldDescriptorProto_TYPE_UINT32,
			descriptorpb.FieldDescriptorProto_TYPE_FIXED32:
			converter = func(v string) (interface{}, error) {
				u, err := strconv.ParseUint(v, 10, 32)
				return uint32(u), err
			}

		case descriptorpb.FieldDescriptorProto_TYPE_BOOL:
			converter = func(v string) (interface{}, error) { return strconv.ParseBool(v) }

		case descriptorpb.FieldDescriptorProto_TYPE_STRING:
			converter = func(v string) (interface{}, error) { return v, nil }

		// Use strconv.Unquote to interpret byte literals and Unicode literals.
		// For example, a user inputs `\x6f\x67\x69\x73\x6f`,
		// His expects "ogiso" in string, but backslashes in the input are not interpreted as an escape sequence.
		// So, we need to call strconv.Unquote to interpret backslashes as an escape sequence.
		case descriptorpb.FieldDescriptorProto_TYPE_BYTES:
			converter = func(v string) (interface{}, error) {
				if r.opts.BytesFromFile {
					b, err := ioutil.ReadFile(v)
					if err == nil {
						return b, nil
					}
				}

				v, err := strconv.Unquote(`"` + v + `"`)
				return []byte(v), err
			}

		default:
			return nil, fmt.Errorf("invalid type: %s", t)
		}

		prefix := r.makePrefix(f)

		return r.input(prefix, f, converter)
	}

	if !f.IsRepeated() {
		v, err := resolve(f)
		if err != nil {
			return err
		}

		return r.msg.TrySetField(f, v)
	}

	color := r.color

	for {
		// Return nil to keep inputted values.
		if !r.addRepeatedField(f) {
			return nil
		}

		r.prompt.SetPrefixColor(color)
		color.Next()

		v, err := resolve(f)
		if err == io.EOF {
			// io.EOF signals the end of inputting repeated field.
			// Return nil to keep inputted values.
			return nil
		}
		if err != nil {
			return err
		}

		if err := r.msg.TryAddRepeatedField(f, v); err != nil {
			return err
		}
	}
}

prompt/promt.go

var (
	ErrAbort = errors.New("abort")
	ErrSkip  = errors.New("skip")
)

Those are the only changes I made. Just paste the fucntions and replace var block and you should have the same code as me. If you still aren't able to reproduce it, my build must be broken or something (becasue it's the google proto package that crashes). If that's the case I can create pull request.

@ktr0731
Copy link
Owner

ktr0731 commented Jul 8, 2022

@relipocere
It seems like the method which causes panic is GetUser, not UpdateGroup. Is a call for UpdateGroup works fine?
If this problem occurs only when calling GetUser, could you also tell me the API definition and the minimum server implementation?

@relipocere
Copy link
Contributor Author

@ktr0731 Yes, UpdateGroup works fine, but I think it's because it has empty response message (without fields). I tried calling it on many handlers and it seems that it crashes when there's non-empty response message body (will it crash if there's a field but it has a default value? idk). So I think the definition of the API here is not relevant. Can you try testing it on any rpc method that returns something?

@ktr0731
Copy link
Owner

ktr0731 commented Jul 8, 2022

@relipocere
I tried to call APIs on a dummy gRPC server, but panic didn't occur. Could you try to call these APIs?

It can be run with the following command:

$ go run github.com/ktr0731/grpc-test@latest -r

@relipocere
Copy link
Contributor Author

@ktr0731 yup, I tried and it works fine with "simple" types. Looks like it breaks when the response message includes a message in itself. Try calling this kind of api:

service MyService{
    rpc GetGroups(GetGroupsRequest) returns (GetGroupsResponse);
}

message GetGroupsRequest{
  repeated int64 group_ids = 1;
}

message GetGroupsResponse{
  repeated Group groups = 1;
}

message Group{
  int64 id = 1;
  string title = 2;
  repeated int64 organization_ids = 3;
  repeated int64 point_ids = 4;
  repeated int64 region_ids = 5;
  repeated int64 user_ids = 6;
}

@ktr0731
Copy link
Owner

ktr0731 commented Jul 8, 2022

@relipocere
Thank you.
I was finally able to reproduce it. But I don't know why this changes causes panic, so we need to dig down the problem.

@relipocere
Copy link
Contributor Author

Well, gotta check the whole call stack. It must be a rouge pointer, can't think of any other reason for it to crash at the google package level

@ktr0731
Copy link
Owner

ktr0731 commented Jul 16, 2022

@relipocere
I noticed a commit merged in #530 causes panic. And now, it has been reverted. So, could you try again with the latest master branch?

@relipocere
Copy link
Contributor Author

@ktr0731 it works fine now-no more panics and --dig-manually works as expected! Do I create PR or will you just push this changes by yourself?

relipocere added a commit to relipocere/evans that referenced this issue Jul 16, 2022
@relipocere
Copy link
Contributor Author

Also completely unrelated, but when you're building the binary you can pass -ldflags="-w" to go build. It will make it "undebugable" with GBD, but the size of the binary becomes noticeably smaller (it went down from 22.4MB to 17.4MB when I tried it).

@ktr0731
Copy link
Owner

ktr0731 commented Jul 22, 2022

Thank you for the beneficial information. We'll add the option!

ktr0731 pushed a commit that referenced this issue Jul 22, 2022
@ktr0731
Copy link
Owner

ktr0731 commented Jul 22, 2022

Fixed in #568

@ktr0731 ktr0731 closed this as completed Jul 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants