blob: cfbcbcb021c93dc35a5264c47db1e9b4db4827e2 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package jsonrpc2
import (
"context"
"errors"
"io"
"math"
"net"
"os"
"time"
"golang.org/x/tools/internal/event"
)
// NOTE: This file provides an experimental API for serving multiple remote
// jsonrpc2 clients over the network. For now, it is intentionally similar to
// net/http, but that may change in the future as we figure out the correct
// semantics.
// A StreamServer is used to serve incoming jsonrpc2 clients communicating over
// a newly created connection.
type StreamServer interface {
ServeStream(context.Context, Conn) error
}
// The ServerFunc type is an adapter that implements the StreamServer interface
// using an ordinary function.
type ServerFunc func(context.Context, Conn) error
// ServeStream calls f(ctx, s).
func (f ServerFunc) ServeStream(ctx context.Context, c Conn) error {
return f(ctx, c)
}
// HandlerServer returns a StreamServer that handles incoming streams using the
// provided handler.
func HandlerServer(h Handler) StreamServer {
return ServerFunc(func(ctx context.Context, conn Conn) error {
conn.Go(ctx, h)
<-conn.Done()
return conn.Err()
})
}
// ListenAndServe starts an jsonrpc2 server on the given address. If
// idleTimeout is non-zero, ListenAndServe exits after there are no clients for
// this duration, otherwise it exits only on error.
func ListenAndServe(ctx context.Context, network, addr string, server StreamServer, idleTimeout time.Duration) error {
ln, err := net.Listen(network, addr)
if err != nil {
return err
}
defer ln.Close()
if network == "unix" {
defer os.Remove(addr)
}
return Serve(ctx, ln, server, idleTimeout)
}
// Serve accepts incoming connections from the network, and handles them using
// the provided server. If idleTimeout is non-zero, ListenAndServe exits after
// there are no clients for this duration, otherwise it exits only on error.
func Serve(ctx context.Context, ln net.Listener, server StreamServer, idleTimeout time.Duration) error {
newConns := make(chan net.Conn)
closedConns := make(chan error)
activeConns := 0
var acceptErr error
go func() {
defer close(newConns)
for {
var nc net.Conn
nc, acceptErr = ln.Accept()
if acceptErr != nil {
return
}
newConns <- nc
}
}()
ctx, cancel := context.WithCancel(ctx)
defer func() {
// Signal the Accept goroutine to stop immediately
// and terminate all newly-accepted connections until it returns.
ln.Close()
for nc := range newConns {
nc.Close()
}
// Cancel pending ServeStream callbacks and wait for them to finish.
cancel()
for activeConns > 0 {
err := <-closedConns
if !isClosingError(err) {
event.Error(ctx, "closed a connection", err)
}
activeConns--
}
}()
// Max duration: ~290 years; surely that's long enough.
const forever = math.MaxInt64
if idleTimeout <= 0 {
idleTimeout = forever
}
connTimer := time.NewTimer(idleTimeout)
defer connTimer.Stop()
for {
select {
case netConn, ok := <-newConns:
if !ok {
return acceptErr
}
if activeConns == 0 && !connTimer.Stop() {
// connTimer.C may receive a value even after Stop returns.
// (See https://golang.org/issue/37196.)
<-connTimer.C
}
activeConns++
stream := NewHeaderStream(netConn)
go func() {
conn := NewConn(stream)
err := server.ServeStream(ctx, conn)
stream.Close()
closedConns <- err
}()
case err := <-closedConns:
if !isClosingError(err) {
event.Error(ctx, "closed a connection", err)
}
activeConns--
if activeConns == 0 {
connTimer.Reset(idleTimeout)
}
case <-connTimer.C:
return ErrIdleTimeout
case <-ctx.Done():
return nil
}
}
}
// isClosingError reports if the error occurs normally during the process of
// closing a network connection. It uses imperfect heuristics that err on the
// side of false negatives, and should not be used for anything critical.
func isClosingError(err error) bool {
if errors.Is(err, io.EOF) {
return true
}
// Per https://github.com/golang/go/issues/4373, this error string should not
// change. This is not ideal, but since the worst that could happen here is
// some superfluous logging, it is acceptable.
if err.Error() == "use of closed network connection" {
return true
}
return false
}