-
Notifications
You must be signed in to change notification settings - Fork 10
/
opts.go
244 lines (211 loc) · 7.49 KB
/
opts.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// Copyright (C) 2017 Michael J. Fromberger. All Rights Reserved.
package jrpc2
import (
"context"
"encoding/json"
"fmt"
"log"
"runtime"
"time"
)
// ServerOptions control the behaviour of a server created by [NewServer]. A
// nil *ServerOptions is valid and provides sensible defaults. It is safe to
// share server options among multiple server instances.
type ServerOptions struct {
// If not nil, send debug text logs here.
Logger Logger
// If not nil, the methods of this value are called to log each request
// received and each response or error returned.
RPCLog RPCLogger
// Instructs the server to allow server callbacks and notifications, a
// non-standard extension to the JSON-RPC protocol. If AllowPush is false,
// the Notify and Callback methods of the server report errors if called.
AllowPush bool
// Instructs the server to disable the built-in rpc.* handler methods.
//
// By default, a server reserves all rpc.* methods, even if the given
// assigner maps them. When this option is true, rpc.* methods are passed
// along to the given assigner.
DisableBuiltin bool
// Allows up to the specified number of goroutines to execute in parallel in
// request handlers. A value less than 1 uses runtime.NumCPU(). Note that
// this setting does not constrain order of issue.
Concurrency int
// If set, this function is called to create a new base request context.
// If unset, the server uses a background context.
NewContext func() context.Context
// If nonzero this value as the server start time; otherwise, use the
// current time when Start is called. All servers created from the same
// options will share the same start time if one is set.
StartTime time.Time
}
func (s *ServerOptions) logFunc() func(string, ...any) {
if s == nil || s.Logger == nil {
return func(string, ...any) {}
}
return s.Logger.Printf
}
func (s *ServerOptions) allowPush() bool { return s != nil && s.AllowPush }
func (s *ServerOptions) allowBuiltin() bool { return s == nil || !s.DisableBuiltin }
func (s *ServerOptions) concurrency() int64 {
if s == nil || s.Concurrency < 1 {
return int64(runtime.NumCPU())
}
return int64(s.Concurrency)
}
func (s *ServerOptions) startTime() time.Time {
if s == nil {
return time.Time{}
}
return s.StartTime
}
func (s *ServerOptions) newContext() func() context.Context {
if s == nil || s.NewContext == nil {
return context.Background
}
return s.NewContext
}
func (s *ServerOptions) rpcLog() RPCLogger {
if s == nil || s.RPCLog == nil {
return nullRPCLogger{}
}
return s.RPCLog
}
// ClientOptions control the behaviour of a client created by [NewClient].
// A nil *ClientOptions is valid and provides sensible defaults.
type ClientOptions struct {
// If not nil, send debug text logs here.
Logger Logger
// If set, this function is called if a notification is received from the
// server. If unset, server notifications are logged and discarded. At
// most one invocation of the callback will be active at a time.
// Server notifications are a non-standard extension of JSON-RPC.
OnNotify func(*Request)
// If set, this function is called if a request is received from the server.
// If unset, server requests are logged and discarded. Multiple invocations
// of the callback handler may be active concurrently.
//
// The callback handler can retrieve the client from its context using the
// jrpc2.ClientFromContext function. The context terminates when the client
// is closed.
//
// If a callback handler panics, the client will recover the panic and
// report a system error back to the server describing the error.
//
// Server callbacks are a non-standard extension of JSON-RPC.
OnCallback Handler
// If set, this function is called when the context for a request terminates.
// The function receives the client and the response that was cancelled.
// The hook can obtain the ID and error value from rsp.
//
// Note that the hook does not receive the request context, which has
// already ended by the time the hook is called.
OnCancel func(cli *Client, rsp *Response)
// If set, this function is called when the client is stopped, either by
// calling its Close method or by disconnection of its channel. The
// arguments are the client itself and the error that caused it to stop.
OnStop func(cli *Client, err error)
}
func (c *ClientOptions) logFunc() func(string, ...any) {
if c == nil || c.Logger == nil {
return func(string, ...any) {}
}
return c.Logger.Printf
}
func (c *ClientOptions) handleNotification() func(*jmessage) {
if c == nil || c.OnNotify == nil {
return nil
}
h := c.OnNotify
return func(req *jmessage) { h(&Request{method: req.M, params: req.P}) }
}
func (c *ClientOptions) handleCancel() func(*Client, *Response) {
if c == nil {
return nil
}
return c.OnCancel
}
func (c *ClientOptions) handleStop() func(*Client, error) {
if c == nil || c.OnStop == nil {
return func(*Client, error) {}
}
return c.OnStop
}
func (c *ClientOptions) handleCallback() func(context.Context, *jmessage) []byte {
if c == nil || c.OnCallback == nil {
return nil
}
cb := c.OnCallback
return func(ctx context.Context, req *jmessage) []byte {
// Recover panics from the callback handler to ensure the server gets a
// response even if the callback fails without a result.
//
// Otherwise, a client and a server (a) running in the same process, and
// (b) where panics are recovered at a higher level, and (c) without
// cleaning up the client, can cause the server to stall in a manner that
// is difficult to debug.
//
// See https://github.com/creachadair/jrpc2/issues/41.
rsp := &jmessage{ID: req.ID}
v, err := panicToError(func() (any, error) {
return cb(ctx, &Request{
id: req.ID,
method: req.M,
params: req.P,
})
})
if err == nil {
rsp.R, err = json.Marshal(v)
}
if err != nil {
rsp.R = nil
if e, ok := err.(*Error); ok {
rsp.E = e
} else {
rsp.E = &Error{Code: ErrorCode(err), Message: err.Error()}
}
}
bits, _ := rsp.toJSON()
return bits
}
}
func panicToError(f func() (any, error)) (v any, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("panic in callback handler: %v", p)
}
}()
return f()
}
// A Logger records text logs from a server or a client. A nil logger discards
// text log input.
type Logger func(text string)
// Printf writes a formatted message to the logger. If lg == nil, the message
// is discarded.
func (lg Logger) Printf(msg string, args ...any) {
if lg != nil {
lg(fmt.Sprintf(msg, args...))
}
}
// StdLogger adapts a [*log.Logger] to a [Logger]. If logger == nil, the
// returned function sends logs to the default logger.
func StdLogger(logger *log.Logger) Logger {
if logger == nil {
return func(text string) { log.Output(2, text) }
}
return func(text string) { logger.Output(2, text) }
}
// An RPCLogger receives callbacks from a server to record the receipt of
// requests and the delivery of responses. These callbacks are invoked
// synchronously with the processing of the request.
type RPCLogger interface {
// Called for each request received prior to invoking its handler.
LogRequest(ctx context.Context, req *Request)
// Called for each response produced by a handler, immediately prior to
// sending it back to the client. The inbound request can be recovered from
// the context using jrpc2.InboundRequest.
LogResponse(ctx context.Context, rsp *Response)
}
type nullRPCLogger struct{}
func (nullRPCLogger) LogRequest(context.Context, *Request) {}
func (nullRPCLogger) LogResponse(context.Context, *Response) {}