caddy/listeners.go
2019-04-02 15:31:02 -06:00

136 lines
3.8 KiB
Go

package caddy2
import (
"fmt"
"net"
"sync"
"sync/atomic"
"time"
)
// Listen returns a listener suitable for use in a Caddy module.
func Listen(network, addr string) (net.Listener, error) {
lnKey := network + "/" + addr
listenersMu.Lock()
defer listenersMu.Unlock()
// if listener already exists, increment usage counter, then return listener
if lnInfo, ok := listeners[lnKey]; ok {
atomic.AddInt32(&lnInfo.usage, 1)
return &fakeCloseListener{usage: &lnInfo.usage, Listener: lnInfo.ln}, nil
}
// or, create new one and save it
ln, err := net.Listen(network, addr)
if err != nil {
return nil, err
}
// make sure to start its usage counter at 1
lnInfo := &listenerUsage{usage: 1, ln: ln}
listeners[lnKey] = lnInfo
return &fakeCloseListener{usage: &lnInfo.usage, Listener: ln}, nil
}
// fakeCloseListener's Close() method is a no-op. This allows
// stopping servers that are using the listener without giving
// up the socket; thus, servers become hot-swappable while the
// listener remains running. Listeners should be re-wrapped in
// a new fakeCloseListener each time the listener is reused.
type fakeCloseListener struct {
closed int32 // accessed atomically
usage *int32 // accessed atomically
net.Listener
}
// Accept accepts connections until Close() is called.
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// if the listener is already "closed", return error
if atomic.LoadInt32(&fcl.closed) == 1 {
return nil, fcl.fakeClosedErr()
}
// wrap underlying accept
conn, err := fcl.Listener.Accept()
if err == nil {
return conn, nil
}
if atomic.LoadInt32(&fcl.closed) == 1 {
// clear the deadline
switch ln := fcl.Listener.(type) {
case *net.TCPListener:
ln.SetDeadline(time.Time{})
case *net.UnixListener:
ln.SetDeadline(time.Time{})
}
// if we cancelled the Accept() by setting a deadline
// on the listener, we need to make sure any callers of
// Accept() think the listener was actually closed;
// if we return the timeout error instead, callers might
// simply retry, leaking goroutines for longer
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return nil, fcl.fakeClosedErr()
}
}
return nil, err
}
// Close stops accepting new connections, but does not
// actually close the underlying listener.
func (fcl *fakeCloseListener) Close() error {
if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) {
// unfortunately, there is no way to cancel any
// currently-blocking calls to Accept() that are
// awaiting connections since we're not actually
// closing the listener; so we cheat by setting
// a deadline in the past, which forces it to
// time out; note that this only works for
// certain types of listeners...
switch ln := fcl.Listener.(type) {
case *net.TCPListener:
ln.SetDeadline(time.Now().Add(-1 * time.Minute))
case *net.UnixListener:
ln.SetDeadline(time.Now().Add(-1 * time.Minute))
}
// since we're no longer using this listener,
// decrement the usage counter
atomic.AddInt32(fcl.usage, -1)
}
return nil
}
func (fcl *fakeCloseListener) fakeClosedErr() error {
return &net.OpError{
Op: "accept",
Net: fcl.Listener.Addr().Network(),
Addr: fcl.Listener.Addr(),
Err: ErrFakeClosed,
}
}
// ErrFakeClosed is the underlying error value returned by
// fakeCloseListener.Accept() after Close() has been called,
// indicating that it is pretending to be closed so that the
// server using it can terminate, while the underlying
// socket is actually left open.
var ErrFakeClosed = fmt.Errorf("listener 'closed' 😉")
// listenerUsage pairs a net.Listener with a
// count of how many servers are using it.
type listenerUsage struct {
usage int32 // accessed atomically
ln net.Listener
}
var (
listeners = make(map[string]*listenerUsage)
listenersMu sync.Mutex
)