1
0
mirror of https://github.com/golang/go synced 2024-09-30 14:18:32 -06:00

internal/jsonrpc2: make the wire structures private

The wire structures do not need to be public, and making them so might make it
harder to keep the package correct without breaking changes if the protocol
changes in the future.

Change-Id: I03a5618c63c9f7691183d4285f88a177ccdd3b35
Reviewed-on: https://go-review.googlesource.com/c/tools/+/227838
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
Ian Cottrell 2020-04-09 23:00:00 -04:00
parent 2595e72532
commit b854406a88
7 changed files with 125 additions and 104 deletions

View File

@ -35,6 +35,29 @@ var (
svresp = make(map[string]*parse.Logmsg) svresp = make(map[string]*parse.Logmsg)
) )
type wireRequest struct {
VersionTag string `json:"jsonrpc"`
Method string `json:"method"`
Params *json.RawMessage `json:"params,omitempty"`
ID *jsonrpc2.ID `json:"id,omitempty"`
}
type wireResponse struct {
VersionTag string `json:"jsonrpc"`
Result *json.RawMessage `json:"result,omitempty"`
Error interface{} `json:"error,omitempty"`
ID *jsonrpc2.ID `json:"id,omitempty"`
}
type wireCombined struct {
VersionTag interface{} `json:"jsonrpc"`
ID *jsonrpc2.ID `json:"id,omitempty"`
Method string `json:"method"`
Params *json.RawMessage `json:"params,omitempty"`
Result *json.RawMessage `json:"result,omitempty"`
Error interface{} `json:"error,omitempty"`
}
func main() { func main() {
log.SetFlags(log.Lshortfile) log.SetFlags(log.Lshortfile)
flag.Usage = func() { flag.Usage = func() {
@ -139,7 +162,7 @@ func main() {
} }
} }
func msgType(c *p.Combined) parse.MsgType { func msgType(c *wireCombined) parse.MsgType {
// Method, Params, ID => request // Method, Params, ID => request
// Method, Params, no-ID => notification // Method, Params, no-ID => notification
// Error => error response // Error => error response
@ -181,25 +204,28 @@ func send(ctx context.Context, l *parse.Logmsg, stream jsonrpc2.Stream, id *json
if err != nil { if err != nil {
n = 0 n = 0
} }
id = &jsonrpc2.ID{Number: int64(n)} id = jsonrpc2.NewIntID(int64(n))
} }
var r interface{} var r interface{}
switch l.Type { switch l.Type {
case parse.ClRequest: case parse.ClRequest:
r = jsonrpc2.WireRequest{ r = wireRequest{
ID: id, VersionTag: "2.0",
Method: l.Method, ID: id,
Params: &y, Method: l.Method,
Params: &y,
} }
case parse.SvResponse: case parse.SvResponse:
r = jsonrpc2.WireResponse{ r = wireResponse{
ID: id, VersionTag: "2.0",
Result: &y, ID: id,
Result: &y,
} }
case parse.ToServer: case parse.ToServer:
r = jsonrpc2.WireRequest{ r = wireRequest{
Method: l.Method, VersionTag: "2.0",
Params: &y, Method: l.Method,
Params: &y,
} }
default: default:
log.Fatalf("sending %s", l.Type) log.Fatalf("sending %s", l.Type)
@ -211,18 +237,10 @@ func send(ctx context.Context, l *parse.Logmsg, stream jsonrpc2.Stream, id *json
stream.Write(ctx, data) stream.Write(ctx, data)
} }
func strID(x *jsonrpc2.ID) string { func respond(ctx context.Context, c *wireCombined, stream jsonrpc2.Stream) {
if x.Name != "" {
log.Printf("strID returns %s", x.Name)
return x.Name
}
return strconv.Itoa(int(x.Number))
}
func respond(ctx context.Context, c *p.Combined, stream jsonrpc2.Stream) {
// c is a server request // c is a server request
// pick out the id, and look for the response in msgs // pick out the id, and look for the response in msgs
id := strID(c.ID) id := fmt.Sprint(c.ID)
for _, l := range msgs { for _, l := range msgs {
if l.ID == id && l.Type == parse.SvResponse { if l.ID == id && l.Type == parse.SvResponse {
// check that the methods match? // check that the methods match?
@ -280,7 +298,7 @@ func mimic(ctx context.Context) {
log.Fatal(err) log.Fatal(err)
} }
stream := jsonrpc2.NewHeaderStream(fromServer, toServer) stream := jsonrpc2.NewHeaderStream(fromServer, toServer)
rchan := make(chan *p.Combined, 10) // do we need buffering? rchan := make(chan *wireCombined, 10) // do we need buffering?
rdr := func() { rdr := func() {
for { for {
buf, _, err := stream.Read(ctx) buf, _, err := stream.Read(ctx)
@ -288,7 +306,7 @@ func mimic(ctx context.Context) {
rchan <- nil // close it instead? rchan <- nil // close it instead?
return return
} }
msg := &p.Combined{} msg := &wireCombined{}
if err := json.Unmarshal(buf, msg); err != nil { if err := json.Unmarshal(buf, msg); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -325,7 +343,7 @@ big:
respond(ctx, x, stream) respond(ctx, x, stream)
continue done // still waiting continue done // still waiting
case parse.ClResponse, parse.ReportErr: case parse.ClResponse, parse.ReportErr:
id := strID(x.ID) id := fmt.Sprint(x.ID)
seenids[id] = true seenids[id] = true
if id == l.ID { if id == l.ID {
break done break done

View File

@ -33,7 +33,7 @@ type Conn struct {
seq int64 // must only be accessed using atomic operations seq int64 // must only be accessed using atomic operations
stream Stream stream Stream
pendingMu sync.Mutex // protects the pending map pendingMu sync.Mutex // protects the pending map
pending map[ID]chan *WireResponse pending map[ID]chan *wireResponse
} }
// Request is sent to a server to represent a Call or Notify operaton. // Request is sent to a server to represent a Call or Notify operaton.
@ -44,7 +44,7 @@ type Request struct {
done []func() done []func()
// The Wire values of the request. // The Wire values of the request.
WireRequest wireRequest
} }
type constError string type constError string
@ -56,7 +56,7 @@ func (e constError) Error() string { return string(e) }
func NewConn(s Stream) *Conn { func NewConn(s Stream) *Conn {
conn := &Conn{ conn := &Conn{
stream: s, stream: s,
pending: make(map[ID]chan *WireResponse), pending: make(map[ID]chan *wireResponse),
} }
return conn return conn
} }
@ -69,7 +69,7 @@ func (c *Conn) Notify(ctx context.Context, method string, params interface{}) (e
if err != nil { if err != nil {
return fmt.Errorf("marshalling notify parameters: %v", err) return fmt.Errorf("marshalling notify parameters: %v", err)
} }
request := &WireRequest{ request := &wireRequest{
Method: method, Method: method,
Params: jsonParams, Params: jsonParams,
} }
@ -98,12 +98,12 @@ func (c *Conn) Notify(ctx context.Context, method string, params interface{}) (e
// result must be of a type you an pass to json.Unmarshal. // result must be of a type you an pass to json.Unmarshal.
func (c *Conn) Call(ctx context.Context, method string, params, result interface{}) (_ ID, err error) { func (c *Conn) Call(ctx context.Context, method string, params, result interface{}) (_ ID, err error) {
// generate a new request identifier // generate a new request identifier
id := ID{Number: atomic.AddInt64(&c.seq, 1)} id := ID{number: atomic.AddInt64(&c.seq, 1)}
jsonParams, err := marshalToRaw(params) jsonParams, err := marshalToRaw(params)
if err != nil { if err != nil {
return id, fmt.Errorf("marshalling call parameters: %v", err) return id, fmt.Errorf("marshalling call parameters: %v", err)
} }
request := &WireRequest{ request := &wireRequest{
ID: &id, ID: &id,
Method: method, Method: method,
Params: jsonParams, Params: jsonParams,
@ -127,7 +127,7 @@ func (c *Conn) Call(ctx context.Context, method string, params, result interface
// are racing the response. Also add a buffer to rchan, so that if we get a // are racing the response. Also add a buffer to rchan, so that if we get a
// wire response between the time this call is cancelled and id is deleted // wire response between the time this call is cancelled and id is deleted
// from c.pending, the send to rchan will not block. // from c.pending, the send to rchan will not block.
rchan := make(chan *WireResponse, 1) rchan := make(chan *wireResponse, 1)
c.pendingMu.Lock() c.pendingMu.Lock()
c.pending[id] = rchan c.pending[id] = rchan
c.pendingMu.Unlock() c.pendingMu.Unlock()
@ -196,16 +196,16 @@ func (r *Request) Reply(ctx context.Context, result interface{}, err error) erro
if err == nil { if err == nil {
raw, err = marshalToRaw(result) raw, err = marshalToRaw(result)
} }
response := &WireResponse{ response := &wireResponse{
Result: raw, Result: raw,
ID: r.ID, ID: r.ID,
} }
if err != nil { if err != nil {
if callErr, ok := err.(*Error); ok { if callErr, ok := err.(*wireError); ok {
response.Error = callErr response.Error = callErr
} else { } else {
response.Error = &Error{Message: err.Error()} response.Error = &wireError{Message: err.Error()}
var wrapped *Error var wrapped *wireError
if errors.As(err, &wrapped) { if errors.As(err, &wrapped) {
// if we wrapped a wire error, keep the code from the wrapped error // if we wrapped a wire error, keep the code from the wrapped error
// but the message from the outer error // but the message from the outer error
@ -239,17 +239,6 @@ func (r *Request) OnReply(do func()) {
r.done = append(r.done, do) r.done = append(r.done, do)
} }
// combined has all the fields of both Request and Response.
// We can decode this and then work out which it is.
type combined struct {
VersionTag VersionTag `json:"jsonrpc"`
ID *ID `json:"id,omitempty"`
Method string `json:"method"`
Params *json.RawMessage `json:"params,omitempty"`
Result *json.RawMessage `json:"result,omitempty"`
Error *Error `json:"error,omitempty"`
}
// Run blocks until the connection is terminated, and returns any error that // Run blocks until the connection is terminated, and returns any error that
// caused the termination. // caused the termination.
// It must be called exactly once for each Conn. // It must be called exactly once for each Conn.
@ -264,7 +253,7 @@ func (c *Conn) Run(runCtx context.Context, handler Handler) error {
return err return err
} }
// read a combined message // read a combined message
msg := &combined{} msg := &wireCombined{}
if err := json.Unmarshal(data, msg); err != nil { if err := json.Unmarshal(data, msg); err != nil {
// a badly formed message arrived, log it and continue // a badly formed message arrived, log it and continue
// we trust the stream to have isolated the error to just this message // we trust the stream to have isolated the error to just this message
@ -285,7 +274,7 @@ func (c *Conn) Run(runCtx context.Context, handler Handler) error {
req := &Request{ req := &Request{
conn: c, conn: c,
WireRequest: WireRequest{ wireRequest: wireRequest{
VersionTag: msg.VersionTag, VersionTag: msg.VersionTag,
Method: msg.Method, Method: msg.Method,
Params: msg.Params, Params: msg.Params,
@ -305,7 +294,7 @@ func (c *Conn) Run(runCtx context.Context, handler Handler) error {
rchan, ok := c.pending[*msg.ID] rchan, ok := c.pending[*msg.ID]
c.pendingMu.Unlock() c.pendingMu.Unlock()
if ok { if ok {
response := &WireResponse{ response := &wireResponse{
Result: msg.Result, Result: msg.Result,
Error: msg.Error, Error: msg.Error,
ID: msg.ID, ID: msg.ID,

View File

@ -34,10 +34,10 @@ var (
ErrServerOverloaded = NewError(-32000, "JSON RPC overloaded") ErrServerOverloaded = NewError(-32000, "JSON RPC overloaded")
) )
// WireRequest is sent to a server to represent a Call or Notify operaton. // wireRequest is sent to a server to represent a Call or Notify operaton.
type WireRequest struct { type wireRequest struct {
// VersionTag is always encoded as the string "2.0" // VersionTag is always encoded as the string "2.0"
VersionTag VersionTag `json:"jsonrpc"` VersionTag wireVersionTag `json:"jsonrpc"`
// Method is a string containing the method name to invoke. // Method is a string containing the method name to invoke.
Method string `json:"method"` Method string `json:"method"`
// Params is either a struct or an array with the parameters of the method. // Params is either a struct or an array with the parameters of the method.
@ -52,19 +52,30 @@ type WireRequest struct {
// It will always have the ID field set to tie it back to a request, and will // It will always have the ID field set to tie it back to a request, and will
// have either the Result or Error fields set depending on whether it is a // have either the Result or Error fields set depending on whether it is a
// success or failure response. // success or failure response.
type WireResponse struct { type wireResponse struct {
// VersionTag is always encoded as the string "2.0" // VersionTag is always encoded as the string "2.0"
VersionTag VersionTag `json:"jsonrpc"` VersionTag wireVersionTag `json:"jsonrpc"`
// Result is the response value, and is required on success. // Result is the response value, and is required on success.
Result *json.RawMessage `json:"result,omitempty"` Result *json.RawMessage `json:"result,omitempty"`
// Error is a structured error response if the call fails. // Error is a structured error response if the call fails.
Error *Error `json:"error,omitempty"` Error *wireError `json:"error,omitempty"`
// ID must be set and is the identifier of the Request this is a response to. // ID must be set and is the identifier of the Request this is a response to.
ID *ID `json:"id,omitempty"` ID *ID `json:"id,omitempty"`
} }
// Error represents a structured error in a Response. // wireCombined has all the fields of both Request and Response.
type Error struct { // We can decode this and then work out which it is.
type wireCombined struct {
VersionTag wireVersionTag `json:"jsonrpc"`
ID *ID `json:"id,omitempty"`
Method string `json:"method"`
Params *json.RawMessage `json:"params,omitempty"`
Result *json.RawMessage `json:"result,omitempty"`
Error *wireError `json:"error,omitempty"`
}
// wireError represents a structured error in a Response.
type wireError struct {
// Code is an error code indicating the type of failure. // Code is an error code indicating the type of failure.
Code int64 `json:"code"` Code int64 `json:"code"`
// Message is a short description of the error. // Message is a short description of the error.
@ -73,36 +84,34 @@ type Error struct {
Data *json.RawMessage `json:"data"` Data *json.RawMessage `json:"data"`
} }
// VersionTag is a special 0 sized struct that encodes as the jsonrpc version // wireVersionTag is a special 0 sized struct that encodes as the jsonrpc version
// tag. // tag.
// It will fail during decode if it is not the correct version tag in the // It will fail during decode if it is not the correct version tag in the
// stream. // stream.
type VersionTag struct{} type wireVersionTag struct{}
// ID is a Request identifier. // ID is a Request identifier.
// Only one of either the Name or Number members will be set, using the
// number form if the Name is the empty string.
type ID struct { type ID struct {
Name string name string
Number int64 number int64
} }
func NewError(code int64, message string) error { func NewError(code int64, message string) error {
return &Error{ return &wireError{
Code: code, Code: code,
Message: message, Message: message,
} }
} }
func (err *Error) Error() string { func (err *wireError) Error() string {
return err.Message return err.Message
} }
func (VersionTag) MarshalJSON() ([]byte, error) { func (wireVersionTag) MarshalJSON() ([]byte, error) {
return json.Marshal("2.0") return json.Marshal("2.0")
} }
func (VersionTag) UnmarshalJSON(data []byte) error { func (wireVersionTag) UnmarshalJSON(data []byte) error {
version := "" version := ""
if err := json.Unmarshal(data, &version); err != nil { if err := json.Unmarshal(data, &version); err != nil {
return err return err
@ -115,6 +124,12 @@ func (VersionTag) UnmarshalJSON(data []byte) error {
const invalidID int64 = math.MaxInt64 const invalidID int64 = math.MaxInt64
// NewIntID returns a new numerical request ID.
func NewIntID(v int64) *ID { return &ID{number: v} }
// NewStringID returns a new string request ID.
func NewStringID(v string) *ID { return &ID{name: v} }
// Format writes the ID to the formatter. // Format writes the ID to the formatter.
// If the rune is q the representation is non ambiguous, // If the rune is q the representation is non ambiguous,
// string forms are quoted, number forms are preceded by a # // string forms are quoted, number forms are preceded by a #
@ -126,24 +141,24 @@ func (id *ID) Format(f fmt.State, r rune) {
switch { switch {
case id == nil: case id == nil:
fmt.Fprintf(f, numF, invalidID) fmt.Fprintf(f, numF, invalidID)
case id.Name != "": case id.name != "":
fmt.Fprintf(f, strF, id.Name) fmt.Fprintf(f, strF, id.name)
default: default:
fmt.Fprintf(f, numF, id.Number) fmt.Fprintf(f, numF, id.number)
} }
} }
func (id *ID) MarshalJSON() ([]byte, error) { func (id *ID) MarshalJSON() ([]byte, error) {
if id.Name != "" { if id.name != "" {
return json.Marshal(id.Name) return json.Marshal(id.name)
} }
return json.Marshal(id.Number) return json.Marshal(id.number)
} }
func (id *ID) UnmarshalJSON(data []byte) error { func (id *ID) UnmarshalJSON(data []byte) error {
*id = ID{} *id = ID{}
if err := json.Unmarshal(data, &id.Number); err == nil { if err := json.Unmarshal(data, &id.number); err == nil {
return nil return nil
} }
return json.Unmarshal(data, &id.Name) return json.Unmarshal(data, &id.name)
} }

View File

@ -35,13 +35,13 @@ var wireIDTestData = []struct {
quoted: `#0`, quoted: `#0`,
}, { }, {
name: `number`, name: `number`,
id: &jsonrpc2.ID{Number: 43}, id: jsonrpc2.NewIntID(43),
encoded: []byte(`43`), encoded: []byte(`43`),
plain: `43`, plain: `43`,
quoted: `#43`, quoted: `#43`,
}, { }, {
name: `string`, name: `string`,
id: &jsonrpc2.ID{Name: "life"}, id: jsonrpc2.NewStringID("life"),
encoded: []byte(`"life"`), encoded: []byte(`"life"`),
plain: `life`, plain: `life`,
quoted: `"life"`, quoted: `"life"`,

View File

@ -533,9 +533,7 @@ func handshaker(client *debugClient, goplsPath string, handler jsonrpc2.Handler)
} }
func sendError(ctx context.Context, req *jsonrpc2.Request, err error) { func sendError(ctx context.Context, req *jsonrpc2.Request, err error) {
if _, ok := err.(*jsonrpc2.Error); !ok { err = fmt.Errorf("%w: %v", jsonrpc2.ErrParse, err)
err = fmt.Errorf("%w: %v", jsonrpc2.ErrParse, err)
}
if err := req.Reply(ctx, nil, err); err != nil { if err := req.Reply(ctx, nil, err); err != nil {
event.Error(ctx, "", err) event.Error(ctx, "", err)
} }

View File

@ -41,15 +41,21 @@ func (s *loggingStream) Write(ctx context.Context, data []byte) (int64, error) {
return count, err return count, err
} }
// Combined has all the fields of both Request and Response. // wireCombined has all the fields of both Request and Response.
// We can decode this and then work out which it is. // We can decode this and then work out which it is.
type Combined struct { type wireCombined struct {
VersionTag jsonrpc2.VersionTag `json:"jsonrpc"` VersionTag interface{} `json:"jsonrpc"`
ID *jsonrpc2.ID `json:"id,omitempty"` ID *jsonrpc2.ID `json:"id,omitempty"`
Method string `json:"method"` Method string `json:"method"`
Params *json.RawMessage `json:"params,omitempty"` Params *json.RawMessage `json:"params,omitempty"`
Result *json.RawMessage `json:"result,omitempty"` Result *json.RawMessage `json:"result,omitempty"`
Error *jsonrpc2.Error `json:"error,omitempty"` Error *wireError `json:"error,omitempty"`
}
type wireError struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data *json.RawMessage `json:"data"`
} }
type req struct { type req struct {
@ -106,11 +112,11 @@ func (m *mapped) setServer(id string, r req) {
const eor = "\r\n\r\n\r\n" const eor = "\r\n\r\n\r\n"
func logCommon(outfd io.Writer, data []byte) (*Combined, time.Time, string) { func logCommon(outfd io.Writer, data []byte) (*wireCombined, time.Time, string) {
if outfd == nil { if outfd == nil {
return nil, time.Time{}, "" return nil, time.Time{}, ""
} }
var v Combined var v wireCombined
err := json.Unmarshal(data, &v) err := json.Unmarshal(data, &v)
if err != nil { if err != nil {
fmt.Fprintf(outfd, "Unmarshal %v\n", err) fmt.Fprintf(outfd, "Unmarshal %v\n", err)
@ -132,7 +138,7 @@ func logOut(outfd io.Writer, data []byte) {
} }
id := fmt.Sprint(v.ID) id := fmt.Sprint(v.ID)
if v.Error != nil { if v.Error != nil {
fmt.Fprintf(outfd, "[Error - %s] Received #%s %s%s", tmfmt, id, v.Error, eor) fmt.Fprintf(outfd, "[Error - %s] Received #%s %s%s", tmfmt, id, v.Error.Message, eor)
return return
} }
buf := strings.Builder{} buf := strings.Builder{}
@ -171,7 +177,7 @@ func logOut(outfd io.Writer, data []byte) {
if v.Result != nil { if v.Result != nil {
r = string(*v.Result) r = string(*v.Result)
} }
fmt.Fprintf(&buf, "%s\n%s\n%s%s", p, r, v.Error, eor) fmt.Fprintf(&buf, "%s\n%s\n%s%s", p, r, v.Error.Message, eor)
} }
outfd.Write([]byte(buf.String())) outfd.Write([]byte(buf.String()))
} }
@ -187,7 +193,7 @@ func logIn(outfd io.Writer, data []byte) {
// ID !Method Result(might be null, but !Params) => Sending response (could we get an Error?) // ID !Method Result(might be null, but !Params) => Sending response (could we get an Error?)
// !ID Method Params => Sending notification // !ID Method Params => Sending notification
if v.Error != nil { // does this ever happen? if v.Error != nil { // does this ever happen?
fmt.Fprintf(outfd, "[Error - %s] Sent #%s %s%s", tmfmt, id, v.Error, eor) fmt.Fprintf(outfd, "[Error - %s] Sent #%s %s%s", tmfmt, id, v.Error.Message, eor)
return return
} }
buf := strings.Builder{} buf := strings.Builder{}
@ -230,7 +236,7 @@ func logIn(outfd io.Writer, data []byte) {
if v.Result != nil { if v.Result != nil {
r = string(*v.Result) r = string(*v.Result)
} }
fmt.Fprintf(&buf, "%s\n%s\n%s%s", p, r, v.Error, eor) fmt.Fprintf(&buf, "%s\n%s\n%s%s", p, r, v.Error.Message, eor)
} }
outfd.Write([]byte(buf.String())) outfd.Write([]byte(buf.String()))
} }

View File

@ -48,15 +48,13 @@ func CancelHandler(handler jsonrpc2.Handler) jsonrpc2.Handler {
if err := json.Unmarshal(*req.Params, &params); err != nil { if err := json.Unmarshal(*req.Params, &params); err != nil {
return sendParseError(ctx, req, err) return sendParseError(ctx, req, err)
} }
v := jsonrpc2.ID{}
if n, ok := params.ID.(float64); ok { if n, ok := params.ID.(float64); ok {
v.Number = int64(n) canceller(*jsonrpc2.NewIntID(int64(n)))
} else if s, ok := params.ID.(string); ok { } else if s, ok := params.ID.(string); ok {
v.Name = s canceller(*jsonrpc2.NewStringID(s))
} else { } else {
return sendParseError(ctx, req, fmt.Errorf("Request ID %v malformed", params.ID)) return sendParseError(ctx, req, fmt.Errorf("request ID %v malformed", params.ID))
} }
canceller(v)
return req.Reply(ctx, nil, nil) return req.Reply(ctx, nil, nil)
} }
} }
@ -78,8 +76,5 @@ func cancelCall(ctx context.Context, conn *jsonrpc2.Conn, id jsonrpc2.ID) {
} }
func sendParseError(ctx context.Context, req *jsonrpc2.Request, err error) error { func sendParseError(ctx context.Context, req *jsonrpc2.Request, err error) error {
if _, ok := err.(*jsonrpc2.Error); !ok { return req.Reply(ctx, nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err))
err = fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
}
return req.Reply(ctx, nil, err)
} }